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

59 lines
4.0 KiB
Markdown

# 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:
```ts
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.