@blockcast/fec-worker 0.1.0-main.02dfe3276f87

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 (48) hide show
  1. package/README.md +223 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/block-order.d.ts +38 -0
  4. package/dist/block-order.d.ts.map +1 -0
  5. package/dist/block-order.js +78 -0
  6. package/dist/block-order.js.map +1 -0
  7. package/dist/fec-worker-client.d.ts +93 -0
  8. package/dist/fec-worker-client.d.ts.map +1 -0
  9. package/dist/fec-worker-client.js +346 -0
  10. package/dist/fec-worker-client.js.map +1 -0
  11. package/dist/fec-worker-types.d.ts +380 -0
  12. package/dist/fec-worker-types.d.ts.map +1 -0
  13. package/dist/fec-worker-types.js +11 -0
  14. package/dist/fec-worker-types.js.map +1 -0
  15. package/dist/fec-worker.d.ts +14 -0
  16. package/dist/fec-worker.d.ts.map +1 -0
  17. package/dist/fec-worker.js +859 -0
  18. package/dist/fec-worker.js.map +7 -0
  19. package/dist/index.d.ts +10 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +8 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/shred-fec-worker.d.ts +20 -0
  24. package/dist/shred-fec-worker.d.ts.map +1 -0
  25. package/dist/shred-fec-worker.js +370 -0
  26. package/dist/shred-fec-worker.js.map +7 -0
  27. package/dist/shred-worker-client.d.ts +104 -0
  28. package/dist/shred-worker-client.d.ts.map +1 -0
  29. package/dist/shred-worker-client.js +181 -0
  30. package/dist/shred-worker-client.js.map +1 -0
  31. package/dist/shred-worker-types.d.ts +194 -0
  32. package/dist/shred-worker-types.d.ts.map +1 -0
  33. package/dist/shred-worker-types.js +62 -0
  34. package/dist/shred-worker-types.js.map +1 -0
  35. package/dist/transfer.d.ts +9 -0
  36. package/dist/transfer.d.ts.map +1 -0
  37. package/dist/transfer.js +13 -0
  38. package/dist/transfer.js.map +1 -0
  39. package/package.json +59 -0
  40. package/src/block-order.ts +93 -0
  41. package/src/fec-worker-client.ts +392 -0
  42. package/src/fec-worker-types.ts +399 -0
  43. package/src/fec-worker.ts +1502 -0
  44. package/src/index.ts +35 -0
  45. package/src/shred-fec-worker.ts +589 -0
  46. package/src/shred-worker-client.ts +222 -0
  47. package/src/shred-worker-types.ts +209 -0
  48. package/src/transfer.ts +12 -0
