first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# `provider/midi-context`
## Purpose
Defines the two React contexts that wire `<MidiProvider>` to its
descendant hooks. `MidiReactContext` carries the resolved
`MidiContextValue` — status, `WebMIDIAccess`, any caught error, the
`sysex` opt-in flag, and a monotonically-incrementing `stateVersion`
counter bumped on each Web MIDI `statechange` event. The presence of
`stateVersion` lets `useSyncExternalStore`-based hooks observe live
device plug/unplug changes without each hook holding its own
`statechange` listener.
`MidiRequestContext` is a per-tree override slot for the
`navigator.requestMIDIAccess` function. Tests provide a deterministic
in-memory implementation here; production renders leave it unset and
the provider falls back to the real global. This is a real injection
seam — not a mock — so the integration path remains exercised
verbatim under both real and fake transports.
## Features
- Two distinct contexts so hooks that only need the resolved access
re-render only on `stateVersion` bumps, not on injection changes.
- `null` sentinel on `MidiReactContext` triggers
`REACT_MIDI.NO_PROVIDER` in every public hook.
- `MidiStatus` is the four-value lifecycle: `loading`, `granted`,
`denied`, `unsupported`.
## Usage: Basic
```tsx
import { MidiReactContext } from "@suaveplan/react-midi";
import { useContext } from "react";
function StatusBadge() {
const ctx = useContext(MidiReactContext);
return <span>{ctx?.status ?? "no-provider"}</span>;
}
```
## Usage: Advanced
```tsx
import { MidiProvider, MidiRequestContext } from "@suaveplan/react-midi";
// Inject a deterministic fake in tests:
function fakeRequest() {
return Promise.resolve(fakeAccessShape);
}
<MidiRequestContext.Provider value={fakeRequest as never}>
<MidiProvider sysex>{children}</MidiProvider>
</MidiRequestContext.Provider>;
```
## API Reference
- `MidiStatus = "loading" | "granted" | "denied" | "unsupported"`.
- `MidiContextValue { status, access, error, sysex, stateVersion }`.
- `MidiReactContext: React.Context<MidiContextValue | null>`.
- `MidiRequestContext: React.Context<WebMIDIRequestFn | null>`.
## Implementation Notes
`stateVersion` exists because the Web MIDI `statechange` event fires
mutations on `MIDIAccess.inputs` / `outputs` in place — the underlying
`Map` identity does not change. `useSyncExternalStore` therefore needs
a sentinel value that *does* change to trigger a re-render; the
provider increments `stateVersion` inside the `statechange` callback.