80 lines
3.7 KiB
Markdown
80 lines
3.7 KiB
Markdown
# react-midi spec delta — enterprise-react-midi
|
|
|
|
## ADDED 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 and `navigator.requestMIDIAccess` is a spy
|
|
- **THEN** the spy is called exactly once across the double-mount sequence
|
|
|
|
#### Scenario: Permission denial surfaces as status
|
|
|
|
- **WHEN** the browser rejects `requestMIDIAccess` with `NotAllowedError`
|
|
- **THEN** `useMidi()` returns `{ status: "denied", access: null }`; no `MidiError` is thrown to the React tree (caller chooses how to handle)
|
|
|
|
#### Scenario: Unsupported platform short-circuits
|
|
|
|
- **WHEN** `navigator.requestMIDIAccess` is 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 `MIDIInput` delivers `[0x91, 0x40, 0x64]`
|
|
- **THEN** the hook's `onMessage` subscriber 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 `MIDIAccess` fires `statechange` with a newly-connected input
|
|
- **THEN** the next render of `useMidiDevices()` returns the input in its `inputs` array
|
|
|
|
**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 `useNRPN` subscriber 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.send` is 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 `MeterProvider` snapshot shows the matching count
|
|
|
|
**Verified by:** `src/telemetry/telemetry.test.ts`
|