Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
# 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
│
▼
<PackedResourceProvider value={manager} resolver={resolver}>
│
▼
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<string>;
}
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`.