6.1 KiB
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.
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
// 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
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
- Mount with
packedUrl !== null→status = "loading", hook callsmanager.load(url, opts)with an internalAbortControllerwhose signal merges with any externally-passedopts.signal. - Success →
status = "success",url+blobpopulated, managerreleaseis registered as cleanup so unmount drops the cache ref. - Failure →
status = "error",erroris aReactPackerErrorwrapping the underlying error on.cause. - Re-render with different
packedUrl→ previous controller aborts, previous URL is released, new load starts. - Unmount → controller aborts, the URL is released, no
setStateruns after unmount (mounted-ref guard). reload()increments an internal counter that re-runs the effect.
Testing strategy
- Real
PackedResourceManagerconstructed per-test viacreatePackedResourceManager({ fetch }). fetchis a tiny closure returningnew Response(Uint8Array, { headers }).- No
vi.mock/vi.spyOnagainst@suaveplan/packer-browser— the manager runs its real loader + cache pipeline against the injectedfetch. This is exactly the pattern used bypacker-browser/src/loader/loader.test.tsand is what CLAUDE.md rule 2 mandates. - Coverage runs via
provider: "istanbul"with 100/100/100/100 thresholds set invitest.config.ts.