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

4.0 KiB

Add MutableUriPluginRegistrar to @suaveplan/audio-ir-core

Package: @suaveplan/audio-ir-core Status: PROPOSED Type: feature

Why

Stage-3 ship of @suaveplan/audio-ir-room-plugin-pool discovered that StorageUriResolver is frozen at construction — there is no resolver.use(plugin) after construction. Pool worked around this by introducing a tiny collector interface, locally:

interface MutableUriPluginRegistrar {
  use(plugin: UriPlugin): void;
}

Consumers pre-construct a collector (typically const ps: UriPlugin[] = []; const target = { use: (p) => ps.push(p) };), call every IR content package's register(registry, target), then pass ps to createStorageUriResolver(...) once. This is the canonical pattern for the whole audio-ir-{family}-plugin-<name> family.

audio-ir-room-plugin-club shipped with a different shape (separate createPlugin() export + register(registry, resolver) with a structural resolver.hasScheme("bundle") check). Two divergent shapes for the same job will balloon as we add more content packages, so the registrar interface must live in one place: this foundation package. A small createUriPluginCollector() helper makes the pattern obvious at call sites.

What changes

  • ADDED: MutableUriPluginRegistrar interface — { use(plugin: UriPlugin): void }. Exported from @suaveplan/audio-ir-core (and re-exported from the root barrel).
  • ADDED: createUriPluginCollector() factory returning MutableUriPluginRegistrar & { readonly plugins: readonly UriPlugin[] }. Wraps an internal array; use() pushes, plugins exposes a frozen snapshot for createStorageUriResolver.
  • ADDED: dep on @suaveplan/storage-uri for the UriPlugin type import (currently absent — audio-ir-core has no storage-uri dep today; this is the first edge). Universal-tier compatible.
  • MODIFIED: package README.md — new ## Composing IR content packages section documenting the collector pattern.

Out of scope

  • Refactoring audio-ir-room-plugin-club and -pool to use the new interface — those are separate per-package changes (align-audio-ir-room-plugin-{club,pool}-register-signature) per AGENTS §193.
  • Any change to the IrRegistry shape, IrManifest schema, or error codes.

Source mapping

Genesis-original — derived from the as-shipped MutableUriPluginRegistrar interface in packages/audio-ir/audio-ir-room-plugin-pool/src/register/register.ts. The interface body moves into audio-ir-core; pool then imports it.

Requirements with no source line

Requirement Justification
every requirement Promotion of an in-tree pattern to its foundation

Verification approach

  • src/registrar/registrar.test.ts — createUriPluginCollector() returns an object satisfying MutableUriPluginRegistrar; use() pushes; plugins is a frozen array snapshot; multiple use() calls preserve order.
  • src/registrar/registrar.spec-conformance.test.ts — type-level: any audio-ir-{family}-plugin-<name> register(registry, target, options?) accepts the collector verbatim (asserted via expectTypeOf).

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/audio-ir-core to enumerate. Today's dependents:

  • @suaveplan/audio-ir-loader — [x] no action needed (reason: loader doesn't compose URI plugins at register time, only consumes them via a pre-built resolver; the new interface doesn't intersect its surface)
  • @suaveplan/audio-ir-room-plugin-club — [ ] tests run / [ ] docs reviewed / [ ] no action needed (separate align-audio-ir-room-plugin-club-register-signature change refactors it to use this new interface)
  • @suaveplan/audio-ir-room-plugin-pool — [ ] tests run / [ ] docs reviewed / [ ] no action needed (separate align-audio-ir-room-plugin-pool-register-signature change switches its local-copy interface to import this one)

Both content-package changes block on this one. They WILL be run as part of their own gates; this change ships independently.