3.8 KiB
Capability Spec: react-midi
Package: @suaveplan/react-midi
Status: Wave 1 shipped (provider + 7 hooks + 2 components + error codes + telemetry).
Requirements
Requirement: MidiProvider grants Web MIDI exactly once
<MidiProvider sysex={boolean}> SHALL call navigator.requestMIDIAccess exactly once for the lifetime of the React tree, regardless of StrictMode double-mount. The resulting MIDIAccess SHALL be cached in a ref so subsequent re-renders observe the same instance. SysEx access is opt-in via prop.
Scenario: StrictMode double-mount does not double-request
- WHEN
<StrictMode><MidiProvider/></StrictMode>mounts andnavigator.requestMIDIAccessis a spy - THEN the spy is called exactly once across the double-mount sequence
Scenario: Permission denial surfaces as status
- WHEN the browser rejects
requestMIDIAccesswithNotAllowedError - THEN
useMidi()returns{ status: "denied", access: null }; noMidiErroris thrown to the React tree (caller chooses how to handle)
Scenario: Unsupported platform short-circuits
- WHEN
navigator.requestMIDIAccessis undefined (no Web MIDI) - THEN
useMidi()returns{ status: "unsupported", access: null }without throwing
Verified by: src/provider/midi-provider.test.tsx
Requirement: Hooks expose typed messages
useMidiInput(portId | "first") SHALL return a MidiPortLike whose onMessage handler receives fully parsed MidiMessage events from @suaveplan/midi, not raw bytes.
Scenario: NoteOn from device → parsed event
- WHEN a fake
MIDIInputdelivers[0x91, 0x40, 0x64] - THEN the hook's
onMessagesubscriber observes{ type: "noteOn", channel: 1, note: 64, velocity: 100 }
Verified by: src/hooks/use-midi-input/use-midi-input.test.tsx
Requirement: Device list reflects connect/disconnect live
useMidiDevices() SHALL re-render when a MIDI device is plugged in or unplugged at runtime (Web MIDI statechange event).
Scenario: Device plug emits new entry
- WHEN a fake
MIDIAccessfiresstatechangewith a newly-connected input - THEN the next render of
useMidiDevices()returns the input in itsinputsarray
Verified by: src/hooks/use-midi-devices/use-midi-devices.test.tsx
Requirement: NRPN / RPN / CC14 hooks wrap universal reducers
useNRPN(portId), useRPN(portId), useCC14(portId) SHALL consume the input port's message stream and yield the higher-level events (NRPNChange, RPNChange, CC14Change) by delegating to the universal reducers in @suaveplan/midi.
Scenario: Four-CC NRPN dance emits one consolidated React event
- WHEN an input sends CC 99/98/6/38
- THEN the
useNRPNsubscriber observes one{ paramHigh, paramLow, value }event, not four CC events
Verified by: src/hooks/use-nrpn/use-nrpn.test.tsx
Requirement: Output port encodes via universal helper
useMidiOutput(portId).send(msg: MidiMessage) SHALL call encodeMessage from @suaveplan/midi and forward the resulting bytes to the underlying MIDIOutput.send.
Scenario: NoteOn sent → encoded bytes hit transport
- WHEN consumer calls
port.send({ type: "noteOn", channel: 0, note: 60, velocity: 100 }) - THEN the underlying
MIDIOutput.sendis called with[0x90, 0x3c, 0x64]
Verified by: src/hooks/use-midi-output/use-midi-output.test.tsx
Requirement: OTel counters increment per port
Every received/sent message SHALL increment react_midi_messages_received_total{port} / react_midi_messages_sent_total{port} counters tagged with the port id.
Scenario: Receive/send counters increment
- WHEN 10 messages flow in either direction
- THEN the in-memory
MeterProvidersnapshot shows the matching count
Verified by: src/telemetry/telemetry.test.ts