@@ -0,0 +1,859 @@
1
+ // Self-contained worker bundle (no bare imports). See build.mjs.
2
+
3
+ // src/transfer.ts
4
+ function toOwnedArrayBuffer(view) {
5
+ const copy = new Uint8Array(view.byteLength);
6
+ copy.set(view);
7
+ return copy.buffer;
8
+ }
9
+
10
+ // src/block-order.ts
11
+ var SPAN = 4294967296;
12
+ var HALF = 2147483648;
13
+ function blockStart(sbn, k) {
14
+ return sbn * k % SPAN;
15
+ }
16
+ function precedingDistance(a, b, k) {
17
+ const distance = (blockStart(b, k) - blockStart(a, k) + SPAN) % SPAN;
18
+ return distance !== 0 && distance < HALF ? distance : null;
19
+ }
20
+ function blockPrecedes(a, b, k) {
21
+ return precedingDistance(a, b, k) !== null;
22
+ }
23
+ function blocksBehind(a, b, k) {
24
+ const distance = precedingDistance(a, b, k);
25
+ return distance === null ? 0 : Math.ceil(distance / k);
26
+ }
27
+ function serialDistance(from, to) {
28
+ return (to - from + SPAN) % SPAN;
29
+ }
30
+ function serialPrecedes(a, b) {
31
+ const distance = serialDistance(a, b);
32
+ return distance !== 0 && distance < HALF;
33
+ }
34
+ function isoBlockContains(ssStart, ssbLength, ssId) {
35
+ return serialDistance(ssStart, ssId) < ssbLength;
36
+ }
37
+ function isoBlocksOverlap(aStart, aLength, bStart, bLength) {
38
+ return isoBlockContains(aStart, aLength, bStart) || isoBlockContains(bStart, bLength, aStart);
39
+ }
40
+ function isoBlocksBehind(a, b, kMax) {
41
+ if (!Number.isSafeInteger(kMax) || kMax <= 0) {
42
+ throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`);
43
+ }
44
+ return serialPrecedes(a, b) ? Math.floor(serialDistance(a, b) / kMax) : 0;
45
+ }
46
+
47
+ // src/fec-worker.ts
48
+ var wasmDecoder = null;
49
+ var config = null;
50
+ var altaEnabled = false;
51
+ var altaVerifier = null;
52
+ var snapshotTimer = null;
53
+ var blockUpdatesSubscribed = false;
54
+ var blocks = /* @__PURE__ */ new Map();
55
+ var acceptedSourceEsis = /* @__PURE__ */ new Map();
56
+ var acceptedRepairEsis = /* @__PURE__ */ new Map();
57
+ var unplacedSources = /* @__PURE__ */ new Map();
58
+ var UINT32_MAX = 4294967295;
59
+ var UINT24_MAX = 16777215;
60
+ var SS_ID_SPAN = 4294967296;
61
+ var SSBG1_LENGTH_FIELD_BYTES = 2;
62
+ var MAX_INTERLEAVE_DEPTH = 255;
63
+ var MAX_SYMBOL_SIZE = 65528;
64
+ var MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK = 56403;
65
+ var sourceSymbols = 0;
66
+ var repairSymbols = 0;
67
+ var blocksComplete = 0;
68
+ var blocksRecovered = 0;
69
+ var blocksFailed = 0;
70
+ var altaVerified = 0;
71
+ var altaFailed = 0;
72
+ var altaFailedDetails = [];
73
+ var altaPending = 0;
74
+ var altaPendingEvictedCap = 0;
75
+ var altaPendingEvictedTtl = 0;
76
+ var altaError = 0;
77
+ var altaInitFailed = false;
78
+ var relayBlocksVerified = 0;
79
+ var relayBlocksFailed = 0;
80
+ var liveSbn = 0;
81
+ var liveSbnKnown = false;
82
+ var repairSymbolSizeMismatch = 0;
83
+ var sourceDroppedRetired = 0;
84
+ var sourceDroppedTerminal = 0;
85
+ var sourceDroppedDuplicate = 0;
86
+ var sourceUnplacedRetired = 0;
87
+ var recoveryTimes = [];
88
+ var pendingVerify = /* @__PURE__ */ new Map();
89
+ var pendingVerifyCap = 0;
90
+ var pendingVerifyTtlMs = 0;
91
+ function emit(event, transfer) {
92
+ self.postMessage(event, transfer ?? []);
93
+ }
94
+ function isUint32(value) {
95
+ return Number.isInteger(value) && value >= 0 && value <= UINT32_MAX;
96
+ }
97
+ function isTerminalBlock(block) {
98
+ return block.state !== "collecting";
99
+ }
100
+ function retireAcceptedIds(sbn) {
101
+ acceptedSourceEsis.delete(sbn);
102
+ acceptedRepairEsis.delete(sbn);
103
+ }
104
+ function clearBlockTracking() {
105
+ blocks.clear();
106
+ acceptedSourceEsis.clear();
107
+ acceptedRepairEsis.clear();
108
+ unplacedSources.clear();
109
+ pendingVerify.clear();
110
+ recoveryTimes.length = 0;
111
+ liveSbn = 0;
112
+ liveSbnKnown = false;
113
+ }
114
+ function isRetiredSbn(sbn) {
115
+ if (config === null || !liveSbnKnown) return false;
116
+ return config.blockProfile === "iso-ssbg1-v1" ? isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth : blocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
117
+ }
118
+ function isIsoProfile() {
119
+ return config?.blockProfile === "iso-ssbg1-v1";
120
+ }
121
+ function isoKnownLength(block) {
122
+ return block.ssbLength ?? 1;
123
+ }
124
+ function findIsoBlock(ssId) {
125
+ for (const block of blocks.values()) {
126
+ if (isoBlockContains(block.sbn, isoKnownLength(block), ssId)) return block;
127
+ }
128
+ return void 0;
129
+ }
130
+ function isRetiredBlock(key, block) {
131
+ return isIsoProfile() ? isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN) : isRetiredSbn(key);
132
+ }
133
+ function trimBlockRetention() {
134
+ for (const [sbn, block] of blocks) {
135
+ if (!isTerminalBlock(block) || !isRetiredBlock(sbn, block)) continue;
136
+ blocks.delete(sbn);
137
+ retireAcceptedIds(sbn);
138
+ }
139
+ }
140
+ function advanceMediaHorizon(sbn) {
141
+ if (!config) return;
142
+ if (liveSbnKnown) {
143
+ const ahead = config.blockProfile === "iso-ssbg1-v1" ? serialPrecedes(liveSbn, sbn) : blockPrecedes(liveSbn, sbn, config.k);
144
+ if (!ahead) return;
145
+ }
146
+ liveSbn = sbn;
147
+ liveSbnKnown = true;
148
+ trimBlockRetention();
149
+ }
150
+ function markBlockTerminal(block, state) {
151
+ block.state = state;
152
+ block.completedTs = performance.now();
153
+ retireAcceptedIds(block.sbn);
154
+ trimBlockRetention();
155
+ }
156
+ function synchronizeDecoderHorizon() {
157
+ if (!wasmDecoder || !liveSbnKnown) return;
158
+ const iso = isIsoProfile();
159
+ const expired = iso ? wasmDecoder.cleanup_by_ss_start(liveSbn) : wasmDecoder.cleanup_by_sbn(liveSbn);
160
+ for (const sbn of expired) {
161
+ const block = blocks.get(sbn);
162
+ if (!block || isTerminalBlock(block)) {
163
+ retireAcceptedIds(sbn);
164
+ continue;
165
+ }
166
+ markBlockTerminal(block, "failed");
167
+ blocksFailed++;
168
+ emitBlockUpdate(block);
169
+ }
170
+ if (iso) retireUnplacedSources();
171
+ trimBlockRetention();
172
+ }
173
+ function retireUnplacedSources() {
174
+ for (const ssId of unplacedSources.keys()) {
175
+ const unreachable = ssId !== liveSbn && !serialPrecedes(ssId, liveSbn);
176
+ if (!unreachable && !isRetiredSbn(ssId)) break;
177
+ unplacedSources.delete(ssId);
178
+ sourceUnplacedRetired++;
179
+ }
180
+ }
181
+ function getOrCreateBlock(sbn, isoSsbLength) {
182
+ let block = blocks.get(sbn);
183
+ if (!block) {
184
+ if (!config) throw new Error("[FecWorker] not configured");
185
+ const now = performance.now();
186
+ block = {
187
+ sbn,
188
+ state: "collecting",
189
+ sourceReceived: 0,
190
+ repairReceived: 0,
191
+ k: isoSsbLength ?? config.k,
192
+ repairCount: config.repairCount,
193
+ firstSymbolTs: now,
194
+ lastSymbolTs: now,
195
+ deadlineTs: now + config.deliveryWindowMs,
196
+ altaVerified: 0,
197
+ relayBlockSigValid: null
198
+ };
199
+ if (isoSsbLength !== void 0) block.ssbLength = isoSsbLength;
200
+ blocks.set(sbn, block);
201
+ trimBlockRetention();
202
+ }
203
+ return block;
204
+ }
205
+ function buildStatsSnapshot() {
206
+ if (!config) throw new Error("[FecWorker] not configured");
207
+ const decoderStats = wasmDecoder?.get_stats();
208
+ const decoder = decoderStats ? {
209
+ sourcePackets: Number(decoderStats.source_packets),
210
+ repairPackets: Number(decoderStats.repair_packets),
211
+ blocksComplete: Number(decoderStats.blocks_complete),
212
+ blocksRecovered: Number(decoderStats.blocks_recovered),
213
+ blocksFailed: Number(decoderStats.blocks_failed),
214
+ bytesRecovered: Number(decoderStats.bytes_recovered),
215
+ pendingBlocks: wasmDecoder?.pending_blocks() ?? 0
216
+ } : void 0;
217
+ const avgRecoveryMs = recoveryTimes.length > 0 ? recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length : 0;
218
+ const maxRecoveryMs = recoveryTimes.length > 0 ? Math.max(...recoveryTimes) : 0;
219
+ const maxBlocks = Math.max(config.interleaveDepth * 3, 24);
220
+ const blockArray = Array.from(blocks.values()).sort((a, b) => b.sbn - a.sbn).slice(0, maxBlocks);
221
+ return {
222
+ config,
223
+ sourceSymbols,
224
+ repairSymbols,
225
+ blocksComplete,
226
+ blocksRecovered,
227
+ blocksFailed,
228
+ recoveryRate: blocksComplete + blocksRecovered > 0 ? Math.round(
229
+ blocksRecovered / (blocksRecovered + blocksFailed) * 100
230
+ ) || 0 : 0,
231
+ altaVerified,
232
+ altaFailed,
233
+ altaPending,
234
+ altaPendingEvictedCap,
235
+ altaPendingEvictedTtl,
236
+ altaError,
237
+ altaInitFailed,
238
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
239
+ altaFailedDetails: altaFailedDetails.slice(),
240
+ relayBlocksVerified,
241
+ relayBlocksFailed,
242
+ liveSbn,
243
+ blocks: blockArray,
244
+ interleaveWindowBlocks: config.interleaveDepth,
245
+ avgRecoveryMs: Math.round(avgRecoveryMs * 100) / 100,
246
+ maxRecoveryMs: Math.round(maxRecoveryMs * 100) / 100,
247
+ repairSymbolSizeMismatch,
248
+ sourceDroppedRetired,
249
+ sourceDroppedTerminal,
250
+ sourceDroppedDuplicate,
251
+ ...config.blockProfile === "iso-ssbg1-v1" ? { sourceUnplacedRetired } : {},
252
+ decoder
253
+ };
254
+ }
255
+ function emitSnapshot() {
256
+ if (!config) return;
257
+ emit({ type: "snapshot", stats: buildStatsSnapshot() });
258
+ }
259
+ function emitBlockUpdate(block) {
260
+ if (!blockUpdatesSubscribed) return;
261
+ emit({ type: "blockUpdate", block: { ...block } });
262
+ }
263
+ function emitRecoveredSymbols(sbn, recoveredBlock, block) {
264
+ if (!config) return;
265
+ const symbolSize = config.symbolSize;
266
+ if (symbolSize <= 0) {
267
+ emit({
268
+ type: "error",
269
+ code: "recovered-symbol-size-unconfigured",
270
+ message: "[FecWorker] recovered block has no configured symbolSize"
271
+ });
272
+ return;
273
+ }
274
+ const ssbLength = block.ssbLength ?? config.k;
275
+ if (recoveredBlock.byteLength !== symbolSize * ssbLength) {
276
+ emit({
277
+ type: "error",
278
+ code: "recovered-block-too-short",
279
+ message: `[FecWorker] recovered block has the wrong length: got ${recoveredBlock.byteLength}B, expected ${symbolSize * ssbLength}B (SSB_length=${ssbLength})`
280
+ });
281
+ return;
282
+ }
283
+ const iso = config.blockProfile === "iso-ssbg1-v1";
284
+ for (let i = 0; i < ssbLength; i++) {
285
+ const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
286
+ if (iso) {
287
+ const invalid = invalidSsbg1Symbol(symbol, symbolSize);
288
+ if (invalid !== null) {
289
+ emit({
290
+ type: "error",
291
+ code: "recovered-ssbg1-symbol-invalid",
292
+ message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`
293
+ });
294
+ continue;
295
+ }
296
+ }
297
+ const recoveredAltaVerified = null;
298
+ if (altaEnabled) altaPending++;
299
+ const buf = toOwnedArrayBuffer(symbol);
300
+ const meta = {
301
+ altaVerified: recoveredAltaVerified,
302
+ recovered: true,
303
+ sbn,
304
+ esi: i,
305
+ ssbLength
306
+ };
307
+ emit({ type: "frame", data: buf, meta }, [buf]);
308
+ }
309
+ }
310
+ function invalidSsbg1Symbol(symbol, symbolSize) {
311
+ const length = symbol[0] << 8 | symbol[1];
312
+ const maxLength = symbolSize - SSBG1_LENGTH_FIELD_BYTES;
313
+ if (length > maxLength) {
314
+ return `length field ${length} exceeds T-2 = ${maxLength}`;
315
+ }
316
+ for (let j = SSBG1_LENGTH_FIELD_BYTES + length; j < symbolSize; j++) {
317
+ if (symbol[j] !== 0) return `non-zero padding at byte ${j} after a ${length}-byte packet`;
318
+ }
319
+ return null;
320
+ }
321
+ function startSnapshotTimer() {
322
+ if (snapshotTimer !== null) clearInterval(snapshotTimer);
323
+ snapshotTimer = setInterval(() => emitSnapshot(), 200);
324
+ }
325
+ function resolveWasmBindings() {
326
+ const bindings = self.__MMT_WASM_BINDINGS__;
327
+ if (!bindings) {
328
+ throw new Error(
329
+ "[FecWorker] WASM bindings not available. Set self.__MMT_WASM_BINDINGS__ = { initSync, MmtFecDecoder } before configure."
330
+ );
331
+ }
332
+ return bindings;
333
+ }
334
+ function verifyDecoderProfile(decoder, expected) {
335
+ let actual;
336
+ try {
337
+ actual = decoder.get_block_profile();
338
+ } catch (err) {
339
+ decoder.free();
340
+ throw new Error(
341
+ `[FecWorker] configure: decoder cannot report its block profile (${err instanceof Error ? err.message : String(err)})`
342
+ );
343
+ }
344
+ if (actual !== expected) {
345
+ decoder.free();
346
+ throw new Error(
347
+ `[FecWorker] configure: decoder runs block profile ${JSON.stringify(actual)}, not the configured "${expected}"`
348
+ );
349
+ }
350
+ }
351
+ function acceptId(ids, key, id) {
352
+ let set = ids.get(key);
353
+ if (!set) {
354
+ set = /* @__PURE__ */ new Set();
355
+ ids.set(key, set);
356
+ }
357
+ set.add(id);
358
+ }
359
+ function recordRecovered(key, block, result) {
360
+ const recovered = new Uint8Array(toOwnedArrayBuffer(result));
361
+ const recoveryMs = performance.now() - block.firstSymbolTs;
362
+ recoveryTimes.push(recoveryMs);
363
+ if (recoveryTimes.length > 1e3) recoveryTimes.shift();
364
+ if (block.state === "collecting") {
365
+ markBlockTerminal(block, "recovered");
366
+ blocksRecovered++;
367
+ emitBlockUpdate(block);
368
+ }
369
+ emitRecoveredSymbols(key, recovered, block);
370
+ }
371
+ function settleSourceArrival(key, block, result) {
372
+ const authoredLength = block.ssbLength ?? block.k;
373
+ if (block.sourceReceived >= authoredLength && block.state === "collecting") {
374
+ markBlockTerminal(block, "complete");
375
+ blocksComplete++;
376
+ emitBlockUpdate(block);
377
+ }
378
+ if (result && result.byteLength > 0 && block.sourceReceived < authoredLength) {
379
+ recordRecovered(key, block, result);
380
+ }
381
+ synchronizeDecoderHorizon();
382
+ }
383
+ function settleRepairArrival(key, block, ssbLength, result) {
384
+ if (block.sourceReceived >= ssbLength && block.state === "collecting") {
385
+ markBlockTerminal(block, "complete");
386
+ blocksComplete++;
387
+ emitBlockUpdate(block);
388
+ } else if (result && result.byteLength > 0) {
389
+ recordRecovered(key, block, result);
390
+ }
391
+ synchronizeDecoderHorizon();
392
+ }
393
+ function feedIsoSource(cmd, view, decoder, cfg) {
394
+ const maxPacketLength = cfg.symbolSize - SSBG1_LENGTH_FIELD_BYTES;
395
+ if (view.byteLength > maxPacketLength) {
396
+ throw new Error(
397
+ `[FecWorker] feedSource: iso-ssbg1-v1 protected packet of ${view.byteLength}B exceeds T-2 = ${maxPacketLength}B`
398
+ );
399
+ }
400
+ const block = findIsoBlock(cmd.ssId);
401
+ if (!block) {
402
+ if (isRetiredSbn(cmd.ssId)) {
403
+ sourceDroppedRetired++;
404
+ return;
405
+ }
406
+ if (unplacedSources.has(cmd.ssId)) {
407
+ sourceDroppedDuplicate++;
408
+ return;
409
+ }
410
+ const result2 = decoder.add_source(cmd.ssId, view, cmd.ts);
411
+ advanceMediaHorizon(cmd.ssId);
412
+ unplacedSources.set(cmd.ssId, performance.now());
413
+ sourceSymbols++;
414
+ if (altaEnabled) altaPending++;
415
+ if (result2 && result2.byteLength > 0) {
416
+ emit({
417
+ type: "error",
418
+ code: "recovered-block-unplaced",
419
+ message: `[FecWorker] Dropping a ${result2.byteLength}B recovered block returned for SS_ID ${cmd.ssId}, which no known block contains`
420
+ });
421
+ }
422
+ synchronizeDecoderHorizon();
423
+ return;
424
+ }
425
+ const key = block.sbn;
426
+ if (isRetiredSbn(key)) {
427
+ sourceDroppedRetired++;
428
+ return;
429
+ }
430
+ if (isTerminalBlock(block)) {
431
+ sourceDroppedTerminal++;
432
+ return;
433
+ }
434
+ const esi = serialDistance(key, cmd.ssId);
435
+ if (acceptedSourceEsis.get(key)?.has(esi)) {
436
+ sourceDroppedDuplicate++;
437
+ return;
438
+ }
439
+ const result = decoder.add_source(cmd.ssId, view, cmd.ts);
440
+ advanceMediaHorizon(cmd.ssId);
441
+ acceptId(acceptedSourceEsis, key, esi);
442
+ block.sourceReceived++;
443
+ block.lastSymbolTs = performance.now();
444
+ sourceSymbols++;
445
+ if (altaEnabled) altaPending++;
446
+ settleSourceArrival(key, block, result);
447
+ }
448
+ function findOverlappingIsoBlock(ssStart, ssbLength) {
449
+ for (const block of blocks.values()) {
450
+ if (isoBlocksOverlap(block.sbn, isoKnownLength(block), ssStart, ssbLength)) return block;
451
+ }
452
+ return void 0;
453
+ }
454
+ function placeIsoBlock(ssStart, ssbLength) {
455
+ const block = getOrCreateBlock(ssStart, ssbLength);
456
+ for (let esi = 0; esi < ssbLength; esi++) {
457
+ const ssId = (ssStart + esi) % SS_ID_SPAN;
458
+ const arrivalTs = unplacedSources.get(ssId);
459
+ if (arrivalTs === void 0) continue;
460
+ unplacedSources.delete(ssId);
461
+ acceptId(acceptedSourceEsis, ssStart, esi);
462
+ block.sourceReceived++;
463
+ if (arrivalTs < block.firstSymbolTs) block.firstSymbolTs = arrivalTs;
464
+ }
465
+ return block;
466
+ }
467
+ function feedIsoRepair(cmd, view, decoder) {
468
+ const key = cmd.ssStart;
469
+ if (isRetiredSbn(key)) return;
470
+ const existingBlock = blocks.get(key);
471
+ if (existingBlock && isTerminalBlock(existingBlock)) return;
472
+ if (existingBlock) {
473
+ if (existingBlock.ssbLength !== cmd.ssbLength) {
474
+ emit({
475
+ type: "error",
476
+ code: "repair-ssb-length-conflict",
477
+ message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (ssStart=${key})`
478
+ });
479
+ return;
480
+ }
481
+ } else {
482
+ const overlapped = findOverlappingIsoBlock(key, cmd.ssbLength);
483
+ if (overlapped) {
484
+ emit({
485
+ type: "error",
486
+ code: "repair-block-overlap",
487
+ message: `[FecWorker] Dropping repair symbol whose block (ssStart=${key}, SSB_length=${cmd.ssbLength}) overlaps known block (ssStart=${overlapped.sbn}, SSB_length=${isoKnownLength(overlapped)})`
488
+ });
489
+ return;
490
+ }
491
+ }
492
+ const repairEsi = cmd.ssbLength + cmd.rsId;
493
+ if (acceptedRepairEsis.get(key)?.has(repairEsi)) return;
494
+ const result = decoder.add_repair(key, cmd.ssbLength, cmd.rsId, view, cmd.ts);
495
+ advanceMediaHorizon(key);
496
+ const block = existingBlock ?? placeIsoBlock(key, cmd.ssbLength);
497
+ acceptId(acceptedRepairEsis, key, repairEsi);
498
+ block.repairReceived++;
499
+ block.lastSymbolTs = performance.now();
500
+ repairSymbols++;
501
+ settleRepairArrival(key, block, cmd.ssbLength, result);
502
+ }
503
+ async function handleConfigure(cmd) {
504
+ if (!cmd.wasmModule) {
505
+ throw new Error("[FecWorker] configure: wasmModule required");
506
+ }
507
+ if (!cmd.config) {
508
+ throw new Error("[FecWorker] configure: config required");
509
+ }
510
+ const blockProfile = cmd.config.blockProfile;
511
+ if (blockProfile === void 0) {
512
+ throw new Error(
513
+ '[FecWorker] configure: blockProfile is required: name the FEC block profile ("k-padded-v1" or "iso-ssbg1-v1")'
514
+ );
515
+ }
516
+ if (blockProfile !== "k-padded-v1" && blockProfile !== "iso-ssbg1-v1") {
517
+ throw new Error(
518
+ `[FecWorker] configure: blockProfile must be "k-padded-v1" or "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`
519
+ );
520
+ }
521
+ if (!Number.isFinite(cmd.config.interleaveDepth) || !Number.isInteger(cmd.config.interleaveDepth) || cmd.config.interleaveDepth <= 0 || cmd.config.interleaveDepth > MAX_INTERLEAVE_DEPTH) {
522
+ throw new Error(
523
+ `[FecWorker] configure: interleaveDepth must be an integer in 1..${MAX_INTERLEAVE_DEPTH} (got ${cmd.config.interleaveDepth})`
524
+ );
525
+ }
526
+ if (!Number.isFinite(cmd.config.interleaveMs) || cmd.config.interleaveMs <= 0) {
527
+ throw new Error(
528
+ `[FecWorker] configure: interleaveMs must be finite and > 0 (got ${cmd.config.interleaveMs})`
529
+ );
530
+ }
531
+ if (!Number.isFinite(cmd.config.deliveryWindowMs) || cmd.config.deliveryWindowMs <= 0) {
532
+ throw new Error(
533
+ `[FecWorker] configure: deliveryWindowMs must be > 0 (got ${cmd.config.deliveryWindowMs})`
534
+ );
535
+ }
536
+ if (!Number.isInteger(cmd.config.k) || cmd.config.k <= 0 || cmd.config.k > MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK) {
537
+ throw new Error(
538
+ `[FecWorker] configure: k must be an integer in 1..${MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK} (got ${cmd.config.k})`
539
+ );
540
+ }
541
+ if (!Number.isInteger(cmd.config.symbolSize) || cmd.config.symbolSize <= 0 || cmd.config.symbolSize > MAX_SYMBOL_SIZE || cmd.config.symbolSize % 8 !== 0) {
542
+ throw new Error(
543
+ `[FecWorker] configure: symbolSize must be an 8-byte-aligned integer in 8..${MAX_SYMBOL_SIZE} (got ${cmd.config.symbolSize})`
544
+ );
545
+ }
546
+ const wasm = resolveWasmBindings();
547
+ wasm.initSync({ module: cmd.wasmModule });
548
+ wasmDecoder?.free();
549
+ wasmDecoder = null;
550
+ clearBlockTracking();
551
+ const decoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth, blockProfile);
552
+ verifyDecoderProfile(decoder, blockProfile);
553
+ wasmDecoder = decoder;
554
+ config = cmd.config;
555
+ altaEnabled = !!cmd.altaPublicKey;
556
+ pendingVerifyCap = Math.ceil(config.interleaveDepth * 1.5);
557
+ pendingVerifyTtlMs = Math.ceil(config.interleaveMs * config.k * 1.5);
558
+ altaVerifier = null;
559
+ altaInitFailed = false;
560
+ if (cmd.altaPublicKey && cmd.altaWasmModule) {
561
+ try {
562
+ const altaGlueUrl = "/wasm/alta/alta_rs.js";
563
+ const altaGlue = await import(
564
+ /* webpackIgnore: true */
565
+ altaGlueUrl
566
+ );
567
+ altaGlue.initSync({ module: cmd.altaWasmModule });
568
+ const keyBytes = new Uint8Array(cmd.altaPublicKey);
569
+ altaVerifier = new altaGlue.JsAltaVerifier(keyBytes);
570
+ } catch (e) {
571
+ console.warn("[FecWorker] ALTA WASM init failed, verification disabled:", e);
572
+ altaVerifier = null;
573
+ altaEnabled = false;
574
+ altaInitFailed = true;
575
+ }
576
+ }
577
+ wasmDecoder.configure_params(config.k, config.symbolSize);
578
+ emit({ type: "capabilities", commands: ["feedSymbols", "subscribeBlockUpdates"] });
579
+ startSnapshotTimer();
580
+ emitSnapshot();
581
+ }
582
+ function handleFeedSource(cmd) {
583
+ if (!wasmDecoder) {
584
+ throw new Error("[FecWorker] feedSource: not configured");
585
+ }
586
+ if (!config) {
587
+ throw new Error("[FecWorker] feedSource: config missing");
588
+ }
589
+ if (!isUint32(cmd.ssId) || !(cmd.data instanceof ArrayBuffer)) {
590
+ throw new Error("[FecWorker] feedSource: invalid source symbol");
591
+ }
592
+ const view = new Uint8Array(cmd.data);
593
+ if (config.blockProfile === "iso-ssbg1-v1") {
594
+ feedIsoSource(cmd, view, wasmDecoder, config);
595
+ return;
596
+ }
597
+ const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
598
+ const esi = cmd.ssId % config.k;
599
+ if (isRetiredSbn(sbn)) {
600
+ sourceDroppedRetired++;
601
+ return;
602
+ }
603
+ const existingBlock = blocks.get(sbn);
604
+ if (existingBlock && isTerminalBlock(existingBlock)) {
605
+ sourceDroppedTerminal++;
606
+ return;
607
+ }
608
+ if (acceptedSourceEsis.get(sbn)?.has(esi)) {
609
+ sourceDroppedDuplicate++;
610
+ return;
611
+ }
612
+ const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
613
+ advanceMediaHorizon(sbn);
614
+ let sourceEsis = acceptedSourceEsis.get(sbn);
615
+ if (!sourceEsis) {
616
+ sourceEsis = /* @__PURE__ */ new Set();
617
+ acceptedSourceEsis.set(sbn, sourceEsis);
618
+ }
619
+ sourceEsis.add(esi);
620
+ const block = getOrCreateBlock(sbn);
621
+ block.sourceReceived++;
622
+ block.lastSymbolTs = performance.now();
623
+ sourceSymbols++;
624
+ if (altaEnabled) altaPending++;
625
+ settleSourceArrival(sbn, block, result);
626
+ }
627
+ function handleFeedRepair(cmd) {
628
+ if (!wasmDecoder) {
629
+ throw new Error("[FecWorker] feedRepair: not configured");
630
+ }
631
+ if (!config) {
632
+ throw new Error("[FecWorker] feedRepair: config missing");
633
+ }
634
+ const iso = config.blockProfile === "iso-ssbg1-v1";
635
+ if (!isUint32(cmd.ssStart) || !isUint32(cmd.ssbLength) || cmd.ssbLength <= 0 || cmd.ssbLength > config.k || !iso && cmd.ssStart % config.k !== 0 || !isUint32(cmd.rsId) || (iso ? cmd.ssbLength : config.k) + cmd.rsId > UINT24_MAX || !(cmd.data instanceof ArrayBuffer)) {
636
+ throw new Error("[FecWorker] feedRepair: invalid repair symbol");
637
+ }
638
+ const view = new Uint8Array(cmd.data);
639
+ if (config.symbolSize > 0 && view.byteLength !== config.symbolSize) {
640
+ repairSymbolSizeMismatch++;
641
+ if (repairSymbolSizeMismatch <= 5 || repairSymbolSizeMismatch % 100 === 0) {
642
+ emit({
643
+ type: "error",
644
+ code: "repair-symbol-size-mismatch",
645
+ message: `[FecWorker] Dropping repair symbol with invalid size: got ${view.byteLength}B, expected ${config.symbolSize}B (ssStart=${cmd.ssStart}, ssbLength=${cmd.ssbLength}, rsId=${cmd.rsId})`
646
+ });
647
+ }
648
+ return;
649
+ }
650
+ if (iso) {
651
+ feedIsoRepair(cmd, view, wasmDecoder);
652
+ return;
653
+ }
654
+ const sbn = Math.floor(cmd.ssStart / config.k);
655
+ const repairEsi = config.k + cmd.rsId;
656
+ if (isRetiredSbn(sbn)) return;
657
+ const existingBlock = blocks.get(sbn);
658
+ if (existingBlock && isTerminalBlock(existingBlock)) return;
659
+ if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
660
+ if (existingBlock?.ssbLength !== void 0 && existingBlock.ssbLength !== cmd.ssbLength) {
661
+ emit({
662
+ type: "error",
663
+ code: "repair-ssb-length-conflict",
664
+ message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`
665
+ });
666
+ return;
667
+ }
668
+ const result = wasmDecoder.add_repair(
669
+ cmd.ssStart,
670
+ cmd.ssbLength,
671
+ cmd.rsId,
672
+ view,
673
+ cmd.ts
674
+ );
675
+ advanceMediaHorizon(sbn);
676
+ let repairEsis = acceptedRepairEsis.get(sbn);
677
+ if (!repairEsis) {
678
+ repairEsis = /* @__PURE__ */ new Set();
679
+ acceptedRepairEsis.set(sbn, repairEsis);
680
+ }
681
+ repairEsis.add(repairEsi);
682
+ const block = getOrCreateBlock(sbn);
683
+ block.ssbLength = cmd.ssbLength;
684
+ block.repairReceived++;
685
+ block.lastSymbolTs = performance.now();
686
+ repairSymbols++;
687
+ settleRepairArrival(sbn, block, cmd.ssbLength, result);
688
+ }
689
+ function handleRelayBlockSig(cmd) {
690
+ const block = blocks.get(cmd.sbn);
691
+ if (block) {
692
+ block.relayBlockSigValid = null;
693
+ emitBlockUpdate(block);
694
+ }
695
+ }
696
+ function handleCleanup(cmd) {
697
+ if (!Array.isArray(cmd.sbns) || cmd.sbns.length === 0 || !cmd.sbns.every(isUint32)) {
698
+ throw new Error("[FecWorker] cleanup: invalid SBNs");
699
+ }
700
+ const sbns = [...new Set(cmd.sbns)].sort((a, b) => a - b);
701
+ const pendingSbns = sbns.filter((sbn) => !isRetiredSbn(sbn));
702
+ try {
703
+ if (!wasmDecoder) {
704
+ throw new Error("[FecWorker] cleanup: not configured");
705
+ }
706
+ if (pendingSbns.length > 0) {
707
+ wasmDecoder.cleanup_blocks_detailed(Uint32Array.from(pendingSbns));
708
+ }
709
+ for (const sbn of pendingSbns) {
710
+ let currentBlock = blocks.get(sbn);
711
+ if (currentBlock && isTerminalBlock(currentBlock)) {
712
+ retireAcceptedIds(sbn);
713
+ continue;
714
+ }
715
+ currentBlock ??= getOrCreateBlock(sbn);
716
+ const authoredLength = isIsoProfile() ? currentBlock.ssbLength : currentBlock.k;
717
+ if (authoredLength !== void 0 && currentBlock.sourceReceived >= authoredLength) {
718
+ markBlockTerminal(currentBlock, "complete");
719
+ blocksComplete++;
720
+ } else {
721
+ markBlockTerminal(currentBlock, "failed");
722
+ blocksFailed++;
723
+ }
724
+ emitBlockUpdate(currentBlock);
725
+ }
726
+ } finally {
727
+ emit({ type: "cleanupComplete", sbns });
728
+ }
729
+ }
730
+ function handleDispose() {
731
+ if (snapshotTimer !== null) {
732
+ clearInterval(snapshotTimer);
733
+ snapshotTimer = null;
734
+ }
735
+ if (altaVerifier) {
736
+ try {
737
+ altaVerifier.free();
738
+ } catch (_e) {
739
+ }
740
+ altaVerifier = null;
741
+ }
742
+ if (wasmDecoder) {
743
+ wasmDecoder.free();
744
+ wasmDecoder = null;
745
+ }
746
+ pendingVerify.clear();
747
+ pendingVerifyCap = 0;
748
+ pendingVerifyTtlMs = 0;
749
+ config = null;
750
+ clearBlockTracking();
751
+ }
752
+ function handleReset() {
753
+ try {
754
+ wasmDecoder?.flush();
755
+ wasmDecoder?.reset_stats();
756
+ } catch (err) {
757
+ emit({
758
+ type: "error",
759
+ code: "reset-failed",
760
+ message: err instanceof Error ? `[FecWorker] reset failed: ${err.message}` : `[FecWorker] reset failed: ${String(err)}`
761
+ });
762
+ return;
763
+ }
764
+ clearBlockTracking();
765
+ sourceSymbols = 0;
766
+ repairSymbols = 0;
767
+ blocksComplete = 0;
768
+ blocksRecovered = 0;
769
+ blocksFailed = 0;
770
+ altaVerified = 0;
771
+ altaFailed = 0;
772
+ altaFailedDetails.length = 0;
773
+ altaPending = 0;
774
+ altaPendingEvictedCap = 0;
775
+ altaPendingEvictedTtl = 0;
776
+ altaError = 0;
777
+ relayBlocksVerified = 0;
778
+ relayBlocksFailed = 0;
779
+ repairSymbolSizeMismatch = 0;
780
+ sourceDroppedRetired = 0;
781
+ sourceDroppedTerminal = 0;
782
+ sourceDroppedDuplicate = 0;
783
+ sourceUnplacedRetired = 0;
784
+ emit({ type: "resetComplete" });
785
+ if (config) emitSnapshot();
786
+ }
787
+ function handleFeedSymbols(cmd) {
788
+ if (!Array.isArray(cmd.symbols)) {
789
+ throw new Error("[FecWorker] feedSymbols: symbols must be an array");
790
+ }
791
+ for (const symbol of cmd.symbols) {
792
+ try {
793
+ if (symbol?.type === "feedSource") handleFeedSource(symbol);
794
+ else if (symbol?.type === "feedRepair") handleFeedRepair(symbol);
795
+ else {
796
+ throw new Error(
797
+ `[FecWorker] feedSymbols: unknown symbol type ${String(symbol?.type)}`
798
+ );
799
+ }
800
+ } catch (err) {
801
+ emit({
802
+ type: "error",
803
+ code: "command-failed",
804
+ message: err instanceof Error ? err.message : `[FecWorker] ${String(err)}`
805
+ });
806
+ }
807
+ }
808
+ }
809
+ async function dispatchCommand(cmd) {
810
+ try {
811
+ switch (cmd.type) {
812
+ case "configure":
813
+ await handleConfigure(cmd);
814
+ break;
815
+ case "feedSource":
816
+ handleFeedSource(cmd);
817
+ break;
818
+ case "feedRepair":
819
+ handleFeedRepair(cmd);
820
+ break;
821
+ case "feedSymbols":
822
+ handleFeedSymbols(cmd);
823
+ break;
824
+ case "relayBlockSig":
825
+ handleRelayBlockSig(cmd);
826
+ break;
827
+ case "cleanup":
828
+ handleCleanup(cmd);
829
+ break;
830
+ case "reset":
831
+ handleReset();
832
+ break;
833
+ case "subscribeBlockUpdates":
834
+ blockUpdatesSubscribed = true;
835
+ break;
836
+ case "dispose":
837
+ handleDispose();
838
+ break;
839
+ default:
840
+ emit({
841
+ type: "error",
842
+ code: "unknown-command",
843
+ message: `Unknown command type: ${cmd.type}`
844
+ });
845
+ }
846
+ } catch (err) {
847
+ emit({
848
+ type: "error",
849
+ code: "command-failed",
850
+ message: err instanceof Error ? err.message : `[FecWorker] ${String(err)}`
851
+ });
852
+ }
853
+ }
854
+ var commandQueue = Promise.resolve();
855
+ self.onmessage = (e) => {
856
+ const cmd = e.data;
857
+ commandQueue = commandQueue.then(() => dispatchCommand(cmd));
858
+ };
859
+ //# sourceMappingURL=fec-worker.js.map