@blockcast/fec-worker 0.3.0 → 0.4.0-main.248efb07e294
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 +15 -20
- package/dist/block-order.d.ts +3 -19
- package/dist/block-order.d.ts.map +1 -1
- package/dist/block-order.js +3 -33
- package/dist/block-order.js.map +1 -1
- package/dist/fec-worker-client.d.ts +6 -16
- package/dist/fec-worker-client.d.ts.map +1 -1
- package/dist/fec-worker-client.js +10 -38
- package/dist/fec-worker-client.js.map +1 -1
- package/dist/fec-worker-types.d.ts +48 -102
- package/dist/fec-worker-types.d.ts.map +1 -1
- package/dist/fec-worker-types.js +2 -2
- package/dist/fec-worker.js +39 -194
- package/dist/fec-worker.js.map +2 -2
- package/package.json +4 -7
- package/src/block-order.ts +3 -37
- package/src/fec-worker-client.ts +8 -40
- package/src/fec-worker-types.ts +48 -102
- package/src/fec-worker.ts +71 -466
package/src/fec-worker.ts
CHANGED
|
@@ -22,8 +22,6 @@ import type {
|
|
|
22
22
|
} from "./fec-worker-types.js";
|
|
23
23
|
import { toOwnedArrayBuffer } from "./transfer.js";
|
|
24
24
|
import {
|
|
25
|
-
blockPrecedes,
|
|
26
|
-
blocksBehind,
|
|
27
25
|
isoBlockContains,
|
|
28
26
|
isoBlocksBehind,
|
|
29
27
|
isoBlocksOverlap,
|
|
@@ -36,10 +34,9 @@ import {
|
|
|
36
34
|
// The decoder derives SBN/ESI internally — do not pre-compute.
|
|
37
35
|
interface WasmFecDecoder {
|
|
38
36
|
/**
|
|
39
|
-
* ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* holds it until a repair names the block containing it.
|
|
37
|
+
* ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID; data is
|
|
38
|
+
* the protected packet, unpadded. The decoder builds the ssbg_mode1 symbol
|
|
39
|
+
* and holds it until a repair names the block containing it.
|
|
43
40
|
*/
|
|
44
41
|
add_source(
|
|
45
42
|
ss_id: number,
|
|
@@ -47,11 +44,9 @@ interface WasmFecDecoder {
|
|
|
47
44
|
timestamp: number,
|
|
48
45
|
): Uint8Array | undefined;
|
|
49
46
|
/**
|
|
50
|
-
* ISO 23008-1 §C.5.3
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* ESI = ssb_length + rs_id; a recovered block is ssb_length x T bytes of
|
|
54
|
-
* ssbg_mode1 symbols.
|
|
47
|
+
* ISO 23008-1 §C.5.3: the block is `[ss_start, ss_start + ssb_length)`
|
|
48
|
+
* with K' = ssb_length and repair ESI = ssb_length + rs_id; a recovered
|
|
49
|
+
* block is ssb_length x T bytes of ssbg_mode1 symbols.
|
|
55
50
|
*/
|
|
56
51
|
add_repair(
|
|
57
52
|
ss_start: number,
|
|
@@ -68,18 +63,18 @@ interface WasmFecDecoder {
|
|
|
68
63
|
*/
|
|
69
64
|
cleanup_blocks_detailed(sbns: Uint32Array): void;
|
|
70
65
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
* more than interleaveDepth `isoBlocksBehind` the live SS_ID; returns the
|
|
78
|
-
* SS_Starts of the blocks it removed.
|
|
66
|
+
* Remove decoder blocks, and held unplaced sources, more than
|
|
67
|
+
* interleaveDepth `isoBlocksBehind` the live SS_ID; returns the SS_Starts
|
|
68
|
+
* of the blocks it removed. It runs the decoder's `retire_unplaced` sweep
|
|
69
|
+
* itself, so the Worker never calls that separately: synchronizeDecoderHorizon
|
|
70
|
+
* calls this after every source and repair it accepts, which is what keeps
|
|
71
|
+
* the decoder's unplaced pool bounded when a block's repairs never arrive.
|
|
79
72
|
*/
|
|
80
73
|
cleanup_by_ss_start(current: number): Uint32Array;
|
|
81
74
|
/** The block profile the decoder was constructed with. */
|
|
82
75
|
get_block_profile(): string;
|
|
76
|
+
/** The `fec.algorithm` the decoder was constructed with; absent on builds that predate Reed-Solomon. */
|
|
77
|
+
get_algorithm?(): string;
|
|
83
78
|
flush(): void;
|
|
84
79
|
pending_blocks(): number;
|
|
85
80
|
reset_stats(): void;
|
|
@@ -97,16 +92,13 @@ interface WasmFecDecoder {
|
|
|
97
92
|
|
|
98
93
|
interface WasmBindings {
|
|
99
94
|
initSync(module: { module: WebAssembly.Module }): unknown;
|
|
100
|
-
MmtFecDecoder: new (interleaveDepth: number, blockProfile: FecBlockProfile) => WasmFecDecoder;
|
|
95
|
+
MmtFecDecoder: new (interleaveDepth: number, blockProfile: FecBlockProfile, algorithm?: FecScheme) => WasmFecDecoder;
|
|
101
96
|
}
|
|
102
97
|
|
|
103
98
|
// --- Worker state (local to this Worker instance) ---
|
|
104
99
|
|
|
105
100
|
let wasmDecoder: WasmFecDecoder | null = null;
|
|
106
101
|
let config: FecTrackConfig | null = null;
|
|
107
|
-
let altaEnabled = false;
|
|
108
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
-
let altaVerifier: any = null; // JsAltaVerifier instance (loaded from ALTA WASM)
|
|
110
102
|
let snapshotTimer: ReturnType<typeof setInterval> | null = null;
|
|
111
103
|
// blockUpdate events are posted only after a client subscribes (BLO-36599).
|
|
112
104
|
let blockUpdatesSubscribed = false;
|
|
@@ -130,6 +122,10 @@ const MAX_INTERLEAVE_DEPTH = 0xff;
|
|
|
130
122
|
const MAX_SYMBOL_SIZE = 0xfff8;
|
|
131
123
|
// RFC 6330 systematic table limit, enforced by the vendored raptorq decoder.
|
|
132
124
|
const MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK = 56_403;
|
|
125
|
+
// draft-ramadan-moq-fec-01 4.5: K + P <= 255, and a block has at least one repair.
|
|
126
|
+
const MAX_REED_SOLOMON_SOURCE_SYMBOLS_PER_BLOCK = 254;
|
|
127
|
+
|
|
128
|
+
type FecScheme = "raptorq" | "reed-solomon";
|
|
133
129
|
|
|
134
130
|
// Aggregate counters
|
|
135
131
|
let sourceSymbols = 0;
|
|
@@ -137,20 +133,6 @@ let repairSymbols = 0;
|
|
|
137
133
|
let blocksComplete = 0;
|
|
138
134
|
let blocksRecovered = 0;
|
|
139
135
|
let blocksFailed = 0;
|
|
140
|
-
let altaVerified = 0;
|
|
141
|
-
let altaFailed = 0; // Crypto-confirmed tamper (MAC / sig / hash chain mismatch)
|
|
142
|
-
const altaFailedDetails: Array<{
|
|
143
|
-
seq: number;
|
|
144
|
-
error: string;
|
|
145
|
-
authLen: number;
|
|
146
|
-
payloadLen: number;
|
|
147
|
-
path: string; // "direct" | "recovered" | "pending-retry"
|
|
148
|
-
}> = [];
|
|
149
|
-
let altaPending = 0; // Unverifiable but not tamper (no trailer, verifier not ready, anchor missing)
|
|
150
|
-
let altaPendingEvictedCap = 0; // Incoming "pending" packets rejected because the queue was at cap after stale-sweep (flood indicator)
|
|
151
|
-
let altaPendingEvictedTtl = 0; // Deferred-queue entries dropped by TTL (retry window expired)
|
|
152
|
-
let altaError = 0; // verifyDetached threw — infrastructure flake, not tamper
|
|
153
|
-
let altaInitFailed = false; // ALTA configured but WASM init failed; altaEnabled was forced off
|
|
154
136
|
let relayBlocksVerified = 0;
|
|
155
137
|
let relayBlocksFailed = 0;
|
|
156
138
|
let liveSbn = 0;
|
|
@@ -188,35 +170,6 @@ let sourceUnplacedRetired = 0;
|
|
|
188
170
|
// Recovery timing
|
|
189
171
|
const recoveryTimes: number[] = [];
|
|
190
172
|
|
|
191
|
-
// ── Deferred ALTA verification queue (08-07 Path 3) ───────────────────────
|
|
192
|
-
//
|
|
193
|
-
// When an ALTA packet arrives whose MAC references point at sequences not yet
|
|
194
|
-
// in the verifier's ring buffer, we can't anchor trust — verify returns "No
|
|
195
|
-
// trust anchor". Typical cause: a predecessor packet was lost and hasn't been
|
|
196
|
-
// FEC-recovered yet. Rather than drop these, hold them in a bounded FIFO and
|
|
197
|
-
// retry verification after every successful verify (direct or FEC-recovered)
|
|
198
|
-
// that seeds the ring buffer with new raw bytes.
|
|
199
|
-
//
|
|
200
|
-
// Zero-copy (1B-B): the queue entry stores a Uint8Array view into the
|
|
201
|
-
// original `data` ArrayBuffer. JS GC keeps the buffer alive via the view
|
|
202
|
-
// reference; no bytes are copied on queue.
|
|
203
|
-
//
|
|
204
|
-
// Timeout entries (TTL exceeded) and cap-rejections count as `altaPending`
|
|
205
|
-
// (2C-B): they're not tamper indicators, they're just packets that couldn't
|
|
206
|
-
// be authenticated within the receive window — either loss or, in the cap
|
|
207
|
-
// case, a flood of unanchorable arrivals (real or attacker-driven) that
|
|
208
|
-
// exceeded the queue budget. See enqueuePendingVerify for the rate-limit
|
|
209
|
-
// protocol.
|
|
210
|
-
interface PendingVerify {
|
|
211
|
-
seq: number;
|
|
212
|
-
auth: Uint8Array;
|
|
213
|
-
payload: Uint8Array;
|
|
214
|
-
arrivalTs: number;
|
|
215
|
-
}
|
|
216
|
-
const pendingVerify = new Map<number, PendingVerify>();
|
|
217
|
-
let pendingVerifyCap = 0;
|
|
218
|
-
let pendingVerifyTtlMs = 0;
|
|
219
|
-
|
|
220
173
|
// --- Helpers ---
|
|
221
174
|
|
|
222
175
|
function emit(event: FecWorkerEvent, transfer?: Transferable[]): void {
|
|
@@ -241,7 +194,6 @@ function clearBlockTracking(): void {
|
|
|
241
194
|
acceptedSourceEsis.clear();
|
|
242
195
|
acceptedRepairEsis.clear();
|
|
243
196
|
unplacedSources.clear();
|
|
244
|
-
pendingVerify.clear();
|
|
245
197
|
recoveryTimes.length = 0;
|
|
246
198
|
liveSbn = 0;
|
|
247
199
|
liveSbnKnown = false;
|
|
@@ -255,15 +207,9 @@ function clearBlockTracking(): void {
|
|
|
255
207
|
*/
|
|
256
208
|
function isRetiredSbn(sbn: number): boolean {
|
|
257
209
|
if (config === null || !liveSbnKnown) return false;
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
return config.
|
|
261
|
-
? isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth
|
|
262
|
-
: blocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function isIsoProfile(): boolean {
|
|
266
|
-
return config?.blockProfile === "iso-ssbg1-v1";
|
|
210
|
+
// `sbn` is an SS_ID (a block's SS_Start or a source's own SS_ID) and
|
|
211
|
+
// liveSbn the live SS_ID; K is only the maximum block length.
|
|
212
|
+
return isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
|
|
267
213
|
}
|
|
268
214
|
|
|
269
215
|
/**
|
|
@@ -291,9 +237,7 @@ function findIsoBlock(ssId: number): FecBlockSnapshot | undefined {
|
|
|
291
237
|
* retired, after which the SS_ID retirement check refuses every member.
|
|
292
238
|
*/
|
|
293
239
|
function isRetiredBlock(key: number, block: FecBlockSnapshot): boolean {
|
|
294
|
-
return
|
|
295
|
-
? isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN)
|
|
296
|
-
: isRetiredSbn(key);
|
|
240
|
+
return isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN);
|
|
297
241
|
}
|
|
298
242
|
|
|
299
243
|
/**
|
|
@@ -310,15 +254,10 @@ function trimBlockRetention(): void {
|
|
|
310
254
|
}
|
|
311
255
|
}
|
|
312
256
|
|
|
313
|
-
/** Advance the live position: an
|
|
257
|
+
/** Advance the live position: an SS_ID / SS_Start, in serial order. */
|
|
314
258
|
function advanceMediaHorizon(sbn: number): void {
|
|
315
259
|
if (!config) return;
|
|
316
|
-
if (liveSbnKnown)
|
|
317
|
-
const ahead = config.blockProfile === "iso-ssbg1-v1"
|
|
318
|
-
? serialPrecedes(liveSbn, sbn)
|
|
319
|
-
: blockPrecedes(liveSbn, sbn, config.k);
|
|
320
|
-
if (!ahead) return;
|
|
321
|
-
}
|
|
260
|
+
if (liveSbnKnown && !serialPrecedes(liveSbn, sbn)) return;
|
|
322
261
|
liveSbn = sbn;
|
|
323
262
|
liveSbnKnown = true;
|
|
324
263
|
trimBlockRetention();
|
|
@@ -336,10 +275,7 @@ function markBlockTerminal(
|
|
|
336
275
|
|
|
337
276
|
function synchronizeDecoderHorizon(): void {
|
|
338
277
|
if (!wasmDecoder || !liveSbnKnown) return;
|
|
339
|
-
const
|
|
340
|
-
const expired = iso
|
|
341
|
-
? wasmDecoder.cleanup_by_ss_start(liveSbn)
|
|
342
|
-
: wasmDecoder.cleanup_by_sbn(liveSbn);
|
|
278
|
+
const expired = wasmDecoder.cleanup_by_ss_start(liveSbn);
|
|
343
279
|
for (const sbn of expired) {
|
|
344
280
|
const block = blocks.get(sbn);
|
|
345
281
|
if (!block || isTerminalBlock(block)) {
|
|
@@ -350,7 +286,7 @@ function synchronizeDecoderHorizon(): void {
|
|
|
350
286
|
blocksFailed++;
|
|
351
287
|
emitBlockUpdate(block);
|
|
352
288
|
}
|
|
353
|
-
|
|
289
|
+
retireUnplacedSources();
|
|
354
290
|
trimBlockRetention();
|
|
355
291
|
}
|
|
356
292
|
|
|
@@ -373,163 +309,6 @@ function retireUnplacedSources(): void {
|
|
|
373
309
|
}
|
|
374
310
|
}
|
|
375
311
|
|
|
376
|
-
/**
|
|
377
|
-
* Run detached ALTA verify against the current verifier. Four-state result:
|
|
378
|
-
* - "valid": crypto passed, packet authentic. Caller increments altaVerified.
|
|
379
|
-
* - "failed": crypto confirmed tamper (MAC/sig mismatch, hash chain mismatch).
|
|
380
|
-
* Caller increments altaFailed. Not retryable.
|
|
381
|
-
* - "pending": anchor missing (MAC refs not in buffer, no signature, not
|
|
382
|
-
* early-warmup). Caller may enqueue for retry after the next
|
|
383
|
-
* successful verify seeds the ring buffer.
|
|
384
|
-
* - "error": verifier threw — WASM trap, unmarshal panic, OOM. Caller
|
|
385
|
-
* increments altaError (infrastructure flake, not tamper).
|
|
386
|
-
* The distinction matters for ops alerting: a spike in
|
|
387
|
-
* altaError indicates a runtime problem, not an attack.
|
|
388
|
-
*/
|
|
389
|
-
let _lastVerifyError = "";
|
|
390
|
-
function runDetachedVerify(
|
|
391
|
-
auth: Uint8Array,
|
|
392
|
-
payload: Uint8Array,
|
|
393
|
-
): "valid" | "failed" | "pending" | "error" {
|
|
394
|
-
if (!altaVerifier) return "pending";
|
|
395
|
-
try {
|
|
396
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
397
|
-
const r: any = altaVerifier.verifyDetached(auth, payload);
|
|
398
|
-
if (r.valid) {
|
|
399
|
-
_lastVerifyError = "";
|
|
400
|
-
return "valid";
|
|
401
|
-
}
|
|
402
|
-
const err = (r.error as string | undefined) ?? "";
|
|
403
|
-
_lastVerifyError = err;
|
|
404
|
-
// The bootstrap-friendly verifier returns this specific error string when
|
|
405
|
-
// the only obstacle to verification is a missing anchor (see alta-rs
|
|
406
|
-
// verifier.rs verify()). Distinguishing this from tamper cases keeps us
|
|
407
|
-
// from queuing packets that are definitively invalid.
|
|
408
|
-
if (err.startsWith("No trust anchor")) return "pending";
|
|
409
|
-
return "failed";
|
|
410
|
-
} catch (e) {
|
|
411
|
-
// Ratelimit: first ~N occurrences get logged. A sustained exception
|
|
412
|
-
// stream indicates a real problem — the altaError counter is the
|
|
413
|
-
// durable signal.
|
|
414
|
-
if (altaError < 5) {
|
|
415
|
-
console.warn("[FecWorker] verifyDetached threw:", e);
|
|
416
|
-
}
|
|
417
|
-
return "error";
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/** Extract u32 BE sequence number from the ALTA authenticator header (offset 0..3). */
|
|
422
|
-
function seqFromAuth(auth: Uint8Array): number {
|
|
423
|
-
return (
|
|
424
|
-
((auth[0]! << 24) | (auth[1]! << 16) | (auth[2]! << 8) | auth[3]!) >>> 0
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Enqueue a packet whose ALTA verification returned "pending" (anchor
|
|
430
|
-
* missing). Dedupes by sequence number.
|
|
431
|
-
*
|
|
432
|
-
* Flood resistance (08-08 review): when the queue is at cap we do NOT
|
|
433
|
-
* evict the oldest entry. The oldest entry is typically the most
|
|
434
|
-
* load-bearing one — it's the packet whose anchor everything else in the
|
|
435
|
-
* queue is waiting on. Evict-oldest-on-insert let an attacker emitting a
|
|
436
|
-
* stream of "No trust anchor" packets flood out legitimate entries and
|
|
437
|
-
* prevent them from ever anchoring. Instead:
|
|
438
|
-
* 1. First try to free stale TTL'd entries (they'd be evicted on next
|
|
439
|
-
* drain anyway — pulling that forward is free).
|
|
440
|
-
* 2. If still full, reject the NEW entry. altaPendingEvictedCap counts
|
|
441
|
-
* these rejections (rate-limit trigger).
|
|
442
|
-
*
|
|
443
|
-
* Stores Uint8Array views into the original data + auth buffers — no copy.
|
|
444
|
-
* The views keep their underlying ArrayBuffers alive via GC references.
|
|
445
|
-
*/
|
|
446
|
-
function enqueuePendingVerify(
|
|
447
|
-
auth: Uint8Array,
|
|
448
|
-
payload: Uint8Array,
|
|
449
|
-
): void {
|
|
450
|
-
const seq = seqFromAuth(auth);
|
|
451
|
-
if (pendingVerify.has(seq)) return;
|
|
452
|
-
|
|
453
|
-
if (pendingVerify.size >= pendingVerifyCap) {
|
|
454
|
-
// Step 1: sweep stale entries so legitimate traffic isn't penalized
|
|
455
|
-
// by TTL'd junk that happens to still occupy slots.
|
|
456
|
-
const now = performance.now();
|
|
457
|
-
for (const [staleSeq, entry] of pendingVerify) {
|
|
458
|
-
if (now - entry.arrivalTs > pendingVerifyTtlMs) {
|
|
459
|
-
pendingVerify.delete(staleSeq);
|
|
460
|
-
altaPendingEvictedTtl++;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
// Step 2: if the queue is still at cap it's a genuine flood —
|
|
464
|
-
// reject the new entry rather than evict an existing one. The
|
|
465
|
-
// rejected packet counts as altaPendingEvictedCap (which is
|
|
466
|
-
// surfaced up through stats for rate-limit alerting).
|
|
467
|
-
if (pendingVerify.size >= pendingVerifyCap) {
|
|
468
|
-
altaPendingEvictedCap++;
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
pendingVerify.set(seq, {
|
|
474
|
-
seq,
|
|
475
|
-
auth,
|
|
476
|
-
payload,
|
|
477
|
-
arrivalTs: performance.now(),
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Retry every queued entry. Called after every successful verify (direct
|
|
483
|
-
* source or FEC-recovered) — new raw bytes in the verifier's ring buffer may
|
|
484
|
-
* unblock previously-pending MAC references.
|
|
485
|
-
*
|
|
486
|
-
* Three outcomes per entry:
|
|
487
|
-
* - "valid" → remove from queue, altaVerified++
|
|
488
|
-
* - "failed" → remove from queue, altaFailed++ (tamper on retry)
|
|
489
|
-
* - "pending" → keep in queue, unless TTL expired (then evict as altaPending)
|
|
490
|
-
*
|
|
491
|
-
* Iteration order: Map preserves insertion order, so we drain oldest-first.
|
|
492
|
-
* No-op when the queue is empty or the verifier isn't ready.
|
|
493
|
-
*/
|
|
494
|
-
function drainPendingVerify(): void {
|
|
495
|
-
if (pendingVerify.size === 0 || !altaVerifier) return;
|
|
496
|
-
const now = performance.now();
|
|
497
|
-
// Collect keys to avoid mutating Map during iteration
|
|
498
|
-
const keys = Array.from(pendingVerify.keys());
|
|
499
|
-
for (const seq of keys) {
|
|
500
|
-
const entry = pendingVerify.get(seq);
|
|
501
|
-
if (!entry) continue;
|
|
502
|
-
if (now - entry.arrivalTs > pendingVerifyTtlMs) {
|
|
503
|
-
pendingVerify.delete(seq);
|
|
504
|
-
altaPendingEvictedTtl++;
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
const result = runDetachedVerify(entry.auth, entry.payload);
|
|
508
|
-
if (result === "valid") {
|
|
509
|
-
pendingVerify.delete(seq);
|
|
510
|
-
altaVerified++;
|
|
511
|
-
} else if (result === "failed") {
|
|
512
|
-
pendingVerify.delete(seq);
|
|
513
|
-
altaFailed++;
|
|
514
|
-
if (altaFailedDetails.length < 16) {
|
|
515
|
-
altaFailedDetails.push({
|
|
516
|
-
seq: entry.seq,
|
|
517
|
-
error: _lastVerifyError,
|
|
518
|
-
authLen: entry.auth.length,
|
|
519
|
-
payloadLen: entry.payload.length,
|
|
520
|
-
path: "pending-retry",
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
} else if (result === "error") {
|
|
524
|
-
// Infrastructure failure — remove from queue, count as altaError.
|
|
525
|
-
// Keeping it around would just re-throw on every subsequent drain.
|
|
526
|
-
pendingVerify.delete(seq);
|
|
527
|
-
altaError++;
|
|
528
|
-
}
|
|
529
|
-
// "pending" → leave in queue for next drain
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
312
|
/**
|
|
534
313
|
* Look up or create a block. `isoSsbLength` creates an iso-ssbg1-v1 block,
|
|
535
314
|
* whose K' is its SSB_length (C.5.3); without it the block has the catalog K.
|
|
@@ -548,8 +327,9 @@ function getOrCreateBlock(sbn: number, isoSsbLength?: number): FecBlockSnapshot
|
|
|
548
327
|
repairCount: config.repairCount,
|
|
549
328
|
firstSymbolTs: now,
|
|
550
329
|
lastSymbolTs: now,
|
|
330
|
+
// A positive interleaveMs makes this the catalog completion deadline;
|
|
331
|
+
// zero makes deliveryWindowMs a receiver-local cleanup bound.
|
|
551
332
|
deadlineTs: now + config.deliveryWindowMs,
|
|
552
|
-
altaVerified: 0,
|
|
553
333
|
relayBlockSigValid: null,
|
|
554
334
|
};
|
|
555
335
|
if (isoSsbLength !== undefined) block.ssbLength = isoSsbLength;
|
|
@@ -583,8 +363,10 @@ function buildStatsSnapshot(): FecTrackStats {
|
|
|
583
363
|
|
|
584
364
|
// Cap block array to interleave window
|
|
585
365
|
const maxBlocks = Math.max(config.interleaveDepth * 3, 24);
|
|
366
|
+
// Newest first in SS_ID serial order, so the cap keeps the newest blocks
|
|
367
|
+
// across the 2^32 wrap too: every block start precedes the live SS_ID.
|
|
586
368
|
const blockArray = Array.from(blocks.values())
|
|
587
|
-
.sort((a, b) =>
|
|
369
|
+
.sort((a, b) => serialDistance(a.sbn, liveSbn) - serialDistance(b.sbn, liveSbn))
|
|
588
370
|
.slice(0, maxBlocks);
|
|
589
371
|
|
|
590
372
|
return {
|
|
@@ -600,15 +382,6 @@ function buildStatsSnapshot(): FecTrackStats {
|
|
|
600
382
|
(blocksRecovered / (blocksRecovered + blocksFailed)) * 100,
|
|
601
383
|
) || 0
|
|
602
384
|
: 0,
|
|
603
|
-
altaVerified,
|
|
604
|
-
altaFailed,
|
|
605
|
-
altaPending,
|
|
606
|
-
altaPendingEvictedCap,
|
|
607
|
-
altaPendingEvictedTtl,
|
|
608
|
-
altaError,
|
|
609
|
-
altaInitFailed,
|
|
610
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
611
|
-
altaFailedDetails: altaFailedDetails.slice() as any,
|
|
612
385
|
relayBlocksVerified,
|
|
613
386
|
relayBlocksFailed,
|
|
614
387
|
liveSbn,
|
|
@@ -620,7 +393,7 @@ function buildStatsSnapshot(): FecTrackStats {
|
|
|
620
393
|
sourceDroppedRetired,
|
|
621
394
|
sourceDroppedTerminal,
|
|
622
395
|
sourceDroppedDuplicate,
|
|
623
|
-
|
|
396
|
+
sourceUnplacedRetired,
|
|
624
397
|
decoder,
|
|
625
398
|
};
|
|
626
399
|
}
|
|
@@ -662,33 +435,23 @@ function emitRecoveredSymbols(
|
|
|
662
435
|
return;
|
|
663
436
|
}
|
|
664
437
|
|
|
665
|
-
const iso = config.blockProfile === "iso-ssbg1-v1";
|
|
666
438
|
for (let i = 0; i < ssbLength; i++) {
|
|
667
439
|
const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
|
|
668
440
|
// ssbg_mode1 symbols are emitted whole: the router trims them by their
|
|
669
441
|
// length field. A field past T-2 or non-zero padding means the
|
|
670
442
|
// recovered symbol is not what was sent, so it is refused, not trimmed.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
443
|
+
const invalid = invalidSsbg1Symbol(symbol, symbolSize);
|
|
444
|
+
if (invalid !== null) {
|
|
445
|
+
emit({
|
|
446
|
+
type: "error",
|
|
447
|
+
code: "recovered-ssbg1-symbol-invalid",
|
|
448
|
+
message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`,
|
|
449
|
+
});
|
|
450
|
+
continue;
|
|
681
451
|
}
|
|
682
452
|
|
|
683
|
-
// Source authentication now runs once at router admission in
|
|
684
|
-
// @blockcast/mmt-manifest-verify. The worker only records that this
|
|
685
|
-
// legacy ALTA-enabled symbol bypassed hot-path verification.
|
|
686
|
-
const recoveredAltaVerified: boolean | null = null;
|
|
687
|
-
if (altaEnabled) altaPending++;
|
|
688
|
-
|
|
689
453
|
const buf = toOwnedArrayBuffer(symbol);
|
|
690
454
|
const meta: FrameMeta = {
|
|
691
|
-
altaVerified: recoveredAltaVerified,
|
|
692
455
|
recovered: true,
|
|
693
456
|
sbn,
|
|
694
457
|
esi: i,
|
|
@@ -857,7 +620,6 @@ function feedIsoSource(
|
|
|
857
620
|
advanceMediaHorizon(cmd.ssId);
|
|
858
621
|
unplacedSources.set(cmd.ssId, performance.now());
|
|
859
622
|
sourceSymbols++;
|
|
860
|
-
if (altaEnabled) altaPending++;
|
|
861
623
|
// No repair has named this source's block, so the decoder cannot have
|
|
862
624
|
// one to recover; data here is a Worker/decoder disagreement that
|
|
863
625
|
// cannot be attributed to a block, and is refused rather than guessed.
|
|
@@ -893,7 +655,6 @@ function feedIsoSource(
|
|
|
893
655
|
block.sourceReceived++;
|
|
894
656
|
block.lastSymbolTs = performance.now();
|
|
895
657
|
sourceSymbols++;
|
|
896
|
-
if (altaEnabled) altaPending++;
|
|
897
658
|
settleSourceArrival(key, block, result);
|
|
898
659
|
}
|
|
899
660
|
|
|
@@ -990,14 +751,17 @@ async function handleConfigure(
|
|
|
990
751
|
const blockProfile: unknown = cmd.config.blockProfile;
|
|
991
752
|
if (blockProfile === undefined) {
|
|
992
753
|
throw new Error(
|
|
993
|
-
'[FecWorker] configure: blockProfile is required: name the FEC block profile ("
|
|
754
|
+
'[FecWorker] configure: blockProfile is required: name the FEC block profile ("iso-ssbg1-v1")',
|
|
994
755
|
);
|
|
995
756
|
}
|
|
996
|
-
if (blockProfile !== "
|
|
757
|
+
if (blockProfile !== "iso-ssbg1-v1") {
|
|
997
758
|
throw new Error(
|
|
998
|
-
`[FecWorker] configure: blockProfile must be "
|
|
759
|
+
`[FecWorker] configure: blockProfile must be "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`,
|
|
999
760
|
);
|
|
1000
761
|
}
|
|
762
|
+
// draft-ramadan-moq-fec-01 4: the two specified schemes. Anything but
|
|
763
|
+
// "reed-solomon" (the legacy "raptor" included) is RaptorQ.
|
|
764
|
+
const algorithm: FecScheme = cmd.config.algorithm === "reed-solomon" ? "reed-solomon" : "raptorq";
|
|
1001
765
|
if (
|
|
1002
766
|
!Number.isFinite(cmd.config.interleaveDepth) ||
|
|
1003
767
|
!Number.isInteger(cmd.config.interleaveDepth) ||
|
|
@@ -1010,10 +774,11 @@ async function handleConfigure(
|
|
|
1010
774
|
}
|
|
1011
775
|
if (
|
|
1012
776
|
!Number.isFinite(cmd.config.interleaveMs) ||
|
|
1013
|
-
cmd.config.interleaveMs
|
|
777
|
+
!Number.isSafeInteger(cmd.config.interleaveMs) ||
|
|
778
|
+
cmd.config.interleaveMs < 0
|
|
1014
779
|
) {
|
|
1015
780
|
throw new Error(
|
|
1016
|
-
`[FecWorker] configure: interleaveMs must be finite
|
|
781
|
+
`[FecWorker] configure: interleaveMs must be a finite non-negative integer (got ${cmd.config.interleaveMs})`,
|
|
1017
782
|
);
|
|
1018
783
|
}
|
|
1019
784
|
if (
|
|
@@ -1024,23 +789,18 @@ async function handleConfigure(
|
|
|
1024
789
|
`[FecWorker] configure: deliveryWindowMs must be > 0 (got ${cmd.config.deliveryWindowMs})`,
|
|
1025
790
|
);
|
|
1026
791
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
cmd.config.k
|
|
1030
|
-
cmd.config.k > MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK
|
|
1031
|
-
) {
|
|
1032
|
-
throw new Error(
|
|
1033
|
-
`[FecWorker] configure: k must be an integer in 1..${MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK} (got ${cmd.config.k})`,
|
|
1034
|
-
);
|
|
792
|
+
const maxK = algorithm === "raptorq" ? MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK : MAX_REED_SOLOMON_SOURCE_SYMBOLS_PER_BLOCK;
|
|
793
|
+
if (!Number.isInteger(cmd.config.k) || cmd.config.k <= 0 || cmd.config.k > maxK) {
|
|
794
|
+
throw new Error(`[FecWorker] configure: ${algorithm} k must be an integer in 1..${maxK} (got ${cmd.config.k})`);
|
|
1035
795
|
}
|
|
1036
796
|
if (
|
|
1037
797
|
!Number.isInteger(cmd.config.symbolSize) ||
|
|
1038
798
|
cmd.config.symbolSize <= 0 ||
|
|
1039
799
|
cmd.config.symbolSize > MAX_SYMBOL_SIZE ||
|
|
1040
|
-
cmd.config.symbolSize % 8 !== 0
|
|
800
|
+
(algorithm === "raptorq" && cmd.config.symbolSize % 8 !== 0)
|
|
1041
801
|
) {
|
|
1042
802
|
throw new Error(
|
|
1043
|
-
`[FecWorker] configure: symbolSize must be an 8-byte-aligned integer in 8..${MAX_SYMBOL_SIZE} (got ${cmd.config.symbolSize})`,
|
|
803
|
+
`[FecWorker] configure: ${algorithm} symbolSize must be ${algorithm === "raptorq" ? "an 8-byte-aligned integer in 8" : "an integer in 1"}..${MAX_SYMBOL_SIZE} (got ${cmd.config.symbolSize})`,
|
|
1044
804
|
);
|
|
1045
805
|
}
|
|
1046
806
|
|
|
@@ -1057,54 +817,16 @@ async function handleConfigure(
|
|
|
1057
817
|
|
|
1058
818
|
// Decoder policy and geometry are explicit at the epoch boundary. The WASM
|
|
1059
819
|
// module has no guessed K/T/interleave slow path.
|
|
1060
|
-
const decoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth, blockProfile);
|
|
820
|
+
const decoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth, blockProfile, algorithm);
|
|
1061
821
|
verifyDecoderProfile(decoder, blockProfile);
|
|
822
|
+
// A build that predates the algorithm argument ignores it and runs RaptorQ.
|
|
823
|
+
const decoderAlgorithm = decoder.get_algorithm?.() ?? "raptorq";
|
|
824
|
+
if (decoderAlgorithm !== algorithm) {
|
|
825
|
+
decoder.free();
|
|
826
|
+
throw new Error(`[FecWorker] configure: decoder runs ${JSON.stringify(decoderAlgorithm)}, not the configured "${algorithm}"`);
|
|
827
|
+
}
|
|
1062
828
|
wasmDecoder = decoder;
|
|
1063
829
|
config = cmd.config;
|
|
1064
|
-
altaEnabled = !!cmd.altaPublicKey;
|
|
1065
|
-
|
|
1066
|
-
// Size the deferred-verify queue strictly from catalog-derived FEC
|
|
1067
|
-
// parameters. CLAUDE.md bans Math.max(x, floor) magic — if catalog
|
|
1068
|
-
// values are zero/missing, throw at init rather than silently applying
|
|
1069
|
-
// a hardcoded floor. Consumers of an undersized queue cap would see
|
|
1070
|
-
// altaPendingEvictedCap spike with no actionable signal.
|
|
1071
|
-
// Cap: 1.5× interleave depth — wide enough to hold one full FEC recovery
|
|
1072
|
-
// window plus jitter, narrow enough to bound memory.
|
|
1073
|
-
pendingVerifyCap = Math.ceil(config.interleaveDepth * 1.5);
|
|
1074
|
-
// TTL: 1.5× block duration (K symbols at one per interleave tick) —
|
|
1075
|
-
// after this many ms, any unresolved entry has missed its FEC window.
|
|
1076
|
-
pendingVerifyTtlMs = Math.ceil(config.interleaveMs * config.k * 1.5);
|
|
1077
|
-
|
|
1078
|
-
// Initialize ALTA WASM verifier if public key and WASM module provided (D-05, D-09)
|
|
1079
|
-
altaVerifier = null;
|
|
1080
|
-
altaInitFailed = false;
|
|
1081
|
-
if (cmd.altaPublicKey && cmd.altaWasmModule) {
|
|
1082
|
-
try {
|
|
1083
|
-
// Load ALTA JS glue (wasm-bindgen output from wasm-pack build).
|
|
1084
|
-
// Mirrors the RaptorQ WASM pattern: initSync({ module }) then construct.
|
|
1085
|
-
// Dynamic import path matches the IWA static asset layout.
|
|
1086
|
-
// Cast via string to prevent TypeScript from attempting static resolution
|
|
1087
|
-
// of a runtime-only URL (same pattern as fec-worker-entry.js loading).
|
|
1088
|
-
const altaGlueUrl = '/wasm/alta/alta_rs.js' as string;
|
|
1089
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1090
|
-
const altaGlue: any = await import(/* webpackIgnore: true */ altaGlueUrl);
|
|
1091
|
-
altaGlue.initSync({ module: cmd.altaWasmModule });
|
|
1092
|
-
const keyBytes = new Uint8Array(cmd.altaPublicKey);
|
|
1093
|
-
altaVerifier = new altaGlue.JsAltaVerifier(keyBytes);
|
|
1094
|
-
} catch (e) {
|
|
1095
|
-
// T-08-15: WASM init failure must NOT silently look like "not-ready-yet".
|
|
1096
|
-
// Force altaEnabled = false so handleFeedSource takes the disabled
|
|
1097
|
-
// branch instead of continuously bumping altaPending under the
|
|
1098
|
-
// misleading "verifier not yet ready" comment. altaInitFailed
|
|
1099
|
-
// flag in the snapshot tells consumers this is an error state, not
|
|
1100
|
-
// an opt-out.
|
|
1101
|
-
console.warn('[FecWorker] ALTA WASM init failed, verification disabled:', e);
|
|
1102
|
-
altaVerifier = null;
|
|
1103
|
-
altaEnabled = false;
|
|
1104
|
-
altaInitFailed = true;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
830
|
wasmDecoder.configure_params(config.k, config.symbolSize);
|
|
1109
831
|
|
|
1110
832
|
// Tell the client which optional commands this Worker accepts, so it can
|
|
@@ -1134,51 +856,7 @@ function handleFeedSource(
|
|
|
1134
856
|
throw new Error("[FecWorker] feedSource: invalid source symbol");
|
|
1135
857
|
}
|
|
1136
858
|
|
|
1137
|
-
|
|
1138
|
-
if (config.blockProfile === "iso-ssbg1-v1") {
|
|
1139
|
-
feedIsoSource(cmd, view, wasmDecoder, config);
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Derive SBN locally for block-level accounting. WASM derives the same
|
|
1144
|
-
// value internally (ISO 23008-1 §C.5.2: SBN = floor(SS_ID / K)).
|
|
1145
|
-
const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
|
|
1146
|
-
const esi = cmd.ssId % config.k;
|
|
1147
|
-
if (isRetiredSbn(sbn)) {
|
|
1148
|
-
sourceDroppedRetired++;
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
const existingBlock = blocks.get(sbn);
|
|
1152
|
-
if (existingBlock && isTerminalBlock(existingBlock)) {
|
|
1153
|
-
sourceDroppedTerminal++;
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (acceptedSourceEsis.get(sbn)?.has(esi)) {
|
|
1157
|
-
sourceDroppedDuplicate++;
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
|
|
1162
|
-
advanceMediaHorizon(sbn);
|
|
1163
|
-
let sourceEsis = acceptedSourceEsis.get(sbn);
|
|
1164
|
-
if (!sourceEsis) {
|
|
1165
|
-
sourceEsis = new Set<number>();
|
|
1166
|
-
acceptedSourceEsis.set(sbn, sourceEsis);
|
|
1167
|
-
}
|
|
1168
|
-
sourceEsis.add(esi);
|
|
1169
|
-
|
|
1170
|
-
// Update block state
|
|
1171
|
-
const block = getOrCreateBlock(sbn);
|
|
1172
|
-
block.sourceReceived++;
|
|
1173
|
-
block.lastSymbolTs = performance.now();
|
|
1174
|
-
sourceSymbols++;
|
|
1175
|
-
|
|
1176
|
-
// The FEC worker must remain decode-only on the per-symbol hot path. ALTA
|
|
1177
|
-
// WASM is retained temporarily for low-rate signaling users, but media is
|
|
1178
|
-
// authenticated by the canonical manifest verifier at router admission.
|
|
1179
|
-
if (altaEnabled) altaPending++;
|
|
1180
|
-
|
|
1181
|
-
settleSourceArrival(sbn, block, result);
|
|
859
|
+
feedIsoSource(cmd, new Uint8Array(cmd.data), wasmDecoder, config);
|
|
1182
860
|
}
|
|
1183
861
|
|
|
1184
862
|
function handleFeedRepair(
|
|
@@ -1190,17 +868,14 @@ function handleFeedRepair(
|
|
|
1190
868
|
if (!config) {
|
|
1191
869
|
throw new Error("[FecWorker] feedRepair: config missing");
|
|
1192
870
|
}
|
|
1193
|
-
// k-padded-v1 blocks start at multiples of K and repair ESI = K + RS_ID;
|
|
1194
871
|
// iso-ssbg1-v1 blocks start at any SS_ID and repair ESI = SSB_length + RS_ID.
|
|
1195
|
-
const iso = config.blockProfile === "iso-ssbg1-v1";
|
|
1196
872
|
if (
|
|
1197
873
|
!isUint32(cmd.ssStart) ||
|
|
1198
874
|
!isUint32(cmd.ssbLength) ||
|
|
1199
875
|
cmd.ssbLength <= 0 ||
|
|
1200
876
|
cmd.ssbLength > config.k ||
|
|
1201
|
-
(!iso && cmd.ssStart % config.k !== 0) ||
|
|
1202
877
|
!isUint32(cmd.rsId) ||
|
|
1203
|
-
|
|
878
|
+
cmd.ssbLength + cmd.rsId > UINT24_MAX ||
|
|
1204
879
|
!(cmd.data instanceof ArrayBuffer)
|
|
1205
880
|
) {
|
|
1206
881
|
throw new Error("[FecWorker] feedRepair: invalid repair symbol");
|
|
@@ -1218,51 +893,7 @@ function handleFeedRepair(
|
|
|
1218
893
|
}
|
|
1219
894
|
return;
|
|
1220
895
|
}
|
|
1221
|
-
|
|
1222
|
-
feedIsoRepair(cmd, view, wasmDecoder);
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
const sbn = Math.floor(cmd.ssStart / config.k);
|
|
1226
|
-
const repairEsi = config.k + cmd.rsId;
|
|
1227
|
-
// The repair path's mirror drops stay silent and uncounted: BLO-35597
|
|
1228
|
-
// defect 2 is a source-symbol shortfall, so only the source side needed
|
|
1229
|
-
// evidence. The asymmetry is deliberate, not an oversight — extend these
|
|
1230
|
-
// the same way if a repair-side shortfall ever needs attributing.
|
|
1231
|
-
if (isRetiredSbn(sbn)) return;
|
|
1232
|
-
const existingBlock = blocks.get(sbn);
|
|
1233
|
-
if (existingBlock && isTerminalBlock(existingBlock)) return;
|
|
1234
|
-
if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
|
|
1235
|
-
// One block has one authored SSB_length; a repair symbol naming another
|
|
1236
|
-
// is dropped before it reaches the decoder (which rejects it too).
|
|
1237
|
-
if (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {
|
|
1238
|
-
emit({
|
|
1239
|
-
type: "error",
|
|
1240
|
-
code: "repair-ssb-length-conflict",
|
|
1241
|
-
message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,
|
|
1242
|
-
});
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const result = wasmDecoder.add_repair(
|
|
1247
|
-
cmd.ssStart,
|
|
1248
|
-
cmd.ssbLength,
|
|
1249
|
-
cmd.rsId,
|
|
1250
|
-
view,
|
|
1251
|
-
cmd.ts,
|
|
1252
|
-
);
|
|
1253
|
-
advanceMediaHorizon(sbn);
|
|
1254
|
-
let repairEsis = acceptedRepairEsis.get(sbn);
|
|
1255
|
-
if (!repairEsis) {
|
|
1256
|
-
repairEsis = new Set<number>();
|
|
1257
|
-
acceptedRepairEsis.set(sbn, repairEsis);
|
|
1258
|
-
}
|
|
1259
|
-
repairEsis.add(repairEsi);
|
|
1260
|
-
const block = getOrCreateBlock(sbn);
|
|
1261
|
-
block.ssbLength = cmd.ssbLength;
|
|
1262
|
-
block.repairReceived++;
|
|
1263
|
-
block.lastSymbolTs = performance.now();
|
|
1264
|
-
repairSymbols++;
|
|
1265
|
-
settleRepairArrival(sbn, block, cmd.ssbLength, result);
|
|
896
|
+
feedIsoRepair(cmd, view, wasmDecoder);
|
|
1266
897
|
}
|
|
1267
898
|
|
|
1268
899
|
function handleRelayBlockSig(
|
|
@@ -1271,8 +902,8 @@ function handleRelayBlockSig(
|
|
|
1271
902
|
// No-op AUTH stub: store sig on block, mark as not-yet-verified
|
|
1272
903
|
const block = blocks.get(cmd.sbn);
|
|
1273
904
|
if (block) {
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
905
|
+
// Relay block signatures are not verified here; source provenance is the
|
|
906
|
+
// signed per-group manifest checked at router admission.
|
|
1276
907
|
block.relayBlockSigValid = null;
|
|
1277
908
|
emitBlockUpdate(block);
|
|
1278
909
|
}
|
|
@@ -1315,10 +946,9 @@ function handleCleanup(
|
|
|
1315
946
|
continue;
|
|
1316
947
|
}
|
|
1317
948
|
currentBlock ??= getOrCreateBlock(sbn);
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
const authoredLength = isIsoProfile() ? currentBlock.ssbLength : currentBlock.k;
|
|
949
|
+
// Compare with the block's SSB_length; a key no repair declared has
|
|
950
|
+
// none, so a block the Worker never knew can only have failed.
|
|
951
|
+
const authoredLength = currentBlock.ssbLength;
|
|
1322
952
|
if (authoredLength !== undefined && currentBlock.sourceReceived >= authoredLength) {
|
|
1323
953
|
markBlockTerminal(currentBlock, "complete");
|
|
1324
954
|
blocksComplete++;
|
|
@@ -1343,29 +973,12 @@ function handleDispose(): void {
|
|
|
1343
973
|
snapshotTimer = null;
|
|
1344
974
|
}
|
|
1345
975
|
|
|
1346
|
-
// Free ALTA verifier WASM object (MUST call to release WASM memory —
|
|
1347
|
-
// Worker.terminate() fires immediately after dispose, so the FinalizationRegistry
|
|
1348
|
-
// callback has no chance to run and this memory would leak without explicit free)
|
|
1349
|
-
if (altaVerifier) {
|
|
1350
|
-
try {
|
|
1351
|
-
altaVerifier.free();
|
|
1352
|
-
} catch (_e) {
|
|
1353
|
-
// Best-effort — verifier may already be freed
|
|
1354
|
-
}
|
|
1355
|
-
altaVerifier = null;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
976
|
// Free WASM decoder (MUST call to release WASM memory)
|
|
1359
977
|
if (wasmDecoder) {
|
|
1360
978
|
wasmDecoder.free();
|
|
1361
979
|
wasmDecoder = null;
|
|
1362
980
|
}
|
|
1363
981
|
|
|
1364
|
-
// Clear deferred-verify queue so pinned ArrayBuffers drop.
|
|
1365
|
-
pendingVerify.clear();
|
|
1366
|
-
pendingVerifyCap = 0;
|
|
1367
|
-
pendingVerifyTtlMs = 0;
|
|
1368
|
-
|
|
1369
982
|
config = null;
|
|
1370
983
|
clearBlockTracking();
|
|
1371
984
|
}
|
|
@@ -1392,13 +1005,6 @@ function handleReset(): void {
|
|
|
1392
1005
|
blocksComplete = 0;
|
|
1393
1006
|
blocksRecovered = 0;
|
|
1394
1007
|
blocksFailed = 0;
|
|
1395
|
-
altaVerified = 0;
|
|
1396
|
-
altaFailed = 0;
|
|
1397
|
-
altaFailedDetails.length = 0;
|
|
1398
|
-
altaPending = 0;
|
|
1399
|
-
altaPendingEvictedCap = 0;
|
|
1400
|
-
altaPendingEvictedTtl = 0;
|
|
1401
|
-
altaError = 0;
|
|
1402
1008
|
relayBlocksVerified = 0;
|
|
1403
1009
|
relayBlocksFailed = 0;
|
|
1404
1010
|
repairSymbolSizeMismatch = 0;
|
|
@@ -1448,8 +1054,7 @@ async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
|
|
|
1448
1054
|
try {
|
|
1449
1055
|
switch (cmd.type) {
|
|
1450
1056
|
case "configure":
|
|
1451
|
-
// handleConfigure
|
|
1452
|
-
// Await it so feed/cleanup messages cannot run against a partially
|
|
1057
|
+
// Await handleConfigure so feed/cleanup messages cannot run against a partially
|
|
1453
1058
|
// configured decoder.
|
|
1454
1059
|
await handleConfigure(cmd);
|
|
1455
1060
|
break;
|