@deadair/plugin-sdk 0.79.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -196,6 +196,42 @@ export interface PluginOAuth {
196
196
  export interface PluginEvents {
197
197
  emit(event: string, payload?: Record<string, unknown>): Promise<void>;
198
198
  }
199
+ /**
200
+ * Whether the station is on air, as a fact the host knows rather than one a plugin guesses at.
201
+ *
202
+ * "On air" is the station holding its mount: it has a programme to play AND somebody to hear it
203
+ * (or the operator has said to air regardless). It is the same question the station asks itself
204
+ * before it puts anything on, so it goes off when the running order runs out, when the operator
205
+ * stops it, and when the last listener has been gone for a while, and on again when any of those
206
+ * is undone. A listener dropping out for a moment does not move it: the station waits a few
207
+ * minutes before it calls an empty room empty.
208
+ *
209
+ * Exists for a plugin holding something expensive that is worth keeping only while the station
210
+ * is airing, a speech model on a graphics card being the case it was written for. The tempting
211
+ * alternative is a timer since the plugin was last called, and that is a guess: a station can be
212
+ * on air with nothing to say for longer than any idle window, and off air a second after the
213
+ * last call.
214
+ *
215
+ * Off until the station has said otherwise, so a plugin initialized before the station first
216
+ * decides reads `false` and then hears the change.
217
+ */
218
+ export interface PluginAir {
219
+ /** Whether the station is on air at this moment. */
220
+ isOn(): boolean;
221
+ /**
222
+ * Hear the station go on or off air. Returns the unsubscribe.
223
+ *
224
+ * Called with the new state, once per change and never twice with the same value. Called
225
+ * after the change rather than inside whatever made it, so a slow listener holds nothing
226
+ * up, and outside any host call, so {@link PluginHost.signal} never aborts in it. A listener
227
+ * that throws or rejects is logged and never counted against your plugin.
228
+ *
229
+ * Disposing your plugin drops every listener it still has, so a forgotten one cannot outlive
230
+ * the instance. Unsubscribe in a `register` disposer all the same; the host's is the
231
+ * backstop, not the plan.
232
+ */
233
+ onChange(listener: (on: boolean) => void | Promise<void>): () => void;
234
+ }
199
235
  /**
200
236
  * A login the station's track fetcher opens its own session with.
201
237
  *
@@ -446,6 +482,8 @@ export interface PluginHost {
446
482
  config: PluginConfigAccess;
447
483
  oauth: PluginOAuth;
448
484
  events: PluginEvents;
485
+ /** Whether the station is on air, and when that changes. See {@link PluginAir}. */
486
+ air: PluginAir;
449
487
  trackFetcher: PluginTrackFetcher;
450
488
  }
451
489
  //# sourceMappingURL=plugin.host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.host.d.ts","sourceRoot":"","sources":["../src/plugin.host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin.host.d.ts","sourceRoot":"","sources":["../src/plugin.host.ts"],"names":[],"mappings":"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"}
@@ -115,6 +115,15 @@ export interface FakePluginHost extends PluginHost {
115
115
  * is found until a test says otherwise, which is what a station on a bridge network sees.
116
116
  */
117
117
  seedDiscovery(found: DiscoveredService[] | ((query: DiscoveryQuery) => DiscoveredService[])): void;
118
+ /**
119
+ * Put the station on or off air, telling every `host.air` listener when it actually changed.
120
+ * Off until a test says otherwise, as on a real station before it first decides. Synchronous,
121
+ * where the real host tells listeners just after the change: a listener that returns a promise
122
+ * is not awaited, so a test that cares awaits what the plugin started.
123
+ */
124
+ setOnAir(on: boolean): void;
125
+ /** How many `host.air` listeners the plugin still holds, so a test can see one was let go of. */
126
+ airListenerCount(): number;
118
127
  }
119
128
  /**
120
129
  * What a test says a scripted reply should be.
@@ -1 +1 @@
1
- {"version":3,"file":"fake.plugin.host.d.ts","sourceRoot":"","sources":["../../src/testing/fake.plugin.host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fake.plugin.host.d.ts","sourceRoot":"","sources":["../../src/testing/fake.plugin.host.ts"],"names":[],"mappings":"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"}
@@ -137,6 +137,8 @@ function createFakePluginHost() {
137
137
  let tlsRefusal;
138
138
  let tlsOpened;
139
139
  let discovered = /* @__PURE__ */ __name(() => [], "discovered");
