Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
# `hooks/use-engine-state`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`useEngineState()` reports the `<AudioProvider>`'s async-init
lifecycle phase as one of `'pending' | 'ready' | 'error'`. It is the
only engine-aware hook that does **not** throw on the pending phase —
its purpose is to *report* that phase so consumers can render a
loading state or gate child trees on engine readiness.
The hook still throws `ReactAudioError(REACT_AUDIO.NO_PROVIDER)` when
rendered outside an `<AudioProvider>` ancestor, mirroring the
contract every public hook honours.
## Features
- Returns `'pending'`, `'ready'`, or `'error'`.
- Throws `REACT_AUDIO.NO_PROVIDER` outside the provider.
- Triggers re-render when the engine resolves (via the provider's
internal `setEngineState`).
- Stable identity across re-renders for the same lifecycle phase.
## Usage: Basic
```tsx
import { useEngineState } from "@suaveplan/react-audio";
function App() {
const engineState = useEngineState();
if (engineState === "pending") return <p>Loading audio…</p>;
if (engineState === "error") return <p>Audio failed to initialise.</p>;
return <Workspace />;
}
```
## Usage: Advanced
```tsx
import { useEngineState } from "@suaveplan/react-audio";
function useIsReady(): boolean {
return useEngineState() === "ready";
}
function Gate({ children }: { children: React.ReactNode }) {
const ready = useIsReady();
return ready ? <>{children}</> : <Spinner />;
}
```
## API Reference
- `useEngineState(): 'pending' | 'ready' | 'error'`.
## Implementation Notes
`useEngineState` reads the provider's React context directly rather
than going through `useSyncExternalStore`. React already batches state
updates inside the provider, so reading the context value is
sufficient and avoids the indirection of a snapshot+subscribe pair.
The hook returns the same `engineState` string instance across
renders within the same phase, so a `useMemo` keyed on it is a
constant-time identity check. The `'error'` state is reachable when
the engine's async init rejects (e.g. the browser refuses to start
an `AudioContext`). The provider's error object is exposed on the
context value too — consumers needing the underlying failure can
combine `useEngineState() === 'error'` with `useAudioEngine` or a
direct context read. The hook is the ONE hook in this package that
deliberately does NOT throw on the pending phase.

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderHook, waitFor } from "@testing-library/react";
import {
REACT_AUDIO_CODES,
ReactAudioError,
} from "../../error-codes/error-codes.js";
import { AudioProvider } from "../../provider/audio-provider.js";
import { makeFakeAudioContext } from "../../test-utils.js";
import { useEngineState } from "./use-engine-state.js";
describe("useEngineState", () => {
it("hook outside provider throws REACT_AUDIO.NO_PROVIDER", {
timeout: 5000,
}, () => {
let caught: unknown;
try {
renderHook(() => useEngineState());
} catch (e) {
caught = e;
}
expect(caught).toBeInstanceOf(ReactAudioError);
expect((caught as ReactAudioError).code).toBe(
REACT_AUDIO_CODES.NO_PROVIDER
);
});
it("reports pending then ready across the init lifecycle", {
timeout: 30000,
}, async () => {
const { result } = renderHook(() => useEngineState(), {
wrapper: ({ children }) => (
<AudioProvider
options={{ adapter: { context: makeFakeAudioContext() } }}
>
{children}
</AudioProvider>
),
});
expect(result.current).toBe("pending");
await waitFor(() => {
expect(result.current).toBe("ready");
});
});
});

View File

@@ -0,0 +1,17 @@
import type { EngineState } from "../../provider/audio-context.js";
import { useAudioContextOrThrow } from "../use-provider-state.js";
/**
* Returns the provider's current engine-init lifecycle phase. Useful
* for rendering a loading state during the async window between
* mount and adapter `init()` resolution.
*
* - Throws `REACT_AUDIO.NO_PROVIDER` if no `<AudioProvider>` is in
* the React tree above.
* - Never throws `REACT_AUDIO.PROVIDER_PENDING` — it *reports* the
* pending phase via the return value.
*/
export function useEngineState(): EngineState {
const ctx = useAudioContextOrThrow();
return ctx.engineState;
}