@blockcast/fec-worker 0.1.0 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +17 -7
  2. package/dist/block-order.d.ts +38 -0
  3. package/dist/block-order.d.ts.map +1 -0
  4. package/dist/block-order.js +78 -0
  5. package/dist/block-order.js.map +1 -0
  6. package/dist/fec-worker-client.d.ts +20 -7
  7. package/dist/fec-worker-client.d.ts.map +1 -1
  8. package/dist/fec-worker-client.js +106 -30
  9. package/dist/fec-worker-client.js.map +1 -1
  10. package/dist/fec-worker-types.d.ts +153 -25
  11. package/dist/fec-worker-types.d.ts.map +1 -1
  12. package/dist/fec-worker.js +382 -88
  13. package/dist/fec-worker.js.map +4 -4
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/shred-fec-worker.js +23 -2
  18. package/dist/shred-fec-worker.js.map +2 -2
  19. package/dist/shred-worker-types.d.ts +16 -1
  20. package/dist/shred-worker-types.d.ts.map +1 -1
  21. package/dist/shred-worker-types.js +16 -1
  22. package/dist/shred-worker-types.js.map +1 -1
  23. package/package.json +8 -3
  24. package/src/block-order.ts +93 -0
  25. package/src/fec-worker-client.ts +115 -32
  26. package/src/fec-worker-types.ts +152 -21
  27. package/src/fec-worker.ts +583 -129
  28. package/src/index.ts +3 -0
  29. package/src/shred-fec-worker.ts +34 -3
  30. package/src/shred-worker-types.ts +16 -1
  31. package/src/__tests__/fec-worker-alta.test.ts +0 -231
  32. package/src/__tests__/fec-worker-cleanup-integration.test.ts +0 -250
  33. package/src/__tests__/fec-worker-pending-queue.test.ts +0 -315
  34. package/src/__tests__/fec-worker-repair-size.test.ts +0 -1029
  35. package/src/__tests__/fec-worker-wasm-contract.test.ts +0 -84
  36. package/src/__tests__/shred-fec-worker.test.ts +0 -448
  37. package/src/fec-worker-client.test.ts +0 -530
  38. package/src/fec-worker-types.test.ts +0 -400
  39. package/src/shred-worker-client.test.ts +0 -182
  40. package/src/transfer.test.ts +0 -24
