@blockcast/fec-worker 0.1.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.
- package/README.md +213 -0
- package/dist/.build-stamp +0 -0
- package/dist/fec-worker-client.d.ts +80 -0
- package/dist/fec-worker-client.d.ts.map +1 -0
- package/dist/fec-worker-client.js +270 -0
- package/dist/fec-worker-client.js.map +1 -0
- package/dist/fec-worker-types.d.ts +252 -0
- package/dist/fec-worker-types.d.ts.map +1 -0
- package/dist/fec-worker-types.js +11 -0
- package/dist/fec-worker-types.js.map +1 -0
- package/dist/fec-worker.d.ts +14 -0
- package/dist/fec-worker.d.ts.map +1 -0
- package/dist/fec-worker.js +565 -0
- package/dist/fec-worker.js.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/shred-fec-worker.d.ts +20 -0
- package/dist/shred-fec-worker.d.ts.map +1 -0
- package/dist/shred-fec-worker.js +349 -0
- package/dist/shred-fec-worker.js.map +7 -0
- package/dist/shred-worker-client.d.ts +104 -0
- package/dist/shred-worker-client.d.ts.map +1 -0
- package/dist/shred-worker-client.js +181 -0
- package/dist/shred-worker-client.js.map +1 -0
- package/dist/shred-worker-types.d.ts +179 -0
- package/dist/shred-worker-types.d.ts.map +1 -0
- package/dist/shred-worker-types.js +47 -0
- package/dist/shred-worker-types.js.map +1 -0
- package/dist/transfer.d.ts +9 -0
- package/dist/transfer.d.ts.map +1 -0
- package/dist/transfer.js +13 -0
- package/dist/transfer.js.map +1 -0
- package/package.json +54 -0
- package/src/__tests__/fec-worker-alta.test.ts +231 -0
- package/src/__tests__/fec-worker-cleanup-integration.test.ts +250 -0
- package/src/__tests__/fec-worker-pending-queue.test.ts +315 -0
- package/src/__tests__/fec-worker-repair-size.test.ts +1029 -0
- package/src/__tests__/fec-worker-wasm-contract.test.ts +84 -0
- package/src/__tests__/shred-fec-worker.test.ts +448 -0
- package/src/fec-worker-client.test.ts +530 -0
- package/src/fec-worker-client.ts +309 -0
- package/src/fec-worker-types.test.ts +400 -0
- package/src/fec-worker-types.ts +268 -0
- package/src/fec-worker.ts +1048 -0
- package/src/index.ts +32 -0
- package/src/shred-fec-worker.ts +558 -0
- package/src/shred-worker-client.test.ts +182 -0
- package/src/shred-worker-client.ts +222 -0
- package/src/shred-worker-types.ts +194 -0
- package/src/transfer.test.ts +24 -0
- package/src/transfer.ts +12 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ALTA verification behavior in FEC Worker.
|
|
3
|
+
*
|
|
4
|
+
* These tests validate the verification logic rules from D-05:
|
|
5
|
+
* - altaVerified increments on valid packets
|
|
6
|
+
* - altaFailed increments on invalid packets
|
|
7
|
+
* - Packets are NEVER dropped regardless of ALTA outcome (count + play)
|
|
8
|
+
* - FrameMeta.altaVerified reflects actual result (true/false/null)
|
|
9
|
+
*
|
|
10
|
+
* The FEC Worker runs in DedicatedWorkerGlobalScope. We test the
|
|
11
|
+
* verification logic by directly simulating the branching behavior
|
|
12
|
+
* documented in fec-worker.ts (lines ~236-263).
|
|
13
|
+
*
|
|
14
|
+
* Note: Full Worker integration tests (with postMessage) require a DOM
|
|
15
|
+
* environment. These unit tests focus on the verification logic rules only.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi } from "vitest";
|
|
19
|
+
|
|
20
|
+
// --- Mock JsAltaVerifier interface (mirrors alta_rs.d.ts) ---
|
|
21
|
+
|
|
22
|
+
interface MockAltaVerifyResult {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
sequence_number: number;
|
|
25
|
+
payload?: Uint8Array;
|
|
26
|
+
error?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface MockJsAltaVerifier {
|
|
30
|
+
verify(packet: Uint8Array): MockAltaVerifyResult;
|
|
31
|
+
free(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeVerifier(valid: boolean, throws?: boolean): MockJsAltaVerifier {
|
|
35
|
+
return {
|
|
36
|
+
verify(_packet: Uint8Array): MockAltaVerifyResult {
|
|
37
|
+
if (throws) throw new Error("WASM error");
|
|
38
|
+
return {
|
|
39
|
+
valid,
|
|
40
|
+
sequence_number: 1,
|
|
41
|
+
payload: valid ? new Uint8Array(100) : undefined,
|
|
42
|
+
error: valid ? null : "invalid signature",
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
free: vi.fn(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Simulate the fec-worker verification logic ---
|
|
50
|
+
// Extracted from fec-worker.ts handleFeedSource() body.
|
|
51
|
+
// This mirrors the real implementation for unit-testability without a Worker context.
|
|
52
|
+
|
|
53
|
+
interface VerifyState {
|
|
54
|
+
altaEnabled: boolean;
|
|
55
|
+
altaVerifier: MockJsAltaVerifier | null;
|
|
56
|
+
altaVerified: number;
|
|
57
|
+
altaFailed: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface VerifyResult {
|
|
61
|
+
symbolAltaVerified: boolean | null;
|
|
62
|
+
packetDropped: boolean; // Must always be false (D-05)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function simulateVerification(
|
|
66
|
+
state: VerifyState,
|
|
67
|
+
packet: Uint8Array,
|
|
68
|
+
): VerifyResult {
|
|
69
|
+
let symbolAltaVerified: boolean | null = null;
|
|
70
|
+
let packetDropped = false;
|
|
71
|
+
|
|
72
|
+
if (state.altaEnabled && state.altaVerifier) {
|
|
73
|
+
try {
|
|
74
|
+
const result = state.altaVerifier.verify(packet);
|
|
75
|
+
if (result.valid) {
|
|
76
|
+
state.altaVerified++;
|
|
77
|
+
symbolAltaVerified = true;
|
|
78
|
+
} else {
|
|
79
|
+
state.altaFailed++;
|
|
80
|
+
symbolAltaVerified = false;
|
|
81
|
+
// D-05: count + play — do NOT return or drop the packet
|
|
82
|
+
}
|
|
83
|
+
} catch (_e) {
|
|
84
|
+
state.altaFailed++;
|
|
85
|
+
symbolAltaVerified = false;
|
|
86
|
+
// WASM error: packet still passes through (D-05)
|
|
87
|
+
}
|
|
88
|
+
} else if (state.altaEnabled && !state.altaVerifier) {
|
|
89
|
+
// Public key present but WASM init failed — not a tamper indicator
|
|
90
|
+
symbolAltaVerified = null;
|
|
91
|
+
}
|
|
92
|
+
// If not altaEnabled: symbolAltaVerified stays null
|
|
93
|
+
|
|
94
|
+
return { symbolAltaVerified, packetDropped };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Tests ---
|
|
98
|
+
|
|
99
|
+
describe("ALTA verification in FEC Worker (D-05 compliance)", () => {
|
|
100
|
+
const packet = new Uint8Array(1320);
|
|
101
|
+
|
|
102
|
+
it("Case 1: altaEnabled=false — no verification attempted, counters stay 0", () => {
|
|
103
|
+
const state: VerifyState = {
|
|
104
|
+
altaEnabled: false,
|
|
105
|
+
altaVerifier: makeVerifier(true),
|
|
106
|
+
altaVerified: 0,
|
|
107
|
+
altaFailed: 0,
|
|
108
|
+
};
|
|
109
|
+
const result = simulateVerification(state, packet);
|
|
110
|
+
expect(result.symbolAltaVerified).toBeNull();
|
|
111
|
+
expect(result.packetDropped).toBe(false);
|
|
112
|
+
expect(state.altaVerified).toBe(0);
|
|
113
|
+
expect(state.altaFailed).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("Case 2: altaEnabled=true, valid packet — altaVerified increments", () => {
|
|
117
|
+
const state: VerifyState = {
|
|
118
|
+
altaEnabled: true,
|
|
119
|
+
altaVerifier: makeVerifier(true),
|
|
120
|
+
altaVerified: 0,
|
|
121
|
+
altaFailed: 0,
|
|
122
|
+
};
|
|
123
|
+
const result = simulateVerification(state, packet);
|
|
124
|
+
expect(result.symbolAltaVerified).toBe(true);
|
|
125
|
+
expect(result.packetDropped).toBe(false);
|
|
126
|
+
expect(state.altaVerified).toBe(1);
|
|
127
|
+
expect(state.altaFailed).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("Case 3: altaEnabled=true, invalid packet — altaFailed increments, packet NOT dropped (D-05)", () => {
|
|
131
|
+
const state: VerifyState = {
|
|
132
|
+
altaEnabled: true,
|
|
133
|
+
altaVerifier: makeVerifier(false),
|
|
134
|
+
altaVerified: 0,
|
|
135
|
+
altaFailed: 0,
|
|
136
|
+
};
|
|
137
|
+
const result = simulateVerification(state, packet);
|
|
138
|
+
expect(result.symbolAltaVerified).toBe(false);
|
|
139
|
+
expect(result.packetDropped).toBe(false); // D-05: count + play
|
|
140
|
+
expect(state.altaVerified).toBe(0);
|
|
141
|
+
expect(state.altaFailed).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("Case 4: altaEnabled=true, altaVerifier=null (WASM init failed) — no crash, no altaFailed increment", () => {
|
|
145
|
+
const state: VerifyState = {
|
|
146
|
+
altaEnabled: true,
|
|
147
|
+
altaVerifier: null, // WASM init failed
|
|
148
|
+
altaVerified: 0,
|
|
149
|
+
altaFailed: 0,
|
|
150
|
+
};
|
|
151
|
+
const result = simulateVerification(state, packet);
|
|
152
|
+
// altaVerified is null (unverified due to init failure, not tamper)
|
|
153
|
+
expect(result.symbolAltaVerified).toBeNull();
|
|
154
|
+
expect(result.packetDropped).toBe(false);
|
|
155
|
+
expect(state.altaVerified).toBe(0);
|
|
156
|
+
// CRITICAL: altaFailed must NOT increment for init failures (not a tamper indicator)
|
|
157
|
+
expect(state.altaFailed).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("Case 5: altaVerifier.verify() throws — altaFailed increments, packet still passes (D-05)", () => {
|
|
161
|
+
const state: VerifyState = {
|
|
162
|
+
altaEnabled: true,
|
|
163
|
+
altaVerifier: makeVerifier(true, /* throws */ true),
|
|
164
|
+
altaVerified: 0,
|
|
165
|
+
altaFailed: 0,
|
|
166
|
+
};
|
|
167
|
+
const result = simulateVerification(state, packet);
|
|
168
|
+
expect(result.symbolAltaVerified).toBe(false);
|
|
169
|
+
expect(result.packetDropped).toBe(false); // D-05: even on WASM error
|
|
170
|
+
expect(state.altaVerified).toBe(0);
|
|
171
|
+
expect(state.altaFailed).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("Case 6: FrameMeta.altaVerified reflects actual result (true/false/null)", () => {
|
|
175
|
+
// Valid packet → true
|
|
176
|
+
const stateValid: VerifyState = { altaEnabled: true, altaVerifier: makeVerifier(true), altaVerified: 0, altaFailed: 0 };
|
|
177
|
+
expect(simulateVerification(stateValid, packet).symbolAltaVerified).toBe(true);
|
|
178
|
+
|
|
179
|
+
// Invalid packet → false
|
|
180
|
+
const stateInvalid: VerifyState = { altaEnabled: true, altaVerifier: makeVerifier(false), altaVerified: 0, altaFailed: 0 };
|
|
181
|
+
expect(simulateVerification(stateInvalid, packet).symbolAltaVerified).toBe(false);
|
|
182
|
+
|
|
183
|
+
// ALTA not enabled → null
|
|
184
|
+
const stateDisabled: VerifyState = { altaEnabled: false, altaVerifier: makeVerifier(true), altaVerified: 0, altaFailed: 0 };
|
|
185
|
+
expect(simulateVerification(stateDisabled, packet).symbolAltaVerified).toBeNull();
|
|
186
|
+
|
|
187
|
+
// ALTA enabled but WASM init failed → null (not false)
|
|
188
|
+
const stateNoWasm: VerifyState = { altaEnabled: true, altaVerifier: null, altaVerified: 0, altaFailed: 0 };
|
|
189
|
+
expect(simulateVerification(stateNoWasm, packet).symbolAltaVerified).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("Multiple valid packets — altaVerified accumulates correctly", () => {
|
|
193
|
+
const state: VerifyState = {
|
|
194
|
+
altaEnabled: true,
|
|
195
|
+
altaVerifier: makeVerifier(true),
|
|
196
|
+
altaVerified: 0,
|
|
197
|
+
altaFailed: 0,
|
|
198
|
+
};
|
|
199
|
+
for (let i = 0; i < 32; i++) {
|
|
200
|
+
simulateVerification(state, packet);
|
|
201
|
+
}
|
|
202
|
+
expect(state.altaVerified).toBe(32);
|
|
203
|
+
expect(state.altaFailed).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("Mixed valid/invalid packets — counters track independently", () => {
|
|
207
|
+
const state: VerifyState = {
|
|
208
|
+
altaEnabled: true,
|
|
209
|
+
altaVerifier: null as unknown as MockJsAltaVerifier,
|
|
210
|
+
altaVerified: 0,
|
|
211
|
+
altaFailed: 0,
|
|
212
|
+
};
|
|
213
|
+
// 20 valid, 5 invalid, 3 throws
|
|
214
|
+
let callCount = 0;
|
|
215
|
+
state.altaVerifier = {
|
|
216
|
+
verify(_p: Uint8Array): MockAltaVerifyResult {
|
|
217
|
+
callCount++;
|
|
218
|
+
if (callCount <= 20) return { valid: true, sequence_number: callCount };
|
|
219
|
+
if (callCount <= 25) return { valid: false, sequence_number: callCount, error: "tampered" };
|
|
220
|
+
throw new Error("WASM failure");
|
|
221
|
+
},
|
|
222
|
+
free: vi.fn(),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < 28; i++) {
|
|
226
|
+
simulateVerification(state, packet);
|
|
227
|
+
}
|
|
228
|
+
expect(state.altaVerified).toBe(20);
|
|
229
|
+
expect(state.altaFailed).toBe(8); // 5 invalid + 3 throws
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { FecWorkerClient } from "../fec-worker-client.js";
|
|
3
|
+
import type {
|
|
4
|
+
FecWorkerCommand,
|
|
5
|
+
FecWorkerEvent,
|
|
6
|
+
FecTrackConfig,
|
|
7
|
+
} from "../fec-worker-types.js";
|
|
8
|
+
|
|
9
|
+
const EMPTY_WASM_MODULE = new WebAssembly.Module(
|
|
10
|
+
new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const CONFIG: FecTrackConfig = {
|
|
14
|
+
codec: "avc1.64001f",
|
|
15
|
+
trackName: "video",
|
|
16
|
+
trackType: "video",
|
|
17
|
+
algorithm: "raptorq",
|
|
18
|
+
k: 4,
|
|
19
|
+
repairCount: 2,
|
|
20
|
+
symbolSize: 8,
|
|
21
|
+
interleaveDepth: 4,
|
|
22
|
+
interleaveMs: 100,
|
|
23
|
+
deliveryWindowMs: 200,
|
|
24
|
+
fecMode: "subframe",
|
|
25
|
+
altaEnabled: false,
|
|
26
|
+
relayBlockSigEnabled: false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function nextTick(): Promise<void> {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function frameCleanupRecords(
|
|
34
|
+
records: Array<{ sbn: number; data: Uint8Array }>,
|
|
35
|
+
): Uint8Array {
|
|
36
|
+
const size = records.reduce((total, record) => total + 8 + record.data.byteLength, 0);
|
|
37
|
+
const framed = new Uint8Array(size);
|
|
38
|
+
const view = new DataView(framed.buffer);
|
|
39
|
+
let offset = 0;
|
|
40
|
+
for (const record of records) {
|
|
41
|
+
view.setUint32(offset, record.sbn, false);
|
|
42
|
+
view.setUint32(offset + 4, record.data.byteLength, false);
|
|
43
|
+
offset += 8;
|
|
44
|
+
framed.set(record.data, offset);
|
|
45
|
+
offset += record.data.byteLength;
|
|
46
|
+
}
|
|
47
|
+
return framed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.unstubAllGlobals();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("FEC worker cleanup integration", () => {
|
|
55
|
+
it("keeps per-SBN green-fill ownership through a coalesced client/worker round trip", async () => {
|
|
56
|
+
const fill16 = new Uint8Array([0, 0, 0, 1, 0x0d]);
|
|
57
|
+
const fill18 = new Uint8Array([0, 0, 0, 1, 0x0d]);
|
|
58
|
+
let cleanupPass = 0;
|
|
59
|
+
const cleanupBlocksDetailed = vi.fn((sbns: Uint32Array) => {
|
|
60
|
+
cleanupPass++;
|
|
61
|
+
return cleanupPass === 1
|
|
62
|
+
? new Uint8Array()
|
|
63
|
+
: frameCleanupRecords([
|
|
64
|
+
...Array.from(sbns, (sbn) => ({
|
|
65
|
+
sbn,
|
|
66
|
+
data: sbn === 16 ? fill16 : fill18,
|
|
67
|
+
})),
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
class MockDecoder {
|
|
72
|
+
static last: MockDecoder | undefined;
|
|
73
|
+
constructor() {
|
|
74
|
+
MockDecoder.last = this;
|
|
75
|
+
}
|
|
76
|
+
add_source = vi.fn(() => undefined);
|
|
77
|
+
add_repair = vi.fn(() => undefined);
|
|
78
|
+
configure_params = vi.fn();
|
|
79
|
+
cleanup_blocks_detailed = cleanupBlocksDetailed;
|
|
80
|
+
cleanup_by_sbn = vi.fn(() => new Uint32Array());
|
|
81
|
+
flush = vi.fn();
|
|
82
|
+
pending_blocks = vi.fn(() => 0);
|
|
83
|
+
reset_stats = vi.fn();
|
|
84
|
+
get_stats = vi.fn(() => ({
|
|
85
|
+
source_packets: 0n,
|
|
86
|
+
repair_packets: 0n,
|
|
87
|
+
blocks_complete: 0n,
|
|
88
|
+
blocks_recovered: 0n,
|
|
89
|
+
blocks_failed: 0n,
|
|
90
|
+
bytes_recovered: 0n,
|
|
91
|
+
recovery_rate: () => 0,
|
|
92
|
+
}));
|
|
93
|
+
free = vi.fn();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let workerOnMessage:
|
|
97
|
+
| ((event: MessageEvent<FecWorkerCommand>) => void)
|
|
98
|
+
| undefined;
|
|
99
|
+
const workerCommands: FecWorkerCommand[] = [];
|
|
100
|
+
const pageWorker = {
|
|
101
|
+
onmessage: null as ((event: MessageEvent<FecWorkerEvent>) => void) | null,
|
|
102
|
+
onerror: null as ((event: ErrorEvent) => void) | null,
|
|
103
|
+
onmessageerror: null as ((event: MessageEvent) => void) | null,
|
|
104
|
+
postMessage: vi.fn((command: FecWorkerCommand) => {
|
|
105
|
+
workerCommands.push(command);
|
|
106
|
+
workerOnMessage?.({ data: command } as MessageEvent<FecWorkerCommand>);
|
|
107
|
+
}),
|
|
108
|
+
terminate: vi.fn(),
|
|
109
|
+
};
|
|
110
|
+
const workerScope = {
|
|
111
|
+
__MMT_WASM_BINDINGS__: {
|
|
112
|
+
initSync: vi.fn(),
|
|
113
|
+
MmtFecDecoder: MockDecoder,
|
|
114
|
+
},
|
|
115
|
+
get onmessage() {
|
|
116
|
+
return workerOnMessage;
|
|
117
|
+
},
|
|
118
|
+
set onmessage(handler) {
|
|
119
|
+
workerOnMessage = handler;
|
|
120
|
+
},
|
|
121
|
+
postMessage: vi.fn((event: FecWorkerEvent) => {
|
|
122
|
+
pageWorker.onmessage?.({ data: event } as MessageEvent<FecWorkerEvent>);
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
vi.stubGlobal("self", workerScope);
|
|
127
|
+
vi.resetModules();
|
|
128
|
+
await import("../fec-worker.ts");
|
|
129
|
+
|
|
130
|
+
const client = new FecWorkerClient(pageWorker as unknown as Worker);
|
|
131
|
+
const completed: number[] = [];
|
|
132
|
+
const fills: Array<{ sbn: number; data: number[] }> = [];
|
|
133
|
+
client.onCleanupComplete((sbn) => completed.push(sbn));
|
|
134
|
+
client.onGreenFill((sbn, data) => {
|
|
135
|
+
fills.push({ sbn, data: [...new Uint8Array(data)] });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
client.configure(CONFIG, EMPTY_WASM_MODULE);
|
|
139
|
+
await nextTick();
|
|
140
|
+
await nextTick();
|
|
141
|
+
client.cleanup(15);
|
|
142
|
+
client.cleanup(16);
|
|
143
|
+
client.cleanup(18);
|
|
144
|
+
for (let i = 0; i < 6; i++) await nextTick();
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
workerCommands
|
|
148
|
+
.filter((command) => command.type === "cleanup")
|
|
149
|
+
.map((command) => command.type === "cleanup" ? command.sbns : []),
|
|
150
|
+
).toEqual([[15], [16, 18]]);
|
|
151
|
+
expect(cleanupBlocksDetailed).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(Array.from(cleanupBlocksDetailed.mock.calls[0]![0])).toEqual([15]);
|
|
153
|
+
expect(Array.from(cleanupBlocksDetailed.mock.calls[1]![0])).toEqual([16, 18]);
|
|
154
|
+
expect(fills).toEqual([
|
|
155
|
+
{ sbn: 16, data: [...fill16] },
|
|
156
|
+
{ sbn: 18, data: [...fill18] },
|
|
157
|
+
]);
|
|
158
|
+
expect(completed).toEqual([15, 16, 18]);
|
|
159
|
+
|
|
160
|
+
client.dispose();
|
|
161
|
+
await nextTick();
|
|
162
|
+
await nextTick();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps reset quarantined after surfacing a decoder reset failure", async () => {
|
|
166
|
+
class MockDecoder {
|
|
167
|
+
static last: MockDecoder | undefined;
|
|
168
|
+
constructor() {
|
|
169
|
+
MockDecoder.last = this;
|
|
170
|
+
}
|
|
171
|
+
add_source = vi.fn(() => undefined);
|
|
172
|
+
add_repair = vi.fn(() => undefined);
|
|
173
|
+
configure_params = vi.fn();
|
|
174
|
+
flush = vi.fn(() => {
|
|
175
|
+
throw new Error("flush failed");
|
|
176
|
+
});
|
|
177
|
+
pending_blocks = vi.fn(() => 0);
|
|
178
|
+
reset_stats = vi.fn();
|
|
179
|
+
get_stats = vi.fn(() => ({
|
|
180
|
+
source_packets: 0n,
|
|
181
|
+
repair_packets: 0n,
|
|
182
|
+
blocks_complete: 0n,
|
|
183
|
+
blocks_recovered: 0n,
|
|
184
|
+
blocks_failed: 0n,
|
|
185
|
+
bytes_recovered: 0n,
|
|
186
|
+
recovery_rate: () => 0,
|
|
187
|
+
}));
|
|
188
|
+
free = vi.fn();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let workerOnMessage:
|
|
192
|
+
| ((event: MessageEvent<FecWorkerCommand>) => void)
|
|
193
|
+
| undefined;
|
|
194
|
+
const pageWorker = {
|
|
195
|
+
onmessage: null as ((event: MessageEvent<FecWorkerEvent>) => void) | null,
|
|
196
|
+
onerror: null as ((event: ErrorEvent) => void) | null,
|
|
197
|
+
onmessageerror: null as ((event: MessageEvent) => void) | null,
|
|
198
|
+
postMessage: vi.fn((command: FecWorkerCommand) => {
|
|
199
|
+
workerOnMessage?.({ data: command } as MessageEvent<FecWorkerCommand>);
|
|
200
|
+
}),
|
|
201
|
+
terminate: vi.fn(),
|
|
202
|
+
};
|
|
203
|
+
const workerScope = {
|
|
204
|
+
__MMT_WASM_BINDINGS__: {
|
|
205
|
+
initSync: vi.fn(),
|
|
206
|
+
MmtFecDecoder: MockDecoder,
|
|
207
|
+
},
|
|
208
|
+
get onmessage() {
|
|
209
|
+
return workerOnMessage;
|
|
210
|
+
},
|
|
211
|
+
set onmessage(handler) {
|
|
212
|
+
workerOnMessage = handler;
|
|
213
|
+
},
|
|
214
|
+
postMessage: vi.fn((event: FecWorkerEvent) => {
|
|
215
|
+
pageWorker.onmessage?.({ data: event } as MessageEvent<FecWorkerEvent>);
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
vi.stubGlobal("self", workerScope);
|
|
220
|
+
vi.resetModules();
|
|
221
|
+
await import("../fec-worker.ts");
|
|
222
|
+
|
|
223
|
+
const client = new FecWorkerClient(pageWorker as unknown as Worker);
|
|
224
|
+
const errors: Array<{ message: string; code: string | undefined }> = [];
|
|
225
|
+
const snapshots: unknown[] = [];
|
|
226
|
+
client.onError((message, code) => errors.push({ message, code }));
|
|
227
|
+
client.onSnapshot((stats) => snapshots.push(stats));
|
|
228
|
+
|
|
229
|
+
client.configure(CONFIG, EMPTY_WASM_MODULE);
|
|
230
|
+
await nextTick();
|
|
231
|
+
await nextTick();
|
|
232
|
+
snapshots.length = 0;
|
|
233
|
+
|
|
234
|
+
client.reset();
|
|
235
|
+
for (let i = 0; i < 4; i++) await nextTick();
|
|
236
|
+
|
|
237
|
+
expect(MockDecoder.last?.flush).toHaveBeenCalledOnce();
|
|
238
|
+
expect(errors).toEqual([
|
|
239
|
+
{
|
|
240
|
+
message: "[FecWorker] reset failed: flush failed",
|
|
241
|
+
code: "reset-failed",
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
expect(snapshots).toHaveLength(0);
|
|
245
|
+
|
|
246
|
+
client.dispose();
|
|
247
|
+
await nextTick();
|
|
248
|
+
await nextTick();
|
|
249
|
+
});
|
|
250
|
+
});
|