Files
2026-05-27 23:29:53 +01:00

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, and manager.load is 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 Blob url, and clears any prior error

Scenario: pre-aborted signal short-circuits with REACT_PACKER_ABORTED

  • WHEN the caller passes a pre-aborted AbortSignal
  • THEN the hook surfaces status = "error" with error.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 no setState warning 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/"] and cache: 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/"] and cache: 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: false and allowedMimePrefixes: ["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.load resolves
  • THEN a <style id="packed-font-<family>"> element exists in document.head, the rule's src URL matches the loaded Blob URL, and fontLoaded === 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 and fontLoaded === false, but the underlying Blob url is populated

Scenario: document.fonts.load rejection surfaces FONT_LOAD_FAILED

  • WHEN document.fonts.load rejects with an Error
  • THEN error.code === "REACT_PACKER_FONT_LOAD_FAILED" and fontLoaded === false

Scenario: unmount removes the injected style element

  • WHEN a font hook unmounts after a successful injection
  • THEN document.getElementById("packed-font-<family>") returns null

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 messageResolver and a load fails
  • THEN the resulting ReactPackerError.messageKey is 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 on result.error.code

Scenario: is(value) is a safe brand check

  • WHEN an arbitrary unknown value is passed to ReactPackerError.is
  • THEN the result is true iff the value is a real ReactPackerError instance 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"