@@ -1,400 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import type {
3
- FecWorkerCommand,
4
- FecWorkerEvent,
5
- FecTrackConfig,
6
- FecBlockSnapshot,
7
- FecTrackStats,
8
- FrameMeta,
9
- } from "./fec-worker-types.js";
10
-
11
- // Helper: compile-time type assertion — if this file compiles, types are correct
12
- function assertType<T>(_value: T): void {
13
- /* compile-time only */
14
- }
15
-
16
- describe("FecWorkerCommand", () => {
17
- it("configure includes wasmModule and optional altaPublicKey (AUTH-03)", () => {
18
- const cmd: FecWorkerCommand = {
19
- type: "configure",
20
- config: {
21
- codec: "avc1.64001f",
22
- trackName: "video",
23
- trackType: "video",
24
- algorithm: "raptor",
25
- k: 32,
26
- repairCount: 8,
27
- symbolSize: 1320,
28
- interleaveDepth: 4,
29
- interleaveMs: 133,
30
- deliveryWindowMs: 1000,
31
- fecMode: "subframe",
32
- altaEnabled: false,
33
- relayBlockSigEnabled: false,
34
- },
35
- wasmModule: {} as WebAssembly.Module,
36
- altaPublicKey: new ArrayBuffer(32),
37
- };
38
- expect(cmd.type).toBe("configure");
39
- assertType<WebAssembly.Module>(cmd.wasmModule);
40
- });
41
-
42
- it("feedSource carries ssId, data ArrayBuffer, ts (matches WASM add_source signature)", () => {
43
- const cmd: FecWorkerCommand = {
44
- type: "feedSource",
45
- ssId: 0,
46
- data: new ArrayBuffer(1320),
47
- ts: 1000,
48
- };
49
- expect(cmd.type).toBe("feedSource");
50
- assertType<ArrayBuffer>(cmd.data);
51
- assertType<number>(cmd.ssId);
52
- });
53
-
54
- it("feedRepair carries ssStart, ssbLength, rsId, data, ts (matches WASM add_repair)", () => {
55
- const cmd: FecWorkerCommand = {
56
- type: "feedRepair",
57
- ssStart: 0,
58
- ssbLength: 4,
59
- rsId: 0,
60
- data: new ArrayBuffer(1320),
61
- ts: 1000,
62
- };
63
- expect(cmd.type).toBe("feedRepair");
64
- assertType<ArrayBuffer>(cmd.data);
65
- assertType<number>(cmd.ssStart);
66
- assertType<number>(cmd.ssbLength);
67
- assertType<number>(cmd.rsId);
68
- });
69
-
70
- it("relayBlockSig carries sbn, sig, hash as ArrayBuffers", () => {
71
- const cmd: FecWorkerCommand = {
72
- type: "relayBlockSig",
73
- sbn: 5,
74
- sig: new ArrayBuffer(64),
75
- hash: new ArrayBuffer(32),
76
- };
77
- expect(cmd.type).toBe("relayBlockSig");
78
- assertType<ArrayBuffer>(cmd.sig);
79
- assertType<ArrayBuffer>(cmd.hash);
80
- });
81
-
82
- it("cleanup carries every coalesced SBN", () => {
83
- const cmd: FecWorkerCommand = {
84
- type: "cleanup",
85
- sbns: [10, 11],
86
- };
87
- expect(cmd.type).toBe("cleanup");
88
- expect(cmd.sbns).toEqual([10, 11]);
89
- });
90
-
91
- it("dispose has no additional fields", () => {
92
- const cmd: FecWorkerCommand = { type: "dispose" };
93
- expect(cmd.type).toBe("dispose");
94
- });
95
-
96
- it("discriminates all 6 command types", () => {
97
- const types = [
98
- "configure",
99
- "feedSource",
100
- "feedRepair",
101
- "relayBlockSig",
102
- "cleanup",
103
- "dispose",
104
- ] as const;
105
- // Verify each type string is assignable to FecWorkerCommand['type']
106
- for (const t of types) {
107
- assertType<FecWorkerCommand["type"]>(t);
108
- }
109
- expect(types).toHaveLength(6);
110
- });
111
- });
112
-
113
- describe("FecWorkerEvent", () => {
114
- it("frame event carries data ArrayBuffer and FrameMeta", () => {
115
- const evt: FecWorkerEvent = {
116
- type: "frame",
117
- data: new ArrayBuffer(1320),
118
- meta: { altaVerified: null, recovered: false, sbn: 0 },
119
- };
120
- expect(evt.type).toBe("frame");
121
- });
122
-
123
- it("snapshot event carries FecTrackStats", () => {
124
- const config: FecTrackConfig = {
125
- codec: "avc1.64001f",
126
- trackName: "video",
127
- trackType: "video",
128
- algorithm: "raptor",
129
- k: 32,
130
- repairCount: 8,
131
- symbolSize: 1320,
132
- interleaveDepth: 4,
133
- interleaveMs: 133,
134
- deliveryWindowMs: 1000,
135
- fecMode: "subframe",
136
- altaEnabled: false,
137
- relayBlockSigEnabled: false,
138
- };
139
- const evt: FecWorkerEvent = {
140
- type: "snapshot",
141
- stats: {
142
- config,
143
- sourceSymbols: 0,
144
- repairSymbols: 0,
145
- blocksComplete: 0,
146
- blocksRecovered: 0,
147
- blocksFailed: 0,
148
- greenFillFrames: 0,
149
- recoveryRate: 0,
150
- altaVerified: 0,
151
- altaFailed: 0,
152
- relayBlocksVerified: 0,
153
- relayBlocksFailed: 0,
154
- liveSbn: 0,
155
- blocks: [],
156
- interleaveWindowBlocks: 0,
157
- avgRecoveryMs: 0,
158
- maxRecoveryMs: 0,
159
- },
160
- };
161
- expect(evt.type).toBe("snapshot");
162
- });
163
-
164
- it("blockUpdate event carries FecBlockSnapshot", () => {
165
- const evt: FecWorkerEvent = {
166
- type: "blockUpdate",
167
- block: {
168
- sbn: 0,
169
- state: "collecting",
170
- sourceReceived: 5,
171
- repairReceived: 0,
172
- k: 32,
173
- repairCount: 8,
174
- firstSymbolTs: 1000,
175
- lastSymbolTs: 1100,
176
- deadlineTs: 2000,
177
- altaVerified: 5,
178
- relayBlockSigValid: null,
179
- },
180
- };
181
- expect(evt.type).toBe("blockUpdate");
182
- });
183
-
184
- it("greenFill event carries sbn and data", () => {
185
- const evt: FecWorkerEvent = {
186
- type: "greenFill",
187
- sbn: 3,
188
- data: new ArrayBuffer(0),
189
- };
190
- expect(evt.type).toBe("greenFill");
191
- });
192
-
193
- it("cleanupComplete event carries every acknowledged SBN", () => {
194
- const evt: FecWorkerEvent = {
195
- type: "cleanupComplete",
196
- sbns: [3, 4],
197
- };
198
- expect(evt.type).toBe("cleanupComplete");
199
- expect(evt.sbns).toEqual([3, 4]);
200
- });
201
-
202
- it("error event carries message string", () => {
203
- const evt: FecWorkerEvent = {
204
- type: "error",
205
- code: "command-failed",
206
- message: "WASM init failed",
207
- };
208
- expect(evt.type).toBe("error");
209
- expect(evt.message).toBe("WASM init failed");
210
- expect(evt.code).toBe("command-failed");
211
- });
212
-
213
- it("discriminates all 6 event types", () => {
214
- const types = [
215
- "frame",
216
- "snapshot",
217
- "blockUpdate",
218
- "greenFill",
219
- "cleanupComplete",
220
- "error",
221
- ] as const;
222
- for (const t of types) {
223
- assertType<FecWorkerEvent["type"]>(t);
224
- }
225
- expect(types).toHaveLength(6);
226
- });
227
- });
228
-
229
- describe("FecTrackConfig", () => {
230
- it("contains altaEnabled boolean (AUTH-01)", () => {
231
- const config: FecTrackConfig = {
232
- codec: "mp4a.40.2",
233
- trackName: "audio",
234
- trackType: "audio",
235
- sampleRate: 48000,
236
- algorithm: "raptor",
237
- k: 16,
238
- repairCount: 4,
239
- symbolSize: 512,
240
- interleaveDepth: 2,
241
- interleaveMs: 42,
242
- deliveryWindowMs: 500,
243
- fecMode: "object",
244
- altaEnabled: true,
245
- relayBlockSigEnabled: false,
246
- };
247
- assertType<boolean>(config.altaEnabled);
248
- expect(config.altaEnabled).toBe(true);
249
- });
250
-
251
- it("contains relayBlockSigEnabled boolean (AUTH-01)", () => {
252
- const config: FecTrackConfig = {
253
- codec: "avc1.64001f",
254
- trackName: "video",
255
- trackType: "video",
256
- resolution: { w: 1280, h: 720 },
257
- framerate: 30,
258
- algorithm: "raptor",
259
- k: 32,
260
- repairCount: 8,
261
- symbolSize: 1320,
262
- interleaveDepth: 4,
263
- interleaveMs: 133,
264
- deliveryWindowMs: 1000,
265
- fecMode: "subframe",
266
- altaEnabled: false,
267
- relayBlockSigEnabled: true,
268
- };
269
- assertType<boolean>(config.relayBlockSigEnabled);
270
- expect(config.relayBlockSigEnabled).toBe(true);
271
- });
272
- });
273
-
274
- describe("FecBlockSnapshot", () => {
275
- it("contains altaVerified number (AUTH-02)", () => {
276
- const block: FecBlockSnapshot = {
277
- sbn: 0,
278
- state: "complete",
279
- sourceReceived: 32,
280
- repairReceived: 0,
281
- k: 32,
282
- repairCount: 8,
283
- firstSymbolTs: 100,
284
- lastSymbolTs: 200,
285
- completedTs: 250,
286
- deadlineTs: 500,
287
- altaVerified: 30,
288
- relayBlockSigValid: true,
289
- };
290
- assertType<number>(block.altaVerified);
291
- expect(block.altaVerified).toBe(30);
292
- });
293
-
294
- it("relayBlockSigValid can be boolean or null (AUTH-02)", () => {
295
- const pending: FecBlockSnapshot = {
296
- sbn: 1,
297
- state: "collecting",
298
- sourceReceived: 10,
299
- repairReceived: 2,
300
- k: 32,
301
- repairCount: 8,
302
- firstSymbolTs: 300,
303
- lastSymbolTs: 400,
304
- deadlineTs: 700,
305
- altaVerified: 8,
306
- relayBlockSigValid: null,
307
- };
308
- assertType<boolean | null>(pending.relayBlockSigValid);
309
- expect(pending.relayBlockSigValid).toBeNull();
310
- });
311
- });
312
-
313
- describe("FecTrackStats", () => {
314
- it("contains all required aggregate fields", () => {
315
- const config: FecTrackConfig = {
316
- codec: "avc1.64001f",
317
- trackName: "video",
318
- trackType: "video",
319
- algorithm: "raptor",
320
- k: 32,
321
- repairCount: 8,
322
- symbolSize: 1320,
323
- interleaveDepth: 4,
324
- interleaveMs: 133,
325
- deliveryWindowMs: 1000,
326
- fecMode: "subframe",
327
- altaEnabled: false,
328
- relayBlockSigEnabled: false,
329
- };
330
- const stats: FecTrackStats = {
331
- config,
332
- sourceSymbols: 1240,
333
- repairSymbols: 312,
334
- blocksComplete: 380,
335
- blocksRecovered: 18,
336
- blocksFailed: 2,
337
- greenFillFrames: 2,
338
- recoveryRate: 90,
339
- altaVerified: 12400,
340
- altaFailed: 3,
341
- relayBlocksVerified: 380,
342
- relayBlocksFailed: 0,
343
- liveSbn: 400,
344
- blocks: [],
345
- interleaveWindowBlocks: 4,
346
- avgRecoveryMs: 42,
347
- maxRecoveryMs: 180,
348
- };
349
- assertType<FecTrackConfig>(stats.config);
350
- assertType<number>(stats.sourceSymbols);
351
- assertType<number>(stats.repairSymbols);
352
- assertType<number>(stats.blocksComplete);
353
- assertType<number>(stats.blocksRecovered);
354
- assertType<number>(stats.blocksFailed);
355
- assertType<number>(stats.greenFillFrames);
356
- assertType<number>(stats.recoveryRate);
357
- assertType<number>(stats.altaVerified);
358
- assertType<number>(stats.altaFailed);
359
- assertType<number>(stats.liveSbn);
360
- assertType<FecBlockSnapshot[]>(stats.blocks);
361
- assertType<number>(stats.interleaveWindowBlocks);
362
- assertType<number>(stats.avgRecoveryMs);
363
- assertType<number>(stats.maxRecoveryMs);
364
- expect(stats.recoveryRate).toBe(90);
365
- });
366
- });
367
-
368
- describe("FrameMeta", () => {
369
- it("altaVerified is boolean | null (AUTH-04)", () => {
370
- const metaWithAlta: FrameMeta = {
371
- altaVerified: true,
372
- recovered: false,
373
- sbn: 5,
374
- };
375
- assertType<boolean | null>(metaWithAlta.altaVerified);
376
- expect(metaWithAlta.altaVerified).toBe(true);
377
-
378
- const metaWithoutAlta: FrameMeta = {
379
- altaVerified: null,
380
- recovered: true,
381
- sbn: 5,
382
- };
383
- expect(metaWithoutAlta.altaVerified).toBeNull();
384
- });
385
-
386
- it("contains recovered boolean plus block and symbol coordinates", () => {
387
- const meta: FrameMeta = {
388
- altaVerified: null,
389
- recovered: true,
390
- sbn: 42,
391
- esi: 3,
392
- };
393
- assertType<boolean>(meta.recovered);
394
- assertType<number>(meta.sbn);
395
- assertType<number | undefined>(meta.esi);
396
- expect(meta.recovered).toBe(true);
397
- expect(meta.sbn).toBe(42);
398
- expect(meta.esi).toBe(3);
399
- });
400
- });
@@ -1,182 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import type {
3
- ShredWorkerCommand,
4
- ShredWorkerEvent,
5
- ShredWorkerStats,
6
- } from "./shred-worker-types.js";
7
- import { ShredWorkerClient } from "./shred-worker-client.js";
8
-
9
- class MockWorker {
10
- onmessage: ((e: MessageEvent) => void) | null = null;
11
- onerror: ((e: ErrorEvent) => void) | null = null;
12
- onmessageerror: ((e: MessageEvent) => void) | null = null;
13
- posted: Array<{ data: ShredWorkerCommand; transfer: Transferable[] }> = [];
14
- terminated = false;
15
-
16
- postMessage(data: ShredWorkerCommand, transfer?: Transferable[]): void {
17
- this.posted.push({ data, transfer: transfer ?? [] });
18
- }
19
- terminate(): void {
20
- this.terminated = true;
21
- }
22
- emit(event: ShredWorkerEvent): void {
23
- this.onmessage?.({ data: event } as MessageEvent);
24
- }
25
- }
26
-
27
- function makeStats(overrides: Partial<ShredWorkerStats> = {}): ShredWorkerStats {
28
- return {
29
- shredsRx: 0,
30
- sourceShreds: 0,
31
- repairShreds: 0,
32
- fecSetsCompleted: 0,
33
- bytesReassembled: 0,
34
- fecSetsRecovered: 0,
35
- pendingFecSets: -1,
36
- malformed: 0,
37
- versionMismatch: 0,
38
- currentSlot: "0",
39
- slotSamples: [],
40
- ...overrides,
41
- };
42
- }
43
-
44
- describe("ShredWorkerClient", () => {
45
- it("posts configure with the config", () => {
46
- const worker = new MockWorker();
47
- const client = new ShredWorkerClient(worker as unknown as Worker);
48
- client.configure({ maxSlots: 5000, symbolSize: 512, wasmUrl: "/vendor/fec/mmt_wasm.js" });
49
- expect(worker.posted[0].data).toEqual({
50
- type: "configure",
51
- config: { maxSlots: 5000, symbolSize: 512, wasmUrl: "/vendor/fec/mmt_wasm.js" },
52
- });
53
- });
54
-
55
- it("transfers the readable stream on attachStream", () => {
56
- const worker = new MockWorker();
57
- const client = new ShredWorkerClient(worker as unknown as Worker);
58
- const readable = new ReadableStream<Uint8Array>();
59
- client.attachStream(readable);
60
- const msg = worker.posted[0];
61
- expect(msg.data.type).toBe("attachStream");
62
- expect(msg.transfer).toEqual([readable]);
63
- });
64
-
65
- it("transfers the frame buffer on feedShred", () => {
66
- const worker = new MockWorker();
67
- const client = new ShredWorkerClient(worker as unknown as Worker);
68
- const frame = new Uint8Array([1, 2, 3]).buffer;
69
- client.feedShred(frame);
70
- const msg = worker.posted[0];
71
- expect(msg.data).toEqual({ type: "feedShred", frame });
72
- expect(msg.transfer).toEqual([frame]);
73
- });
74
-
75
- it("bounds feedShred to maxInflight when the worker never drains", () => {
76
- const worker = new MockWorker();
77
- const client = new ShredWorkerClient(worker as unknown as Worker, { maxInflight: 4 });
78
- for (let i = 0; i < 10; i++) client.feedShred(new Uint8Array([i]).buffer);
79
- const fed = worker.posted.filter((p) => p.data.type === "feedShred");
80
- expect(fed.length).toBe(4); // only maxInflight frames posted; the rest dropped
81
- expect(client.droppedBackpressure).toBe(6);
82
- expect(client.inflight).toBe(4);
83
- });
84
-
85
- it("does not transfer (detach) a frame dropped by the in-flight guard", () => {
86
- const worker = new MockWorker();
87
- const client = new ShredWorkerClient(worker as unknown as Worker, { maxInflight: 1 });
88
- client.feedShred(new Uint8Array([1]).buffer); // posted (outstanding 0 -> 1)
89
- const dropped = new Uint8Array([9, 9, 9]).buffer;
90
- client.feedShred(dropped); // dropped (outstanding 1 >= 1)
91
- // A dropped frame is never added to a transfer list, so the caller's buffer
92
- // is untouched — no attempt to reuse a detached ArrayBuffer.
93
- expect(dropped.byteLength).toBe(3);
94
- expect(worker.posted.filter((p) => p.transfer.includes(dropped)).length).toBe(0);
95
- });
96
-
97
- it("resumes feeding after the worker reports drain progress via stats", () => {
98
- const worker = new MockWorker();
99
- const client = new ShredWorkerClient(worker as unknown as Worker, { maxInflight: 4 });
100
- for (let i = 0; i < 6; i++) client.feedShred(new Uint8Array([i]).buffer);
101
- expect(worker.posted.filter((p) => p.data.type === "feedShred").length).toBe(4);
102
- expect(client.droppedBackpressure).toBe(2);
103
-
104
- // Worker heartbeat: it consumed all 4 queued frames -> window reopens.
105
- worker.emit({ type: "stats", stats: makeStats({ shredsRx: 4 }) });
106
- expect(client.inflight).toBe(0);
107
-
108
- for (let i = 0; i < 3; i++) client.feedShred(new Uint8Array([100 + i]).buffer);
109
- expect(worker.posted.filter((p) => p.data.type === "feedShred").length).toBe(7);
110
- expect(client.droppedBackpressure).toBe(2); // no NEW drops after the window reopened
111
- });
112
-
113
- it("counts malformed + versionMismatch as drained so the window can't inflate", () => {
114
- const worker = new MockWorker();
115
- const client = new ShredWorkerClient(worker as unknown as Worker, { maxInflight: 4 });
116
- for (let i = 0; i < 4; i++) client.feedShred(new Uint8Array([i]).buffer);
117
- // Worker drained all 4, but only 1 was valid; 2 malformed + 1 version skew.
118
- worker.emit({
119
- type: "stats",
120
- stats: makeStats({ shredsRx: 1, malformed: 2, versionMismatch: 1 }),
121
- });
122
- expect(client.inflight).toBe(0);
123
- });
124
-
125
- it("maxInflight <= 0 disables the bound entirely", () => {
126
- const worker = new MockWorker();
127
- const client = new ShredWorkerClient(worker as unknown as Worker, { maxInflight: 0 });
128
- for (let i = 0; i < 50; i++) client.feedShred(new Uint8Array([i]).buffer);
129
- expect(worker.posted.filter((p) => p.data.type === "feedShred").length).toBe(50);
130
- expect(client.droppedBackpressure).toBe(0);
131
- });
132
-
133
- it("posts recoverAll with the provided time", () => {
134
- const worker = new MockWorker();
135
- const client = new ShredWorkerClient(worker as unknown as Worker);
136
- client.recoverAll(9999);
137
- expect(worker.posted[0].data).toEqual({ type: "recoverAll", nowMs: 9999 });
138
- });
139
-
140
- it("dispatches recovered and stats events to callbacks", () => {
141
- const worker = new MockWorker();
142
- const client = new ShredWorkerClient(worker as unknown as Worker);
143
- const recovered: Array<{ len: number; recovered: boolean }> = [];
144
- const statsSeen: ShredWorkerStats[] = [];
145
- client.onRecovered((data, meta) => recovered.push({ len: data.byteLength, recovered: meta.recovered }));
146
- client.onStats((s) => statsSeen.push(s));
147
-
148
- worker.emit({
149
- type: "recovered",
150
- data: new Uint8Array([1, 2, 3, 4]).buffer,
151
- meta: { slot: 5n, fecSetIndex: 1, sendTsUs: 0n, recovered: true },
152
- });
153
- worker.emit({ type: "stats", stats: makeStats({ shredsRx: 7 }) });
154
-
155
- expect(recovered).toEqual([{ len: 4, recovered: true }]);
156
- expect(statsSeen[0].shredsRx).toBe(7);
157
- });
158
-
159
- it("routes error events with their code", () => {
160
- const worker = new MockWorker();
161
- const client = new ShredWorkerClient(worker as unknown as Worker);
162
- const errs: Array<[string, string | undefined]> = [];
163
- client.onError((m, c) => errs.push([m, c]));
164
- worker.emit({ type: "error", message: "boom", code: "wasm-load-failed" });
165
- expect(errs).toEqual([["boom", "wasm-load-failed"]]);
166
- });
167
-
168
- it("dispose posts dispose, terminates, and is idempotent + no-ops afterward", () => {
169
- const worker = new MockWorker();
170
- const client = new ShredWorkerClient(worker as unknown as Worker);
171
- client.dispose();
172
- expect(worker.posted[worker.posted.length - 1]?.data).toEqual({ type: "dispose" });
173
- expect(worker.terminated).toBe(true);
174
- const postedCount = worker.posted.length;
175
- // Post-dispose calls are no-ops.
176
- client.feedShred(new Uint8Array([1]).buffer);
177
- client.attachStream(new ReadableStream<Uint8Array>());
178
- client.recoverAll();
179
- client.dispose();
180
- expect(worker.posted.length).toBe(postedCount);
181
- });
182
- });
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { toOwnedArrayBuffer } from "./transfer.js";
3
-
4
- describe("toOwnedArrayBuffer", () => {
5
- it("copies exactly the bytes in an offset Uint8Array view", () => {
6
- const backing = new Uint8Array([0, 1, 2, 3, 4, 5]);
7
- const view = backing.subarray(2, 5);
8
-
9
- const owned = toOwnedArrayBuffer(view);
10
-
11
- expect([...new Uint8Array(owned)]).toEqual([2, 3, 4]);
12
- expect(owned.byteLength).toBe(3);
13
- expect(owned).not.toBe(backing.buffer);
14
- });
15
-
16
- it("does not alias the source buffer after copying", () => {
17
- const source = new Uint8Array([9, 8, 7]);
18
- const owned = toOwnedArrayBuffer(source);
19
-
20
- source[1] = 1;
21
-
22
- expect([...new Uint8Array(owned)]).toEqual([9, 8, 7]);
23
- });
24
- });