first commit
This commit is contained in:
92
docs/foundation/data/react-packer/README.md
Normal file
92
docs/foundation/data/react-packer/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# @suaveplan/react-packer - React 19 companion for @suaveplan/packer-browser — usePackedResource, usePackedImage, usePackedAudio, usePackedVideo, usePackedFont hooks plus PackedResourceProvider context. Consumes a real PackedResourceManager; React is a peer dep so non-React consumers never pay the cost.
|
||||
|
||||
React 19 companion for `@suaveplan/packer-browser`. Ships
|
||||
`usePackedResource`, `usePackedImage`, `usePackedAudio`,
|
||||
`usePackedVideo`, and `usePackedFont` hooks plus a
|
||||
`PackedResourceProvider` context, so applications can consume packed
|
||||
asset archives from React components without `@suaveplan/packer-browser`
|
||||
having to ship React as a runtime dep.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical error-code registry and concrete error class for the React-tier packer companion. Every failure m…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical error-code registry and concrete error class for the React-tier packer companion. Every failure m…
|
||||
- [use-packed-audio](./src/hooks/use-packed-audio/use-packed-audio.md) — `usePackedAudio` is the audio analogue of `usePackedImage`. It binds a packed audio asset URL to a React component's li…
|
||||
- [use-packed-audio](./src/hooks/use-packed-audio/use-packed-audio.md) — `usePackedAudio` is the audio analogue of `usePackedImage`. It binds a packed audio asset URL to a React component's li…
|
||||
- [use-packed-font](./src/hooks/use-packed-font/use-packed-font.md) — `usePackedFont` loads a packed font asset and registers it with the document so subsequent rules referencing the suppli…
|
||||
- [use-packed-font](./src/hooks/use-packed-font/use-packed-font.md) — `usePackedFont` loads a packed font asset and registers it with the document so subsequent rules referencing the suppli…
|
||||
- [use-packed-image](./src/hooks/use-packed-image/use-packed-image.md) — `usePackedImage` is a thin wrapper around `usePackedResource` that pre-fills sensible defaults for image assets: cachin…
|
||||
- [use-packed-image](./src/hooks/use-packed-image/use-packed-image.md) — `usePackedImage` is a thin wrapper around `usePackedResource` that pre-fills sensible defaults for image assets: cachin…
|
||||
- [use-packed-resource](./src/hooks/use-packed-resource/use-packed-resource.md) — `usePackedResource` is the foundation hook of `@suaveplan/react-packer`. It binds a packed-archive virtual URL (e.g. `/…
|
||||
- [use-packed-resource](./src/hooks/use-packed-resource/use-packed-resource.md) — `usePackedResource` is the foundation hook of `@suaveplan/react-packer`. It binds a packed-archive virtual URL (e.g. `/…
|
||||
- [use-packed-video](./src/hooks/use-packed-video/use-packed-video.md) — `usePackedVideo` binds a packed video asset URL to a React component's lifecycle. Unlike the image and audio variants,…
|
||||
- [use-packed-video](./src/hooks/use-packed-video/use-packed-video.md) — `usePackedVideo` binds a packed video asset URL to a React component's lifecycle. Unlike the image and audio variants,…
|
||||
- [packed-resource-provider](./src/provider/packed-resource-provider/packed-resource-provider.md) — `PackedResourceProvider` lets a React tree explicitly inject a `PackedResourceManager` from `@suaveplan/packer-browser`…
|
||||
- [packed-resource-provider](./src/provider/packed-resource-provider/packed-resource-provider.md) — `PackedResourceProvider` lets a React tree explicitly inject a `PackedResourceManager` from `@suaveplan/packer-browser`…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/react-packer @suaveplan/packer-browser
|
||||
```
|
||||
|
||||
`react` and `react-dom` (≥ 19.0.0) are peer dependencies — the package
|
||||
does not bundle them.
|
||||
|
||||
## Quick start
|
||||
|
||||
```tsx
|
||||
import { createPackedResourceManager } from "@suaveplan/packer-browser";
|
||||
import { PackedResourceProvider, usePackedImage } from "@suaveplan/react-packer";
|
||||
|
||||
const manager = createPackedResourceManager();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PackedResourceProvider value={{ manager }}>
|
||||
<Logo />
|
||||
</PackedResourceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Logo() {
|
||||
const { url, loading, error } = usePackedImage("/assets/logo.packed");
|
||||
if (loading) return <div className="logo logo--loading" />;
|
||||
if (error !== null) return <div className="logo logo--error" />;
|
||||
return url !== null ? <img alt="logo" src={url} /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
If you do not mount a `PackedResourceProvider`, hooks fall back to the
|
||||
singleton at `@suaveplan/packer-browser/singleton`. Adopters that prefer
|
||||
zero-config get a working manager for free; adopters that want explicit
|
||||
lifecycle (tests, SSR, multi-tenant apps) mount a provider and skip the
|
||||
singleton entirely.
|
||||
|
||||
## Features
|
||||
|
||||
- Five hooks: `usePackedResource` (generic), `usePackedImage`,
|
||||
`usePackedAudio`, `usePackedVideo`, `usePackedFont`.
|
||||
- React 19 context provider for explicit `PackedResourceManager` and
|
||||
`MessageResolver` injection.
|
||||
- `AbortSignal` honored throughout — pre-aborted signals short-circuit,
|
||||
unmount cancels the in-flight load, and changing the packed URL
|
||||
aborts the previous one.
|
||||
- Stable error registry — `REACT_PACKER_CODES` + `ReactPackerError`
|
||||
branded subclass over `@suaveplan/error`.
|
||||
- `cleanup` releases the cached entry on unmount so the manager's LRU
|
||||
can evict.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md)
|
||||
- [`src/types/react-packer.types.md`](./src/types/react-packer.types.md)
|
||||
- [`src/provider/packed-resource-provider/packed-resource-provider.md`](./src/provider/packed-resource-provider/packed-resource-provider.md)
|
||||
- [`src/hooks/use-packed-resource/use-packed-resource.md`](./src/hooks/use-packed-resource/use-packed-resource.md)
|
||||
- [`src/hooks/use-packed-image/use-packed-image.md`](./src/hooks/use-packed-image/use-packed-image.md)
|
||||
- [`src/hooks/use-packed-audio/use-packed-audio.md`](./src/hooks/use-packed-audio/use-packed-audio.md)
|
||||
- [`src/hooks/use-packed-video/use-packed-video.md`](./src/hooks/use-packed-video/use-packed-video.md)
|
||||
- [`src/hooks/use-packed-font/use-packed-font.md`](./src/hooks/use-packed-font/use-packed-font.md)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# `error-codes` — stable error registry for `@suaveplan/react-packer`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the canonical error-code registry and concrete error class for
|
||||
the React-tier packer companion. Every failure mode surfaced through
|
||||
`usePackedResource` and its variants funnels through `ReactPackerError`,
|
||||
which extends `@suaveplan/error`'s `SuaveplanError` so consumers get
|
||||
cause-chain traversal, OTel span-event tagging, and i18n message
|
||||
resolution for free.
|
||||
|
||||
This module is the single source of truth for `REACT_PACKER_*` codes —
|
||||
no other file may invent new ones. Hooks and the provider import the
|
||||
exported constants and class to raise typed errors; tests assert on the
|
||||
code constants rather than string-comparing message keys.
|
||||
|
||||
## Features
|
||||
|
||||
- `REACT_PACKER_CODES` — frozen registry of three codes: `LOAD_FAILED`,
|
||||
`FONT_LOAD_FAILED`, `ABORTED`.
|
||||
- `ReactPackerError` — `SuaveplanError` subclass with a unique brand
|
||||
`Symbol` so cross-realm `instanceof` failures still permit safe
|
||||
discrimination via `ReactPackerError.is(value)`.
|
||||
- `cause` preservation — when a hook wraps a `PackerBrowserError` or a
|
||||
`DOMException` from `document.fonts.load`, the original error rides on
|
||||
`.cause` for downstream debugging and OTel attribute capture.
|
||||
- Stable shape — adding new codes requires a new OpenSpec change so the
|
||||
public registry never drifts mid-version.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { REACT_PACKER_CODES, ReactPackerError } from "@suaveplan/react-packer";
|
||||
|
||||
try {
|
||||
await loadSomething();
|
||||
} catch (cause) {
|
||||
throw new ReactPackerError(
|
||||
REACT_PACKER_CODES.LOAD_FAILED,
|
||||
"react-packer.load-failed",
|
||||
{ cause }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { REACT_PACKER_CODES, ReactPackerError } from "@suaveplan/react-packer";
|
||||
|
||||
function onError(value: unknown): string {
|
||||
if (ReactPackerError.is(value)) {
|
||||
switch (value.code) {
|
||||
case REACT_PACKER_CODES.LOAD_FAILED:
|
||||
return "Could not unpack the resource.";
|
||||
case REACT_PACKER_CODES.FONT_LOAD_FAILED:
|
||||
return "Font failed to load; falling back to system font.";
|
||||
case REACT_PACKER_CODES.ABORTED:
|
||||
return "Cancelled.";
|
||||
default:
|
||||
return "Unexpected error.";
|
||||
}
|
||||
}
|
||||
return "Unknown error.";
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `REACT_PACKER_CODES` — `Readonly<{ LOAD_FAILED, FONT_LOAD_FAILED, ABORTED }>`
|
||||
registered via `@suaveplan/error`'s `defineCodes("REACT_PACKER", ...)`.
|
||||
- `ReactPackerErrorCode` — union of the values of `REACT_PACKER_CODES`.
|
||||
- `ReactPackerError(code, messageKey, options?)` — constructor accepts
|
||||
the registered code, an i18n message key, and the standard
|
||||
`SuaveplanErrorOptions` (including `cause` and `params`).
|
||||
- `ReactPackerError.is(value: unknown): value is ReactPackerError` —
|
||||
returns `true` only when `value` is a real `ReactPackerError` carrying
|
||||
the brand symbol.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The brand `Symbol` is module-local, so passing a forged object with
|
||||
the same shape will not satisfy `is(...)`. This is intentional —
|
||||
duck-typed substitutes break OTel reporting and cause chains.
|
||||
- `SuaveplanError` already pins `.code` and `.message` (resolved from
|
||||
the messageKey) as read-only fields; subclasses do not need to
|
||||
redeclare them.
|
||||
- The brand assignment in the constructor is what makes `super(code, ...)`
|
||||
safe — TypeScript's definite-assignment analysis is satisfied by the
|
||||
`public readonly [BRAND]: true` field plus the constructor write.
|
||||
@@ -0,0 +1,81 @@
|
||||
# `usePackedAudio` — audio-tuned packed-resource hook
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`usePackedAudio` is the audio analogue of `usePackedImage`. It binds a
|
||||
packed audio asset URL to a React component's lifecycle, defaults to
|
||||
caching the underlying bytes (audio cues are typically re-triggered),
|
||||
and narrows the manager's MIME allow-list to `audio/`-prefixed types so
|
||||
misconfigured archives serving non-audio bytes fail fast at the loader
|
||||
rather than at `<audio>` decode time.
|
||||
|
||||
The return shape matches `UsePackedResourceReturn` so callers swap
|
||||
between variant hooks without touching downstream rendering code.
|
||||
|
||||
## Features
|
||||
|
||||
- `cache: true` default; opt out per call.
|
||||
- `allowedMimePrefixes: ["audio/"]` default.
|
||||
- Forwards `verifyKey`, `verifyChecksum`, and `signal` to the underlying
|
||||
generic hook.
|
||||
- Identical return shape to `usePackedResource`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { usePackedAudio } from "@suaveplan/react-packer";
|
||||
|
||||
function CueButton({ cue }: { cue: string | null }) {
|
||||
const { url, loading } = usePackedAudio(cue);
|
||||
if (loading || url === null) return <button disabled>…</button>;
|
||||
return (
|
||||
<button type="button" onClick={() => new Audio(url).play()}>
|
||||
Play
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { usePackedAudio } from "@suaveplan/react-packer";
|
||||
|
||||
function SignedCue({
|
||||
cue,
|
||||
verifyKey,
|
||||
}: {
|
||||
cue: string;
|
||||
verifyKey: Uint8Array;
|
||||
}) {
|
||||
const { url, error, reload } = usePackedAudio(cue, {
|
||||
verifyChecksum: true,
|
||||
verifyKey,
|
||||
allowedMimePrefixes: ["audio/ogg", "audio/webm"],
|
||||
});
|
||||
if (error !== null) return <button onClick={reload}>Retry</button>;
|
||||
return url !== null ? <audio controls src={url} /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `usePackedAudio(packedUrl, options?): UsePackedResourceReturn`
|
||||
- Defaults differ from `usePackedResource`:
|
||||
- `cache` defaults to `true`.
|
||||
- `allowedMimePrefixes` defaults to `["audio/"]`.
|
||||
- All other options pass through.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Explicit caller options always win. Passing `{ cache: false }`
|
||||
disables caching for that hook instance; passing
|
||||
`{ allowedMimePrefixes: [] }` opts out of MIME enforcement entirely.
|
||||
- The default MIME prefix is checked against the response's
|
||||
`Content-Type` by the loader; the hook itself does not parse MIME.
|
||||
- Audio decoding remains the consumer's responsibility — the hook only
|
||||
produces the Blob URL.
|
||||
@@ -0,0 +1,122 @@
|
||||
# `usePackedFont` — font-tuned packed-resource hook
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`usePackedFont` loads a packed font asset and registers it with the
|
||||
document so subsequent rules referencing the supplied `font-family`
|
||||
render with the correct face. The hook builds on `usePackedResource`
|
||||
for the load lifecycle and adds:
|
||||
|
||||
1. `@font-face` rule injection — a `<style id="packed-font-<family>">`
|
||||
element is appended to `document.head` once the Blob URL is ready.
|
||||
2. `document.fonts.load("1em <family>")` waiting — the hook exposes a
|
||||
`fontLoaded` flag that flips to `true` once the browser reports the
|
||||
face as loaded, so consumers can opt into FOIT/FOUT strategies.
|
||||
3. Cleanup — the injected style element is removed when the component
|
||||
unmounts or the font family changes.
|
||||
|
||||
## Features
|
||||
|
||||
- `cache: true` default with a font-friendly MIME allow-list covering
|
||||
`font/`, `application/font*`, and `application/octet-stream`.
|
||||
- Idempotent style injection keyed by `packed-font-<family>` — multiple
|
||||
components mounting the same family share the rule.
|
||||
- `fontLoaded` flag wired to `document.fonts.load`, with a safe
|
||||
fallback when the document or the font loading API is unavailable
|
||||
(SSR, Node tests).
|
||||
- Failure to load a font surfaces as
|
||||
`ReactPackerError(FONT_LOAD_FAILED)`; the underlying byte load still
|
||||
exposes its own errors through `result.error` when relevant.
|
||||
- Strict TS: no `any` casts, no implicit `document` access — every
|
||||
global read goes through a typed adapter so non-DOM environments
|
||||
return clean defaults.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { usePackedFont } from "@suaveplan/react-packer";
|
||||
|
||||
function Heading({ src }: { src: string }) {
|
||||
const { fontLoaded } = usePackedFont(src, "BodyText");
|
||||
return (
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: fontLoaded ? "BodyText" : "system-ui",
|
||||
}}
|
||||
>
|
||||
Hello
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { usePackedFont } from "@suaveplan/react-packer";
|
||||
|
||||
function SignedHeading({
|
||||
src,
|
||||
family,
|
||||
key,
|
||||
}: {
|
||||
src: string;
|
||||
family: string;
|
||||
key: Uint8Array;
|
||||
}) {
|
||||
const { fontLoaded, error, reload } = usePackedFont(src, family, {
|
||||
verifyChecksum: true,
|
||||
verifyKey: key,
|
||||
});
|
||||
if (error !== null)
|
||||
return (
|
||||
<button onClick={reload}>Retry ({error.code})</button>
|
||||
);
|
||||
return (
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: fontLoaded ? family : "system-ui",
|
||||
opacity: fontLoaded ? 1 : 0.6,
|
||||
transition: "opacity 120ms ease",
|
||||
}}
|
||||
>
|
||||
Hello
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `usePackedFont(packedUrl, fontFamily?, options?): UsePackedFontReturn`
|
||||
- `packedUrl: string | null | undefined` — the packed asset URL.
|
||||
- `fontFamily?: string` — the family name to use in the injected rule.
|
||||
When omitted, no `@font-face` rule is injected and `fontLoaded`
|
||||
remains `false`, but the underlying Blob URL is still produced.
|
||||
- `options: UsePackedResourceOptions` — forwarded to `usePackedResource`
|
||||
with font-tuned defaults overlaid (`cache: true`, font-friendly MIME
|
||||
allow-list).
|
||||
- Returns `UsePackedFontReturn` — `UsePackedResourceReturn` plus
|
||||
`fontLoaded: boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The style element is keyed by `packed-font-<family>`. Mounting two
|
||||
hooks with the same family results in a single style element shared
|
||||
across them; the second mount rewrites `textContent` with the same
|
||||
rule, which is idempotent at the CSSOM level.
|
||||
- `document.fonts.load(...)` failures (browser-specific, e.g. corrupt
|
||||
font bytes) become `ReactPackerError(FONT_LOAD_FAILED)` with the
|
||||
original rejection on `.cause` and surface on `result.error`,
|
||||
shadowing any prior byte-load error from the base hook.
|
||||
- In environments without `document` (SSR, Node tests without a DOM)
|
||||
the hook still works at the byte level — `fontLoaded` simply stays
|
||||
`false`.
|
||||
- The hook deliberately does not call `document.fonts.add(face)` —
|
||||
injecting `@font-face` via stylesheet is what the browser will pick
|
||||
up anyway, and avoiding the `FontFace` constructor keeps the hook
|
||||
portable to environments where that constructor is absent.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `usePackedImage` — image-tuned packed-resource hook
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`usePackedImage` is a thin wrapper around `usePackedResource` that
|
||||
pre-fills sensible defaults for image assets: caching is enabled (images
|
||||
tend to be re-used across renders) and the manager's MIME allow-list is
|
||||
narrowed to `image/`-prefixed types so a misconfigured archive serving
|
||||
HTML or JavaScript under a virtual image URL fails fast at the loader
|
||||
boundary rather than the `<img>` tag.
|
||||
|
||||
The hook returns the same `UsePackedResourceReturn` shape as the generic
|
||||
hook, so it is drop-in for any component that already consumes
|
||||
`usePackedResource`.
|
||||
|
||||
## Features
|
||||
|
||||
- `cache: true` default — caller can opt out with `{ cache: false }`.
|
||||
- `allowedMimePrefixes: ["image/"]` default — caller can broaden or
|
||||
narrow.
|
||||
- Forwards `verifyKey`, `verifyChecksum`, and `signal` untouched.
|
||||
- Identical return shape to `usePackedResource` for trivial swapping.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { usePackedImage } from "@suaveplan/react-packer";
|
||||
|
||||
function Avatar({ src }: { src: string | null }) {
|
||||
const { url, loading } = usePackedImage(src);
|
||||
if (loading) return <div className="avatar avatar--loading" />;
|
||||
return url !== null ? <img alt="" className="avatar" src={url} /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { usePackedImage } from "@suaveplan/react-packer";
|
||||
|
||||
function SignedAvatar({
|
||||
src,
|
||||
key,
|
||||
}: {
|
||||
src: string;
|
||||
key: Uint8Array;
|
||||
}) {
|
||||
const { url, error } = usePackedImage(src, {
|
||||
verifyChecksum: true,
|
||||
verifyKey: key,
|
||||
allowedMimePrefixes: ["image/avif", "image/webp"],
|
||||
});
|
||||
if (error !== null) return <span>{error.code}</span>;
|
||||
return url !== null ? <img alt="" src={url} /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `usePackedImage(packedUrl, options?): UsePackedResourceReturn`
|
||||
- Behaviour is identical to `usePackedResource` except defaults differ:
|
||||
- `cache` defaults to `true`.
|
||||
- `allowedMimePrefixes` defaults to `["image/"]`.
|
||||
- All other options pass through unchanged.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Defaults apply only when the caller omits the key — explicit `false`
|
||||
or an empty array opt out of caching / MIME enforcement respectively.
|
||||
- The MIME allow-list is honoured by the manager's loader, not by the
|
||||
hook itself; non-image archives surface as
|
||||
`ReactPackerError(LOAD_FAILED)` with the underlying
|
||||
`PackerBrowserError(UNSUPPORTED_RESOURCE_TYPE)` on `.cause`.
|
||||
- The constant `IMAGE_MIME_PREFIXES` is module-local and frozen via
|
||||
`ReadonlyArray` typing so consumers cannot mutate the default.
|
||||
@@ -0,0 +1,121 @@
|
||||
# `usePackedResource` — generic React hook for `@suaveplan/packer-browser`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`usePackedResource` is the foundation hook of `@suaveplan/react-packer`.
|
||||
It binds a packed-archive virtual URL (e.g. `/packed/logo.bundle.asset`)
|
||||
to a React component's lifecycle, loading the underlying bytes through
|
||||
the active `PackedResourceManager`, exposing the resulting Blob URL on
|
||||
success, and releasing the cached entry on unmount.
|
||||
|
||||
The hook is intentionally low-level — variant hooks
|
||||
(`usePackedImage`, `usePackedAudio`, `usePackedVideo`, `usePackedFont`)
|
||||
delegate to it with media-tuned defaults. Consumers can also use the
|
||||
generic hook directly when they need full control over caching,
|
||||
checksum verification, signature keys, or MIME allow-lists.
|
||||
|
||||
## Features
|
||||
|
||||
- Lifecycle-correct: `idle` → `loading` → `success` / `error`, with
|
||||
every state transition guarded by a `mounted` ref so unmounted
|
||||
components never `setState`.
|
||||
- `AbortSignal` propagation: the hook owns an internal `AbortController`,
|
||||
merges any caller-supplied signal into it, and aborts on unmount,
|
||||
`packedUrl` change, or `reload()`.
|
||||
- Cache awareness: `manager.release(url, verifyKey)` runs in cleanup so
|
||||
the cache can evict entries no React component still references.
|
||||
- Error normalisation: every thrown value is wrapped as
|
||||
`ReactPackerError` with a stable code (`LOAD_FAILED` or `ABORTED`)
|
||||
and the original on `.cause`.
|
||||
- Pluggable: resolves its `PackedResourceManager` via
|
||||
`usePackedResourceManager()`, so the same hook works against a
|
||||
provider-supplied manager (tests, multi-tenant apps) or the singleton
|
||||
(zero-config consumers).
|
||||
- Reload: the returned `reload()` thunk increments an internal counter
|
||||
that re-runs the effect against a fresh `AbortController`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { usePackedResource } from "@suaveplan/react-packer";
|
||||
|
||||
function Asset({ src }: { src: string }) {
|
||||
const { url, loading, error } = usePackedResource(src);
|
||||
if (loading) return <span>Loading…</span>;
|
||||
if (error !== null) return <span>{error.message}</span>;
|
||||
return url !== null ? <img alt="" src={url} /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { usePackedResource } from "@suaveplan/react-packer";
|
||||
|
||||
function VerifiedAsset({
|
||||
src,
|
||||
verifyKey,
|
||||
signal,
|
||||
}: {
|
||||
src: string | null;
|
||||
verifyKey: Uint8Array;
|
||||
signal: AbortSignal;
|
||||
}) {
|
||||
const { url, status, error, reload } = usePackedResource(src, {
|
||||
cache: false,
|
||||
verifyChecksum: true,
|
||||
verifyKey,
|
||||
signal,
|
||||
allowedMimePrefixes: ["application/octet-stream"],
|
||||
});
|
||||
|
||||
if (status === "idle") return <button onClick={reload}>Load</button>;
|
||||
if (status === "loading") return <span>Verifying…</span>;
|
||||
if (status === "error") {
|
||||
return (
|
||||
<>
|
||||
<span>{error?.code}</span>
|
||||
<button onClick={reload}>Retry</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return url !== null ? <a href={url}>Open</a> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `usePackedResource(packedUrl, options?): UsePackedResourceReturn`
|
||||
- `packedUrl: string | null | undefined` — the packed asset URL. `null`
|
||||
or `undefined` keeps the hook in the `idle` state and does not invoke
|
||||
the manager.
|
||||
- `options: UsePackedResourceOptions`
|
||||
- `cache?: boolean` — opt out of the manager cache for this load.
|
||||
- `verifyChecksum?: boolean` — gate the unpack on checksum match.
|
||||
- `verifyKey?: Uint8Array` — Ed25519 verify key for signed archives.
|
||||
- `signal?: AbortSignal` — caller-supplied cancellation signal.
|
||||
- `allowedMimePrefixes?: ReadonlyArray<string>` — MIME allow-list.
|
||||
- Returns `{ url, loading, error, status, reload }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The mounted ref pattern is the same one used elsewhere
|
||||
(`drive-react-sharing/use-sharing`); we avoid the strict-mode double
|
||||
invoke trap by running the cleanup callback unconditionally.
|
||||
- The `optionsRef` indirection keeps the `useEffect` dependency list
|
||||
stable: only `packedUrl`, `verifyKey`, the external `signal`, the
|
||||
`reloadCounter`, the active `manager`, and the active `resolver`
|
||||
participate in change detection. Inline option-object literals from
|
||||
callers therefore do not retrigger the effect.
|
||||
- `error.code` is `REACT_PACKER_ABORTED` whenever the underlying
|
||||
rejection is an `AbortError` *or* a `PackerBrowserError` whose code
|
||||
is `PACKER_BROWSER_ABORTED` — both reach the catch arm. Otherwise the
|
||||
failure surfaces as `LOAD_FAILED` with the original error on
|
||||
`.cause`.
|
||||
- `manager.release` is called on every cleanup even when `cache: false`
|
||||
was supplied; the manager treats unknown keys as no-ops, so the call
|
||||
is safe and lets the cache rebalance refcounts on remount.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `usePackedVideo` — video-tuned packed-resource hook
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`usePackedVideo` binds a packed video asset URL to a React component's
|
||||
lifecycle. Unlike the image and audio variants, video defaults to
|
||||
`cache: false` because video assets are typically large enough that
|
||||
caching them in a Blob LRU dominates the cache budget and starves other
|
||||
asset classes; the underlying manager will still de-duplicate concurrent
|
||||
loads of the same URL via its in-flight Promise map.
|
||||
|
||||
The hook narrows the MIME allow-list to `video/`-prefixed types so
|
||||
misconfigured archives fail fast at the loader rather than at
|
||||
`<video>` decode time.
|
||||
|
||||
## Features
|
||||
|
||||
- `cache: false` default — video is opted out of the LRU by default.
|
||||
- `allowedMimePrefixes: ["video/"]` default.
|
||||
- Forwards `verifyKey`, `verifyChecksum`, and `signal`.
|
||||
- Caller can re-enable caching with `{ cache: true }` when an explicit
|
||||
budget warrants it (e.g. a short looping background video).
|
||||
- Identical return shape to `usePackedResource`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { usePackedVideo } from "@suaveplan/react-packer";
|
||||
|
||||
function Trailer({ src }: { src: string | null }) {
|
||||
const { url, loading } = usePackedVideo(src);
|
||||
if (loading) return <div className="trailer trailer--loading" />;
|
||||
return url !== null ? <video controls src={url} /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { usePackedVideo } from "@suaveplan/react-packer";
|
||||
|
||||
function LoopingBackground({ src }: { src: string }) {
|
||||
// Loops are short — opting back into caching for this one component.
|
||||
const { url } = usePackedVideo(src, {
|
||||
cache: true,
|
||||
allowedMimePrefixes: ["video/webm"],
|
||||
});
|
||||
return url !== null ? (
|
||||
<video autoPlay loop muted playsInline src={url} />
|
||||
) : null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `usePackedVideo(packedUrl, options?): UsePackedResourceReturn`
|
||||
- Defaults differ from `usePackedResource`:
|
||||
- `cache` defaults to `false`.
|
||||
- `allowedMimePrefixes` defaults to `["video/"]`.
|
||||
- All other options pass through.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `cache: false` means the manager will load the bytes and create a
|
||||
Blob URL, but will not insert the entry into its LRU. The Blob URL
|
||||
remains valid until `release()` runs in the hook's cleanup; the
|
||||
underlying Blob is then eligible for GC.
|
||||
- Consumers who play many videos sequentially should consider the
|
||||
default — caching one then evicting it on the next load is the usual
|
||||
pattern; the manager's release semantics make that explicit and
|
||||
predictable.
|
||||
- The MIME allow-list does not constrain the codec, only the wrapper.
|
||||
If you need codec gating, do it in the consumer with a probe.
|
||||
@@ -0,0 +1,105 @@
|
||||
# `PackedResourceProvider` — React context for `PackedResourceManager` DI
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`PackedResourceProvider` lets a React tree explicitly inject a
|
||||
`PackedResourceManager` from `@suaveplan/packer-browser` so every
|
||||
`usePackedResource*` hook in its subtree consumes that manager instead
|
||||
of the module-level singleton. This makes lifecycle and configuration
|
||||
explicit: tests construct a fresh manager with an injected `fetch`,
|
||||
storybook stories vary cache size and signature-verification, SSR
|
||||
boundaries swap in a no-op manager, and applications that ship multiple
|
||||
panes each with their own asset budgets isolate their caches.
|
||||
|
||||
The provider is optional. Hooks rendered outside any provider transparently
|
||||
fall back to `packedResourceManager` from
|
||||
`@suaveplan/packer-browser/singleton`, preserving the upstream
|
||||
zero-config experience for consumers that don't need DI.
|
||||
|
||||
## Features
|
||||
|
||||
- React 19 context with a `null` default and a stable
|
||||
`useContext`-driven resolution path.
|
||||
- Memoised value object so child consumers don't re-render when the
|
||||
parent re-renders without changing manager identity or resolver.
|
||||
- Companion hook `usePackedResourceManager()` that resolves the active
|
||||
manager — context wins, singleton fallback when no provider exists.
|
||||
- Optional `messageResolver` field on the context value plumbs an
|
||||
`@suaveplan/types` `MessageResolver` through to every
|
||||
`ReactPackerError` raised by hooks in the subtree.
|
||||
- Nesting: child providers shadow parent providers normally.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { createPackedResourceManager } from "@suaveplan/packer-browser";
|
||||
import { PackedResourceProvider, usePackedImage } from "@suaveplan/react-packer";
|
||||
|
||||
const manager = createPackedResourceManager();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PackedResourceProvider value={{ manager }}>
|
||||
<Logo />
|
||||
</PackedResourceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Logo() {
|
||||
const { url, loading } = usePackedImage("/assets/logo.packed");
|
||||
if (loading) return null;
|
||||
return url !== null ? <img src={url} alt="" /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { createPackedResourceManager } from "@suaveplan/packer-browser";
|
||||
import { PackedResourceProvider } from "@suaveplan/react-packer";
|
||||
import type { MessageResolver } from "@suaveplan/types";
|
||||
|
||||
const resolver: MessageResolver = (key, params) =>
|
||||
`[${key}]${JSON.stringify(params ?? {})}`;
|
||||
|
||||
const manager = createPackedResourceManager({
|
||||
maxCacheSize: 50,
|
||||
fetch: async (url) => new Response(/* ... */),
|
||||
});
|
||||
|
||||
export function Tree({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<PackedResourceProvider value={{ manager, messageResolver: resolver }}>
|
||||
{children}
|
||||
</PackedResourceProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PackedResourceProvider({ value, children })` — React component.
|
||||
`value` is `{ manager: PackedResourceManager; messageResolver?: MessageResolver }`.
|
||||
- `usePackedResourceManager(): PackedResourceManager` — resolves from
|
||||
context or falls back to the singleton.
|
||||
- `usePackedMessageResolver(): MessageResolver | undefined` — returns
|
||||
the provider-supplied resolver, or `undefined` outside a provider.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The fallback singleton is imported statically from
|
||||
`@suaveplan/packer-browser/singleton`. Importing this package will
|
||||
therefore eagerly construct the singleton; teams that want maximum
|
||||
tree-shaking should always mount a provider and skip the singleton
|
||||
module entirely (which they get for free by configuring their bundler
|
||||
to externalise unused subpaths).
|
||||
- The provider value is wrapped in `useMemo` keyed on the `manager` and
|
||||
`messageResolver` references, so passing a new `value={{ manager }}`
|
||||
literal on every render is fine — descendants do not see a change.
|
||||
- Hooks call `useContext(PackedResourceContext)` once per render and
|
||||
never call into the singleton when a provider is mounted, so test
|
||||
isolation is exact.
|
||||
@@ -0,0 +1,85 @@
|
||||
# `react-packer.types` — shared types for `@suaveplan/react-packer`
|
||||
|
||||
## Purpose
|
||||
|
||||
This module defines the small set of TypeScript interfaces that the
|
||||
React-tier packer package exposes on its public surface. Keeping the
|
||||
types in a dedicated module — rather than co-located with each
|
||||
implementation file — makes it easy to import the shapes without
|
||||
pulling React into a non-React context (e.g. a type-only re-export to a
|
||||
shared declarations file in an application).
|
||||
|
||||
These types are deliberately structural: any object satisfying the
|
||||
shape works, so consumers can substitute their own equivalents at the
|
||||
test boundary without coupling to suaveplan classes.
|
||||
|
||||
## Features
|
||||
|
||||
- `PackedResourceStatus` — the four-state lifecycle string union
|
||||
(`"idle" | "loading" | "success" | "error"`).
|
||||
- `UsePackedResourceOptions` — the per-call option bag accepted by
|
||||
every hook in the package.
|
||||
- `UsePackedResourceReturn` — the common return shape exposing `url`,
|
||||
`loading`, `error`, `status`, `reload`.
|
||||
- `UsePackedFontReturn` — `UsePackedResourceReturn` extended with
|
||||
`fontLoaded: boolean`.
|
||||
- `PackedResourceProviderValue` — the context value supplied by the
|
||||
`PackedResourceProvider` (manager + optional resolver).
|
||||
- Re-exports `PackedResourceLoadCallOptions` and
|
||||
`PackedResourceManager` from `@suaveplan/packer-browser` so consumers
|
||||
can type their own DI bootstrap code without dual-importing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type {
|
||||
UsePackedResourceOptions,
|
||||
UsePackedResourceReturn,
|
||||
} from "@suaveplan/react-packer";
|
||||
|
||||
function withDefaults(opts: UsePackedResourceOptions): UsePackedResourceOptions {
|
||||
return { cache: true, ...opts };
|
||||
}
|
||||
|
||||
function reportStatus(result: UsePackedResourceReturn): string {
|
||||
return result.status === "loading" ? "Loading…" : (result.url ?? "");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type {
|
||||
PackedResourceProviderValue,
|
||||
UsePackedFontReturn,
|
||||
} from "@suaveplan/react-packer";
|
||||
import { createPackedResourceManager } from "@suaveplan/packer-browser";
|
||||
|
||||
const value: PackedResourceProviderValue = {
|
||||
manager: createPackedResourceManager(),
|
||||
messageResolver: (key) => key,
|
||||
};
|
||||
|
||||
function isFontReady(result: UsePackedFontReturn): boolean {
|
||||
return result.status === "success" && result.fontLoaded;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PackedResourceStatus = "idle" | "loading" | "success" | "error"`
|
||||
- `UsePackedResourceOptions` — `{ cache?, verifyChecksum?, verifyKey?, signal?, allowedMimePrefixes? }`
|
||||
- `UsePackedResourceReturn` — `{ url, loading, error, status, reload }`
|
||||
- `UsePackedFontReturn` — `UsePackedResourceReturn & { fontLoaded: boolean }`
|
||||
- `PackedResourceProviderValue` — `{ manager, messageResolver? }`
|
||||
- Re-exports: `PackedResourceLoadCallOptions`, `PackedResourceManager`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Every property is `readonly` on the return interfaces so React
|
||||
consumers cannot accidentally mutate state outside the hook.
|
||||
- The status union is closed; adding a new state requires a new
|
||||
spec change and a downstream major-version bump.
|
||||
- The provider value's `manager` and `messageResolver` are both
|
||||
reference-tracked by the provider's `useMemo`, so descendants only
|
||||
re-render when those identities change.
|
||||
Reference in New Issue
Block a user