@voiceinput/provider 0.1.0-beta.1

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.
package/dist/test.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { VoiceInputError, VoiceInputProviderV1, VoiceInputProviderV1CallOptions, VoiceInputProviderV1StreamPart, VoiceTranscriptionOptions } from "./index.js";
2
+ //#region src/test.d.ts
3
+ interface FakeVoiceInputProviderOptions {
4
+ provider?: string;
5
+ modelId?: string;
6
+ sampleRate?: number;
7
+ autoOpen?: boolean;
8
+ autoCloseOnFinish?: boolean;
9
+ validateOptions?: (options: VoiceTranscriptionOptions) => void;
10
+ }
11
+ interface FakeVoiceInputProviderSessionSnapshot {
12
+ readonly callOptions: VoiceInputProviderV1CallOptions;
13
+ readonly audioChunks: readonly Int16Array[];
14
+ readonly finishCallCount: number;
15
+ readonly abortCallCount: number;
16
+ readonly closed: boolean;
17
+ readonly aborted: boolean;
18
+ }
19
+ interface FakeVoiceInputProviderController {
20
+ readonly sessions: readonly FakeVoiceInputProviderSessionSnapshot[];
21
+ waitForSession(index?: number): Promise<FakeVoiceInputProviderSessionSnapshot>;
22
+ resolveOpen(index?: number): void;
23
+ rejectOpen(error: unknown, index?: number): void;
24
+ emit(part: VoiceInputProviderV1StreamPart, index?: number): void;
25
+ close(index?: number): void;
26
+ fail(error: VoiceInputError, index?: number): void;
27
+ timeout(index?: number): PromiseLike<void> | void;
28
+ }
29
+ interface FakeVoiceInputProvider {
30
+ provider: VoiceInputProviderV1;
31
+ controller: FakeVoiceInputProviderController;
32
+ }
33
+ declare function createFakeVoiceInputProvider(options?: FakeVoiceInputProviderOptions): FakeVoiceInputProvider;
34
+ interface VoiceInputProviderV1ConformanceHarness {
35
+ provider: VoiceInputProviderV1;
36
+ controller: FakeVoiceInputProviderController;
37
+ }
38
+ interface VoiceInputProviderV1ConformanceCase {
39
+ name: string;
40
+ run(): Promise<void>;
41
+ }
42
+ interface VoiceInputProviderV1ConformanceOptions {
43
+ createHarness(): VoiceInputProviderV1ConformanceHarness;
44
+ createAccumulatorSession(provider: VoiceInputProviderV1, audioSource: {
45
+ prepare(): PromiseLike<{
46
+ readonly stream: ReadableStream<Int16Array>;
47
+ start(): PromiseLike<void> | void;
48
+ stop(): PromiseLike<void> | void;
49
+ abort(reason?: unknown): void;
50
+ }>;
51
+ }): {
52
+ getSnapshot(): {
53
+ readonly finalTranscript: string;
54
+ };
55
+ start(): PromiseLike<void> | void;
56
+ cancel(): PromiseLike<void> | void;
57
+ };
58
+ errorTaxonomy: {
59
+ readonly createProvider?: () => VoiceInputProviderV1;
60
+ readonly createUnsupportedBrowserProvider: () => VoiceInputProviderV1;
61
+ readonly invalidOptions: VoiceTranscriptionOptions;
62
+ readonly malformedUnsupportedOptions: VoiceTranscriptionOptions;
63
+ readonly unsupportedOptions: VoiceTranscriptionOptions;
64
+ };
65
+ }
66
+ declare class VoiceInputProviderConformanceError extends Error {
67
+ constructor(message: string);
68
+ }
69
+ declare function createVoiceInputProviderV1ConformanceCases(options: VoiceInputProviderV1ConformanceOptions): readonly VoiceInputProviderV1ConformanceCase[];
70
+ //#endregion
71
+ export { FakeVoiceInputProvider, FakeVoiceInputProviderController, FakeVoiceInputProviderOptions, FakeVoiceInputProviderSessionSnapshot, VoiceInputProviderConformanceError, VoiceInputProviderV1ConformanceCase, VoiceInputProviderV1ConformanceHarness, VoiceInputProviderV1ConformanceOptions, createFakeVoiceInputProvider, createVoiceInputProviderV1ConformanceCases };
72
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","names":[],"sources":["../src/test.ts"],"mappings":";;UASiB;EACf;EACA;EACA;EACA;EACA;EACA,mBAAmB,SAAS;;UAGb;WACN,aAAa;WACb,sBAAsB;WACtB;WACA;WACA;WACA;;UAGM;WACN,mBAAmB;EAC5B,eACE,iBACC,QAAQ;EACX,YAAY;EACZ,WAAW,gBAAgB;EAC3B,KAAK,MAAM,gCAAgC;EAC3C,MAAM;EACN,KAAK,OAAO,iBAAiB;EAC7B,QAAQ,iBAAiB;;UAGV;EACf,UAAU;EACV,YAAY;;iBAoBE,6BACd,UAAS,gCACR;UA6Oc;EACf,UAAU;EACV,YAAY;;UAGG;EACf;EACA,OAAO;;UAGQ;EACf,iBAAiB;EACjB,yBACE,UAAU,sBACV;IACE,WAAW;eACA,QAAQ,eAAe;MAChC,SAAS;MACT,QAAQ;MACR,MAAM;;;IAIV;eAA0B;;IAC1B,SAAS;IACT,UAAU;;EAEZ;aACW,uBAAuB;aACvB,wCAAwC;aACxC,gBAAgB;aAChB,6BAA6B;aAC7B,oBAAoB;;;cAIpB,2CAA2C;EACtD,YAAY;;iBAwEE,2CACd,SAAS,kDACC"}
package/dist/test.js ADDED
@@ -0,0 +1,474 @@
1
+ import { VoiceInputError } from "./index.js";
2
+ //#region src/test.ts
3
+ function createFakeVoiceInputProvider(options = {}) {
4
+ const sessions = [];
5
+ const waiters = /* @__PURE__ */ new Map();
6
+ const autoOpen = options.autoOpen ?? true;
7
+ const autoCloseOnFinish = options.autoCloseOnFinish ?? true;
8
+ const getSession = (index = sessions.length - 1) => {
9
+ const session = sessions[index];
10
+ if (session === void 0) throw new Error(`Fake provider session ${index} does not exist.`);
11
+ return session;
12
+ };
13
+ const getSnapshot = (session) => Object.freeze({
14
+ callOptions: Object.freeze({ ...session.callOptions }),
15
+ audioChunks: Object.freeze([...session.audioChunks]),
16
+ finishCallCount: session.finishCallCount,
17
+ abortCallCount: session.abortCallCount,
18
+ closed: session.closed,
19
+ aborted: session.aborted
20
+ });
21
+ const close = (session) => {
22
+ if (session.closed) return;
23
+ session.closed = true;
24
+ session.streamController.close();
25
+ };
26
+ const emit = (part, index) => {
27
+ const session = getSession(index);
28
+ if (session.closed) return;
29
+ if (part.type === "interim" || part.type === "final") {
30
+ session.streamController.enqueue({
31
+ ...part,
32
+ segmentId: part.segmentId ?? `fake:${session.segmentSequence}`
33
+ });
34
+ if (part.type === "final") session.segmentSequence += 1;
35
+ } else session.streamController.enqueue(part);
36
+ };
37
+ const controller = {
38
+ get sessions() {
39
+ return Object.freeze(sessions.map(getSnapshot));
40
+ },
41
+ async waitForSession(index = 0) {
42
+ const session = sessions[index];
43
+ if (session !== void 0) return getSnapshot(session);
44
+ return await new Promise((resolve) => {
45
+ const existingWaiters = waiters.get(index) ?? [];
46
+ existingWaiters.push(resolve);
47
+ waiters.set(index, existingWaiters);
48
+ });
49
+ },
50
+ resolveOpen(index) {
51
+ getSession(index).open();
52
+ },
53
+ rejectOpen(error, index) {
54
+ getSession(index).reject(error);
55
+ },
56
+ emit,
57
+ close(index) {
58
+ close(getSession(index));
59
+ },
60
+ fail(error, index) {
61
+ emit({
62
+ type: "error",
63
+ error
64
+ }, index);
65
+ close(getSession(index));
66
+ },
67
+ timeout(index) {
68
+ const session = getSession(index);
69
+ emit({
70
+ type: "error",
71
+ error: new VoiceInputError({
72
+ code: "network-error",
73
+ message: "The fake provider timed out.",
74
+ provider: options.provider ?? "fake",
75
+ retryable: true
76
+ })
77
+ }, index);
78
+ close(session);
79
+ }
80
+ };
81
+ return {
82
+ provider: {
83
+ specificationVersion: "v1",
84
+ provider: options.provider ?? "fake",
85
+ modelId: options.modelId ?? "fake-model",
86
+ sampleRate: options.sampleRate ?? 16e3,
87
+ validateOptions: options.validateOptions ?? (() => {}),
88
+ doOpen(callOptions) {
89
+ let streamController;
90
+ const stream = new ReadableStream({ start(controller_) {
91
+ streamController = controller_;
92
+ } });
93
+ if (streamController === void 0) throw new Error("The fake provider stream did not initialize.");
94
+ let resolveOpen = () => {};
95
+ let rejectOpen = () => {};
96
+ const openPromise = new Promise((resolve, reject) => {
97
+ resolveOpen = resolve;
98
+ rejectOpen = reject;
99
+ });
100
+ const mutableSession = {
101
+ segmentSequence: 0,
102
+ callOptions,
103
+ audioChunks: [],
104
+ finishCallCount: 0,
105
+ abortCallCount: 0,
106
+ closed: false,
107
+ aborted: false,
108
+ finished: false,
109
+ opened: false,
110
+ settled: false,
111
+ streamController,
112
+ publicSession: void 0,
113
+ open() {
114
+ if (mutableSession.settled) return;
115
+ mutableSession.opened = true;
116
+ mutableSession.settled = true;
117
+ resolveOpen(mutableSession.publicSession);
118
+ },
119
+ reject(error) {
120
+ if (mutableSession.settled) return;
121
+ mutableSession.settled = true;
122
+ close(mutableSession);
123
+ rejectOpen(error);
124
+ }
125
+ };
126
+ const publicSession = {
127
+ stream,
128
+ sendAudio(chunk) {
129
+ if (!mutableSession.closed && !mutableSession.aborted) mutableSession.audioChunks.push(chunk);
130
+ },
131
+ finish() {
132
+ if (mutableSession.closed || mutableSession.aborted || mutableSession.finished) return;
133
+ mutableSession.finished = true;
134
+ mutableSession.finishCallCount += 1;
135
+ if (autoCloseOnFinish) close(mutableSession);
136
+ },
137
+ abort(reason) {
138
+ if (mutableSession.closed || mutableSession.aborted) return;
139
+ mutableSession.aborted = true;
140
+ mutableSession.abortCallCount += 1;
141
+ close(mutableSession);
142
+ if (!mutableSession.settled) {
143
+ mutableSession.settled = true;
144
+ rejectOpen(reason);
145
+ }
146
+ }
147
+ };
148
+ mutableSession.publicSession = publicSession;
149
+ const index = sessions.push(mutableSession) - 1;
150
+ for (const resolve of waiters.get(index) ?? []) resolve(getSnapshot(mutableSession));
151
+ waiters.delete(index);
152
+ callOptions.abortSignal.addEventListener("abort", () => publicSession.abort(callOptions.abortSignal.reason), { once: true });
153
+ if (callOptions.abortSignal.aborted) publicSession.abort(callOptions.abortSignal.reason);
154
+ else if (autoOpen) mutableSession.open();
155
+ return openPromise;
156
+ }
157
+ },
158
+ controller
159
+ };
160
+ }
161
+ var VoiceInputProviderConformanceError = class extends Error {
162
+ constructor(message) {
163
+ super(message);
164
+ this.name = "VoiceInputProviderConformanceError";
165
+ }
166
+ };
167
+ function assert(condition, message) {
168
+ if (!condition) throw new VoiceInputProviderConformanceError(message);
169
+ }
170
+ async function readStream(stream) {
171
+ const parts = [];
172
+ for await (const part of stream) parts.push(part);
173
+ return parts;
174
+ }
175
+ async function openSession(harness) {
176
+ const sessionPromise = Promise.resolve().then(() => harness.provider.doOpen({ abortSignal: new AbortController().signal }));
177
+ await harness.controller.waitForSession();
178
+ harness.controller.resolveOpen();
179
+ return await sessionPromise;
180
+ }
181
+ function createConformanceAudioSource() {
182
+ return { prepare() {
183
+ let closed = false;
184
+ let streamController;
185
+ const stream = new ReadableStream({ start(controller) {
186
+ streamController = controller;
187
+ } });
188
+ const close = () => {
189
+ if (!closed) {
190
+ closed = true;
191
+ streamController?.close();
192
+ }
193
+ };
194
+ return Promise.resolve({
195
+ stream,
196
+ start() {},
197
+ stop: close,
198
+ abort: close
199
+ });
200
+ } };
201
+ }
202
+ async function waitFor(condition, message) {
203
+ for (let attempt = 0; attempt < 20; attempt += 1) {
204
+ if (condition()) return;
205
+ await new Promise((resolve) => setTimeout(resolve, 0));
206
+ }
207
+ throw new VoiceInputProviderConformanceError(message);
208
+ }
209
+ function createVoiceInputProviderV1ConformanceCases(options) {
210
+ return [
211
+ {
212
+ name: "declares valid v1 metadata and accepts baseline options",
213
+ async run() {
214
+ const { provider } = options.createHarness();
215
+ assert(provider.specificationVersion === "v1", "Expected v1 spec.");
216
+ assert(provider.provider.length > 0, "Provider ID must not be empty.");
217
+ assert(provider.modelId.length > 0, "Model ID must not be empty.");
218
+ assert(Number.isInteger(provider.sampleRate) && provider.sampleRate > 0, "Sample rate must be a positive integer.");
219
+ provider.validateOptions({});
220
+ }
221
+ },
222
+ {
223
+ name: "streams normalized parts in order and closes terminally",
224
+ async run() {
225
+ const harness = options.createHarness();
226
+ const session = await openSession(harness);
227
+ const partsPromise = readStream(session.stream);
228
+ harness.controller.emit({ type: "speech-start" });
229
+ harness.controller.emit({
230
+ type: "interim",
231
+ text: "hel"
232
+ });
233
+ harness.controller.emit({
234
+ type: "interim",
235
+ text: "hello"
236
+ });
237
+ harness.controller.emit({
238
+ type: "final",
239
+ text: "hello"
240
+ });
241
+ harness.controller.emit({ type: "speech-end" });
242
+ harness.controller.close();
243
+ const parts = await partsPromise;
244
+ const transcripts = parts.filter((part) => part.type === "interim" || part.type === "final");
245
+ assert(transcripts.every((part) => typeof part.segmentId === "string" && part.segmentId.length > 0), "Normalized transcript parts must include a non-empty segmentId.");
246
+ assert(new Set(transcripts.map((part) => part.segmentId)).size === 1, "Interim revisions and their final must keep the same segmentId.");
247
+ const partTypes = parts.map((part) => part.type);
248
+ assert(partTypes.join(",") === "speech-start,interim,interim,final,speech-end", `Unexpected stream order: ${partTypes.join(",")}`);
249
+ harness.controller.emit({
250
+ type: "final",
251
+ text: "late"
252
+ });
253
+ const lateReader = session.stream.getReader();
254
+ const lateResult = await lateReader.read();
255
+ lateReader.releaseLock();
256
+ assert(lateResult.done, `Output must not be emitted after closure: ${JSON.stringify(lateResult)}.`);
257
+ }
258
+ },
259
+ {
260
+ name: "accepts audio chunks and finishes idempotently",
261
+ async run() {
262
+ const harness = options.createHarness();
263
+ const session = await openSession(harness);
264
+ await session.sendAudio(new Int16Array([1, 2]));
265
+ await session.sendAudio(new Int16Array([3, 4]));
266
+ await session.finish();
267
+ await session.finish();
268
+ const snapshot = harness.controller.sessions[0];
269
+ assert(snapshot !== void 0, "Expected a provider session.");
270
+ assert(snapshot.audioChunks.length === 2, "Expected two audio chunks.");
271
+ assert(snapshot.finishCallCount === 1, "finish() must have only one observable effect.");
272
+ }
273
+ },
274
+ {
275
+ name: "drains a delayed provider final after finish",
276
+ async run() {
277
+ const harness = options.createHarness();
278
+ const session = await openSession(harness);
279
+ const partsPromise = readStream(session.stream);
280
+ await session.sendAudio(new Int16Array([1]));
281
+ harness.controller.emit({
282
+ type: "interim",
283
+ text: "draft"
284
+ });
285
+ const finishPromise = Promise.resolve(session.finish());
286
+ await Promise.resolve();
287
+ harness.controller.emit({
288
+ type: "final",
289
+ text: "corrected."
290
+ });
291
+ harness.controller.close();
292
+ await finishPromise;
293
+ const finals = (await partsPromise).filter((part) => part.type === "final" && part.text.length > 0);
294
+ assert(finals.length === 1, "Expected one delayed final part.");
295
+ assert(finals[0]?.type === "final" && finals[0].text === "corrected.", "finish() must preserve the provider's corrected final text.");
296
+ }
297
+ },
298
+ {
299
+ name: "never promotes interim text when finish has no final",
300
+ async run() {
301
+ const harness = options.createHarness();
302
+ const session = await openSession(harness);
303
+ const partsPromise = readStream(session.stream);
304
+ await session.sendAudio(new Int16Array([1]));
305
+ harness.controller.emit({
306
+ type: "interim",
307
+ text: "mutable draft"
308
+ });
309
+ const finishPromise = Promise.resolve(session.finish());
310
+ await Promise.resolve();
311
+ harness.controller.close();
312
+ await finishPromise;
313
+ assert(!(await partsPromise).some((part) => part.type === "final" && part.text.length > 0), "Interim text must not be relabeled as final during finish().");
314
+ }
315
+ },
316
+ {
317
+ name: "feeds multiple final chunks through the shared accumulator",
318
+ async run() {
319
+ const harness = options.createHarness();
320
+ const accumulator = options.createAccumulatorSession(harness.provider, createConformanceAudioSource());
321
+ const startPromise = Promise.resolve(accumulator.start());
322
+ await harness.controller.waitForSession();
323
+ harness.controller.resolveOpen();
324
+ await startPromise;
325
+ harness.controller.emit({
326
+ type: "final",
327
+ text: "one"
328
+ });
329
+ harness.controller.emit({
330
+ type: "final",
331
+ text: "two"
332
+ });
333
+ harness.controller.emit({
334
+ type: "final",
335
+ text: ","
336
+ });
337
+ harness.controller.emit({
338
+ type: "final",
339
+ text: "three"
340
+ });
341
+ await waitFor(() => accumulator.getSnapshot().finalTranscript === "one two, three", `Unexpected accumulated transcript: ${accumulator.getSnapshot().finalTranscript}`);
342
+ await accumulator.cancel();
343
+ }
344
+ },
345
+ {
346
+ name: "maps a terminal timeout and closes the stream",
347
+ async run() {
348
+ const harness = options.createHarness();
349
+ const session = await openSession(harness);
350
+ const partsPromise = readStream(session.stream);
351
+ await session.sendAudio(new Int16Array([1]));
352
+ const finishOutcome = Promise.resolve(session.finish()).then(() => void 0, () => void 0);
353
+ await harness.controller.timeout();
354
+ await finishOutcome;
355
+ const errorParts = (await partsPromise).filter((part) => part.type === "error");
356
+ assert(errorParts.length === 1, "Expected one terminal timeout error.");
357
+ assert(errorParts[0]?.error.code === "network-error", `Expected network-error, received ${errorParts[0]?.error.code}.`);
358
+ assert(errorParts[0]?.error.retryable, "Provider timeouts must be retryable.");
359
+ }
360
+ },
361
+ {
362
+ name: "aborts immediately and idempotently",
363
+ async run() {
364
+ const harness = options.createHarness();
365
+ const session = await openSession(harness);
366
+ const partsPromise = readStream(session.stream);
367
+ session.abort("test");
368
+ session.abort("test-again");
369
+ await partsPromise;
370
+ const snapshot = harness.controller.sessions[0];
371
+ assert(snapshot?.aborted, "Session was not marked aborted.");
372
+ assert(snapshot.closed, "Abort must close the provider session.");
373
+ assert(snapshot.abortCallCount === 1, "abort() must have only one observable effect.");
374
+ harness.controller.emit({
375
+ type: "final",
376
+ text: "late"
377
+ });
378
+ const lateReader = session.stream.getReader();
379
+ const lateResult = await lateReader.read();
380
+ lateReader.releaseLock();
381
+ assert(lateResult.done, `Output must not be emitted after abort: ${JSON.stringify(lateResult)}.`);
382
+ }
383
+ },
384
+ {
385
+ name: "honors abort signals while opening",
386
+ async run() {
387
+ const harness = options.createHarness();
388
+ const abortController = new AbortController();
389
+ const sessionPromise = Promise.resolve().then(() => harness.provider.doOpen({ abortSignal: abortController.signal }));
390
+ await harness.controller.waitForSession();
391
+ abortController.abort("cancelled");
392
+ let rejected = false;
393
+ try {
394
+ await sessionPromise;
395
+ } catch {
396
+ rejected = true;
397
+ }
398
+ assert(rejected, "An aborted open must reject.");
399
+ assert(harness.controller.sessions[0]?.aborted, "An aborted open must tear down its pending session.");
400
+ }
401
+ },
402
+ {
403
+ name: "reports an abnormal failure before stream completion",
404
+ async run() {
405
+ const harness = options.createHarness();
406
+ const partsPromise = readStream((await openSession(harness)).stream);
407
+ harness.controller.fail(new VoiceInputError({
408
+ code: "provider-error",
409
+ message: "Synthetic provider failure.",
410
+ provider: harness.provider.provider
411
+ }));
412
+ const parts = await partsPromise;
413
+ assert(parts.length === 1, "Expected exactly one error part.");
414
+ assert(parts[0]?.type === "error", "Expected an error part.");
415
+ assert(parts[0]?.type === "error" && VoiceInputError.isInstance(parts[0].error), "Provider failures must use VoiceInputError.");
416
+ }
417
+ },
418
+ {
419
+ name: "classifies malformed portable options as invalid configuration",
420
+ async run() {
421
+ const provider = options.errorTaxonomy.createProvider?.() ?? options.createHarness().provider;
422
+ const error = captureValidationError(provider, options.errorTaxonomy.invalidOptions);
423
+ assert(error.code === "invalid-configuration", `Expected invalid-configuration, received ${error.code}.`);
424
+ assert(error.provider === provider.provider, "Validation errors must identify their provider.");
425
+ assert(error.cause instanceof Error, "Invalid configuration must preserve its validation cause.");
426
+ }
427
+ },
428
+ {
429
+ name: "classifies valid unavailable options as unsupported features",
430
+ async run() {
431
+ const provider = options.errorTaxonomy.createProvider?.() ?? options.createHarness().provider;
432
+ const error = captureValidationError(provider, options.errorTaxonomy.unsupportedOptions);
433
+ assert(error.code === "unsupported-feature", `Expected unsupported-feature, received ${error.code}.`);
434
+ assert(error.provider === provider.provider, "Unsupported-feature errors must identify their provider.");
435
+ }
436
+ },
437
+ {
438
+ name: "reports malformed options before capability limits",
439
+ async run() {
440
+ const error = captureValidationError(options.errorTaxonomy.createProvider?.() ?? options.createHarness().provider, options.errorTaxonomy.malformedUnsupportedOptions);
441
+ assert(error.code === "invalid-configuration", `Expected malformed precedence, received ${error.code}.`);
442
+ }
443
+ },
444
+ {
445
+ name: "classifies missing runtime APIs as unsupported browser",
446
+ async run() {
447
+ const provider = options.errorTaxonomy.createUnsupportedBrowserProvider();
448
+ let caught;
449
+ try {
450
+ await provider.doOpen({ abortSignal: new AbortController().signal });
451
+ } catch (error) {
452
+ caught = error;
453
+ }
454
+ assert(VoiceInputError.isInstance(caught), "Missing browser APIs must produce VoiceInputError.");
455
+ assert(caught.code === "unsupported-browser", `Expected unsupported-browser, received ${caught.code}.`);
456
+ assert(caught.provider === provider.provider, "Unsupported-browser errors must identify their provider.");
457
+ }
458
+ }
459
+ ];
460
+ }
461
+ function captureValidationError(provider, options) {
462
+ let caught;
463
+ try {
464
+ provider.validateOptions(options);
465
+ } catch (error) {
466
+ caught = error;
467
+ }
468
+ assert(VoiceInputError.isInstance(caught), "Validation failures must use VoiceInputError.");
469
+ return caught;
470
+ }
471
+ //#endregion
472
+ export { VoiceInputProviderConformanceError, createFakeVoiceInputProvider, createVoiceInputProviderV1ConformanceCases };
473
+
474
+ //# sourceMappingURL=test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.js","names":[],"sources":["../src/test.ts"],"sourcesContent":["import type {\n VoiceInputProviderV1,\n VoiceInputProviderV1CallOptions,\n VoiceInputProviderV1Session,\n VoiceInputProviderV1StreamPart,\n VoiceTranscriptionOptions,\n} from \"./index.js\";\nimport { VoiceInputError } from \"./index.js\";\n\nexport interface FakeVoiceInputProviderOptions {\n provider?: string;\n modelId?: string;\n sampleRate?: number;\n autoOpen?: boolean;\n autoCloseOnFinish?: boolean;\n validateOptions?: (options: VoiceTranscriptionOptions) => void;\n}\n\nexport interface FakeVoiceInputProviderSessionSnapshot {\n readonly callOptions: VoiceInputProviderV1CallOptions;\n readonly audioChunks: readonly Int16Array[];\n readonly finishCallCount: number;\n readonly abortCallCount: number;\n readonly closed: boolean;\n readonly aborted: boolean;\n}\n\nexport interface FakeVoiceInputProviderController {\n readonly sessions: readonly FakeVoiceInputProviderSessionSnapshot[];\n waitForSession(\n index?: number,\n ): Promise<FakeVoiceInputProviderSessionSnapshot>;\n resolveOpen(index?: number): void;\n rejectOpen(error: unknown, index?: number): void;\n emit(part: VoiceInputProviderV1StreamPart, index?: number): void;\n close(index?: number): void;\n fail(error: VoiceInputError, index?: number): void;\n timeout(index?: number): PromiseLike<void> | void;\n}\n\nexport interface FakeVoiceInputProvider {\n provider: VoiceInputProviderV1;\n controller: FakeVoiceInputProviderController;\n}\n\ninterface MutableFakeSession {\n segmentSequence: number;\n callOptions: VoiceInputProviderV1CallOptions;\n audioChunks: Int16Array[];\n finishCallCount: number;\n abortCallCount: number;\n closed: boolean;\n aborted: boolean;\n finished: boolean;\n opened: boolean;\n settled: boolean;\n streamController: ReadableStreamDefaultController<VoiceInputProviderV1StreamPart>;\n publicSession: VoiceInputProviderV1Session;\n open(): void;\n reject(error: unknown): void;\n}\n\nexport function createFakeVoiceInputProvider(\n options: FakeVoiceInputProviderOptions = {},\n): FakeVoiceInputProvider {\n const sessions: MutableFakeSession[] = [];\n const waiters = new Map<\n number,\n Array<(session: FakeVoiceInputProviderSessionSnapshot) => void>\n >();\n const autoOpen = options.autoOpen ?? true;\n const autoCloseOnFinish = options.autoCloseOnFinish ?? true;\n\n const getSession = (index = sessions.length - 1): MutableFakeSession => {\n const session = sessions[index];\n\n if (session === undefined) {\n throw new Error(`Fake provider session ${index} does not exist.`);\n }\n\n return session;\n };\n\n const getSnapshot = (\n session: MutableFakeSession,\n ): FakeVoiceInputProviderSessionSnapshot =>\n Object.freeze({\n callOptions: Object.freeze({ ...session.callOptions }),\n audioChunks: Object.freeze([...session.audioChunks]),\n finishCallCount: session.finishCallCount,\n abortCallCount: session.abortCallCount,\n closed: session.closed,\n aborted: session.aborted,\n });\n\n const close = (session: MutableFakeSession): void => {\n if (session.closed) {\n return;\n }\n\n session.closed = true;\n session.streamController.close();\n };\n\n const emit = (part: VoiceInputProviderV1StreamPart, index?: number): void => {\n const session = getSession(index);\n\n if (session.closed) {\n return;\n }\n\n if (part.type === \"interim\" || part.type === \"final\") {\n session.streamController.enqueue({\n ...part,\n segmentId: part.segmentId ?? `fake:${session.segmentSequence}`,\n });\n if (part.type === \"final\") session.segmentSequence += 1;\n } else session.streamController.enqueue(part);\n };\n\n const controller: FakeVoiceInputProviderController = {\n get sessions() {\n return Object.freeze(sessions.map(getSnapshot));\n },\n async waitForSession(index = 0) {\n const session = sessions[index];\n\n if (session !== undefined) {\n return getSnapshot(session);\n }\n\n return await new Promise<FakeVoiceInputProviderSessionSnapshot>(\n (resolve) => {\n const existingWaiters = waiters.get(index) ?? [];\n existingWaiters.push(resolve);\n waiters.set(index, existingWaiters);\n },\n );\n },\n resolveOpen(index) {\n getSession(index).open();\n },\n rejectOpen(error, index) {\n getSession(index).reject(error);\n },\n emit,\n close(index) {\n close(getSession(index));\n },\n fail(error, index) {\n emit({ type: \"error\", error }, index);\n close(getSession(index));\n },\n timeout(index) {\n const session = getSession(index);\n emit(\n {\n type: \"error\",\n error: new VoiceInputError({\n code: \"network-error\",\n message: \"The fake provider timed out.\",\n provider: options.provider ?? \"fake\",\n retryable: true,\n }),\n },\n index,\n );\n close(session);\n },\n };\n\n const provider: VoiceInputProviderV1 = {\n specificationVersion: \"v1\",\n provider: options.provider ?? \"fake\",\n modelId: options.modelId ?? \"fake-model\",\n sampleRate: options.sampleRate ?? 16_000,\n validateOptions: options.validateOptions ?? (() => {}),\n doOpen(callOptions) {\n let streamController:\n | ReadableStreamDefaultController<VoiceInputProviderV1StreamPart>\n | undefined;\n const stream = new ReadableStream<VoiceInputProviderV1StreamPart>({\n start(controller_) {\n streamController = controller_;\n },\n });\n\n if (streamController === undefined) {\n throw new Error(\"The fake provider stream did not initialize.\");\n }\n\n let resolveOpen: (\n session: VoiceInputProviderV1Session,\n ) => void = () => {};\n let rejectOpen: (error: unknown) => void = () => {};\n const openPromise = new Promise<VoiceInputProviderV1Session>(\n (resolve, reject) => {\n resolveOpen = resolve;\n rejectOpen = reject;\n },\n );\n\n const mutableSession: MutableFakeSession = {\n segmentSequence: 0,\n callOptions,\n audioChunks: [],\n finishCallCount: 0,\n abortCallCount: 0,\n closed: false,\n aborted: false,\n finished: false,\n opened: false,\n settled: false,\n streamController,\n publicSession: undefined as unknown as VoiceInputProviderV1Session,\n open() {\n if (mutableSession.settled) {\n return;\n }\n\n mutableSession.opened = true;\n mutableSession.settled = true;\n resolveOpen(mutableSession.publicSession);\n },\n reject(error) {\n if (mutableSession.settled) {\n return;\n }\n\n mutableSession.settled = true;\n close(mutableSession);\n rejectOpen(error);\n },\n };\n\n const publicSession: VoiceInputProviderV1Session = {\n stream,\n sendAudio(chunk) {\n if (!mutableSession.closed && !mutableSession.aborted) {\n mutableSession.audioChunks.push(chunk);\n }\n },\n finish() {\n if (\n mutableSession.closed ||\n mutableSession.aborted ||\n mutableSession.finished\n ) {\n return;\n }\n\n mutableSession.finished = true;\n mutableSession.finishCallCount += 1;\n\n if (autoCloseOnFinish) {\n close(mutableSession);\n }\n },\n abort(reason) {\n if (mutableSession.closed || mutableSession.aborted) {\n return;\n }\n\n mutableSession.aborted = true;\n mutableSession.abortCallCount += 1;\n close(mutableSession);\n\n if (!mutableSession.settled) {\n mutableSession.settled = true;\n rejectOpen(reason);\n }\n },\n };\n\n mutableSession.publicSession = publicSession;\n const index = sessions.push(mutableSession) - 1;\n\n for (const resolve of waiters.get(index) ?? []) {\n resolve(getSnapshot(mutableSession));\n }\n waiters.delete(index);\n\n callOptions.abortSignal.addEventListener(\n \"abort\",\n () => publicSession.abort(callOptions.abortSignal.reason),\n { once: true },\n );\n\n if (callOptions.abortSignal.aborted) {\n publicSession.abort(callOptions.abortSignal.reason);\n } else if (autoOpen) {\n mutableSession.open();\n }\n\n return openPromise;\n },\n };\n\n return { provider, controller };\n}\n\nexport interface VoiceInputProviderV1ConformanceHarness {\n provider: VoiceInputProviderV1;\n controller: FakeVoiceInputProviderController;\n}\n\nexport interface VoiceInputProviderV1ConformanceCase {\n name: string;\n run(): Promise<void>;\n}\n\nexport interface VoiceInputProviderV1ConformanceOptions {\n createHarness(): VoiceInputProviderV1ConformanceHarness;\n createAccumulatorSession(\n provider: VoiceInputProviderV1,\n audioSource: {\n prepare(): PromiseLike<{\n readonly stream: ReadableStream<Int16Array>;\n start(): PromiseLike<void> | void;\n stop(): PromiseLike<void> | void;\n abort(reason?: unknown): void;\n }>;\n },\n ): {\n getSnapshot(): { readonly finalTranscript: string };\n start(): PromiseLike<void> | void;\n cancel(): PromiseLike<void> | void;\n };\n errorTaxonomy: {\n readonly createProvider?: () => VoiceInputProviderV1;\n readonly createUnsupportedBrowserProvider: () => VoiceInputProviderV1;\n readonly invalidOptions: VoiceTranscriptionOptions;\n readonly malformedUnsupportedOptions: VoiceTranscriptionOptions;\n readonly unsupportedOptions: VoiceTranscriptionOptions;\n };\n}\n\nexport class VoiceInputProviderConformanceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"VoiceInputProviderConformanceError\";\n }\n}\n\nfunction assert(condition: unknown, message: string): asserts condition {\n if (!condition) {\n throw new VoiceInputProviderConformanceError(message);\n }\n}\n\nasync function readStream(\n stream: ReadableStream<VoiceInputProviderV1StreamPart>,\n): Promise<VoiceInputProviderV1StreamPart[]> {\n const parts: VoiceInputProviderV1StreamPart[] = [];\n\n for await (const part of stream) {\n parts.push(part);\n }\n\n return parts;\n}\n\nasync function openSession(\n harness: VoiceInputProviderV1ConformanceHarness,\n): Promise<VoiceInputProviderV1Session> {\n const sessionPromise = Promise.resolve().then(() =>\n harness.provider.doOpen({\n abortSignal: new AbortController().signal,\n }),\n );\n await harness.controller.waitForSession();\n harness.controller.resolveOpen();\n return await sessionPromise;\n}\n\nfunction createConformanceAudioSource() {\n return {\n prepare() {\n let closed = false;\n let streamController:\n ReadableStreamDefaultController<Int16Array> | undefined;\n const stream = new ReadableStream<Int16Array>({\n start(controller) {\n streamController = controller;\n },\n });\n const close = (): void => {\n if (!closed) {\n closed = true;\n streamController?.close();\n }\n };\n return Promise.resolve({ stream, start() {}, stop: close, abort: close });\n },\n };\n}\n\nasync function waitFor(\n condition: () => boolean,\n message: string,\n): Promise<void> {\n for (let attempt = 0; attempt < 20; attempt += 1) {\n if (condition()) {\n return;\n }\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n }\n throw new VoiceInputProviderConformanceError(message);\n}\n\nexport function createVoiceInputProviderV1ConformanceCases(\n options: VoiceInputProviderV1ConformanceOptions,\n): readonly VoiceInputProviderV1ConformanceCase[] {\n return [\n {\n name: \"declares valid v1 metadata and accepts baseline options\",\n async run() {\n const { provider } = options.createHarness();\n assert(provider.specificationVersion === \"v1\", \"Expected v1 spec.\");\n assert(provider.provider.length > 0, \"Provider ID must not be empty.\");\n assert(provider.modelId.length > 0, \"Model ID must not be empty.\");\n assert(\n Number.isInteger(provider.sampleRate) && provider.sampleRate > 0,\n \"Sample rate must be a positive integer.\",\n );\n provider.validateOptions({});\n },\n },\n {\n name: \"streams normalized parts in order and closes terminally\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n const partsPromise = readStream(session.stream);\n harness.controller.emit({ type: \"speech-start\" });\n harness.controller.emit({ type: \"interim\", text: \"hel\" });\n harness.controller.emit({ type: \"interim\", text: \"hello\" });\n harness.controller.emit({ type: \"final\", text: \"hello\" });\n harness.controller.emit({ type: \"speech-end\" });\n harness.controller.close();\n const parts = await partsPromise;\n const transcripts = parts.filter(\n (part) => part.type === \"interim\" || part.type === \"final\",\n );\n assert(\n transcripts.every(\n (part) =>\n typeof part.segmentId === \"string\" && part.segmentId.length > 0,\n ),\n \"Normalized transcript parts must include a non-empty segmentId.\",\n );\n assert(\n new Set(transcripts.map((part) => part.segmentId)).size === 1,\n \"Interim revisions and their final must keep the same segmentId.\",\n );\n const partTypes = parts.map((part) => part.type);\n assert(\n partTypes.join(\",\") ===\n \"speech-start,interim,interim,final,speech-end\",\n `Unexpected stream order: ${partTypes.join(\",\")}`,\n );\n\n harness.controller.emit({ type: \"final\", text: \"late\" });\n const lateReader = session.stream.getReader();\n const lateResult = await lateReader.read();\n lateReader.releaseLock();\n assert(\n lateResult.done,\n `Output must not be emitted after closure: ${JSON.stringify(lateResult)}.`,\n );\n },\n },\n {\n name: \"accepts audio chunks and finishes idempotently\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n await session.sendAudio(new Int16Array([1, 2]));\n await session.sendAudio(new Int16Array([3, 4]));\n await session.finish();\n await session.finish();\n const snapshot = harness.controller.sessions[0];\n assert(snapshot !== undefined, \"Expected a provider session.\");\n assert(snapshot.audioChunks.length === 2, \"Expected two audio chunks.\");\n assert(\n snapshot.finishCallCount === 1,\n \"finish() must have only one observable effect.\",\n );\n },\n },\n {\n name: \"drains a delayed provider final after finish\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n const partsPromise = readStream(session.stream);\n await session.sendAudio(new Int16Array([1]));\n harness.controller.emit({ type: \"interim\", text: \"draft\" });\n const finishPromise = Promise.resolve(session.finish());\n\n await Promise.resolve();\n harness.controller.emit({ type: \"final\", text: \"corrected.\" });\n harness.controller.close();\n await finishPromise;\n\n const parts = await partsPromise;\n const finals = parts.filter(\n (part) => part.type === \"final\" && part.text.length > 0,\n );\n assert(finals.length === 1, \"Expected one delayed final part.\");\n assert(\n finals[0]?.type === \"final\" && finals[0].text === \"corrected.\",\n \"finish() must preserve the provider's corrected final text.\",\n );\n },\n },\n {\n name: \"never promotes interim text when finish has no final\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n const partsPromise = readStream(session.stream);\n await session.sendAudio(new Int16Array([1]));\n harness.controller.emit({ type: \"interim\", text: \"mutable draft\" });\n const finishPromise = Promise.resolve(session.finish());\n\n await Promise.resolve();\n harness.controller.close();\n await finishPromise;\n\n const parts = await partsPromise;\n assert(\n !parts.some((part) => part.type === \"final\" && part.text.length > 0),\n \"Interim text must not be relabeled as final during finish().\",\n );\n },\n },\n {\n name: \"feeds multiple final chunks through the shared accumulator\",\n async run() {\n const harness = options.createHarness();\n const accumulator = options.createAccumulatorSession(\n harness.provider,\n createConformanceAudioSource(),\n );\n const startPromise = Promise.resolve(accumulator.start());\n await harness.controller.waitForSession();\n harness.controller.resolveOpen();\n await startPromise;\n harness.controller.emit({ type: \"final\", text: \"one\" });\n harness.controller.emit({ type: \"final\", text: \"two\" });\n harness.controller.emit({ type: \"final\", text: \",\" });\n harness.controller.emit({ type: \"final\", text: \"three\" });\n await waitFor(\n () => accumulator.getSnapshot().finalTranscript === \"one two, three\",\n `Unexpected accumulated transcript: ${accumulator.getSnapshot().finalTranscript}`,\n );\n await accumulator.cancel();\n },\n },\n {\n name: \"maps a terminal timeout and closes the stream\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n const partsPromise = readStream(session.stream);\n await session.sendAudio(new Int16Array([1]));\n const finishOutcome = Promise.resolve(session.finish()).then(\n () => undefined,\n () => undefined,\n );\n\n await harness.controller.timeout();\n await finishOutcome;\n const parts = await partsPromise;\n const errorParts = parts.filter((part) => part.type === \"error\");\n assert(errorParts.length === 1, \"Expected one terminal timeout error.\");\n assert(\n errorParts[0]?.error.code === \"network-error\",\n `Expected network-error, received ${errorParts[0]?.error.code}.`,\n );\n assert(\n errorParts[0]?.error.retryable,\n \"Provider timeouts must be retryable.\",\n );\n },\n },\n {\n name: \"aborts immediately and idempotently\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n const partsPromise = readStream(session.stream);\n session.abort(\"test\");\n session.abort(\"test-again\");\n await partsPromise;\n const snapshot = harness.controller.sessions[0];\n assert(snapshot?.aborted, \"Session was not marked aborted.\");\n assert(snapshot.closed, \"Abort must close the provider session.\");\n assert(\n snapshot.abortCallCount === 1,\n \"abort() must have only one observable effect.\",\n );\n harness.controller.emit({ type: \"final\", text: \"late\" });\n const lateReader = session.stream.getReader();\n const lateResult = await lateReader.read();\n lateReader.releaseLock();\n assert(\n lateResult.done,\n `Output must not be emitted after abort: ${JSON.stringify(lateResult)}.`,\n );\n },\n },\n {\n name: \"honors abort signals while opening\",\n async run() {\n const harness = options.createHarness();\n const abortController = new AbortController();\n const sessionPromise = Promise.resolve().then(() =>\n harness.provider.doOpen({ abortSignal: abortController.signal }),\n );\n await harness.controller.waitForSession();\n abortController.abort(\"cancelled\");\n\n let rejected = false;\n try {\n await sessionPromise;\n } catch {\n rejected = true;\n }\n\n assert(rejected, \"An aborted open must reject.\");\n assert(\n harness.controller.sessions[0]?.aborted,\n \"An aborted open must tear down its pending session.\",\n );\n },\n },\n {\n name: \"reports an abnormal failure before stream completion\",\n async run() {\n const harness = options.createHarness();\n const session = await openSession(harness);\n const partsPromise = readStream(session.stream);\n harness.controller.fail(\n new VoiceInputError({\n code: \"provider-error\",\n message: \"Synthetic provider failure.\",\n provider: harness.provider.provider,\n }),\n );\n const parts = await partsPromise;\n assert(parts.length === 1, \"Expected exactly one error part.\");\n assert(parts[0]?.type === \"error\", \"Expected an error part.\");\n assert(\n parts[0]?.type === \"error\" &&\n VoiceInputError.isInstance(parts[0].error),\n \"Provider failures must use VoiceInputError.\",\n );\n },\n },\n {\n name: \"classifies malformed portable options as invalid configuration\",\n async run() {\n const provider =\n options.errorTaxonomy.createProvider?.() ??\n options.createHarness().provider;\n const error = captureValidationError(\n provider,\n options.errorTaxonomy.invalidOptions,\n );\n assert(\n error.code === \"invalid-configuration\",\n `Expected invalid-configuration, received ${error.code}.`,\n );\n assert(\n error.provider === provider.provider,\n \"Validation errors must identify their provider.\",\n );\n assert(\n error.cause instanceof Error,\n \"Invalid configuration must preserve its validation cause.\",\n );\n },\n },\n {\n name: \"classifies valid unavailable options as unsupported features\",\n async run() {\n const provider =\n options.errorTaxonomy.createProvider?.() ??\n options.createHarness().provider;\n const error = captureValidationError(\n provider,\n options.errorTaxonomy.unsupportedOptions,\n );\n assert(\n error.code === \"unsupported-feature\",\n `Expected unsupported-feature, received ${error.code}.`,\n );\n assert(\n error.provider === provider.provider,\n \"Unsupported-feature errors must identify their provider.\",\n );\n },\n },\n {\n name: \"reports malformed options before capability limits\",\n async run() {\n const provider =\n options.errorTaxonomy.createProvider?.() ??\n options.createHarness().provider;\n const error = captureValidationError(\n provider,\n options.errorTaxonomy.malformedUnsupportedOptions,\n );\n assert(\n error.code === \"invalid-configuration\",\n `Expected malformed precedence, received ${error.code}.`,\n );\n },\n },\n {\n name: \"classifies missing runtime APIs as unsupported browser\",\n async run() {\n const provider =\n options.errorTaxonomy.createUnsupportedBrowserProvider();\n let caught: unknown;\n try {\n await provider.doOpen({\n abortSignal: new AbortController().signal,\n });\n } catch (error) {\n caught = error;\n }\n assert(\n VoiceInputError.isInstance(caught),\n \"Missing browser APIs must produce VoiceInputError.\",\n );\n assert(\n caught.code === \"unsupported-browser\",\n `Expected unsupported-browser, received ${caught.code}.`,\n );\n assert(\n caught.provider === provider.provider,\n \"Unsupported-browser errors must identify their provider.\",\n );\n },\n },\n ];\n}\n\nfunction captureValidationError(\n provider: VoiceInputProviderV1,\n options: VoiceTranscriptionOptions,\n): VoiceInputError {\n let caught: unknown;\n try {\n provider.validateOptions(options);\n } catch (error) {\n caught = error;\n }\n assert(\n VoiceInputError.isInstance(caught),\n \"Validation failures must use VoiceInputError.\",\n );\n return caught;\n}\n"],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_index = require("./index.cjs");
3
+ //#region src/transport.ts
4
+ const MAX_BUFFERED_BYTES = 1048576;
5
+ const DRAIN_TIMEOUT_MS = 5e3;
6
+ /** Keeps nominal sends synchronous; congested sends are abortable and bounded. */
7
+ function sendWithBackpressure(socket, bytes, signal, provider, send) {
8
+ const failure = () => new require_index.VoiceInputError({
9
+ code: "network-error",
10
+ provider,
11
+ retryable: true,
12
+ message: `The ${provider} audio connection is stalled or closed.`
13
+ });
14
+ if (signal.aborted || socket.readyState !== 1 || bytes > MAX_BUFFERED_BYTES) throw failure();
15
+ if ((socket.bufferedAmount ?? 0) + bytes <= MAX_BUFFERED_BYTES) return send();
16
+ return new Promise((resolve, reject) => {
17
+ const started = Date.now();
18
+ let timer;
19
+ const cleanup = () => {
20
+ clearTimeout(timer);
21
+ signal.removeEventListener("abort", abort);
22
+ };
23
+ const abort = () => {
24
+ cleanup();
25
+ reject(failure());
26
+ };
27
+ const check = () => {
28
+ if (signal.aborted || socket.readyState !== 1 || Date.now() - started >= DRAIN_TIMEOUT_MS) abort();
29
+ else if ((socket.bufferedAmount ?? 0) + bytes <= MAX_BUFFERED_BYTES) {
30
+ cleanup();
31
+ try {
32
+ send();
33
+ resolve();
34
+ } catch (error) {
35
+ reject(error);
36
+ }
37
+ } else timer = setTimeout(check, 25);
38
+ };
39
+ signal.addEventListener("abort", abort, { once: true });
40
+ timer = setTimeout(check, 25);
41
+ });
42
+ }
43
+ //#endregion
44
+ exports.sendWithBackpressure = sendWithBackpressure;
45
+
46
+ //# sourceMappingURL=transport.cjs.map