140
+ let onAir = false;
141
+ const airListeners = /* @__PURE__ */ new Set();
140
142
  const fetchImpl = vi.fn(async (url, init) => {
141
143
  calls.push({
142
144
  url,
@@ -203,6 +205,15 @@ function createFakePluginHost() {
203
205
  events: {
204
206
  emit: vi.fn()
205
207
  },
208
+ air: {
209
+ isOn: vi.fn(() => onAir),
210
+ onChange: vi.fn((listener) => {
211
+ airListeners.add(listener);
212
+ return () => {
213
+ airListeners.delete(listener);
214
+ };
215
+ })
216
+ },
206
217
  trackFetcher: {
207
218
  serve: vi.fn(async () => fetchedTrack),
208
219
  // Answers the same canned stream as `serve`: a plugin under test cares that it asked the
@@ -230,6 +241,16 @@ function createFakePluginHost() {
230
241
  seedDiscovery(found) {
231
242
  discovered = typeof found === "function" ? found : () => found;
232
243
  },
244
+ setOnAir(on) {
245
+ if (on === onAir) return;
246
+ onAir = on;
247
+ for (const listener of [
248
+ ...airListeners
249
+ ]) void listener(on);
250
+ },
251
+ airListenerCount() {
252
+ return airListeners.size;
253
+ },
233
254
  seedRemainingMs(ms) {
234
255
  remainingMs = ms;
235
256
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/fake.plugin.host.ts"],"sourcesContent":["import { vi } from 'vitest';\n\nimport type {\n DiscoveredService,\n DiscoveryQuery,\n HostFetchInit,\n HostFetchMethod,\n PluginHost,\n PluginSocket,\n PluginTlsOptions,\n PluginTlsSocket,\n ProviderStream,\n ProviderTrack,\n} from '../index.js';\n\n/**\n * Shipped from the SDK rather than copied into each plugin, which three of them\n * were doing — two of those carrying a comment saying the copy was deliberate\n * because plugins do not depend on each other.\n *\n * That reasoning held for the copies and does not hold for this: every plugin\n * already depends on the SDK, and `PluginHost` is the SDK's own contract, so its\n * test double belongs beside the interface it doubles. A plugin importing this\n * gains no dependency on any other plugin. It also means a host method added to\n * the contract turns up in one fake rather than being added to three and\n * forgotten in the other three.\n *\n * Reached as `@deadair/plugin-sdk/testing`, which is kept out of the runtime\n * entry so nothing ships a dependency on vitest.\n */\n\n/** One `host.fetch` call, recorded with only the fields tests care about. */\nexport interface RecordedFetchCall {\n url: string;\n method: HostFetchMethod | undefined;\n headers: Record<string, string> | undefined;\n body: string | undefined;\n}\n\n/**\n * One `host.socket` the plugin opened, driven from the test: frames the plugin sent are in `sent`,\n * and `receive` / `closeFromServer` play the far end.\n */\nexport interface FakePluginSocket extends PluginSocket {\n readonly url: string;\n /** Every text frame the plugin sent, in order. */\n readonly sent: string[];\n /** Whether either end has closed it. */\n readonly closed: boolean;\n /** The frames the plugin sent, each parsed as JSON. */\n sentJson(): unknown[];\n /** Deliver one text frame to the plugin's listeners. */\n receive(text: string): void;\n /** Deliver `JSON.stringify(value)`. */\n receiveJson(value: unknown): void;\n /** Close it from the far end, telling the plugin's close listeners. */\n closeFromServer(code?: number, reason?: string): void;\n}\n\n/**\n * One `host.tls` connection the plugin opened, driven from the test: bytes the plugin wrote are in\n * `sent`, and `receive` / `closeFromServer` play the device.\n */\nexport interface FakePluginTlsSocket extends PluginTlsSocket {\n readonly url: string;\n /** The options the plugin opened it with. */\n readonly options: PluginTlsOptions | undefined;\n /** Every write the plugin made, in order, one entry per `send`. */\n readonly sent: Uint8Array[];\n /** Whether either end has closed it. */\n readonly closed: boolean;\n /** Deliver one chunk to the plugin's data listeners. */\n receive(bytes: Uint8Array): void;\n /** Close it from the far end, telling the plugin's close listeners. */\n closeFromServer(reason?: string): void;\n}\n\n/**\n * A `PluginHost` for tests. `fetch` replays a queue of scripted\n * `Response`s (FIFO, one per call) — or a custom handler installed\n * with `setFetchImpl`, for call-count-driven scenarios like a 401-then-200\n * retry — and records every call it received in `calls`. `storage`, `config`,\n * `secrets` and the `oauth` vault are in-memory and can be seeded or read\n * directly, bypassing the `PluginHost` methods, so a test can assert on state\n * without going through the code under test twice.\n */\nexport interface FakePluginHost extends PluginHost {\n /** Every `host.fetch` call so far, in call order. */\n readonly calls: RecordedFetchCall[];\n /** Set what `host.remainingMs()` reports, for testing budget shedding. */\n seedRemainingMs(ms: number): void;\n /** Push one scripted response onto the back of the reply queue. */\n queueResponse(response: FakeResponseInit): void;\n /** Replace the fetch handler outright; overrides the queue while set. */\n setFetchImpl(impl: (url: string, init?: HostFetchInit) => Promise<Response>): void;\n /** Seed the value `host.config.get()` resolves to. */\n seedConfig(config: Record<string, unknown>): void;\n /** Seed a value `host.secrets.get(key)` resolves to. */\n seedSecret(key: string, value: string): void;\n /** Seed a storage entry directly, bypassing `host.storage.set`. */\n seedStorage(key: string, value: unknown): void;\n /** Read a storage entry directly, bypassing `host.storage.get`. */\n getStorageEntry(key: string): unknown;\n /** All storage keys currently set, unfiltered. */\n storageKeys(): string[];\n /** Seed the oauth vault directly, bypassing `host.oauth.saveTokens`. */\n seedTokens(tokens: Record<string, string>): void;\n /** Read the oauth vault directly, bypassing `host.oauth.getTokens`. */\n getVaultTokens(): Record<string, string> | undefined;\n /** Set what `host.trackFetcher.serve()` answers with; `undefined` means \"this station has no fetcher\". */\n seedFetchedTrack(stream: ProviderStream | undefined): void;\n /**\n * Set what `host.trackFetcher.playlistTracks()` answers with. `undefined` means \"this station\n * has no fetcher\"; pass an `Error` to make it throw, which is the fetcher having tried and\n * failed and is a different case for a plugin to handle.\n */\n seedFetchedPlaylist(tracks: ProviderTrack[] | Error | undefined): void;\n /** Every socket `host.socket` opened, in order, closed ones included. */\n readonly sockets: FakePluginSocket[];\n /** Make every later `host.socket` reject with `error`; `undefined` lets them open again. */\n refuseSockets(error: Error | undefined): void;\n /** Every connection `host.tls` opened, in order, closed ones included. */\n readonly tlsSockets: FakePluginTlsSocket[];\n /** Make every later `host.tls` reject with `error`; `undefined` lets them open again. */\n refuseTls(error: Error | undefined): void;\n /**\n * Called with each connection as `host.tls` opens it, before the plugin sees it, so a test can\n * script the device's answers to whatever the plugin writes.\n */\n onTlsOpen(listener: ((socket: FakePluginTlsSocket) => void) | undefined): void;\n /**\n * What `host.discover` answers from now on: a fixed list, or one worked out per query. Nothing\n * is found until a test says otherwise, which is what a station on a bridge network sees.\n */\n seedDiscovery(found: DiscoveredService[] | ((query: DiscoveryQuery) => DiscoveredService[])): void;\n}\n\n/**\n * What `host.remainingMs()` reports unless a test says otherwise: what a live\n * call reads on its first line. The host's default per-call deadline is 8s, and\n * the clock has already been running for a few milliseconds by the time plugin\n * code asks, so a live call never sees the whole 8s. A plugin that refuses to\n * start unless a full request timeout is left therefore never starts at all,\n * and a default above the real deadline hid exactly that: every test passed\n * while every live call returned nothing.\n */\nconst DEFAULT_REMAINING_MS = 8_000 - 5;\n\n/**\n * What a test says a scripted reply should be.\n *\n * Not `ResponseInit`, because `url` is not on it: `host.fetch` reports the last\n * hop of the redirect chain there, and a `Response` built by hand has an empty\n * one unless it is defined in.\n */\nexport interface FakeResponseInit {\n status?: number;\n statusText?: string;\n headers?: Record<string, string>;\n /**\n * Text for a JSON or error reply, bytes for a binary one.\n *\n * `Uint8Array<ArrayBuffer>` rather than a bare `Uint8Array`: the DOM lib's\n * `BodyInit` accepts a view over a real `ArrayBuffer`, and the unparameterized\n * form widens to `ArrayBufferLike`, which includes `SharedArrayBuffer` and is\n * not something `Response` will take. `new Uint8Array([...])` already has the\n * narrow type, so no test has to say so.\n */\n body?: string | Uint8Array<ArrayBuffer>;\n url?: string;\n}\n\n/** Statuses whose `Response` must carry a null body, or the constructor throws. */\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/** Builds a default 200 `Response`, overridable field by field. */\nexport function fakeHostFetchResponse(init: FakeResponseInit = {}): Response {\n const status = init.status ?? 200;\n\n const response = new Response(NULL_BODY_STATUSES.has(status) ? null : (init.body ?? ''), {\n status,\n statusText: init.statusText ?? 'OK',\n headers: init.headers ?? { 'content-type': 'application/json' },\n });\n\n // Read-only on a constructed response, and part of what the host promises.\n Object.defineProperty(response, 'url', { value: init.url ?? 'https://example.test/' });\n\n return response;\n}\n\n/** What a {@link FakeRoute} answers: a status (200 when left out) and a body, sent as JSON unless it is already text. */\nexport interface FakeReply {\n status?: number;\n body: unknown;\n}\n\n/** One path a {@link routedFetch} knows, answering `undefined` for a URL that is not its own. */\nexport type FakeRoute = (url: URL, init?: HostFetchInit) => FakeReply | undefined;\n\n/**\n * A `setFetchImpl` that answers by route, for a plugin test that talks to several paths.\n *\n * The first route that answers wins. Every unrouted call is a 500, so a test that forgot a route\n * fails loudly rather than reading an empty answer. Three provider plugins wrote this same\n * dispatcher into their own tests before it lived here.\n *\n * ```ts\n * const users: FakeRoute = url => (url.pathname === '/Users' ? { body: [{ Name: 'station', Id: 'u1' }] } : undefined);\n * host.setFetchImpl(routedFetch([users, ...routes]));\n * ```\n */\nexport function routedFetch(routes: readonly FakeRoute[]): (url: string, init?: HostFetchInit) => Promise<Response> {\n return async (raw, init) => {\n const url = new URL(raw);\n let reply: FakeReply = { status: 500, body: 'unrouted' };\n for (const route of routes) {\n const found = route(url, init);\n if (found !== undefined) {\n reply = found;\n break;\n }\n }\n\n return fakeHostFetchResponse({\n status: reply.status,\n body: typeof reply.body === 'string' ? reply.body : JSON.stringify(reply.body),\n url: raw,\n });\n };\n}\n\n/** A socket that is open until either end closes it. Exported for a test that wants one on its own. */\nexport function createFakePluginSocket(url: string): FakePluginSocket {\n const messageListeners: ((text: string) => void)[] = [];\n const closeListeners: ((code?: number, reason?: string) => void)[] = [];\n const sent: string[] = [];\n let closed = false;\n\n const shut = (code: number | undefined, reason: string | undefined): void => {\n if (closed) return;\n closed = true;\n for (const listener of closeListeners) listener(code, reason);\n };\n\n return {\n url,\n sent,\n get closed() {\n return closed;\n },\n send: vi.fn((text: string) => {\n if (!closed) sent.push(text);\n }),\n close: vi.fn((code?: number, reason?: string) => shut(code ?? 1000, reason)),\n onMessage: listener => void messageListeners.push(listener),\n onClose: listener => void closeListeners.push(listener),\n sentJson: () => sent.map(text => JSON.parse(text) as unknown),\n receive(text) {\n if (closed) throw new Error(`fake plugin socket: ${url} is closed`);\n for (const listener of messageListeners) listener(text);\n },\n receiveJson(value) {\n this.receive(JSON.stringify(value));\n },\n closeFromServer(code = 1000, reason = '') {\n shut(code, reason);\n },\n };\n}\n\n/** A TLS connection that is open until either end closes it. Exported for a test that wants one on its own. */\nexport function createFakePluginTlsSocket(url: string, options?: PluginTlsOptions): FakePluginTlsSocket {\n const dataListeners: ((bytes: Uint8Array) => void)[] = [];\n const closeListeners: ((reason?: string) => void)[] = [];\n const sent: Uint8Array[] = [];\n let closed = false;\n\n const shut = (reason: string | undefined): void => {\n if (closed) return;\n closed = true;\n for (const listener of closeListeners) listener(reason);\n };\n\n return {\n url,\n options,\n sent,\n get closed() {\n return closed;\n },\n send: vi.fn((bytes: Uint8Array) => {\n if (!closed) sent.push(bytes);\n }),\n close: vi.fn(() => shut(undefined)),\n onData: listener => void dataListeners.push(listener),\n onClose: listener => void closeListeners.push(listener),\n receive(bytes) {\n if (closed) throw new Error(`fake plugin tls socket: ${url} is closed`);\n for (const listener of dataListeners) listener(bytes);\n },\n closeFromServer(reason) {\n shut(reason);\n },\n };\n}\n\nexport function createFakePluginHost(): FakePluginHost {\n const calls: RecordedFetchCall[] = [];\n const queue: Response[] = [];\n let customImpl: ((url: string, init?: HostFetchInit) => Promise<Response>) | undefined;\n\n const storageData = new Map<string, unknown>();\n let configData: Record<string, unknown> = {};\n const secretsData = new Map<string, string>();\n let tokens: Record<string, string> | undefined;\n let remainingMs = DEFAULT_REMAINING_MS;\n let fetchedTrack: ProviderStream | undefined = { url: 'http://127.0.0.1:3679/track/song-1?t=signed', expiresAt: 1_893_456_000_000 };\n // Nothing by default: a plugin should ask its own API first, and a test that means to exercise\n // the fetcher says so.\n let fetchedPlaylist: ProviderTrack[] | Error | undefined;\n const sockets: FakePluginSocket[] = [];\n let socketRefusal: Error | undefined;\n const tlsSockets: FakePluginTlsSocket[] = [];\n let tlsRefusal: Error | undefined;\n let tlsOpened: ((socket: FakePluginTlsSocket) => void) | undefined;\n let discovered: (query: DiscoveryQuery) => DiscoveredService[] = () => [];\n\n const fetchImpl = vi.fn(async (url: string, init?: HostFetchInit): Promise<Response> => {\n calls.push({ url, method: init?.method, headers: init?.headers, body: init?.body });\n if (customImpl) return customImpl(url, init);\n const next = queue.shift();\n if (!next) throw new Error(`fake plugin host: no response queued for ${init?.method ?? 'GET'} ${url}`);\n return next;\n });\n\n const host: FakePluginHost = {\n logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },\n fetch: fetchImpl,\n socket: vi.fn(async (url: string) => {\n if (socketRefusal !== undefined) throw socketRefusal;\n const socket = createFakePluginSocket(url);\n sockets.push(socket);\n return socket;\n }),\n tls: vi.fn(async (url: string, options?: PluginTlsOptions) => {\n if (tlsRefusal !== undefined) throw tlsRefusal;\n const socket = createFakePluginTlsSocket(url, options);\n tlsSockets.push(socket);\n tlsOpened?.(socket);\n return socket;\n }),\n discover: vi.fn(async (query: DiscoveryQuery) => discovered(query)),\n // Never aborts: these tests are about what a plugin does with a reply,\n // not about being cancelled half way through one.\n signal: new AbortController().signal,\n remainingMs: vi.fn(() => remainingMs),\n storage: {\n get: vi.fn(async (key: string) => storageData.get(key)),\n set: vi.fn(async (key: string, value: unknown) => {\n storageData.set(key, value);\n }),\n delete: vi.fn(async (key: string) => {\n storageData.delete(key);\n }),\n list: vi.fn(async (prefix?: string) => [...storageData.keys()].filter(key => !prefix || key.startsWith(prefix))),\n },\n secrets: { get: vi.fn(async (key: string) => secretsData.get(key)) },\n config: { get: vi.fn(async () => configData) },\n oauth: {\n getRedirectUri: vi.fn(async () => 'https://example.test/callback'),\n saveTokens: vi.fn(async (next: Record<string, string>) => {\n tokens = next;\n }),\n getTokens: vi.fn(async () => tokens),\n },\n events: { emit: vi.fn() },\n trackFetcher: {\n serve: vi.fn(async () => fetchedTrack),\n // Answers the same canned stream as `serve`: a plugin under test cares that it asked the\n // host for audio and passed the answer through, not which helper the station happens to run\n // behind it. `fetchedTrack` is what a test sets to say \"the station has a fetcher\" or, as\n // undefined, \"it has not got one\".\n servePlayer: vi.fn(async () => fetchedTrack),\n playlistTracks: vi.fn(async () => {\n if (fetchedPlaylist instanceof Error) throw fetchedPlaylist;\n return fetchedPlaylist;\n }),\n },\n calls,\n sockets,\n refuseSockets(error) {\n socketRefusal = error;\n },\n tlsSockets,\n refuseTls(error) {\n tlsRefusal = error;\n },\n onTlsOpen(listener) {\n tlsOpened = listener;\n },\n seedDiscovery(found) {\n discovered = typeof found === 'function' ? found : () => found;\n },\n seedRemainingMs(ms) {\n remainingMs = ms;\n },\n queueResponse(response: FakeResponseInit) {\n queue.push(fakeHostFetchResponse(response));\n },\n setFetchImpl(impl) {\n customImpl = impl;\n },\n seedConfig(config) {\n configData = config;\n },\n seedSecret(key, value) {\n secretsData.set(key, value);\n },\n seedStorage(key, value) {\n storageData.set(key, value);\n },\n getStorageEntry(key) {\n return storageData.get(key);\n },\n storageKeys() {\n return [...storageData.keys()];\n },\n seedTokens(next) {\n tokens = next;\n },\n getVaultTokens() {\n return tokens;\n },\n seedFetchedTrack(stream) {\n fetchedTrack = stream;\n },\n seedFetchedPlaylist(tracks) {\n fetchedPlaylist = tracks;\n },\n };\n\n return host;\n}\n"],"mappings":"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","names":["vi","DEFAULT_REMAINING_MS","NULL_BODY_STATUSES","Set","fakeHostFetchResponse","init","status","response","Response","has","body","statusText","headers","Object","defineProperty","value","url","routedFetch","routes","raw","URL","reply","route","found","undefined","JSON","stringify","createFakePluginSocket","messageListeners","closeListeners","sent","closed","shut","code","reason","listener","send","vi","fn","text","push","close","onMessage","onClose","sentJson","map","parse","receive","Error","receiveJson","closeFromServer","createFakePluginTlsSocket","options","dataListeners","bytes","onData","createFakePluginHost","calls","queue","customImpl","storageData","Map","configData","secretsData","tokens","remainingMs","fetchedTrack","expiresAt","fetchedPlaylist","sockets","socketRefusal","tlsSockets","tlsRefusal","tlsOpened","discovered","fetchImpl","method","next","shift","host","logger","debug","info","warn","error","fetch","socket","tls","discover","query","signal","AbortController","storage","get","key","set","delete","list","prefix","keys","filter","startsWith","secrets","config","oauth","getRedirectUri","saveTokens","getTokens","events","emit","trackFetcher","serve","servePlayer","playlistTracks","refuseSockets","refuseTls","onTlsOpen","seedDiscovery","seedRemainingMs","ms","queueResponse","setFetchImpl","impl","seedConfig","seedSecret","seedStorage","getStorageEntry","storageKeys","seedTokens","getVaultTokens","seedFetchedTrack","stream","seedFetchedPlaylist","tracks"]}
1
+ {"version":3,"sources":["../../src/testing/fake.plugin.host.ts"],"sourcesContent":["import { vi } from 'vitest';\n\nimport type {\n DiscoveredService,\n DiscoveryQuery,\n HostFetchInit,\n HostFetchMethod,\n PluginHost,\n PluginSocket,\n PluginTlsOptions,\n PluginTlsSocket,\n ProviderStream,\n ProviderTrack,\n} from '../index.js';\n\n/**\n * Shipped from the SDK rather than copied into each plugin, which three of them\n * were doing — two of those carrying a comment saying the copy was deliberate\n * because plugins do not depend on each other.\n *\n * That reasoning held for the copies and does not hold for this: every plugin\n * already depends on the SDK, and `PluginHost` is the SDK's own contract, so its\n * test double belongs beside the interface it doubles. A plugin importing this\n * gains no dependency on any other plugin. It also means a host method added to\n * the contract turns up in one fake rather than being added to three and\n * forgotten in the other three.\n *\n * Reached as `@deadair/plugin-sdk/testing`, which is kept out of the runtime\n * entry so nothing ships a dependency on vitest.\n */\n\n/** One `host.fetch` call, recorded with only the fields tests care about. */\nexport interface RecordedFetchCall {\n url: string;\n method: HostFetchMethod | undefined;\n headers: Record<string, string> | undefined;\n body: string | undefined;\n}\n\n/**\n * One `host.socket` the plugin opened, driven from the test: frames the plugin sent are in `sent`,\n * and `receive` / `closeFromServer` play the far end.\n */\nexport interface FakePluginSocket extends PluginSocket {\n readonly url: string;\n /** Every text frame the plugin sent, in order. */\n readonly sent: string[];\n /** Whether either end has closed it. */\n readonly closed: boolean;\n /** The frames the plugin sent, each parsed as JSON. */\n sentJson(): unknown[];\n /** Deliver one text frame to the plugin's listeners. */\n receive(text: string): void;\n /** Deliver `JSON.stringify(value)`. */\n receiveJson(value: unknown): void;\n /** Close it from the far end, telling the plugin's close listeners. */\n closeFromServer(code?: number, reason?: string): void;\n}\n\n/**\n * One `host.tls` connection the plugin opened, driven from the test: bytes the plugin wrote are in\n * `sent`, and `receive` / `closeFromServer` play the device.\n */\nexport interface FakePluginTlsSocket extends PluginTlsSocket {\n readonly url: string;\n /** The options the plugin opened it with. */\n readonly options: PluginTlsOptions | undefined;\n /** Every write the plugin made, in order, one entry per `send`. */\n readonly sent: Uint8Array[];\n /** Whether either end has closed it. */\n readonly closed: boolean;\n /** Deliver one chunk to the plugin's data listeners. */\n receive(bytes: Uint8Array): void;\n /** Close it from the far end, telling the plugin's close listeners. */\n closeFromServer(reason?: string): void;\n}\n\n/**\n * A `PluginHost` for tests. `fetch` replays a queue of scripted\n * `Response`s (FIFO, one per call) — or a custom handler installed\n * with `setFetchImpl`, for call-count-driven scenarios like a 401-then-200\n * retry — and records every call it received in `calls`. `storage`, `config`,\n * `secrets` and the `oauth` vault are in-memory and can be seeded or read\n * directly, bypassing the `PluginHost` methods, so a test can assert on state\n * without going through the code under test twice.\n */\nexport interface FakePluginHost extends PluginHost {\n /** Every `host.fetch` call so far, in call order. */\n readonly calls: RecordedFetchCall[];\n /** Set what `host.remainingMs()` reports, for testing budget shedding. */\n seedRemainingMs(ms: number): void;\n /** Push one scripted response onto the back of the reply queue. */\n queueResponse(response: FakeResponseInit): void;\n /** Replace the fetch handler outright; overrides the queue while set. */\n setFetchImpl(impl: (url: string, init?: HostFetchInit) => Promise<Response>): void;\n /** Seed the value `host.config.get()` resolves to. */\n seedConfig(config: Record<string, unknown>): void;\n /** Seed a value `host.secrets.get(key)` resolves to. */\n seedSecret(key: string, value: string): void;\n /** Seed a storage entry directly, bypassing `host.storage.set`. */\n seedStorage(key: string, value: unknown): void;\n /** Read a storage entry directly, bypassing `host.storage.get`. */\n getStorageEntry(key: string): unknown;\n /** All storage keys currently set, unfiltered. */\n storageKeys(): string[];\n /** Seed the oauth vault directly, bypassing `host.oauth.saveTokens`. */\n seedTokens(tokens: Record<string, string>): void;\n /** Read the oauth vault directly, bypassing `host.oauth.getTokens`. */\n getVaultTokens(): Record<string, string> | undefined;\n /** Set what `host.trackFetcher.serve()` answers with; `undefined` means \"this station has no fetcher\". */\n seedFetchedTrack(stream: ProviderStream | undefined): void;\n /**\n * Set what `host.trackFetcher.playlistTracks()` answers with. `undefined` means \"this station\n * has no fetcher\"; pass an `Error` to make it throw, which is the fetcher having tried and\n * failed and is a different case for a plugin to handle.\n */\n seedFetchedPlaylist(tracks: ProviderTrack[] | Error | undefined): void;\n /** Every socket `host.socket` opened, in order, closed ones included. */\n readonly sockets: FakePluginSocket[];\n /** Make every later `host.socket` reject with `error`; `undefined` lets them open again. */\n refuseSockets(error: Error | undefined): void;\n /** Every connection `host.tls` opened, in order, closed ones included. */\n readonly tlsSockets: FakePluginTlsSocket[];\n /** Make every later `host.tls` reject with `error`; `undefined` lets them open again. */\n refuseTls(error: Error | undefined): void;\n /**\n * Called with each connection as `host.tls` opens it, before the plugin sees it, so a test can\n * script the device's answers to whatever the plugin writes.\n */\n onTlsOpen(listener: ((socket: FakePluginTlsSocket) => void) | undefined): void;\n /**\n * What `host.discover` answers from now on: a fixed list, or one worked out per query. Nothing\n * is found until a test says otherwise, which is what a station on a bridge network sees.\n */\n seedDiscovery(found: DiscoveredService[] | ((query: DiscoveryQuery) => DiscoveredService[])): void;\n /**\n * Put the station on or off air, telling every `host.air` listener when it actually changed.\n * Off until a test says otherwise, as on a real station before it first decides. Synchronous,\n * where the real host tells listeners just after the change: a listener that returns a promise\n * is not awaited, so a test that cares awaits what the plugin started.\n */\n setOnAir(on: boolean): void;\n /** How many `host.air` listeners the plugin still holds, so a test can see one was let go of. */\n airListenerCount(): number;\n}\n\n/**\n * What `host.remainingMs()` reports unless a test says otherwise: what a live\n * call reads on its first line. The host's default per-call deadline is 8s, and\n * the clock has already been running for a few milliseconds by the time plugin\n * code asks, so a live call never sees the whole 8s. A plugin that refuses to\n * start unless a full request timeout is left therefore never starts at all,\n * and a default above the real deadline hid exactly that: every test passed\n * while every live call returned nothing.\n */\nconst DEFAULT_REMAINING_MS = 8_000 - 5;\n\n/**\n * What a test says a scripted reply should be.\n *\n * Not `ResponseInit`, because `url` is not on it: `host.fetch` reports the last\n * hop of the redirect chain there, and a `Response` built by hand has an empty\n * one unless it is defined in.\n */\nexport interface FakeResponseInit {\n status?: number;\n statusText?: string;\n headers?: Record<string, string>;\n /**\n * Text for a JSON or error reply, bytes for a binary one.\n *\n * `Uint8Array<ArrayBuffer>` rather than a bare `Uint8Array`: the DOM lib's\n * `BodyInit` accepts a view over a real `ArrayBuffer`, and the unparameterized\n * form widens to `ArrayBufferLike`, which includes `SharedArrayBuffer` and is\n * not something `Response` will take. `new Uint8Array([...])` already has the\n * narrow type, so no test has to say so.\n */\n body?: string | Uint8Array<ArrayBuffer>;\n url?: string;\n}\n\n/** Statuses whose `Response` must carry a null body, or the constructor throws. */\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/** Builds a default 200 `Response`, overridable field by field. */\nexport function fakeHostFetchResponse(init: FakeResponseInit = {}): Response {\n const status = init.status ?? 200;\n\n const response = new Response(NULL_BODY_STATUSES.has(status) ? null : (init.body ?? ''), {\n status,\n statusText: init.statusText ?? 'OK',\n headers: init.headers ?? { 'content-type': 'application/json' },\n });\n\n // Read-only on a constructed response, and part of what the host promises.\n Object.defineProperty(response, 'url', { value: init.url ?? 'https://example.test/' });\n\n return response;\n}\n\n/** What a {@link FakeRoute} answers: a status (200 when left out) and a body, sent as JSON unless it is already text. */\nexport interface FakeReply {\n status?: number;\n body: unknown;\n}\n\n/** One path a {@link routedFetch} knows, answering `undefined` for a URL that is not its own. */\nexport type FakeRoute = (url: URL, init?: HostFetchInit) => FakeReply | undefined;\n\n/**\n * A `setFetchImpl` that answers by route, for a plugin test that talks to several paths.\n *\n * The first route that answers wins. Every unrouted call is a 500, so a test that forgot a route\n * fails loudly rather than reading an empty answer. Three provider plugins wrote this same\n * dispatcher into their own tests before it lived here.\n *\n * ```ts\n * const users: FakeRoute = url => (url.pathname === '/Users' ? { body: [{ Name: 'station', Id: 'u1' }] } : undefined);\n * host.setFetchImpl(routedFetch([users, ...routes]));\n * ```\n */\nexport function routedFetch(routes: readonly FakeRoute[]): (url: string, init?: HostFetchInit) => Promise<Response> {\n return async (raw, init) => {\n const url = new URL(raw);\n let reply: FakeReply = { status: 500, body: 'unrouted' };\n for (const route of routes) {\n const found = route(url, init);\n if (found !== undefined) {\n reply = found;\n break;\n }\n }\n\n return fakeHostFetchResponse({\n status: reply.status,\n body: typeof reply.body === 'string' ? reply.body : JSON.stringify(reply.body),\n url: raw,\n });\n };\n}\n\n/** A socket that is open until either end closes it. Exported for a test that wants one on its own. */\nexport function createFakePluginSocket(url: string): FakePluginSocket {\n const messageListeners: ((text: string) => void)[] = [];\n const closeListeners: ((code?: number, reason?: string) => void)[] = [];\n const sent: string[] = [];\n let closed = false;\n\n const shut = (code: number | undefined, reason: string | undefined): void => {\n if (closed) return;\n closed = true;\n for (const listener of closeListeners) listener(code, reason);\n };\n\n return {\n url,\n sent,\n get closed() {\n return closed;\n },\n send: vi.fn((text: string) => {\n if (!closed) sent.push(text);\n }),\n close: vi.fn((code?: number, reason?: string) => shut(code ?? 1000, reason)),\n onMessage: listener => void messageListeners.push(listener),\n onClose: listener => void closeListeners.push(listener),\n sentJson: () => sent.map(text => JSON.parse(text) as unknown),\n receive(text) {\n if (closed) throw new Error(`fake plugin socket: ${url} is closed`);\n for (const listener of messageListeners) listener(text);\n },\n receiveJson(value) {\n this.receive(JSON.stringify(value));\n },\n closeFromServer(code = 1000, reason = '') {\n shut(code, reason);\n },\n };\n}\n\n/** A TLS connection that is open until either end closes it. Exported for a test that wants one on its own. */\nexport function createFakePluginTlsSocket(url: string, options?: PluginTlsOptions): FakePluginTlsSocket {\n const dataListeners: ((bytes: Uint8Array) => void)[] = [];\n const closeListeners: ((reason?: string) => void)[] = [];\n const sent: Uint8Array[] = [];\n let closed = false;\n\n const shut = (reason: string | undefined): void => {\n if (closed) return;\n closed = true;\n for (const listener of closeListeners) listener(reason);\n };\n\n return {\n url,\n options,\n sent,\n get closed() {\n return closed;\n },\n send: vi.fn((bytes: Uint8Array) => {\n if (!closed) sent.push(bytes);\n }),\n close: vi.fn(() => shut(undefined)),\n onData: listener => void dataListeners.push(listener),\n onClose: listener => void closeListeners.push(listener),\n receive(bytes) {\n if (closed) throw new Error(`fake plugin tls socket: ${url} is closed`);\n for (const listener of dataListeners) listener(bytes);\n },\n closeFromServer(reason) {\n shut(reason);\n },\n };\n}\n\nexport function createFakePluginHost(): FakePluginHost {\n const calls: RecordedFetchCall[] = [];\n const queue: Response[] = [];\n let customImpl: ((url: string, init?: HostFetchInit) => Promise<Response>) | undefined;\n\n const storageData = new Map<string, unknown>();\n let configData: Record<string, unknown> = {};\n const secretsData = new Map<string, string>();\n let tokens: Record<string, string> | undefined;\n let remainingMs = DEFAULT_REMAINING_MS;\n let fetchedTrack: ProviderStream | undefined = { url: 'http://127.0.0.1:3679/track/song-1?t=signed', expiresAt: 1_893_456_000_000 };\n // Nothing by default: a plugin should ask its own API first, and a test that means to exercise\n // the fetcher says so.\n let fetchedPlaylist: ProviderTrack[] | Error | undefined;\n const sockets: FakePluginSocket[] = [];\n let socketRefusal: Error | undefined;\n const tlsSockets: FakePluginTlsSocket[] = [];\n let tlsRefusal: Error | undefined;\n let tlsOpened: ((socket: FakePluginTlsSocket) => void) | undefined;\n let discovered: (query: DiscoveryQuery) => DiscoveredService[] = () => [];\n let onAir = false;\n const airListeners = new Set<(on: boolean) => void | Promise<void>>();\n\n const fetchImpl = vi.fn(async (url: string, init?: HostFetchInit): Promise<Response> => {\n calls.push({ url, method: init?.method, headers: init?.headers, body: init?.body });\n if (customImpl) return customImpl(url, init);\n const next = queue.shift();\n if (!next) throw new Error(`fake plugin host: no response queued for ${init?.method ?? 'GET'} ${url}`);\n return next;\n });\n\n const host: FakePluginHost = {\n logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },\n fetch: fetchImpl,\n socket: vi.fn(async (url: string) => {\n if (socketRefusal !== undefined) throw socketRefusal;\n const socket = createFakePluginSocket(url);\n sockets.push(socket);\n return socket;\n }),\n tls: vi.fn(async (url: string, options?: PluginTlsOptions) => {\n if (tlsRefusal !== undefined) throw tlsRefusal;\n const socket = createFakePluginTlsSocket(url, options);\n tlsSockets.push(socket);\n tlsOpened?.(socket);\n return socket;\n }),\n discover: vi.fn(async (query: DiscoveryQuery) => discovered(query)),\n // Never aborts: these tests are about what a plugin does with a reply,\n // not about being cancelled half way through one.\n signal: new AbortController().signal,\n remainingMs: vi.fn(() => remainingMs),\n storage: {\n get: vi.fn(async (key: string) => storageData.get(key)),\n set: vi.fn(async (key: string, value: unknown) => {\n storageData.set(key, value);\n }),\n delete: vi.fn(async (key: string) => {\n storageData.delete(key);\n }),\n list: vi.fn(async (prefix?: string) => [...storageData.keys()].filter(key => !prefix || key.startsWith(prefix))),\n },\n secrets: { get: vi.fn(async (key: string) => secretsData.get(key)) },\n config: { get: vi.fn(async () => configData) },\n oauth: {\n getRedirectUri: vi.fn(async () => 'https://example.test/callback'),\n saveTokens: vi.fn(async (next: Record<string, string>) => {\n tokens = next;\n }),\n getTokens: vi.fn(async () => tokens),\n },\n events: { emit: vi.fn() },\n air: {\n isOn: vi.fn(() => onAir),\n onChange: vi.fn((listener: (on: boolean) => void | Promise<void>) => {\n airListeners.add(listener);\n return () => {\n airListeners.delete(listener);\n };\n }),\n },\n trackFetcher: {\n serve: vi.fn(async () => fetchedTrack),\n // Answers the same canned stream as `serve`: a plugin under test cares that it asked the\n // host for audio and passed the answer through, not which helper the station happens to run\n // behind it. `fetchedTrack` is what a test sets to say \"the station has a fetcher\" or, as\n // undefined, \"it has not got one\".\n servePlayer: vi.fn(async () => fetchedTrack),\n playlistTracks: vi.fn(async () => {\n if (fetchedPlaylist instanceof Error) throw fetchedPlaylist;\n return fetchedPlaylist;\n }),\n },\n calls,\n sockets,\n refuseSockets(error) {\n socketRefusal = error;\n },\n tlsSockets,\n refuseTls(error) {\n tlsRefusal = error;\n },\n onTlsOpen(listener) {\n tlsOpened = listener;\n },\n seedDiscovery(found) {\n discovered = typeof found === 'function' ? found : () => found;\n },\n setOnAir(on) {\n if (on === onAir) return;\n onAir = on;\n for (const listener of [...airListeners]) void listener(on);\n },\n airListenerCount() {\n return airListeners.size;\n },\n seedRemainingMs(ms) {\n remainingMs = ms;\n },\n queueResponse(response: FakeResponseInit) {\n queue.push(fakeHostFetchResponse(response));\n },\n setFetchImpl(impl) {\n customImpl = impl;\n },\n seedConfig(config) {\n configData = config;\n },\n seedSecret(key, value) {\n secretsData.set(key, value);\n },\n seedStorage(key, value) {\n storageData.set(key, value);\n },\n getStorageEntry(key) {\n return storageData.get(key);\n },\n storageKeys() {\n return [...storageData.keys()];\n },\n seedTokens(next) {\n tokens = next;\n },\n getVaultTokens() {\n return tokens;\n },\n seedFetchedTrack(stream) {\n fetchedTrack = stream;\n },\n seedFetchedPlaylist(tracks) {\n fetchedPlaylist = tracks;\n },\n };\n\n return host;\n}\n"],"mappings":";;;;;AAAA,SAASA,UAAU;AA2JnB,IAAMC,uBAAuB,MAAQ;AA2BrC,IAAMC,qBAAqB,oBAAIC,IAAI;EAAC;EAAK;EAAK;EAAK;CAAI;AAGhD,SAASC,sBAAsBC,OAAyB,CAAC,GAAC;AAC7D,QAAMC,SAASD,KAAKC,UAAU;AAE9B,QAAMC,WAAW,IAAIC,SAASN,mBAAmBO,IAAIH,MAAAA,IAAU,OAAQD,KAAKK,QAAQ,IAAK;IACrFJ;IACAK,YAAYN,KAAKM,cAAc;IAC/BC,SAASP,KAAKO,WAAW;MAAE,gBAAgB;IAAmB;EAClE,CAAA;AAGAC,SAAOC,eAAeP,UAAU,OAAO;IAAEQ,OAAOV,KAAKW,OAAO;EAAwB,CAAA;AAEpF,SAAOT;AACX;AAbgBH;AAoCT,SAASa,YAAYC,QAA4B;AACpD,SAAO,OAAOC,KAAKd,SAAAA;AACf,UAAMW,MAAM,IAAII,IAAID,GAAAA;AACpB,QAAIE,QAAmB;MAAEf,QAAQ;MAAKI,MAAM;IAAW;AACvD,eAAWY,SAASJ,QAAQ;AACxB,YAAMK,QAAQD,MAAMN,KAAKX,IAAAA;AACzB,UAAIkB,UAAUC,QAAW;AACrBH,gBAAQE;AACR;MACJ;IACJ;AAEA,WAAOnB,sBAAsB;MACzBE,QAAQe,MAAMf;MACdI,MAAM,OAAOW,MAAMX,SAAS,WAAWW,MAAMX,OAAOe,KAAKC,UAAUL,MAAMX,IAAI;MAC7EM,KAAKG;IACT,CAAA;EACJ;AACJ;AAlBgBF;AAqBT,SAASU,uBAAuBX,KAAW;AAC9C,QAAMY,mBAA+C,CAAA;AACrD,QAAMC,iBAA+D,CAAA;AACrE,QAAMC,OAAiB,CAAA;AACvB,MAAIC,SAAS;AAEb,QAAMC,OAAO,wBAACC,MAA0BC,WAAAA;AACpC,QAAIH,OAAQ;AACZA,aAAS;AACT,eAAWI,YAAYN,eAAgBM,UAASF,MAAMC,MAAAA;EAC1D,GAJa;AAMb,SAAO;IACHlB;IACAc;IACA,IAAIC,SAAAA;AACA,aAAOA;IACX;IACAK,MAAMC,GAAGC,GAAG,CAACC,SAAAA;AACT,UAAI,CAACR,OAAQD,MAAKU,KAAKD,IAAAA;IAC3B,CAAA;IACAE,OAAOJ,GAAGC,GAAG,CAACL,MAAeC,WAAoBF,KAAKC,QAAQ,KAAMC,MAAAA,CAAAA;IACpEQ,WAAWP,wBAAAA,aAAY,KAAKP,iBAAiBY,KAAKL,QAAAA,GAAvCA;IACXQ,SAASR,wBAAAA,aAAY,KAAKN,eAAeW,KAAKL,QAAAA,GAArCA;IACTS,UAAU,6BAAMd,KAAKe,IAAIN,CAAAA,SAAQd,KAAKqB,MAAMP,IAAAA,CAAAA,GAAlC;IACVQ,QAAQR,MAAI;AACR,UAAIR,OAAQ,OAAM,IAAIiB,MAAM,uBAAuBhC,GAAAA,YAAe;AAClE,iBAAWmB,YAAYP,iBAAkBO,UAASI,IAAAA;IACtD;IACAU,YAAYlC,OAAK;AACb,WAAKgC,QAAQtB,KAAKC,UAAUX,KAAAA,CAAAA;IAChC;IACAmC,gBAAgBjB,OAAO,KAAMC,SAAS,IAAE;AACpCF,WAAKC,MAAMC,MAAAA;IACf;EACJ;AACJ;AApCgBP;AAuCT,SAASwB,0BAA0BnC,KAAaoC,SAA0B;AAC7E,QAAMC,gBAAiD,CAAA;AACvD,QAAMxB,iBAAgD,CAAA;AACtD,QAAMC,OAAqB,CAAA;AAC3B,MAAIC,SAAS;AAEb,QAAMC,OAAO,wBAACE,WAAAA;AACV,QAAIH,OAAQ;AACZA,aAAS;AACT,eAAWI,YAAYN,eAAgBM,UAASD,MAAAA;EACpD,GAJa;AAMb,SAAO;IACHlB;IACAoC;IACAtB;IACA,IAAIC,SAAAA;AACA,aAAOA;IACX;IACAK,MAAMC,GAAGC,GAAG,CAACgB,UAAAA;AACT,UAAI,CAACvB,OAAQD,MAAKU,KAAKc,KAAAA;IAC3B,CAAA;IACAb,OAAOJ,GAAGC,GAAG,MAAMN,KAAKR,MAAAA,CAAAA;IACxB+B,QAAQpB,wBAAAA,aAAY,KAAKkB,cAAcb,KAAKL,QAAAA,GAApCA;IACRQ,SAASR,wBAAAA,aAAY,KAAKN,eAAeW,KAAKL,QAAAA,GAArCA;IACTY,QAAQO,OAAK;AACT,UAAIvB,OAAQ,OAAM,IAAIiB,MAAM,2BAA2BhC,GAAAA,YAAe;AACtE,iBAAWmB,YAAYkB,cAAelB,UAASmB,KAAAA;IACnD;IACAJ,gBAAgBhB,QAAM;AAClBF,WAAKE,MAAAA;IACT;EACJ;AACJ;AAjCgBiB;AAmCT,SAASK,uBAAAA;AACZ,QAAMC,QAA6B,CAAA;AACnC,QAAMC,QAAoB,CAAA;AAC1B,MAAIC;AAEJ,QAAMC,cAAc,oBAAIC,IAAAA;AACxB,MAAIC,aAAsC,CAAC;AAC3C,QAAMC,cAAc,oBAAIF,IAAAA;AACxB,MAAIG;AACJ,MAAIC,cAAchE;AAClB,MAAIiE,eAA2C;IAAElD,KAAK;IAA+CmD,WAAW;EAAkB;AAGlI,MAAIC;AACJ,QAAMC,UAA8B,CAAA;AACpC,MAAIC;AACJ,QAAMC,aAAoC,CAAA;AAC1C,MAAIC;AACJ,MAAIC;AACJ,MAAIC,aAA6D,6BAAM,CAAA,GAAN;AACjE,MAAIC,QAAQ;AACZ,QAAMC,eAAe,oBAAIzE,IAAAA;AAEzB,QAAM0E,YAAYxC,GAAGC,GAAG,OAAOtB,KAAaX,SAAAA;AACxCoD,UAAMjB,KAAK;MAAExB;MAAK8D,QAAQzE,MAAMyE;MAAQlE,SAASP,MAAMO;MAASF,MAAML,MAAMK;IAAK,CAAA;AACjF,QAAIiD,WAAY,QAAOA,WAAW3C,KAAKX,IAAAA;AACvC,UAAM0E,OAAOrB,MAAMsB,MAAK;AACxB,QAAI,CAACD,KAAM,OAAM,IAAI/B,MAAM,4CAA4C3C,MAAMyE,UAAU,KAAA,IAAS9D,GAAAA,EAAK;AACrG,WAAO+D;EACX,CAAA;AAEA,QAAME,OAAuB;IACzBC,QAAQ;MAAEC,OAAO9C,GAAGC,GAAE;MAAI8C,MAAM/C,GAAGC,GAAE;MAAI+C,MAAMhD,GAAGC,GAAE;MAAIgD,OAAOjD,GAAGC,GAAE;IAAG;IACvEiD,OAAOV;IACPW,QAAQnD,GAAGC,GAAG,OAAOtB,QAAAA;AACjB,UAAIsD,kBAAkB9C,OAAW,OAAM8C;AACvC,YAAMkB,SAAS7D,uBAAuBX,GAAAA;AACtCqD,cAAQ7B,KAAKgD,MAAAA;AACb,aAAOA;IACX,CAAA;IACAC,KAAKpD,GAAGC,GAAG,OAAOtB,KAAaoC,YAAAA;AAC3B,UAAIoB,eAAehD,OAAW,OAAMgD;AACpC,YAAMgB,SAASrC,0BAA0BnC,KAAKoC,OAAAA;AAC9CmB,iBAAW/B,KAAKgD,MAAAA;AAChBf,kBAAYe,MAAAA;AACZ,aAAOA;IACX,CAAA;IACAE,UAAUrD,GAAGC,GAAG,OAAOqD,UAA0BjB,WAAWiB,KAAAA,CAAAA;;;IAG5DC,QAAQ,IAAIC,gBAAAA,EAAkBD;IAC9B3B,aAAa5B,GAAGC,GAAG,MAAM2B,WAAAA;IACzB6B,SAAS;MACLC,KAAK1D,GAAGC,GAAG,OAAO0D,QAAgBpC,YAAYmC,IAAIC,GAAAA,CAAAA;MAClDC,KAAK5D,GAAGC,GAAG,OAAO0D,KAAajF,UAAAA;AAC3B6C,oBAAYqC,IAAID,KAAKjF,KAAAA;MACzB,CAAA;MACAmF,QAAQ7D,GAAGC,GAAG,OAAO0D,QAAAA;AACjBpC,oBAAYsC,OAAOF,GAAAA;MACvB,CAAA;MACAG,MAAM9D,GAAGC,GAAG,OAAO8D,WAAoB;WAAIxC,YAAYyC,KAAI;QAAIC,OAAON,CAAAA,QAAO,CAACI,UAAUJ,IAAIO,WAAWH,MAAAA,CAAAA,CAAAA;IAC3G;IACAI,SAAS;MAAET,KAAK1D,GAAGC,GAAG,OAAO0D,QAAgBjC,YAAYgC,IAAIC,GAAAA,CAAAA;IAAM;IACnES,QAAQ;MAAEV,KAAK1D,GAAGC,GAAG,YAAYwB,UAAAA;IAAY;IAC7C4C,OAAO;MACHC,gBAAgBtE,GAAGC,GAAG,YAAY,+BAAA;MAClCsE,YAAYvE,GAAGC,GAAG,OAAOyC,SAAAA;AACrBf,iBAASe;MACb,CAAA;MACA8B,WAAWxE,GAAGC,GAAG,YAAY0B,MAAAA;IACjC;IACA8C,QAAQ;MAAEC,MAAM1E,GAAGC,GAAE;IAAG;IACxB0E,KAAK;MACDC,MAAM5E,GAAGC,GAAG,MAAMqC,KAAAA;MAClBuC,UAAU7E,GAAGC,GAAG,CAACH,aAAAA;AACbyC,qBAAauC,IAAIhF,QAAAA;AACjB,eAAO,MAAA;AACHyC,uBAAasB,OAAO/D,QAAAA;QACxB;MACJ,CAAA;IACJ;IACAiF,cAAc;MACVC,OAAOhF,GAAGC,GAAG,YAAY4B,YAAAA;;;;;MAKzBoD,aAAajF,GAAGC,GAAG,YAAY4B,YAAAA;MAC/BqD,gBAAgBlF,GAAGC,GAAG,YAAA;AAClB,YAAI8B,2BAA2BpB,MAAO,OAAMoB;AAC5C,eAAOA;MACX,CAAA;IACJ;IACAX;IACAY;IACAmD,cAAclC,OAAK;AACfhB,sBAAgBgB;IACpB;IACAf;IACAkD,UAAUnC,OAAK;AACXd,mBAAac;IACjB;IACAoC,UAAUvF,UAAQ;AACdsC,kBAAYtC;IAChB;IACAwF,cAAcpG,OAAK;AACfmD,mBAAa,OAAOnD,UAAU,aAAaA,QAAQ,MAAMA;IAC7D;IACAqG,SAASC,IAAE;AACP,UAAIA,OAAOlD,MAAO;AAClBA,cAAQkD;AACR,iBAAW1F,YAAY;WAAIyC;QAAe,MAAKzC,SAAS0F,EAAAA;IAC5D;IACAC,mBAAAA;AACI,aAAOlD,aAAamD;IACxB;IACAC,gBAAgBC,IAAE;AACdhE,oBAAcgE;IAClB;IACAC,cAAc3H,UAA0B;AACpCmD,YAAMlB,KAAKpC,sBAAsBG,QAAAA,CAAAA;IACrC;IACA4H,aAAaC,MAAI;AACbzE,mBAAayE;IACjB;IACAC,WAAW5B,QAAM;AACb3C,mBAAa2C;IACjB;IACA6B,WAAWtC,KAAKjF,OAAK;AACjBgD,kBAAYkC,IAAID,KAAKjF,KAAAA;IACzB;IACAwH,YAAYvC,KAAKjF,OAAK;AAClB6C,kBAAYqC,IAAID,KAAKjF,KAAAA;IACzB;IACAyH,gBAAgBxC,KAAG;AACf,aAAOpC,YAAYmC,IAAIC,GAAAA;IAC3B;IACAyC,cAAAA;AACI,aAAO;WAAI7E,YAAYyC,KAAI;;IAC/B;IACAqC,WAAW3D,MAAI;AACXf,eAASe;IACb;IACA4D,iBAAAA;AACI,aAAO3E;IACX;IACA4E,iBAAiBC,QAAM;AACnB3E,qBAAe2E;IACnB;IACAC,oBAAoBC,QAAM;AACtB3E,wBAAkB2E;IACtB;EACJ;AAEA,SAAO9D;AACX;AA3JgBzB;","names":["vi","DEFAULT_REMAINING_MS","NULL_BODY_STATUSES","Set","fakeHostFetchResponse","init","status","response","Response","has","body","statusText","headers","Object","defineProperty","value","url","routedFetch","routes","raw","URL","reply","route","found","undefined","JSON","stringify","createFakePluginSocket","messageListeners","closeListeners","sent","closed","shut","code","reason","listener","send","vi","fn","text","push","close","onMessage","onClose","sentJson","map","parse","receive","Error","receiveJson","closeFromServer","createFakePluginTlsSocket","options","dataListeners","bytes","onData","createFakePluginHost","calls","queue","customImpl","storageData","Map","configData","secretsData","tokens","remainingMs","fetchedTrack","expiresAt","fetchedPlaylist","sockets","socketRefusal","tlsSockets","tlsRefusal","tlsOpened","discovered","onAir","airListeners","fetchImpl","method","next","shift","host","logger","debug","info","warn","error","fetch","socket","tls","discover","query","signal","AbortController","storage","get","key","set","delete","list","prefix","keys","filter","startsWith","secrets","config","oauth","getRedirectUri","saveTokens","getTokens","events","emit","air","isOn","onChange","add","trackFetcher","serve","servePlayer","playlistTracks","refuseSockets","refuseTls","onTlsOpen","seedDiscovery","setOnAir","on","airListenerCount","size","seedRemainingMs","ms","queueResponse","setFetchImpl","impl","seedConfig","seedSecret","seedStorage","getStorageEntry","storageKeys","seedTokens","getVaultTokens","seedFetchedTrack","stream","seedFetchedPlaylist","tracks"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deadair/plugin-sdk",
3
- "version": "0.79.0",
3
+ "version": "0.81.0",
4
4
  "description": "The contract a deadair plugin is written against: the manifest, the capabilities, the host it is handed, and a fake host for its tests.",
5
5
  "keywords": [
6
6
  "deadair",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "vitest": "^5.0.3",
50
- "zod": "^4.6.5",
50
+ "zod": "^4.6.6",
51
51
  "@repo/config-eslint": "0.0.0",
52
52
  "@repo/config-typescript": "0.0.0"
53
53
  },