# Design — `@suaveplan/react-packer` ## Architecture `@suaveplan/react-packer` is a React 19 companion that wraps a `PackedResourceManager` from `@suaveplan/packer-browser` in a small set of hooks plus a React context. ```text Consumer app │ ▼ │ ▼ usePackedResource(packedUrl) ─┐ usePackedImage(packedUrl) │ usePackedAudio(packedUrl) ├─► manager.load(url, opts) usePackedVideo(packedUrl) │ │ usePackedFont(packedUrl, family) ─┘ ▼ PackedResourceLoader (fetch + verify + unpack) PackedResourceCache (LRU + Blob URLs) ``` The provider is optional. If no provider is mounted in the tree, the hooks resolve the singleton via dynamic import of `@suaveplan/packer-browser/singleton`. This keeps the default zero-config import cost zero — singleton construction only runs in trees that don't inject a manager. Adopters that care about tree-shaking, multiple managers, or explicit lifecycle (test rigs, SSR) mount a provider and never touch the singleton. ## Public surface ```ts // hooks/use-packed-resource export interface UsePackedResourceOptions { cache?: boolean; verifyChecksum?: boolean; verifyKey?: Uint8Array; signal?: AbortSignal; allowedMimePrefixes?: ReadonlyArray; } export interface UsePackedResourceReturn { url: string | null; blob: Blob | null; loading: boolean; error: ReactPackerError | null; status: "idle" | "loading" | "success" | "error"; reload: () => void; } export function usePackedResource( packedUrl: string | null | undefined, options?: UsePackedResourceOptions ): UsePackedResourceReturn; // hooks/use-packed-image — default { cache: true, allowedMimePrefixes: ["image/"] } export function usePackedImage( packedUrl: string | null | undefined, options?: UsePackedResourceOptions ): UsePackedResourceReturn; // hooks/use-packed-audio — default { cache: true, allowedMimePrefixes: ["audio/"] } // hooks/use-packed-video — default { cache: false, allowedMimePrefixes: ["video/"] } // hooks/use-packed-font — default { cache: true, allowedMimePrefixes: ["font/", "application/font", "application/octet-stream"] } export interface UsePackedFontReturn extends UsePackedResourceReturn { fontLoaded: boolean; } export function usePackedFont( packedUrl: string | null | undefined, fontFamily?: string, options?: UsePackedResourceOptions ): UsePackedFontReturn; // provider export interface PackedResourceProviderValue { manager: PackedResourceManager; messageResolver?: MessageResolver; } export function PackedResourceProvider(props: { value: PackedResourceProviderValue; children?: ReactNode; }): ReactElement; export function usePackedResourceManager(): PackedResourceManager; ``` ## Error model A single error code registry is registered with `@suaveplan/error`: | Code | Raised when | |---|---| | `REACT_PACKER_LOAD_FAILED` | `manager.load(...)` threw and the hook is reporting it. The original error is preserved on `.cause`. | | `REACT_PACKER_FONT_LOAD_FAILED` | `document.fonts.load(...)` rejected. Hook surfaces `fontLoaded = false` and the wrapped error on `result.error`. | | `REACT_PACKER_ABORTED` | A pre-aborted signal was passed to a load; rejected before fetching. | `ReactPackerError extends SuaveplanError` and carries a brand symbol; `ReactPackerError.is(value)` discriminates safely across realms. ## File layout ```text src/ index.ts # barrel — explicit named re-exports error-codes/ error-codes.ts error-codes.md error-codes.test.ts index.ts types/ react-packer.types.ts react-packer.types.md index.ts provider/ packed-resource-provider/ packed-resource-provider.tsx packed-resource-provider.md packed-resource-provider.test.tsx packed-resource-provider.stories.tsx index.ts hooks/ use-packed-resource/ use-packed-resource.ts use-packed-resource.md use-packed-resource.test.tsx use-packed-resource.stories.tsx index.ts use-packed-image/ use-packed-image.ts use-packed-image.md use-packed-image.test.tsx use-packed-image.stories.tsx index.ts use-packed-audio/ use-packed-audio.ts use-packed-audio.md use-packed-audio.test.tsx use-packed-audio.stories.tsx index.ts use-packed-video/ use-packed-video.ts use-packed-video.md use-packed-video.test.tsx use-packed-video.stories.tsx index.ts use-packed-font/ use-packed-font.ts use-packed-font.md use-packed-font.test.tsx use-packed-font.stories.tsx index.ts ``` ## Lifecycle semantics 1. Mount with `packedUrl !== null` → `status = "loading"`, hook calls `manager.load(url, opts)` with an internal `AbortController` whose signal merges with any externally-passed `opts.signal`. 2. Success → `status = "success"`, `url` + `blob` populated, manager `release` is registered as cleanup so unmount drops the cache ref. 3. Failure → `status = "error"`, `error` is a `ReactPackerError` wrapping the underlying error on `.cause`. 4. Re-render with different `packedUrl` → previous controller aborts, previous URL is released, new load starts. 5. Unmount → controller aborts, the URL is released, no `setState` runs after unmount (mounted-ref guard). 6. `reload()` increments an internal counter that re-runs the effect. ## Testing strategy - Real `PackedResourceManager` constructed per-test via `createPackedResourceManager({ fetch })`. - `fetch` is a tiny closure returning `new Response(Uint8Array, { headers })`. - No `vi.mock`/`vi.spyOn` against `@suaveplan/packer-browser` — the manager runs its real loader + cache pipeline against the injected `fetch`. This is exactly the pattern used by `packer-browser/src/loader/loader.test.ts` and is what CLAUDE.md rule 2 mandates. - Coverage runs via `provider: "istanbul"` with 100/100/100/100 thresholds set in `vitest.config.ts`.