Initial Commit
This commit is contained in:
74
packages/media/audio-ir/audio-ir-room-plugin-pool/README.md
Normal file
74
packages/media/audio-ir/audio-ir-room-plugin-pool/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# @suaveplan/audio-ir-room-plugin-pool - Original audio-ir content package: ships the outdoor-swimming-pool room impulse response paired with a https CDN fallback. Universal.
|
||||
|
||||
Original audio-ir content package. Ships a single `room`-family
|
||||
impulse response — `pool-outdoor-swimming-pool` — sourced from the
|
||||
outdoor-swimming-pool capture in the Suaveplan internal media library,
|
||||
paired with a CDN fallback at `https://swch.b-cdn.net/_/audio/ir/room/roompool.wav`.
|
||||
|
||||
Universal; no browser, DOM, or Node-specific APIs in the public
|
||||
surface. The shipped bundle plugin uses `import.meta.url` to locate the
|
||||
in-tarball `files/` directory at runtime.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [manifest](./src/manifest/manifest.md) — Declares the lone `IrManifestEntry` shipped by `@suaveplan/audio-ir-room-plugin-pool`. The entry's `uri` is a `fallback…
|
||||
- [manifest](./src/manifest/manifest.md) — Declares the lone `IrManifestEntry` shipped by `@suaveplan/audio-ir-room-plugin-pool`. The entry's `uri` is a `fallback…
|
||||
- [register](./src/register/register.md) — Single entry point for plugging `@suaveplan/audio-ir-room-plugin-pool` into a consumer's `IrRegistry` + `StorageUriReso…
|
||||
- [register](./src/register/register.md) — Single entry point for plugging `@suaveplan/audio-ir-room-plugin-pool` into a consumer's `IrRegistry` + `StorageUriReso…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Modules
|
||||
|
||||
- `./manifest` — `manifest: IrManifest`, plus the canonical constants
|
||||
(`BUNDLE_PACKAGE_SLUG`, `BUNDLE_URI`, `CDN_URI`, `ENTRY_ID`,
|
||||
`CHANNELS`, `SAMPLE_RATE`).
|
||||
- `./register` — `register(registry, target, options?)` and the
|
||||
re-exported `MutableUriPluginRegistrar` type (sourced from
|
||||
`@suaveplan/audio-ir-core`). Wires the bundle plugin onto a
|
||||
consumer-supplied registrar and calls `registerManifest`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import {
|
||||
createIrRegistry,
|
||||
createUriPluginCollector,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import { createIrLoader } from "@suaveplan/audio-ir-loader";
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { fallbackUriPlugin } from "@suaveplan/storage-uri-plugin-fallback";
|
||||
import { register } from "@suaveplan/audio-ir-room-plugin-pool";
|
||||
|
||||
const registry = createIrRegistry();
|
||||
const collector = createUriPluginCollector();
|
||||
register(registry, collector);
|
||||
|
||||
// Append the consumer's https://-aware plugin via collector.use(...).
|
||||
const innerPlugins = [...collector.plugins];
|
||||
const innerResolver = createStorageUriResolver(innerPlugins);
|
||||
const fb = fallbackUriPlugin({
|
||||
resolver: innerResolver,
|
||||
policy: { errorClass: "any" },
|
||||
});
|
||||
const resolver = createStorageUriResolver([...innerPlugins, fb]);
|
||||
|
||||
const loader = createIrLoader({
|
||||
registry,
|
||||
resolver,
|
||||
decoders: { wav: decodeWav },
|
||||
});
|
||||
const ir = await loader.load("pool-outdoor-swimming-pool");
|
||||
```
|
||||
|
||||
## License & sources
|
||||
|
||||
The bundled WAV (`files/outdoor-swimming-pool.wav`) was copied from the
|
||||
Suaveplan internal media library at
|
||||
`/mnt/c/Users/Administrator/Mountain Duck/74a17879-9780-426d-8bd9-7ed805f53d41/media/_/audio/ir/room/outdoor-swimming-pool.wav`.
|
||||
The CDN copy lives at `https://swch.b-cdn.net/_/audio/ir/room/roompool.wav`.
|
||||
|
||||
License: **All Rights Reserved — Suaveplan internal media library**. Do
|
||||
not redistribute the bundled WAV outside Suaveplan distributions.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "@suaveplan/audio-ir-room-plugin-pool",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "Genesis-original audio-ir content package: ships the outdoor-swimming-pool room impulse response paired with a https CDN fallback. Universal.",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./manifest": {
|
||||
"import": {
|
||||
"types": "./dist/manifest/index.d.ts",
|
||||
"default": "./dist/manifest/index.js"
|
||||
}
|
||||
},
|
||||
"./register": {
|
||||
"import": {
|
||||
"types": "./dist/register/index.d.ts",
|
||||
"default": "./dist/register/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/ tests/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/ tests/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/ tests/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/ tests/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/audio-ir-core": "workspace:^",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"@suaveplan/storage-uri": "workspace:^",
|
||||
"@suaveplan/storage-uri-plugin-bundle": "workspace:^",
|
||||
"@suaveplan/storage-uri-plugin-fallback": "workspace:^",
|
||||
"@suaveplan/types": "workspace:^",
|
||||
"tslib": "catalog:build",
|
||||
"zod": "catalog:forms"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentelemetry/api": "catalog:observability",
|
||||
"@suaveplan/audio-decode-wav": "workspace:^",
|
||||
"@suaveplan/audio-ir-loader": "workspace:^",
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"files",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type { MutableUriPluginRegistrar } from "@suaveplan/audio-ir-core";
|
||||
export {
|
||||
BUNDLE_PACKAGE_SLUG,
|
||||
BUNDLE_URI,
|
||||
CDN_URI,
|
||||
CHANNELS,
|
||||
ENTRY_ID,
|
||||
manifest,
|
||||
SAMPLE_RATE,
|
||||
} from "./manifest/manifest.js";
|
||||
export { type RegisterOptions, register } from "./register/register.js";
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
BUNDLE_PACKAGE_SLUG,
|
||||
BUNDLE_URI,
|
||||
CDN_URI,
|
||||
CHANNELS,
|
||||
ENTRY_ID,
|
||||
manifest,
|
||||
SAMPLE_RATE,
|
||||
} from "./manifest.js";
|
||||
@@ -0,0 +1,89 @@
|
||||
# `manifest` — single-entry `IrManifest` for the outdoor-swimming-pool IR
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Declares the lone `IrManifestEntry` shipped by `@suaveplan/audio-ir-room-plugin-pool`.
|
||||
The entry's `uri` is a `fallback://?u=<bundle>&u=<https>` URI built at module
|
||||
load by `buildFallbackUri` from `@suaveplan/storage-uri-plugin-fallback` —
|
||||
when the consuming resolver has both a `bundle://` plugin and an `https://`
|
||||
plugin wired, the shipped `fallback://` walk hits the in-package WAV first
|
||||
and only reaches for the CDN copy when the local file is absent.
|
||||
|
||||
The manifest also pins the **decoded** `channels` and `sampleRate` of the
|
||||
bundled WAV (`channels = 1`, `sampleRate = 48000`) so the loader's
|
||||
`validateAndFinalize` step can reject any subsequent re-encode that drifts
|
||||
from those values. Both numbers were obtained by feeding
|
||||
`files/outdoor-swimming-pool.wav` through `decodeWav` once at
|
||||
manifest-authoring time.
|
||||
|
||||
## Features
|
||||
|
||||
- Round-trips through `IrManifestSchema` at module load — any drift in the
|
||||
schema fails the build before `dist/` is emitted.
|
||||
- `BUNDLE_PACKAGE_SLUG`, `BUNDLE_URI`, `CDN_URI`, `ENTRY_ID`, `CHANNELS`,
|
||||
`SAMPLE_RATE` exported as named `const` literals so consumers can refer
|
||||
to them without re-deriving from `manifest.entries[0]`.
|
||||
- Frozen at runtime (`IrManifestSchema` applies `Object.freeze` to the
|
||||
parsed manifest + every entry).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { manifest } from "@suaveplan/audio-ir-room-plugin-pool/manifest";
|
||||
|
||||
console.log(manifest.packageSlug); // "pool"
|
||||
console.log(manifest.entries[0].id); // "pool-outdoor-swimming-pool"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
BUNDLE_URI,
|
||||
CDN_URI,
|
||||
ENTRY_ID,
|
||||
manifest,
|
||||
} from "@suaveplan/audio-ir-room-plugin-pool/manifest";
|
||||
import { parseFallbackUri } from "@suaveplan/storage-uri-plugin-fallback";
|
||||
|
||||
const entry = manifest.entries[0];
|
||||
const parsed = parseFallbackUri(entry.uri);
|
||||
console.log(parsed.candidates); // [BUNDLE_URI, CDN_URI]
|
||||
console.log(entry.id === ENTRY_ID); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| --- | --- | --- |
|
||||
| `manifest` | `IrManifest` | Frozen single-entry manifest. |
|
||||
| `BUNDLE_PACKAGE_SLUG` | `"audio-ir-room-plugin-pool"` | Slug expected by the bundle plugin. |
|
||||
| `BUNDLE_URI` | string literal | Bundle-local URI for the WAV. |
|
||||
| `CDN_URI` | string literal | HTTPS CDN URI for the WAV. |
|
||||
| `ENTRY_ID` | `"pool-outdoor-swimming-pool"` | Entry id within the registry. |
|
||||
| `CHANNELS` | `1` | Decoded channel count of the WAV. |
|
||||
| `SAMPLE_RATE` | `48000` | Decoded sample rate of the WAV (Hz). |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The manifest constants are NOT optional — the spec for this package
|
||||
requires them to equal the values measured by decoding
|
||||
`files/outdoor-swimming-pool.wav` at authoring time. If the bundled WAV is
|
||||
ever re-mastered, both `CHANNELS` and `SAMPLE_RATE` MUST be updated to
|
||||
match the new decode result, and the file-presence integration test
|
||||
SHOULD re-run as part of that change to assert the new constants.
|
||||
|
||||
`buildFallbackUri([BUNDLE_URI, CDN_URI])` runs at module load. The
|
||||
ordering of candidates IS the failover policy — bundle wins when present,
|
||||
CDN wins when bundle reports `BUNDLE_URI.NOT_FOUND`. Reversing the array
|
||||
would silently flip that policy without a separate spec change, so the
|
||||
constants are kept verbose and explicit rather than inlined.
|
||||
|
||||
The `license` field intentionally says "All Rights Reserved — Suaveplan
|
||||
internal media library" because the source media library has no public
|
||||
license attached. Consumers SHOULD NOT redistribute the bundled WAV
|
||||
outside the Suaveplan distribution.
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IrManifestSchema } from "@suaveplan/audio-ir-core";
|
||||
import { parseFallbackUri } from "@suaveplan/storage-uri-plugin-fallback";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
BUNDLE_PACKAGE_SLUG,
|
||||
BUNDLE_URI,
|
||||
CDN_URI,
|
||||
CHANNELS,
|
||||
ENTRY_ID,
|
||||
manifest,
|
||||
SAMPLE_RATE,
|
||||
} from "./manifest.js";
|
||||
|
||||
describe("manifest", () => {
|
||||
it("manifest parses through IrManifestSchema", { timeout: 5000 }, () => {
|
||||
const reparsed = IrManifestSchema.parse(manifest);
|
||||
expect(reparsed).toEqual(manifest);
|
||||
expect(manifest.packageSlug).toBe("pool");
|
||||
expect(manifest.family).toBe("room");
|
||||
expect(manifest.entries.length).toBe(1);
|
||||
const entry = manifest.entries[0];
|
||||
expect(entry).toBeDefined();
|
||||
if (entry === undefined) throw new Error("unreachable");
|
||||
expect(entry.id).toBe(ENTRY_ID);
|
||||
expect(entry.id).toBe("pool-outdoor-swimming-pool");
|
||||
expect(entry.family).toBe("room");
|
||||
expect(entry.channels).toBe(CHANNELS);
|
||||
expect(entry.sampleRate).toBe(SAMPLE_RATE);
|
||||
expect(entry.gainDb).toBe(0);
|
||||
expect(entry.license.length).toBeGreaterThan(0);
|
||||
expect(entry.source.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("URI parses through parseFallbackUri with two ordered candidates", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const entry = manifest.entries[0];
|
||||
if (entry === undefined) throw new Error("unreachable");
|
||||
const parsed = parseFallbackUri(entry.uri);
|
||||
expect(parsed.candidates.length).toBe(2);
|
||||
const first = parsed.candidates[0];
|
||||
const second = parsed.candidates[1];
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
if (first === undefined || second === undefined) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(first.startsWith("bundle://audio-ir-room-plugin-pool/")).toBe(
|
||||
true
|
||||
);
|
||||
expect(first).toBe(BUNDLE_URI);
|
||||
expect(second).toBe(CDN_URI);
|
||||
expect(second).toBe(
|
||||
"https://swch.b-cdn.net/_/audio/ir/room/roompool.wav"
|
||||
);
|
||||
});
|
||||
|
||||
it("exports the canonical constants", { timeout: 5000 }, () => {
|
||||
expect(BUNDLE_PACKAGE_SLUG).toBe("audio-ir-room-plugin-pool");
|
||||
expect(CHANNELS).toBe(1);
|
||||
expect(SAMPLE_RATE).toBe(48_000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IrManifest } from "@suaveplan/audio-ir-core";
|
||||
import { IrManifestSchema } from "@suaveplan/audio-ir-core";
|
||||
import { buildFallbackUri } from "@suaveplan/storage-uri-plugin-fallback";
|
||||
|
||||
/**
|
||||
* Slug embedded in the bundle URL. MUST match the `packageSlug` passed to
|
||||
* `bundleUriPlugin({ packageSlug })` inside `register` — the bundle plugin
|
||||
* uses this to validate that the URL authority matches the consuming
|
||||
* package.
|
||||
*/
|
||||
export const BUNDLE_PACKAGE_SLUG = "audio-ir-room-plugin-pool" as const;
|
||||
|
||||
/**
|
||||
* Bundle-local URI for the shipped WAV. Resolves against the package's
|
||||
* `files/` directory at runtime via `bundleUriPlugin`.
|
||||
*/
|
||||
export const BUNDLE_URI =
|
||||
"bundle://audio-ir-room-plugin-pool/outdoor-swimming-pool.wav" as const;
|
||||
|
||||
/**
|
||||
* CDN fallback URI. The shipped `fallback://` walk targets this when the
|
||||
* bundle adapter reports `BUNDLE_URI.NOT_FOUND`.
|
||||
*/
|
||||
export const CDN_URI =
|
||||
"https://swch.b-cdn.net/_/audio/ir/room/roompool.wav" as const;
|
||||
|
||||
/**
|
||||
* Entry id for the lone IR shipped by this package.
|
||||
*/
|
||||
export const ENTRY_ID = "pool-outdoor-swimming-pool" as const;
|
||||
|
||||
/**
|
||||
* Decoded channel count of `files/outdoor-swimming-pool.wav` — measured by
|
||||
* decoding the bundled file at manifest-authoring time with
|
||||
* `@suaveplan/audio-decode-wav`.
|
||||
*/
|
||||
export const CHANNELS = 1 as const;
|
||||
|
||||
/**
|
||||
* Decoded sample rate of `files/outdoor-swimming-pool.wav` (Hz) — measured by
|
||||
* decoding the bundled file at manifest-authoring time.
|
||||
*/
|
||||
export const SAMPLE_RATE = 48_000 as const;
|
||||
|
||||
/**
|
||||
* Single-entry `IrManifest` validated through `IrManifestSchema`.
|
||||
*
|
||||
* The lone entry's `uri` is a `fallback://?u=<bundle>&u=<https>` URI in
|
||||
* declared order — bundle first, CDN second. The shipped `fallback://` plugin
|
||||
* handles the ordered walk + transient classification.
|
||||
*/
|
||||
export const manifest: IrManifest = IrManifestSchema.parse({
|
||||
packageSlug: "pool",
|
||||
family: "room",
|
||||
entries: [
|
||||
{
|
||||
id: ENTRY_ID,
|
||||
family: "room",
|
||||
uri: buildFallbackUri([BUNDLE_URI, CDN_URI]),
|
||||
channels: CHANNELS,
|
||||
sampleRate: SAMPLE_RATE,
|
||||
gainDb: 0,
|
||||
license: "All Rights Reserved — Suaveplan internal media library",
|
||||
source: "Mountain Duck media library + https://swch.b-cdn.net CDN",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { MutableUriPluginRegistrar } from "@suaveplan/audio-ir-core";
|
||||
export { type RegisterOptions, register } from "./register.js";
|
||||
@@ -0,0 +1,124 @@
|
||||
# `register` — install bundle plugin + register manifest
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Single entry point for plugging `@suaveplan/audio-ir-room-plugin-pool` into a
|
||||
consumer's `IrRegistry` + `StorageUriResolver` composition. Constructs the
|
||||
package-scoped `bundleUriPlugin` (rooted at the in-package `files/`
|
||||
directory via `import.meta.url`) and hands it to a `MutableUriPluginRegistrar`,
|
||||
then calls `registerManifest(registry, manifest)` so the lone
|
||||
`pool-outdoor-swimming-pool` entry is reachable via `registry.get(id)`.
|
||||
|
||||
The `MutableUriPluginRegistrar` shape is the canonical collector interface
|
||||
shipped by `@suaveplan/audio-ir-core` — see that package's
|
||||
`registrar/registrar.md` for the type definition and the
|
||||
`createUriPluginCollector()` factory every consumer should reach for
|
||||
first. This package only consumes the interface; it does NOT redeclare it.
|
||||
|
||||
The helper deliberately does NOT install the `fallback://` plugin or any
|
||||
`https://` plugin. Both are resolver-composition concerns the consumer
|
||||
owns once per resolver (the `fallback://` plugin is stateful across
|
||||
candidates; sharing a single instance keeps the per-candidate counter
|
||||
attribution consistent across every content package that uses it).
|
||||
|
||||
## Features
|
||||
|
||||
- One call wires both the bundle adapter and the manifest registration.
|
||||
- Uses `import.meta.url` to locate `files/` — works in both Node and any
|
||||
modern bundler that preserves package URLs.
|
||||
- No fallback plugin registration — single-responsibility wrt content.
|
||||
- `target` is typed against the foundation's `MutableUriPluginRegistrar`
|
||||
so a single shape works across every `audio-ir-{family}-plugin-<name>`
|
||||
content package.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createIrRegistry,
|
||||
createUriPluginCollector,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { register } from "@suaveplan/audio-ir-room-plugin-pool/register";
|
||||
|
||||
const registry = createIrRegistry();
|
||||
const collector = createUriPluginCollector();
|
||||
register(registry, collector);
|
||||
|
||||
const resolver = createStorageUriResolver(collector.plugins);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose multiple content packages into a single resolver, then wrap an
|
||||
HTTPS-aware plugin and the shared `fallbackUriPlugin` on top:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createIrRegistry,
|
||||
createUriPluginCollector,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import { createIrLoader } from "@suaveplan/audio-ir-loader";
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { fallbackUriPlugin } from "@suaveplan/storage-uri-plugin-fallback";
|
||||
import { register } from "@suaveplan/audio-ir-room-plugin-pool/register";
|
||||
|
||||
const registry = createIrRegistry();
|
||||
const collector = createUriPluginCollector();
|
||||
register(registry, collector);
|
||||
|
||||
// Append the consumer's https://-aware plugin.
|
||||
const innerPlugins = [...collector.plugins, myHttpsPlugin];
|
||||
const innerResolver = createStorageUriResolver(innerPlugins);
|
||||
const outerResolver = createStorageUriResolver([
|
||||
...innerPlugins,
|
||||
fallbackUriPlugin({ resolver: innerResolver, policy: { errorClass: "any" } }),
|
||||
]);
|
||||
|
||||
const loader = createIrLoader({
|
||||
registry,
|
||||
resolver: outerResolver,
|
||||
decoders: { wav: decodeWav },
|
||||
});
|
||||
const ir = await loader.load("pool-outdoor-swimming-pool");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| --- | --- | --- |
|
||||
| `register(registry, target, options?)` | function | Wire the bundle plugin into `target` and register the manifest into `registry`. |
|
||||
| `RegisterOptions` | interface | Optional `{ fetch?: FetchLike }` override for the bundle adapter (use `nodeFileFetch` under Node). |
|
||||
| `MutableUriPluginRegistrar` | type | Re-exported from `@suaveplan/audio-ir-core`. Canonical `{ use(plugin: UriPlugin): void }` collector shape. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`new URL("../../files/", import.meta.url)` is intentional — the
|
||||
`manifest/manifest.ts` file lives at `src/manifest/manifest.ts` in the
|
||||
source tree, and at `dist/manifest/manifest.js` after build. In both
|
||||
cases the relative climb two levels up from the file's URL lands on the
|
||||
package root, where `files/` lives at the same level as `dist/` and
|
||||
`src/`. The trailing slash is required by `bundleUriPlugin` — it
|
||||
rejects `rootUrl` values whose pathname does not end with `/`.
|
||||
|
||||
The bundle plugin's `packageSlug` is `"audio-ir-room-plugin-pool"`,
|
||||
which matches the `bundle://audio-ir-room-plugin-pool/...` authority in
|
||||
the manifest's URI. Mismatch would surface as `BUNDLE_URI.INVALID_URI`
|
||||
at first dispatch, not at registration — so the manifest's bundle URI
|
||||
and `BUNDLE_PACKAGE_SLUG` are both exported from `manifest.ts` as
|
||||
single-source-of-truth constants.
|
||||
|
||||
The helper does NOT clear or re-init the registry. Repeated calls
|
||||
against the same registry will fail on `AUDIO_IR.DUPLICATE_ID` from
|
||||
`registerManifest`, which is the desired behaviour — content packages
|
||||
should install exactly once per process.
|
||||
|
||||
`MutableUriPluginRegistrar` is no longer declared locally; the type is
|
||||
imported from `@suaveplan/audio-ir-core` so every content package
|
||||
targets exactly one shape. The function body is unchanged across the
|
||||
type-only switch.
|
||||
@@ -0,0 +1,115 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
createIrRegistry,
|
||||
createUriPluginCollector,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import {
|
||||
createStorageUriResolver,
|
||||
STORAGE_URI_CODES,
|
||||
StorageUriError,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { ENTRY_ID } from "../manifest/manifest.js";
|
||||
import { register } from "./register.js";
|
||||
|
||||
describe("register", () => {
|
||||
it("register populates collector and registry via foundation interface", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const collector = createUriPluginCollector();
|
||||
const registry = createIrRegistry();
|
||||
register(registry, collector);
|
||||
|
||||
expect(collector.plugins.length).toBe(1);
|
||||
const installed = collector.plugins[0];
|
||||
expect(installed).toBeDefined();
|
||||
if (installed === undefined) throw new Error("unreachable");
|
||||
expect(installed.scheme).toBe("bundle");
|
||||
|
||||
// The resolver, once constructed from the collected plugin list,
|
||||
// resolves `bundle://...` to this plugin (proves the package put
|
||||
// the plugin onto the registrar).
|
||||
const resolver = createStorageUriResolver([...collector.plugins]);
|
||||
expect(resolver.hasScheme("bundle")).toBe(true);
|
||||
|
||||
expect(registry.has(ENTRY_ID)).toBe(true);
|
||||
const entry = registry.get(ENTRY_ID);
|
||||
expect(entry.family).toBe("room");
|
||||
});
|
||||
|
||||
it("MutableUriPluginRegistrar is imported from @suaveplan/audio-ir-core not declared locally", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sourcePath = fileURLToPath(
|
||||
new URL("./register.ts", import.meta.url)
|
||||
);
|
||||
const source = readFileSync(sourcePath, "utf8");
|
||||
// Match either the single-line or biome's multi-line wrapped form
|
||||
// of the type-import. Both must come from `@suaveplan/audio-ir-core`.
|
||||
expect(source).toMatch(
|
||||
/import\s+type\s+\{[^}]*\bMutableUriPluginRegistrar\b[^}]*\}\s+from\s+"@suaveplan\/audio-ir-core"/s
|
||||
);
|
||||
expect(source).not.toMatch(/^interface MutableUriPluginRegistrar/m);
|
||||
expect(source).not.toMatch(
|
||||
/^export interface MutableUriPluginRegistrar/m
|
||||
);
|
||||
});
|
||||
|
||||
it("does not register the fallback plugin", { timeout: 5000 }, async () => {
|
||||
const collector = createUriPluginCollector();
|
||||
const registry = createIrRegistry();
|
||||
register(registry, collector);
|
||||
|
||||
// Build a resolver with ONLY what `register` added — i.e. the
|
||||
// bundle plugin. The fallback plugin is NOT in the list.
|
||||
const resolver = createStorageUriResolver([...collector.plugins]);
|
||||
expect(resolver.hasScheme("fallback")).toBe(false);
|
||||
|
||||
// Confirm dispatching a `fallback://` URI through the resolver
|
||||
// surfaces `STORAGE_URI.UNKNOWN_SCHEME` — proving the package
|
||||
// did not silently install a fallback plugin behind our back.
|
||||
await expect(
|
||||
resolver.read("fallback://?u=bundle%3A%2F%2Fx%2Fy")
|
||||
).rejects.toMatchObject({
|
||||
code: STORAGE_URI_CODES.UNKNOWN_SCHEME,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not register an https plugin either", { timeout: 5000 }, () => {
|
||||
const collector = createUriPluginCollector();
|
||||
register(createIrRegistry(), collector);
|
||||
const resolver = createStorageUriResolver([...collector.plugins]);
|
||||
expect(resolver.hasScheme("https")).toBe(false);
|
||||
expect(resolver.hasScheme("http")).toBe(false);
|
||||
});
|
||||
|
||||
it("re-running register throws DUPLICATE_ID", { timeout: 5000 }, () => {
|
||||
const registry = createIrRegistry();
|
||||
const collector = createUriPluginCollector();
|
||||
register(registry, collector);
|
||||
expect(() => register(registry, collector)).toThrow();
|
||||
});
|
||||
|
||||
it("register returns void and throws on registry shape mismatch", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Sanity: passing a non-registry surfaces a thrown error from
|
||||
// `registerManifest` → `registry.register`, not a silent no-op.
|
||||
const target = {
|
||||
use: (_p: UriPlugin) => {},
|
||||
};
|
||||
const fakeRegistry = {} as ReturnType<typeof createIrRegistry>;
|
||||
expect(() => register(fakeRegistry, target)).toThrow();
|
||||
|
||||
// Sanity check: StorageUriError shape is what we expect (smoke
|
||||
// the import).
|
||||
const err = new StorageUriError(
|
||||
STORAGE_URI_CODES.UNKNOWN_SCHEME,
|
||||
"smoke",
|
||||
{ scheme: "x" }
|
||||
);
|
||||
expect(err.code).toBe(STORAGE_URI_CODES.UNKNOWN_SCHEME);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import type {
|
||||
IrRegistry,
|
||||
MutableUriPluginRegistrar,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import { registerManifest } from "@suaveplan/audio-ir-core";
|
||||
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
|
||||
import type { FetchLike } from "@suaveplan/types";
|
||||
import { BUNDLE_PACKAGE_SLUG, manifest } from "../manifest/manifest.js";
|
||||
|
||||
/** Optional overrides for `register`. */
|
||||
export interface RegisterOptions {
|
||||
/**
|
||||
* Override the `fetch` implementation passed to `bundleUriPlugin`. Useful
|
||||
* under Node (which does NOT support `fetch('file://...')`) — pass
|
||||
* `nodeFileFetch` from `@suaveplan/storage-uri-plugin-bundle/node-fetch`.
|
||||
* Defaults to `globalThis.fetch`.
|
||||
*/
|
||||
readonly fetch?: FetchLike;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install this content package's bundle plugin onto `target` and register
|
||||
* every entry from `manifest` into `registry`.
|
||||
*
|
||||
* `target` conforms to the canonical `MutableUriPluginRegistrar` interface
|
||||
* shipped by `@suaveplan/audio-ir-core` — typically a
|
||||
* `createUriPluginCollector()` instance from the foundation, but any
|
||||
* `{ use(plugin: UriPlugin): void }` sink works.
|
||||
*
|
||||
* `register` does NOT install the shipped `fallback://` plugin — that
|
||||
* responsibility lives at the resolver-composition layer so a single
|
||||
* `fallbackUriPlugin` instance is shared across every fallback-using
|
||||
* content package. It also does NOT install an `https://` plugin —
|
||||
* choose one (or write a thin `globalThis.fetch` wrapper) at the same
|
||||
* layer as `fallbackUriPlugin`.
|
||||
*
|
||||
* `rootUrl` is computed from `import.meta.url` so the package's
|
||||
* `files/` directory is located deterministically regardless of where
|
||||
* the consumer installs the published tarball.
|
||||
*/
|
||||
export function register(
|
||||
registry: IrRegistry,
|
||||
target: MutableUriPluginRegistrar,
|
||||
options?: RegisterOptions
|
||||
): void {
|
||||
const plugin = bundleUriPlugin({
|
||||
packageSlug: BUNDLE_PACKAGE_SLUG,
|
||||
rootUrl: new URL("../../files/", import.meta.url),
|
||||
...(options?.fetch !== undefined ? { fetch: options.fetch } : {}),
|
||||
});
|
||||
target.use(plugin);
|
||||
registerManifest(registry, manifest);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
import {
|
||||
createIrRegistry,
|
||||
createUriPluginCollector,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import { createIrLoader } from "@suaveplan/audio-ir-loader";
|
||||
import {
|
||||
createStorageUriResolver,
|
||||
STORAGE_URI_CODES,
|
||||
StorageUriError,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import { fallbackUriPlugin } from "@suaveplan/storage-uri-plugin-fallback";
|
||||
import type { FetchLike } from "@suaveplan/types";
|
||||
|
||||
/**
|
||||
* Minimal `file://` `FetchLike` for Node — Node's `globalThis.fetch` rejects
|
||||
* `file://` URLs, so we substitute one that round-trips via `node:fs`. Lives
|
||||
* in the test file by design (no new package introduced).
|
||||
*/
|
||||
const nodeFileFetch: FetchLike = async (input, init) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL((input as Request).url);
|
||||
if (url.protocol !== "file:") {
|
||||
return globalThis.fetch(
|
||||
input as Parameters<typeof globalThis.fetch>[0],
|
||||
init
|
||||
);
|
||||
}
|
||||
const path = fileURLToPath(url);
|
||||
try {
|
||||
const bytes = await readFile(path);
|
||||
const meta = await stat(path);
|
||||
return new Response(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-length": String(meta.size),
|
||||
"content-type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "ENOENT") {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { ENTRY_ID, manifest } from "../../src/manifest/manifest.js";
|
||||
import { register } from "../../src/register/register.js";
|
||||
|
||||
/**
|
||||
* Minimal in-test https `UriPlugin` wrapping `globalThis.fetch`. Lives in
|
||||
* the test file by design — there is no shipped storage-uri plugin for
|
||||
* https today, and the integration test must not introduce a new package.
|
||||
*/
|
||||
function makeHttpsPlugin(spy: { calls: number }): UriPlugin {
|
||||
return {
|
||||
scheme: "https",
|
||||
adapter: {
|
||||
async get(key: string) {
|
||||
spy.calls += 1;
|
||||
const url = `https://${key}`;
|
||||
const res = await fetch(url);
|
||||
if (res.status === 404) return undefined;
|
||||
if (!res.ok) {
|
||||
throw new StorageUriError(
|
||||
STORAGE_URI_CODES.ADAPTER_ERROR,
|
||||
`HTTP ${res.status} from ${url}`,
|
||||
{ uri: url, scheme: "https" }
|
||||
);
|
||||
}
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
},
|
||||
async set() {
|
||||
return;
|
||||
},
|
||||
async delete() {
|
||||
return true;
|
||||
},
|
||||
async exists() {
|
||||
spy.calls += 1;
|
||||
return false;
|
||||
},
|
||||
async list() {
|
||||
return { keys: [], total: 0, hasMore: false };
|
||||
},
|
||||
async clear() {
|
||||
return;
|
||||
},
|
||||
},
|
||||
parse(uri: string) {
|
||||
const noScheme = uri.replace(/^https:\/\//, "");
|
||||
return { key: noScheme };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("integration: bundle-only", () => {
|
||||
it("bundle candidate wins without fetching HTTPS via foundation collector", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const collector = createUriPluginCollector();
|
||||
const registry = createIrRegistry();
|
||||
register(registry, collector, { fetch: nodeFileFetch });
|
||||
|
||||
const spy = { calls: 0 };
|
||||
const httpsPlugin = makeHttpsPlugin(spy);
|
||||
collector.use(httpsPlugin);
|
||||
|
||||
const plugins: readonly UriPlugin[] = collector.plugins;
|
||||
const innerResolver = createStorageUriResolver([...plugins]);
|
||||
const fb = fallbackUriPlugin({
|
||||
resolver: innerResolver,
|
||||
policy: { errorClass: "any" },
|
||||
});
|
||||
const outerResolver = createStorageUriResolver([...plugins, fb]);
|
||||
|
||||
const loader = createIrLoader({
|
||||
registry,
|
||||
resolver: outerResolver,
|
||||
decoders: { wav: decodeWav },
|
||||
});
|
||||
|
||||
const ir = await loader.load(ENTRY_ID);
|
||||
expect(ir.id).toBe(ENTRY_ID);
|
||||
expect(ir.family).toBe("room");
|
||||
expect(ir.channels).toBe(manifest.entries[0]?.channels);
|
||||
expect(ir.sampleRate).toBe(manifest.entries[0]?.sampleRate);
|
||||
expect(Object.isFrozen(ir)).toBe(true);
|
||||
|
||||
// Critical assertion: HTTPS adapter was never invoked.
|
||||
expect(spy.calls).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,351 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { MeterProvider } from "@opentelemetry/api";
|
||||
import type { FetchLike } from "@suaveplan/types";
|
||||
|
||||
/** Minimal `file://` `FetchLike` for Node — Node's `globalThis.fetch` rejects file:// URLs. */
|
||||
const nodeFileFetch: FetchLike = async (input, init) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL((input as Request).url);
|
||||
if (url.protocol !== "file:") {
|
||||
return globalThis.fetch(
|
||||
input as Parameters<typeof globalThis.fetch>[0],
|
||||
init
|
||||
);
|
||||
}
|
||||
const path = fileURLToPath(url);
|
||||
try {
|
||||
const bytes = await readFile(path);
|
||||
const meta = await stat(path);
|
||||
return new Response(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-length": String(meta.size),
|
||||
"content-type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
import {
|
||||
createIrRegistry,
|
||||
createUriPluginCollector,
|
||||
registerManifest,
|
||||
} from "@suaveplan/audio-ir-core";
|
||||
import {
|
||||
createIrLoader,
|
||||
getInstruments as getLoaderInstruments,
|
||||
resetInstruments as resetLoaderInstruments,
|
||||
setAudioIrLoaderMeterProvider,
|
||||
} from "@suaveplan/audio-ir-loader";
|
||||
import {
|
||||
createStorageUriResolver,
|
||||
STORAGE_URI_CODES,
|
||||
StorageUriError,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
|
||||
import {
|
||||
fallbackUriPlugin,
|
||||
parseFallbackUri,
|
||||
resetFallbackInstruments,
|
||||
setFallbackMeterProvider,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import {
|
||||
BUNDLE_PACKAGE_SLUG,
|
||||
ENTRY_ID,
|
||||
manifest,
|
||||
} from "../../src/manifest/manifest.js";
|
||||
|
||||
interface CounterObs {
|
||||
readonly name: string;
|
||||
readonly value: number;
|
||||
readonly attributes: Record<string, unknown>;
|
||||
}
|
||||
interface HistogramObs {
|
||||
readonly name: string;
|
||||
readonly value: number;
|
||||
readonly attributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function installRecorder(): {
|
||||
counters: CounterObs[];
|
||||
histograms: HistogramObs[];
|
||||
} {
|
||||
const counters: CounterObs[] = [];
|
||||
const histograms: HistogramObs[] = [];
|
||||
const provider = {
|
||||
getMeter: () => ({
|
||||
createCounter: (name: string) => ({
|
||||
add: (value: number, attrs?: Record<string, unknown>) => {
|
||||
counters.push({ name, value, attributes: attrs ?? {} });
|
||||
},
|
||||
}),
|
||||
createHistogram: (name: string) => ({
|
||||
record: (value: number, attrs?: Record<string, unknown>) => {
|
||||
histograms.push({ name, value, attributes: attrs ?? {} });
|
||||
},
|
||||
}),
|
||||
createObservableGauge: () => ({
|
||||
addCallback: () => {},
|
||||
}),
|
||||
}),
|
||||
} as unknown as MeterProvider;
|
||||
setAudioIrLoaderMeterProvider(provider);
|
||||
setFallbackMeterProvider(provider);
|
||||
getLoaderInstruments(true);
|
||||
return { counters, histograms };
|
||||
}
|
||||
|
||||
function makeHttpPlugin(): UriPlugin {
|
||||
return {
|
||||
scheme: "http",
|
||||
adapter: {
|
||||
async get(key: string) {
|
||||
const url = `http://${key}`;
|
||||
const res = await fetch(url);
|
||||
if (res.status === 404) return undefined;
|
||||
if (!res.ok) {
|
||||
throw new StorageUriError(
|
||||
STORAGE_URI_CODES.ADAPTER_ERROR,
|
||||
`HTTP ${res.status} from ${url}`,
|
||||
{ uri: url, scheme: "http" }
|
||||
);
|
||||
}
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
},
|
||||
async set() {
|
||||
return;
|
||||
},
|
||||
async delete() {
|
||||
return true;
|
||||
},
|
||||
async exists(key: string) {
|
||||
const res = await fetch(`http://${key}`, { method: "HEAD" });
|
||||
return res.ok;
|
||||
},
|
||||
async list() {
|
||||
return { keys: [], total: 0, hasMore: false };
|
||||
},
|
||||
async clear() {
|
||||
return;
|
||||
},
|
||||
},
|
||||
parse(uri: string) {
|
||||
const noScheme = uri.replace(/^http:\/\//, "");
|
||||
return { key: noScheme };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a 16-bit mono silent WAV at the manifest's declared sample rate. */
|
||||
function buildSilentWav(
|
||||
sampleRate: number,
|
||||
samplesPerChannel: number
|
||||
): Uint8Array {
|
||||
const dataBytes = samplesPerChannel * 2;
|
||||
const buf = new Uint8Array(44 + dataBytes);
|
||||
const dv = new DataView(buf.buffer);
|
||||
const writeAscii = (off: number, s: string) => {
|
||||
for (let i = 0; i < s.length; i++) buf[off + i] = s.charCodeAt(i);
|
||||
};
|
||||
writeAscii(0, "RIFF");
|
||||
dv.setUint32(4, 36 + dataBytes, true);
|
||||
writeAscii(8, "WAVE");
|
||||
writeAscii(12, "fmt ");
|
||||
dv.setUint32(16, 16, true);
|
||||
dv.setUint16(20, 1, true);
|
||||
dv.setUint16(22, 1, true);
|
||||
dv.setUint32(24, sampleRate, true);
|
||||
dv.setUint32(28, sampleRate * 2, true);
|
||||
dv.setUint16(32, 2, true);
|
||||
dv.setUint16(34, 16, true);
|
||||
writeAscii(36, "data");
|
||||
dv.setUint32(40, dataBytes, true);
|
||||
return buf;
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
server: Server;
|
||||
port: number;
|
||||
tempDir: string;
|
||||
}
|
||||
|
||||
let harness: Harness | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const tempDir = await mkdtemp(
|
||||
join(tmpdir(), "audio-ir-room-plugin-pool-test-")
|
||||
);
|
||||
// Create the package-slug subdirectory but DO NOT place the WAV inside —
|
||||
// so the bundle adapter's GET reports `BUNDLE_URI.NOT_FOUND`.
|
||||
await mkdir(join(tempDir, BUNDLE_PACKAGE_SLUG), { recursive: true });
|
||||
|
||||
const wavBytes = buildSilentWav(
|
||||
manifest.entries[0]?.sampleRate ?? 48_000,
|
||||
manifest.entries[0]?.channels ?? 1
|
||||
);
|
||||
// Use a generous body so the decode validator has something to work
|
||||
// with — 4096 samples = ~85ms @ 48kHz.
|
||||
const richBytes = buildSilentWav(
|
||||
manifest.entries[0]?.sampleRate ?? 48_000,
|
||||
4096
|
||||
);
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
if (req.url === "/roompool.wav") {
|
||||
res.writeHead(200, { "Content-Type": "audio/wav" });
|
||||
res.end(Buffer.from(richBytes));
|
||||
return;
|
||||
}
|
||||
if (req.url === "/empty.wav") {
|
||||
res.writeHead(200, { "Content-Type": "audio/wav" });
|
||||
res.end(Buffer.from(wavBytes));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
await new Promise<void>((resolve) =>
|
||||
server.listen(0, "127.0.0.1", resolve)
|
||||
);
|
||||
const port = (server.address() as AddressInfo).port;
|
||||
harness = { server, port, tempDir };
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (harness !== undefined) {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
harness?.server.close((err) => (err ? reject(err) : resolve()))
|
||||
);
|
||||
await rm(harness.tempDir, { recursive: true, force: true });
|
||||
}
|
||||
setAudioIrLoaderMeterProvider(undefined);
|
||||
setFallbackMeterProvider(undefined);
|
||||
resetLoaderInstruments();
|
||||
resetFallbackInstruments();
|
||||
}, 30000);
|
||||
|
||||
describe("integration: fallback-walk", () => {
|
||||
it("bundle miss falls through to HTTPS via foundation collector", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
if (harness === undefined) {
|
||||
throw new Error("harness not initialised");
|
||||
}
|
||||
const rec = installRecorder();
|
||||
|
||||
// Build a bundle plugin rooted at the empty temp directory so
|
||||
// any `bundle://audio-ir-room-plugin-pool/...` GET surfaces
|
||||
// `BUNDLE_URI.NOT_FOUND`.
|
||||
const bundlePlugin = bundleUriPlugin({
|
||||
packageSlug: BUNDLE_PACKAGE_SLUG,
|
||||
rootUrl: new URL(`file://${harness.tempDir}/`),
|
||||
fetch: nodeFileFetch,
|
||||
});
|
||||
const httpPlugin = makeHttpPlugin();
|
||||
|
||||
// Build the plugin list via the foundation collector. This mirrors
|
||||
// how production callers compose plugins across content packages
|
||||
// before constructing the resolver.
|
||||
const collector = createUriPluginCollector();
|
||||
collector.use(bundlePlugin);
|
||||
collector.use(httpPlugin);
|
||||
|
||||
// The manifest URI points at https://swch.b-cdn.net/... — we
|
||||
// want the integration test to hit our local fixture instead.
|
||||
// Compose a substitute fallback URI that mirrors the manifest's
|
||||
// bundle/http(s) structure but targets the fixture.
|
||||
const bundleCandidate = manifest.entries[0]?.uri ?? ""; // unused — kept for parity assertion
|
||||
void bundleCandidate;
|
||||
const parsedManifestUri = parseFallbackUri(
|
||||
manifest.entries[0]?.uri ?? ""
|
||||
);
|
||||
expect(parsedManifestUri.candidates.length).toBe(2);
|
||||
|
||||
const httpFixtureUri = `http://127.0.0.1:${harness.port}/roompool.wav`;
|
||||
const fixtureFallbackUri = `fallback://?u=${encodeURIComponent(
|
||||
parsedManifestUri.candidates[0] ?? ""
|
||||
)}&u=${encodeURIComponent(httpFixtureUri)}`;
|
||||
|
||||
const innerResolver = createStorageUriResolver([...collector.plugins]);
|
||||
const fb = fallbackUriPlugin({
|
||||
resolver: innerResolver,
|
||||
policy: { errorClass: "any" },
|
||||
});
|
||||
const outerResolver = createStorageUriResolver([
|
||||
...collector.plugins,
|
||||
fb,
|
||||
]);
|
||||
|
||||
// Register the manifest, but with a substituted URI on the
|
||||
// sole entry so the loader walks bundle → http instead of
|
||||
// bundle → https.
|
||||
const registry = createIrRegistry();
|
||||
const baseEntry = manifest.entries[0];
|
||||
if (baseEntry === undefined) throw new Error("unreachable");
|
||||
const fixtureManifest = {
|
||||
...manifest,
|
||||
entries: [
|
||||
{
|
||||
...baseEntry,
|
||||
uri: fixtureFallbackUri,
|
||||
},
|
||||
],
|
||||
};
|
||||
registerManifest(registry, fixtureManifest);
|
||||
|
||||
const loader = createIrLoader({
|
||||
registry,
|
||||
resolver: outerResolver,
|
||||
decoders: { wav: decodeWav },
|
||||
});
|
||||
const ir = await loader.load(ENTRY_ID);
|
||||
expect(ir.id).toBe(ENTRY_ID);
|
||||
expect(ir.family).toBe("room");
|
||||
expect(ir.channels).toBe(baseEntry.channels);
|
||||
expect(ir.sampleRate).toBe(baseEntry.sampleRate);
|
||||
|
||||
// The shipped fallback plugin emits its per-candidate counter
|
||||
// under the name `storage_uri.fallback_total` (verified against
|
||||
// packages/storage/storage-uri-plugin-fallback/src/observability/otel.ts).
|
||||
// We expect candidate_index:0 to have at least one `transient`
|
||||
// sample (the bundle miss).
|
||||
const fbTransient = rec.counters.filter(
|
||||
(c) =>
|
||||
c.name === "storage_uri.fallback_total" &&
|
||||
c.attributes["candidate_index"] === 0 &&
|
||||
c.attributes["outcome"] === "transient"
|
||||
);
|
||||
expect(fbTransient.length).toBe(1);
|
||||
|
||||
// Loader bytes_read_total should tick once with family=room.
|
||||
const bytesRead = rec.counters.filter(
|
||||
(c) =>
|
||||
c.name === "audio_ir_loader.bytes_read_total" &&
|
||||
c.attributes["family"] === "room"
|
||||
);
|
||||
expect(bytesRead.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { CHANNELS, SAMPLE_RATE } from "../../src/manifest/manifest.js";
|
||||
|
||||
const WAV_URL = new URL(
|
||||
"../../files/outdoor-swimming-pool.wav",
|
||||
import.meta.url
|
||||
);
|
||||
|
||||
describe("integration: file-presence", () => {
|
||||
it("outdoor-swimming-pool.wav exists on disk and decodes", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const path = fileURLToPath(WAV_URL);
|
||||
const meta = await stat(path);
|
||||
expect(meta.isFile()).toBe(true);
|
||||
expect(meta.size).toBeGreaterThan(0);
|
||||
|
||||
const bytes = await readFile(path);
|
||||
const decoded = await decodeWav(new Uint8Array(bytes));
|
||||
expect(decoded.channels).toBe(CHANNELS);
|
||||
expect(decoded.sampleRate).toBe(SAMPLE_RATE);
|
||||
expect(decoded.channelData.length).toBe(CHANNELS);
|
||||
expect(decoded.bytesDecoded).toBe(meta.size);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "tests/**/*.test.ts", "dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/audio-ir-room-plugin-pool",
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
sourcemap: true,
|
||||
lib: { entry: "src/index.ts", formats: ["es"] },
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [
|
||||
"@suaveplan/audio-ir-core",
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/storage-uri",
|
||||
"@suaveplan/storage-uri-plugin-bundle",
|
||||
"@suaveplan/storage-uri-plugin-fallback",
|
||||
"@suaveplan/types",
|
||||
"tslib",
|
||||
"zod",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createTestConfig } from "@suaveplan/testing/config";
|
||||
|
||||
export default createTestConfig({
|
||||
testTimeout: 30000,
|
||||
});
|
||||
Reference in New Issue
Block a user