9.9 KiB
Capability Spec Delta: react-packer (add-react-packer)
Package: @suaveplan/react-packer
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: REACT-PACKER-1 — usePackedResource lifecycle
The package SHALL export usePackedResource(packedUrl, options?), a React hook that resolves a PackedResourceManager from context (or the singleton fallback), invokes manager.load(url, opts), and returns { url, loading, error, status, reload }. The hook MUST honour AbortSignal: it merges any caller-supplied signal with its own internal AbortController, aborts on unmount, aborts on packedUrl change, and aborts on reload(). On a null/undefined packedUrl the hook MUST settle on status = "idle" without invoking the manager.
Scenario: nullable packed URL produces idle state
- WHEN the hook is mounted with
packedUrl = null - THEN
status === "idle",url === null,loading === false, andmanager.loadis never called
Scenario: success transitions loading → success
- WHEN the manager resolves a real Blob URL for the given packed URL
- THEN the hook transitions
loading(true) →success, exposes the Bloburl, and clears any priorerror
Scenario: pre-aborted signal short-circuits with REACT_PACKER_ABORTED
- WHEN the caller passes a pre-aborted
AbortSignal - THEN the hook surfaces
status = "error"witherror.code === "REACT_PACKER_ABORTED"and never calls the manager
Scenario: unmount releases the cached resource
- WHEN a successful load completes and the component unmounts
- THEN
manager.release(url, opts.verifyKey)runs exactly once and nosetStatewarning fires
Verified by: src/hooks/use-packed-resource/use-packed-resource.test.tsx::"is idle when packedUrl is null" AND src/hooks/use-packed-resource/use-packed-resource.test.tsx::"transitions loading then success on a real manager" AND src/hooks/use-packed-resource/use-packed-resource.test.tsx::"surfaces REACT_PACKER_ABORTED on a pre-aborted signal" AND src/hooks/use-packed-resource/use-packed-resource.test.tsx::"releases the cached entry on unmount" AND src/hooks/use-packed-resource/use-packed-resource.test.tsx::"reload() re-runs the loader and resets state" AND src/hooks/use-packed-resource/use-packed-resource.test.tsx::"changing packedUrl aborts the prior in-flight load" AND src/hooks/use-packed-resource/use-packed-resource.test.tsx::"wraps a manager failure as REACT_PACKER_LOAD_FAILED"
Requirement: REACT-PACKER-2 — usePackedImage with image-tuned defaults
The package SHALL export usePackedImage(packedUrl, options?) which delegates to usePackedResource with cache: true and allowedMimePrefixes: ["image/"] unless the caller overrides them. The hook MUST preserve the full UsePackedResourceReturn shape.
Scenario: image hook applies image MIME allow-list
- WHEN the hook is mounted and the underlying manager call inspects options
- THEN the call includes
allowedMimePrefixes: ["image/"]andcache: true
Scenario: caller override wins over defaults
- WHEN the caller passes
{ cache: false } - THEN the manager call receives
cache: false
Verified by: src/hooks/use-packed-image/use-packed-image.test.tsx::"loads an image-tuned packed resource" AND src/hooks/use-packed-image/use-packed-image.test.tsx::"caller can override the cache default"
Requirement: REACT-PACKER-3 — usePackedAudio with audio-tuned defaults
The package SHALL export usePackedAudio(packedUrl, options?) which delegates to usePackedResource with cache: true and allowedMimePrefixes: ["audio/"] unless overridden.
Scenario: audio hook applies audio MIME allow-list
- WHEN the hook resolves a packed URL
- THEN the manager receives
allowedMimePrefixes: ["audio/"]andcache: true
Verified by: src/hooks/use-packed-audio/use-packed-audio.test.tsx::"loads an audio-tuned packed resource" AND src/hooks/use-packed-audio/use-packed-audio.test.tsx::"forwards caller-supplied verifyKey to the manager"
Requirement: REACT-PACKER-4 — usePackedVideo with video-tuned defaults
The package SHALL export usePackedVideo(packedUrl, options?) which delegates to usePackedResource with cache: false (videos are large and dominate cache budgets) and allowedMimePrefixes: ["video/"] unless overridden.
Scenario: video hook opts out of caching by default
- WHEN the hook resolves a packed URL with no caller options
- THEN the manager receives
cache: falseandallowedMimePrefixes: ["video/"]
Verified by: src/hooks/use-packed-video/use-packed-video.test.tsx::"loads a video-tuned packed resource without caching" AND src/hooks/use-packed-video/use-packed-video.test.tsx::"caller can opt back into caching"
Requirement: REACT-PACKER-5 — usePackedFont injects @font-face
The package SHALL export usePackedFont(packedUrl, fontFamily?, options?) which delegates to usePackedResource with font-tuned defaults, injects a @font-face stylesheet (<style id="packed-font-<family>">) into document.head, waits for document.fonts.load("1em <family>"), and exposes fontLoaded: boolean alongside the standard return. Unmount MUST remove the injected <style> element. When document.fonts.load rejects, the hook MUST surface error.code === "REACT_PACKER_FONT_LOAD_FAILED" and leave fontLoaded = false.
Scenario: successful font load injects @font-face and flips fontLoaded
- WHEN the manager resolves the font Blob URL and
document.fonts.loadresolves - THEN a
<style id="packed-font-<family>">element exists indocument.head, the rule'ssrcURL matches the loaded Blob URL, andfontLoaded === true
Scenario: font-family omitted skips injection but still loads the bytes
- WHEN the hook is mounted with
fontFamily = undefined - THEN no
<style>element is injected andfontLoaded === false, but the underlying Bloburlis populated
Scenario: document.fonts.load rejection surfaces FONT_LOAD_FAILED
- WHEN
document.fonts.loadrejects with anError - THEN
error.code === "REACT_PACKER_FONT_LOAD_FAILED"andfontLoaded === false
Scenario: unmount removes the injected style element
- WHEN a font hook unmounts after a successful injection
- THEN
document.getElementById("packed-font-<family>")returnsnull
Verified by: src/hooks/use-packed-font/use-packed-font.test.tsx::"injects @font-face on successful load" AND src/hooks/use-packed-font/use-packed-font.test.tsx::"omitting fontFamily skips style injection" AND src/hooks/use-packed-font/use-packed-font.test.tsx::"wraps fonts.load rejection as REACT_PACKER_FONT_LOAD_FAILED" AND src/hooks/use-packed-font/use-packed-font.test.tsx::"removes the injected style element on unmount"
Requirement: REACT-PACKER-6 — PackedResourceProvider context
The package SHALL export PackedResourceProvider, a React context provider that supplies a PackedResourceManager and optional MessageResolver to every hook in its subtree. Hooks MUST resolve their manager via useContext(PackedResourceContext) first and consult the optional resolver when constructing ReactPackerError instances.
Scenario: hook uses the provider's manager
- WHEN a hook is rendered inside
<PackedResourceProvider value={{ manager }}> - THEN that hook calls
manager.load(...)rather than the singleton
Scenario: provider supplies a MessageResolver
- WHEN the provider's value supplies a custom
messageResolverand a load fails - THEN the resulting
ReactPackerError.messageKeyis the key the resolver returned
Verified by: src/provider/packed-resource-provider/packed-resource-provider.test.tsx::"hook consumes the provider-supplied manager" AND src/provider/packed-resource-provider/packed-resource-provider.test.tsx::"messageResolver is threaded into raised errors"
Requirement: REACT-PACKER-7 — Stable error hierarchy
The package SHALL export REACT_PACKER_CODES (a defineCodes(...) registry) and ReactPackerError extends SuaveplanError. The registry MUST include LOAD_FAILED, FONT_LOAD_FAILED, and ABORTED. ReactPackerError.is(value) MUST return true only for instances carrying the brand symbol, even across realms.
Scenario: every registered code is reachable from a production path
- WHEN the consumer triggers a load failure, font load rejection, or pre-aborted signal
- THEN each respective
REACT_PACKER_*code is raised by exactly one hook code path and surfaces onresult.error.code
Scenario: is(value) is a safe brand check
- WHEN an arbitrary
unknownvalue is passed toReactPackerError.is - THEN the result is
trueiff the value is a realReactPackerErrorinstance with the brand symbol set
Verified by: src/error-codes/error-codes.test.ts::"registers LOAD_FAILED, FONT_LOAD_FAILED, ABORTED" AND src/error-codes/error-codes.test.ts::"is(value) returns true only for branded instances" AND src/error-codes/error-codes.test.ts::"preserves cause through the SuaveplanError chain"
Requirement: REACT-PACKER-8 — Singleton fallback when no provider is mounted
The package SHALL resolve a working PackedResourceManager even when no <PackedResourceProvider> is mounted. In that case it MUST defer to @suaveplan/packer-browser/singleton so adopters who do not need DI get a zero-config experience.
Scenario: hook outside a provider uses the singleton
- WHEN a hook is rendered with no surrounding provider
- THEN
usePackedResourceManager()returns the module-singleton instance from@suaveplan/packer-browser/singleton
Verified by: src/provider/packed-resource-provider/packed-resource-provider.test.tsx::"falls back to the singleton when no provider is mounted"