@blockcast/fec-worker 0.1.0-main.10a8b91bb731 → 0.1.0-main.144feb8d235c

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 CHANGED
@@ -187,9 +187,13 @@ The `snapshot` event delivers a `FecTrackStats` every 200 ms (catalog-derived; n
187
187
  | `recoveryRate` | `recovered / (recovered + failed)`, 0–100 |
188
188
  | `avgRecoveryMs`, `maxRecoveryMs` | Rolling decode latency (capped at 1000 samples) |
189
189
  | `liveSbn` | Highest source-block number observed |
190
+ | `repairSymbolSizeMismatch` | Repair symbols rejected before WASM because their byte length did not match `T` |
191
+ | `sourceDroppedRetired` / `sourceDroppedTerminal` / `sourceDroppedDuplicate` | Source symbols dropped before WASM, by **first matching** cause (see below) |
190
192
  | `blocks` | Up to `max(D × 3, 24)` recent `FecBlockSnapshot`s for live visualization |
191
193
  | `interleaveWindowBlocks` | `D` from config |
192
194
 
195
+ The three `sourceDropped*` counters split by cause for the same reason the ALTA four-way split exists — so a dashboard can tell the failure modes apart. They are tested in a fixed order, each returning early — `Retired` → `Terminal` → `Duplicate` — so a symbol counts once, under the **first** cause that matches: a symbol that is both past retention and a repeat counts only as `Retired`, and `Terminal` absorbs every post-settle duplicate. A block that settles at K-1 source symbols (BLO-35597 defect 2) is explained by a late symbol past retention (`Retired`), a symbol for a block that had already settled — complete, recovered **or failed** (`Terminal`), or a repeated `(SBN, ESI)` (`Duplicate`). `Retired` and `Terminal` narrow the cause; they do not attribute it. `Duplicate` does: it cannot fire once a block settles — the `Terminal` check returns first, and `markBlockTerminal` retires the block's accepted-ESI set anyway — so a non-zero `Duplicate` is positive evidence that a repeated `(SBN, ESI)` reached the worker while the block was **still collecting** — usually a duplicate packet on the wire, sometimes an SBN/ESI derivation collision. It attributes the **timing**, not the origin. A symbol lost in transit never reaches the worker and increments nothing, so **all three reading zero is not evidence of a publisher defect** — wire loss produces an identical shape. Separate those with `FecBlockSnapshot.ssbLength`: below `k` is a genuinely short authored block, equal to `k` with fewer accepted is loss-or-late, undefined (no repair symbol yet) leaves both open. All three are cumulative across decoder epochs, so they stay comparable against the `sourceSymbols` denominator; only a reset zeroes them.
196
+
193
197
  When ALTA is enabled, the snapshot also carries the four-state verification accounting — `altaVerified` (crypto passed), `altaFailed` (tamper-confirmed), `altaPending` (unverifiable, e.g. missing trailer / late-joiner anchor gap), `altaError` (verifier threw — infrastructure flake, not tamper) — plus `altaInitFailed` (true if the ALTA WASM didn't initialize), `altaPendingEvictedCap` / `altaPendingEvictedTtl` (deferred-queue eviction reasons), and a bounded `altaFailedDetails[]` ring with the last ≤16 tamper events. The four-way split exists so ops alerting can distinguish a network problem (`altaPending` rising) from an attack (`altaFailed` rising) from a runtime bug (`altaError` rising).
194
198
 
195
199
  `FecTrackStats` is also the shape consumed downstream by `@blockcast/mmt-container`'s `CmcdProducer` and the diagnostics `fec-panel` — keep it stable when you extend it.
@@ -32,7 +32,11 @@ export interface FecTrackConfig {
32
32
  repairCount: number;
33
33
  /** Bytes per symbol */
34
34
  symbolSize: number;
35
- /** Interleave depth in frames (D) */
35
+ /** Retained decoder horizon as a BLOCK COUNT (SBN distance), not D.
36
+ * `advanceMediaHorizon` does `liveSbn - interleaveDepth` and
37
+ * `MmtFecDecoder::new` documents it as "the number of interleaved blocks".
38
+ * Never assign `deriveFecInterleaveDepth()` here — that returns media units
39
+ * per block, which is a different quantity (BLO-35597). */
36
40
  interleaveDepth: number;
37
41
  /** Interleave depth in milliseconds (D * frameDurationMs) */
38
42
  interleaveMs: number;
@@ -140,6 +144,43 @@ export interface FecTrackStats {
140
144
  maxRecoveryMs: number;
141
145
  /** Repair symbols rejected before WASM because their byte length did not match T. */
142
146
  repairSymbolSizeMismatch?: number;
147
+ /**
148
+ * Source symbols whose SBN had already fallen below the retained window.
149
+ *
150
+ * This and its two peers below are dropped before WASM and narrow the
151
+ * explanations for a block that settles at K-1 source symbols (BLO-35597
152
+ * defect 2): a late symbol past retention, a symbol for a block that had
153
+ * already settled (complete, recovered *or failed*), or a repeated
154
+ * (SBN, ESI).
155
+ *
156
+ * They narrow; they do not uniquely attribute. A symbol lost in transit
157
+ * never reaches the worker and increments nothing, so all three reading
158
+ * zero on a short block means only that no *receiver-side* drop was
159
+ * recorded — wire loss and a short authored block both remain open. Use
160
+ * `FecBlockSnapshot.ssbLength` to separate them: below `k` is a genuinely
161
+ * short authored block, equal to `k` with fewer accepted is loss-or-late.
162
+ * `ssbLength` is undefined until a repair symbol declares it, in which case
163
+ * both stay open.
164
+ *
165
+ * All three are cumulative across decoder epochs: `configure` clears block
166
+ * tracking but none of them, so they stay comparable against the
167
+ * `sourceSymbols` denominator. Only a reset zeroes them.
168
+ */
169
+ sourceDroppedRetired?: number;
170
+ /** Source symbols for a block that had already completed, recovered or failed. */
171
+ sourceDroppedTerminal?: number;
172
+ /**
173
+ * Source symbols repeating an (SBN, ESI) already accepted in the current
174
+ * epoch; `configure` clears the accepted-ESI sets.
175
+ *
176
+ * Unlike the other two, this one cannot fire once a block settles — the
177
+ * terminal check returns first, and marking a block terminal retires its
178
+ * accepted-ESI set anyway — so a non-zero value means the repeat reached
179
+ * the worker while the block was **still collecting**. It attributes the
180
+ * timing, not the origin: usually a duplicate packet on the wire,
181
+ * sometimes an SBN/ESI derivation collision.
182
+ */
183
+ sourceDroppedDuplicate?: number;
143
184
  /** Native decoder counters, converted to numbers so browser diagnostics remain JSON-safe. */
144
185
  decoder?: {
145
186
  sourcePackets: number;
@@ -1 +1 @@
1
- {"version":3,"file":"fec-worker-types.d.ts","sourceRoot":"","sources":["../src/fec-worker-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fec-worker-types.d.ts","sourceRoot":"","sources":["../src/fec-worker-types.ts"],"names":[],"mappings":"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"}
@@ -40,6 +40,9 @@ var relayBlocksFailed = 0;
40
40
  var liveSbn = 0;
41
41
  var retiredSbnFrontier = -1;
42
42
  var repairSymbolSizeMismatch = 0;
43
+ var sourceDroppedRetired = 0;
44
+ var sourceDroppedTerminal = 0;
45
+ var sourceDroppedDuplicate = 0;
43
46
  var recoveryTimes = [];
44
47
  var pendingVerify = /* @__PURE__ */ new Map();
45
48
  var pendingVerifyCap = 0;
@@ -167,6 +170,9 @@ function buildStatsSnapshot() {
167
170
  avgRecoveryMs: Math.round(avgRecoveryMs * 100) / 100,
168
171
  maxRecoveryMs: Math.round(maxRecoveryMs * 100) / 100,
169
172
  repairSymbolSizeMismatch,
173
+ sourceDroppedRetired,
174
+ sourceDroppedTerminal,
175
+ sourceDroppedDuplicate,
170
176
  decoder
171
177
  };
172
178
  }
@@ -305,10 +311,19 @@ function handleFeedSource(cmd) {
305
311
  const view = new Uint8Array(cmd.data);
306
312
  const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
307
313
  const esi = cmd.ssId % config.k;
308
- if (sbn <= retiredSbnFrontier) return;
314
+ if (sbn <= retiredSbnFrontier) {
315
+ sourceDroppedRetired++;
316
+ return;
317
+ }
309
318
  const existingBlock = blocks.get(sbn);
310
- if (existingBlock && isTerminalBlock(existingBlock)) return;
311
- if (acceptedSourceEsis.get(sbn)?.has(esi)) return;
319
+ if (existingBlock && isTerminalBlock(existingBlock)) {
320
+ sourceDroppedTerminal++;
321
+ return;
322
+ }
323
+ if (acceptedSourceEsis.get(sbn)?.has(esi)) {
324
+ sourceDroppedDuplicate++;
325
+ return;
326
+ }
312
327
  const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
313
328
  advanceMediaHorizon(sbn);
314
329
  let sourceEsis = acceptedSourceEsis.get(sbn);
@@ -505,6 +520,9 @@ function handleReset() {
505
520
  relayBlocksVerified = 0;
506
521
  relayBlocksFailed = 0;
507
522
  repairSymbolSizeMismatch = 0;
523
+ sourceDroppedRetired = 0;
524
+ sourceDroppedTerminal = 0;
525
+ sourceDroppedDuplicate = 0;
508
526
  emit({ type: "resetComplete" });
509
527
  if (config) emitSnapshot();
510
528
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/transfer.ts", "../src/fec-worker.ts"],
4
- "sourcesContent": ["/**\n * Return a fresh ArrayBuffer containing exactly the bytes in `view`.\n *\n * Worker outputs must not transfer buffers that might be owned by a WASM\n * binding or by a larger shared packet. A fresh JS-owned buffer gives\n * postMessage() exclusive ownership without detaching unrelated memory.\n */\nexport function toOwnedArrayBuffer(view: Uint8Array): ArrayBuffer {\n\tconst copy = new Uint8Array(view.byteLength);\n\tcopy.set(view);\n\treturn copy.buffer;\n}\n", "/**\n * @blockcast/fec-worker \u2014 Worker Entry Point\n *\n * Runs inside DedicatedWorkerGlobalScope. Receives FecWorkerCommand messages,\n * drives the MmtFecDecoder WASM module, emits FecWorkerEvent messages back.\n *\n * WASM bindings are resolved via:\n * 1. (self as any).__MMT_WASM_BINDINGS__ \u2014 set by consumer's loader/importmap\n * 2. Falls back to error if not available (no defaults)\n *\n * This file must NOT import DOM globals incompatible with Worker scope.\n */\n\nimport type {\n\tFecWorkerCommand,\n\tFecWorkerEvent,\n\tFecTrackConfig,\n\tFecTrackStats,\n\tFecBlockSnapshot,\n\tFrameMeta,\n} from \"./fec-worker-types.js\";\nimport { toOwnedArrayBuffer } from \"./transfer.js\";\n\n// --- WASM binding types (from mmt-wasm) ---\n// Signatures MUST match a freshly generated external mmt_wasm.d.ts exactly.\n// The decoder derives SBN/ESI internally \u2014 do not pre-compute.\ninterface WasmFecDecoder {\n\t/** ISO 23008-1 \u00A7C.5.2: ss_id is the flat 32-bit Source Symbol ID. */\n\tadd_source(\n\t\tss_id: number,\n\t\tdata: Uint8Array,\n\t\ttimestamp: number,\n\t): Uint8Array | undefined;\n\t/** ISO 23008-1 \u00A7C.5.3: repair ESI = configured K + rs_id, SBN = ss_start / configured K. */\n\tadd_repair(\n\t\tss_start: number,\n\t\tssb_length: number,\n\t\trs_id: number,\n\t\tdata: Uint8Array,\n\t\ttimestamp: number,\n\t): Uint8Array | undefined;\n\tconfigure_params(k: number, symbolSize: number): void;\n\t/**\n\t * Retire the named blocks (last-chance decode + stats), driven by the\n\t * manager's exact deadlines. Nothing is synthesised for a lost block:\n\t * concealment is the sink's hold-last-good-frame.\n\t */\n\tcleanup_blocks_detailed(sbns: Uint32Array): void;\n\t/** Remove decoder blocks below its configured live-SBN interleave horizon. */\n\tcleanup_by_sbn(current_sbn: number): Uint32Array;\n\tflush(): void;\n\tpending_blocks(): number;\n\treset_stats(): void;\n\tget_stats(): {\n\t\tsource_packets: bigint;\n\t\trepair_packets: bigint;\n\t\tblocks_complete: bigint;\n\t\tblocks_recovered: bigint;\n\t\tblocks_failed: bigint;\n\t\tbytes_recovered: bigint;\n\t\trecovery_rate(): number;\n\t};\n\tfree(): void;\n}\n\ninterface WasmBindings {\n\tinitSync(module: { module: WebAssembly.Module }): unknown;\n\tMmtFecDecoder: new (interleaveDepth: number) => WasmFecDecoder;\n}\n\n// --- Worker state (local to this Worker instance) ---\n\nlet wasmDecoder: WasmFecDecoder | null = null;\nlet config: FecTrackConfig | null = null;\nlet altaEnabled = false;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet altaVerifier: any = null; // JsAltaVerifier instance (loaded from ALTA WASM)\nlet snapshotTimer: ReturnType<typeof setInterval> | null = null;\n// blockUpdate events are posted only after a client subscribes (BLO-36599).\nlet blockUpdatesSubscribed = false;\n\n// Block tracking\nconst blocks = new Map<number, FecBlockSnapshot>();\nconst acceptedSourceEsis = new Map<number, Set<number>>();\nconst acceptedRepairEsis = new Map<number, Set<number>>();\nconst UINT32_MAX = 0xffff_ffff;\nconst UINT24_MAX = 0xff_ffff;\n// ISO/IEC 23008-1 AL-FEC signaling carries interleave_depth in one byte.\nconst MAX_INTERLEAVE_DEPTH = 0xff;\n// Rust stores T as u16 and this decoder profile fixes RFC 6330 Al=8.\nconst MAX_SYMBOL_SIZE = 0xfff8;\n// RFC 6330 systematic table limit, enforced by the vendored raptorq decoder.\nconst MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK = 56_403;\n\n// Aggregate counters\nlet sourceSymbols = 0;\nlet repairSymbols = 0;\nlet blocksComplete = 0;\nlet blocksRecovered = 0;\nlet blocksFailed = 0;\nlet altaVerified = 0;\nlet altaFailed = 0; // Crypto-confirmed tamper (MAC / sig / hash chain mismatch)\nconst altaFailedDetails: Array<{\n\tseq: number;\n\terror: string;\n\tauthLen: number;\n\tpayloadLen: number;\n\tpath: string; // \"direct\" | \"recovered\" | \"pending-retry\"\n}> = [];\nlet altaPending = 0; // Unverifiable but not tamper (no trailer, verifier not ready, anchor missing)\nlet altaPendingEvictedCap = 0; // Incoming \"pending\" packets rejected because the queue was at cap after stale-sweep (flood indicator)\nlet altaPendingEvictedTtl = 0; // Deferred-queue entries dropped by TTL (retry window expired)\nlet altaError = 0; // verifyDetached threw \u2014 infrastructure flake, not tamper\nlet altaInitFailed = false; // ALTA configured but WASM init failed; altaEnabled was forced off\nlet relayBlocksVerified = 0;\nlet relayBlocksFailed = 0;\nlet liveSbn = 0;\n// Highest SBN that has fallen strictly below the decoder's retained\n// interleave window. Numeric ordering is per decoder epoch; reset/configure\n// starts a new epoch before the 32-bit source-symbol counter can wrap.\nlet retiredSbnFrontier = -1;\nlet repairSymbolSizeMismatch = 0;\n\n// Recovery timing\nconst recoveryTimes: number[] = [];\n\n// \u2500\u2500 Deferred ALTA verification queue (08-07 Path 3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// When an ALTA packet arrives whose MAC references point at sequences not yet\n// in the verifier's ring buffer, we can't anchor trust \u2014 verify returns \"No\n// trust anchor\". Typical cause: a predecessor packet was lost and hasn't been\n// FEC-recovered yet. Rather than drop these, hold them in a bounded FIFO and\n// retry verification after every successful verify (direct or FEC-recovered)\n// that seeds the ring buffer with new raw bytes.\n//\n// Zero-copy (1B-B): the queue entry stores a Uint8Array view into the\n// original `data` ArrayBuffer. JS GC keeps the buffer alive via the view\n// reference; no bytes are copied on queue.\n//\n// Timeout entries (TTL exceeded) and cap-rejections count as `altaPending`\n// (2C-B): they're not tamper indicators, they're just packets that couldn't\n// be authenticated within the receive window \u2014 either loss or, in the cap\n// case, a flood of unanchorable arrivals (real or attacker-driven) that\n// exceeded the queue budget. See enqueuePendingVerify for the rate-limit\n// protocol.\ninterface PendingVerify {\n\tseq: number;\n\tauth: Uint8Array;\n\tpayload: Uint8Array;\n\tarrivalTs: number;\n}\nconst pendingVerify = new Map<number, PendingVerify>();\nlet pendingVerifyCap = 0;\nlet pendingVerifyTtlMs = 0;\n\n// --- Helpers ---\n\nfunction emit(event: FecWorkerEvent, transfer?: Transferable[]): void {\n\tself.postMessage(event, transfer ?? []);\n}\n\nfunction isUint32(value: number): boolean {\n\treturn Number.isInteger(value) && value >= 0 && value <= UINT32_MAX;\n}\n\nfunction isTerminalBlock(block: FecBlockSnapshot): boolean {\n\treturn block.state !== \"collecting\";\n}\n\nfunction retireAcceptedIds(sbn: number): void {\n\tacceptedSourceEsis.delete(sbn);\n\tacceptedRepairEsis.delete(sbn);\n}\n\nfunction clearBlockTracking(): void {\n\tblocks.clear();\n\tacceptedSourceEsis.clear();\n\tacceptedRepairEsis.clear();\n\tpendingVerify.clear();\n\trecoveryTimes.length = 0;\n\tliveSbn = 0;\n\tretiredSbnFrontier = -1;\n}\n\n/**\n * Retire terminal diagnostics only after the decoder's monotonic interleave\n * horizon proves that the SBN can no longer be valid out-of-order media.\n * Admission keeps the compact frontier as the durable tombstone, so deletion\n * never reopens a finalized block.\n */\nfunction trimBlockRetention(): void {\n\tfor (const [sbn, block] of blocks) {\n\t\tif (!isTerminalBlock(block) || sbn > retiredSbnFrontier) continue;\n\t\tblocks.delete(sbn);\n\t\tretireAcceptedIds(sbn);\n\t}\n}\n\nfunction advanceMediaHorizon(sbn: number): void {\n\tif (!config || sbn <= liveSbn) return;\n\tliveSbn = sbn;\n\tconst firstRetainedSbn = Math.max(0, liveSbn - config.interleaveDepth);\n\tretiredSbnFrontier = Math.max(retiredSbnFrontier, firstRetainedSbn - 1);\n\ttrimBlockRetention();\n}\n\nfunction markBlockTerminal(\n\tblock: FecBlockSnapshot,\n\tstate: \"complete\" | \"recovered\" | \"failed\",\n): void {\n\tblock.state = state;\n\tblock.completedTs = performance.now();\n\tretireAcceptedIds(block.sbn);\n\ttrimBlockRetention();\n}\n\nfunction synchronizeDecoderHorizon(): void {\n\tif (!wasmDecoder) return;\n\tconst expired = wasmDecoder.cleanup_by_sbn(liveSbn);\n\tfor (const sbn of expired) {\n\t\tconst block = blocks.get(sbn);\n\t\tif (!block || isTerminalBlock(block)) {\n\t\t\tretireAcceptedIds(sbn);\n\t\t\tcontinue;\n\t\t}\n\t\tmarkBlockTerminal(block, \"failed\");\n\t\tblocksFailed++;\n\t\temitBlockUpdate(block);\n\t}\n\ttrimBlockRetention();\n}\n\n/**\n * Run detached ALTA verify against the current verifier. Four-state result:\n * - \"valid\": crypto passed, packet authentic. Caller increments altaVerified.\n * - \"failed\": crypto confirmed tamper (MAC/sig mismatch, hash chain mismatch).\n * Caller increments altaFailed. Not retryable.\n * - \"pending\": anchor missing (MAC refs not in buffer, no signature, not\n * early-warmup). Caller may enqueue for retry after the next\n * successful verify seeds the ring buffer.\n * - \"error\": verifier threw \u2014 WASM trap, unmarshal panic, OOM. Caller\n * increments altaError (infrastructure flake, not tamper).\n * The distinction matters for ops alerting: a spike in\n * altaError indicates a runtime problem, not an attack.\n */\nlet _lastVerifyError = \"\";\nfunction runDetachedVerify(\n\tauth: Uint8Array,\n\tpayload: Uint8Array,\n): \"valid\" | \"failed\" | \"pending\" | \"error\" {\n\tif (!altaVerifier) return \"pending\";\n\ttry {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst r: any = altaVerifier.verifyDetached(auth, payload);\n\t\tif (r.valid) {\n\t\t\t_lastVerifyError = \"\";\n\t\t\treturn \"valid\";\n\t\t}\n\t\tconst err = (r.error as string | undefined) ?? \"\";\n\t\t_lastVerifyError = err;\n\t\t// The bootstrap-friendly verifier returns this specific error string when\n\t\t// the only obstacle to verification is a missing anchor (see alta-rs\n\t\t// verifier.rs verify()). Distinguishing this from tamper cases keeps us\n\t\t// from queuing packets that are definitively invalid.\n\t\tif (err.startsWith(\"No trust anchor\")) return \"pending\";\n\t\treturn \"failed\";\n\t} catch (e) {\n\t\t// Ratelimit: first ~N occurrences get logged. A sustained exception\n\t\t// stream indicates a real problem \u2014 the altaError counter is the\n\t\t// durable signal.\n\t\tif (altaError < 5) {\n\t\t\tconsole.warn(\"[FecWorker] verifyDetached threw:\", e);\n\t\t}\n\t\treturn \"error\";\n\t}\n}\n\n/** Extract u32 BE sequence number from the ALTA authenticator header (offset 0..3). */\nfunction seqFromAuth(auth: Uint8Array): number {\n\treturn (\n\t\t((auth[0]! << 24) | (auth[1]! << 16) | (auth[2]! << 8) | auth[3]!) >>> 0\n\t);\n}\n\n/**\n * Enqueue a packet whose ALTA verification returned \"pending\" (anchor\n * missing). Dedupes by sequence number.\n *\n * Flood resistance (08-08 review): when the queue is at cap we do NOT\n * evict the oldest entry. The oldest entry is typically the most\n * load-bearing one \u2014 it's the packet whose anchor everything else in the\n * queue is waiting on. Evict-oldest-on-insert let an attacker emitting a\n * stream of \"No trust anchor\" packets flood out legitimate entries and\n * prevent them from ever anchoring. Instead:\n * 1. First try to free stale TTL'd entries (they'd be evicted on next\n * drain anyway \u2014 pulling that forward is free).\n * 2. If still full, reject the NEW entry. altaPendingEvictedCap counts\n * these rejections (rate-limit trigger).\n *\n * Stores Uint8Array views into the original data + auth buffers \u2014 no copy.\n * The views keep their underlying ArrayBuffers alive via GC references.\n */\nfunction enqueuePendingVerify(\n\tauth: Uint8Array,\n\tpayload: Uint8Array,\n): void {\n\tconst seq = seqFromAuth(auth);\n\tif (pendingVerify.has(seq)) return;\n\n\tif (pendingVerify.size >= pendingVerifyCap) {\n\t\t// Step 1: sweep stale entries so legitimate traffic isn't penalized\n\t\t// by TTL'd junk that happens to still occupy slots.\n\t\tconst now = performance.now();\n\t\tfor (const [staleSeq, entry] of pendingVerify) {\n\t\t\tif (now - entry.arrivalTs > pendingVerifyTtlMs) {\n\t\t\t\tpendingVerify.delete(staleSeq);\n\t\t\t\taltaPendingEvictedTtl++;\n\t\t\t}\n\t\t}\n\t\t// Step 2: if the queue is still at cap it's a genuine flood \u2014\n\t\t// reject the new entry rather than evict an existing one. The\n\t\t// rejected packet counts as altaPendingEvictedCap (which is\n\t\t// surfaced up through stats for rate-limit alerting).\n\t\tif (pendingVerify.size >= pendingVerifyCap) {\n\t\t\taltaPendingEvictedCap++;\n\t\t\treturn;\n\t\t}\n\t}\n\n\tpendingVerify.set(seq, {\n\t\tseq,\n\t\tauth,\n\t\tpayload,\n\t\tarrivalTs: performance.now(),\n\t});\n}\n\n/**\n * Retry every queued entry. Called after every successful verify (direct\n * source or FEC-recovered) \u2014 new raw bytes in the verifier's ring buffer may\n * unblock previously-pending MAC references.\n *\n * Three outcomes per entry:\n * - \"valid\" \u2192 remove from queue, altaVerified++\n * - \"failed\" \u2192 remove from queue, altaFailed++ (tamper on retry)\n * - \"pending\" \u2192 keep in queue, unless TTL expired (then evict as altaPending)\n *\n * Iteration order: Map preserves insertion order, so we drain oldest-first.\n * No-op when the queue is empty or the verifier isn't ready.\n */\nfunction drainPendingVerify(): void {\n\tif (pendingVerify.size === 0 || !altaVerifier) return;\n\tconst now = performance.now();\n\t// Collect keys to avoid mutating Map during iteration\n\tconst keys = Array.from(pendingVerify.keys());\n\tfor (const seq of keys) {\n\t\tconst entry = pendingVerify.get(seq);\n\t\tif (!entry) continue;\n\t\tif (now - entry.arrivalTs > pendingVerifyTtlMs) {\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaPendingEvictedTtl++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst result = runDetachedVerify(entry.auth, entry.payload);\n\t\tif (result === \"valid\") {\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaVerified++;\n\t\t} else if (result === \"failed\") {\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaFailed++;\n\t\t\tif (altaFailedDetails.length < 16) {\n\t\t\t\taltaFailedDetails.push({\n\t\t\t\t\tseq: entry.seq,\n\t\t\t\t\terror: _lastVerifyError,\n\t\t\t\t\tauthLen: entry.auth.length,\n\t\t\t\t\tpayloadLen: entry.payload.length,\n\t\t\t\t\tpath: \"pending-retry\",\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (result === \"error\") {\n\t\t\t// Infrastructure failure \u2014 remove from queue, count as altaError.\n\t\t\t// Keeping it around would just re-throw on every subsequent drain.\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaError++;\n\t\t}\n\t\t// \"pending\" \u2192 leave in queue for next drain\n\t}\n}\n\nfunction getOrCreateBlock(sbn: number): FecBlockSnapshot {\n\tlet block = blocks.get(sbn);\n\tif (!block) {\n\t\tif (!config) throw new Error(\"[FecWorker] not configured\");\n\t\tconst now = performance.now();\n\t\tblock = {\n\t\t\tsbn,\n\t\t\tstate: \"collecting\",\n\t\t\tsourceReceived: 0,\n\t\t\trepairReceived: 0,\n\t\t\tk: config.k,\n\t\t\trepairCount: config.repairCount,\n\t\t\tfirstSymbolTs: now,\n\t\t\tlastSymbolTs: now,\n\t\t\tdeadlineTs: now + config.deliveryWindowMs,\n\t\t\taltaVerified: 0,\n\t\t\trelayBlockSigValid: null,\n\t\t};\n\t\tblocks.set(sbn, block);\n\t\ttrimBlockRetention();\n\t}\n\treturn block;\n}\n\nfunction buildStatsSnapshot(): FecTrackStats {\n\tif (!config) throw new Error(\"[FecWorker] not configured\");\n\tconst decoderStats = wasmDecoder?.get_stats();\n\tconst decoder = decoderStats\n\t\t? {\n\t\t\tsourcePackets: Number(decoderStats.source_packets),\n\t\t\trepairPackets: Number(decoderStats.repair_packets),\n\t\t\tblocksComplete: Number(decoderStats.blocks_complete),\n\t\t\tblocksRecovered: Number(decoderStats.blocks_recovered),\n\t\t\tblocksFailed: Number(decoderStats.blocks_failed),\n\t\t\tbytesRecovered: Number(decoderStats.bytes_recovered),\n\t\t\tpendingBlocks: wasmDecoder?.pending_blocks() ?? 0,\n\t\t}\n\t\t: undefined;\n\n\tconst avgRecoveryMs =\n\t\trecoveryTimes.length > 0\n\t\t\t? recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length\n\t\t\t: 0;\n\tconst maxRecoveryMs =\n\t\trecoveryTimes.length > 0 ? Math.max(...recoveryTimes) : 0;\n\n\t// Cap block array to interleave window\n\tconst maxBlocks = Math.max(config.interleaveDepth * 3, 24);\n\tconst blockArray = Array.from(blocks.values())\n\t\t.sort((a, b) => b.sbn - a.sbn)\n\t\t.slice(0, maxBlocks);\n\n\treturn {\n\t\tconfig,\n\t\tsourceSymbols,\n\t\trepairSymbols,\n\t\tblocksComplete,\n\t\tblocksRecovered,\n\t\tblocksFailed,\n\t\trecoveryRate:\n\t\t\tblocksComplete + blocksRecovered > 0\n\t\t\t\t? Math.round(\n\t\t\t\t\t\t(blocksRecovered / (blocksRecovered + blocksFailed)) * 100,\n\t\t\t\t\t) || 0\n\t\t\t\t: 0,\n\t\taltaVerified,\n\t\taltaFailed,\n\t\taltaPending,\n\t\taltaPendingEvictedCap,\n\t\taltaPendingEvictedTtl,\n\t\taltaError,\n\t\taltaInitFailed,\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\taltaFailedDetails: altaFailedDetails.slice() as any,\n\t\trelayBlocksVerified,\n\t\trelayBlocksFailed,\n\t\tliveSbn,\n\t\tblocks: blockArray,\n\t\tinterleaveWindowBlocks: config.interleaveDepth,\n\t\tavgRecoveryMs: Math.round(avgRecoveryMs * 100) / 100,\n\t\tmaxRecoveryMs: Math.round(maxRecoveryMs * 100) / 100,\n\t\trepairSymbolSizeMismatch,\n\t\tdecoder,\n\t};\n}\n\nfunction emitSnapshot(): void {\n\tif (!config) return;\n\temit({ type: \"snapshot\", stats: buildStatsSnapshot() });\n}\n\nfunction emitBlockUpdate(block: FecBlockSnapshot): void {\n\tif (!blockUpdatesSubscribed) return;\n\temit({ type: \"blockUpdate\", block: { ...block } });\n}\n\nfunction emitRecoveredSymbols(\n\tsbn: number,\n\trecoveredBlock: Uint8Array,\n\tblock: FecBlockSnapshot,\n): void {\n\tif (!config) return;\n\tconst symbolSize = config.symbolSize;\n\tif (symbolSize <= 0) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"recovered-symbol-size-unconfigured\",\n\t\t\tmessage: \"[FecWorker] recovered block has no configured symbolSize\",\n\t\t});\n\t\treturn;\n\t}\n\t// The decoder returns only the block's authored source symbols\n\t// (SSB_length x T); the encoder's zero padding is not media.\n\tconst ssbLength = block.ssbLength ?? config.k;\n\tif (recoveredBlock.byteLength !== symbolSize * ssbLength) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"recovered-block-too-short\",\n\t\t\tmessage: `[FecWorker] recovered block has the wrong length: got ${recoveredBlock.byteLength}B, expected ${symbolSize * ssbLength}B (SSB_length=${ssbLength})`,\n\t\t});\n\t\treturn;\n\t}\n\n\tfor (let i = 0; i < ssbLength; i++) {\n\t\tconst symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);\n\n\t\t// Source authentication now runs once at router admission in\n\t\t// @blockcast/mmt-manifest-verify. The worker only records that this\n\t\t// legacy ALTA-enabled symbol bypassed hot-path verification.\n\t\tconst recoveredAltaVerified: boolean | null = null;\n\t\tif (altaEnabled) altaPending++;\n\n\t\tconst buf = toOwnedArrayBuffer(symbol);\n\t\tconst meta: FrameMeta = {\n\t\t\taltaVerified: recoveredAltaVerified,\n\t\t\trecovered: true,\n\t\t\tsbn,\n\t\t\tesi: i,\n\t\t\tssbLength,\n\t\t};\n\t\temit({ type: \"frame\", data: buf, meta }, [buf]);\n\t}\n}\n\nfunction startSnapshotTimer(): void {\n\tif (snapshotTimer !== null) clearInterval(snapshotTimer);\n\tsnapshotTimer = setInterval(() => emitSnapshot(), 200);\n}\n\nfunction resolveWasmBindings(): WasmBindings {\n\tconst bindings = (self as unknown as Record<string, unknown>)\n\t\t.__MMT_WASM_BINDINGS__ as WasmBindings | undefined;\n\tif (!bindings) {\n\t\tthrow new Error(\n\t\t\t\"[FecWorker] WASM bindings not available. Set self.__MMT_WASM_BINDINGS__ = { initSync, MmtFecDecoder } before configure.\",\n\t\t);\n\t}\n\treturn bindings;\n}\n\n// --- Command handlers ---\n\nasync function handleConfigure(\n\tcmd: Extract<FecWorkerCommand, { type: \"configure\" }>,\n): Promise<void> {\n\tif (!cmd.wasmModule) {\n\t\tthrow new Error(\"[FecWorker] configure: wasmModule required\");\n\t}\n\tif (!cmd.config) {\n\t\tthrow new Error(\"[FecWorker] configure: config required\");\n\t}\n\tif (\n\t\t!Number.isFinite(cmd.config.interleaveDepth) ||\n\t\t!Number.isInteger(cmd.config.interleaveDepth) ||\n\t\tcmd.config.interleaveDepth <= 0 ||\n\t\tcmd.config.interleaveDepth > MAX_INTERLEAVE_DEPTH\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: interleaveDepth must be an integer in 1..${MAX_INTERLEAVE_DEPTH} (got ${cmd.config.interleaveDepth})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isFinite(cmd.config.interleaveMs) ||\n\t\tcmd.config.interleaveMs <= 0\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: interleaveMs must be finite and > 0 (got ${cmd.config.interleaveMs})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isFinite(cmd.config.deliveryWindowMs) ||\n\t\tcmd.config.deliveryWindowMs <= 0\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: deliveryWindowMs must be > 0 (got ${cmd.config.deliveryWindowMs})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isInteger(cmd.config.k) ||\n\t\tcmd.config.k <= 0 ||\n\t\tcmd.config.k > MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: k must be an integer in 1..${MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK} (got ${cmd.config.k})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isInteger(cmd.config.symbolSize) ||\n\t\tcmd.config.symbolSize <= 0 ||\n\t\tcmd.config.symbolSize > MAX_SYMBOL_SIZE ||\n\t\tcmd.config.symbolSize % 8 !== 0\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: symbolSize must be an 8-byte-aligned integer in 8..${MAX_SYMBOL_SIZE} (got ${cmd.config.symbolSize})`,\n\t\t);\n\t}\n\n\tconst wasm = resolveWasmBindings();\n\n\t// Initialize WASM from pre-compiled Module (no fetch, no re-compile)\n\twasm.initSync({ module: cmd.wasmModule });\n\n\t// A configure command starts a new decoder epoch. No terminal tombstone,\n\t// accepted-symbol ID, or active block from the previous decoder may affect it.\n\twasmDecoder?.free();\n\twasmDecoder = null;\n\tclearBlockTracking();\n\n\t// Decoder policy and geometry are explicit at the epoch boundary. The WASM\n\t// module has no guessed K/T/interleave slow path.\n\twasmDecoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth);\n\tconfig = cmd.config;\n\taltaEnabled = !!cmd.altaPublicKey;\n\n\t// Size the deferred-verify queue strictly from catalog-derived FEC\n\t// parameters. CLAUDE.md bans Math.max(x, floor) magic \u2014 if catalog\n\t// values are zero/missing, throw at init rather than silently applying\n\t// a hardcoded floor. Consumers of an undersized queue cap would see\n\t// altaPendingEvictedCap spike with no actionable signal.\n\t// Cap: 1.5\u00D7 interleave depth \u2014 wide enough to hold one full FEC recovery\n\t// window plus jitter, narrow enough to bound memory.\n\tpendingVerifyCap = Math.ceil(config.interleaveDepth * 1.5);\n\t// TTL: 1.5\u00D7 block duration (K symbols at one per interleave tick) \u2014\n\t// after this many ms, any unresolved entry has missed its FEC window.\n\tpendingVerifyTtlMs = Math.ceil(config.interleaveMs * config.k * 1.5);\n\n\t// Initialize ALTA WASM verifier if public key and WASM module provided (D-05, D-09)\n\taltaVerifier = null;\n\taltaInitFailed = false;\n\tif (cmd.altaPublicKey && cmd.altaWasmModule) {\n\t\ttry {\n\t\t\t// Load ALTA JS glue (wasm-bindgen output from wasm-pack build).\n\t\t\t// Mirrors the RaptorQ WASM pattern: initSync({ module }) then construct.\n\t\t\t// Dynamic import path matches the IWA static asset layout.\n\t\t\t// Cast via string to prevent TypeScript from attempting static resolution\n\t\t\t// of a runtime-only URL (same pattern as fec-worker-entry.js loading).\n\t\t\tconst altaGlueUrl = '/wasm/alta/alta_rs.js' as string;\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\t\tconst altaGlue: any = await import(/* webpackIgnore: true */ altaGlueUrl);\n\t\t\taltaGlue.initSync({ module: cmd.altaWasmModule });\n\t\t\tconst keyBytes = new Uint8Array(cmd.altaPublicKey);\n\t\t\taltaVerifier = new altaGlue.JsAltaVerifier(keyBytes);\n\t\t} catch (e) {\n\t\t\t// T-08-15: WASM init failure must NOT silently look like \"not-ready-yet\".\n\t\t\t// Force altaEnabled = false so handleFeedSource takes the disabled\n\t\t\t// branch instead of continuously bumping altaPending under the\n\t\t\t// misleading \"verifier not yet ready\" comment. altaInitFailed\n\t\t\t// flag in the snapshot tells consumers this is an error state, not\n\t\t\t// an opt-out.\n\t\t\tconsole.warn('[FecWorker] ALTA WASM init failed, verification disabled:', e);\n\t\t\taltaVerifier = null;\n\t\t\taltaEnabled = false;\n\t\t\taltaInitFailed = true;\n\t\t}\n\t}\n\n\twasmDecoder.configure_params(config.k, config.symbolSize);\n\n\t// Tell the client which optional commands this Worker accepts, so it can\n\t// batch symbols without risking a Worker that predates the batch command.\n\temit({ type: \"capabilities\", commands: [\"feedSymbols\", \"subscribeBlockUpdates\"] });\n\n\t// Start periodic snapshot emission (200ms floor)\n\tstartSnapshotTimer();\n\n\t// Emit initial snapshot\n\temitSnapshot();\n}\n\nfunction handleFeedSource(\n\tcmd: Extract<FecWorkerCommand, { type: \"feedSource\" }>,\n): void {\n\tif (!wasmDecoder) {\n\t\tthrow new Error(\"[FecWorker] feedSource: not configured\");\n\t}\n\tif (!config) {\n\t\tthrow new Error(\"[FecWorker] feedSource: config missing\");\n\t}\n\tif (\n\t\t!isUint32(cmd.ssId) ||\n\t\t!(cmd.data instanceof ArrayBuffer)\n\t) {\n\t\tthrow new Error(\"[FecWorker] feedSource: invalid source symbol\");\n\t}\n\n\tconst view = new Uint8Array(cmd.data);\n\n\t// Derive SBN locally for block-level accounting. WASM derives the same\n\t// value internally (ISO 23008-1 \u00A7C.5.2: SBN = floor(SS_ID / K)).\n\tconst sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;\n\tconst esi = cmd.ssId % config.k;\n\tif (sbn <= retiredSbnFrontier) return;\n\tconst existingBlock = blocks.get(sbn);\n\tif (existingBlock && isTerminalBlock(existingBlock)) return;\n\tif (acceptedSourceEsis.get(sbn)?.has(esi)) return;\n\n\tconst result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);\n\tadvanceMediaHorizon(sbn);\n\tlet sourceEsis = acceptedSourceEsis.get(sbn);\n\tif (!sourceEsis) {\n\t\tsourceEsis = new Set<number>();\n\t\tacceptedSourceEsis.set(sbn, sourceEsis);\n\t}\n\tsourceEsis.add(esi);\n\n\t// Update block state\n\tconst block = getOrCreateBlock(sbn);\n\tblock.sourceReceived++;\n\tblock.lastSymbolTs = performance.now();\n\tsourceSymbols++;\n\n\t// The FEC worker must remain decode-only on the per-symbol hot path. ALTA\n\t// WASM is retained temporarily for low-rate signaling users, but media is\n\t// authenticated by the canonical manifest verifier at router admission.\n\tif (altaEnabled) altaPending++;\n\n\t// Check if block is complete (all authored source symbols received:\n\t// SSB_length once a repair symbol declared it, otherwise K)\n\tconst authoredLength = block.ssbLength ?? block.k;\n\tif (\n\t\tblock.sourceReceived >= authoredLength &&\n\t\tblock.state === \"collecting\"\n\t) {\n\t\tmarkBlockTerminal(block, \"complete\");\n\t\tblocksComplete++;\n\t\temitBlockUpdate(block);\n\t}\n\n\t\t// A source-triggered return can be either clean block completion or recovery\n\t\t// of earlier missing symbols after repair arrived. Clean completion is\n\t\t// already routed by the source path, so only emit when the block did not\n\t\t// reach K source arrivals.\n\t\tif (result && result.byteLength > 0 && block.sourceReceived < authoredLength) {\n\t\t\tconst recovered = new Uint8Array(toOwnedArrayBuffer(result));\n\t\t\tconst recoveryMs = performance.now() - block.firstSymbolTs;\n\t\t\trecoveryTimes.push(recoveryMs);\n\t\t\tif (recoveryTimes.length > 1000) recoveryTimes.shift();\n\n\t\t\tif (block.state === \"collecting\") {\n\t\t\t\tmarkBlockTerminal(block, \"recovered\");\n\t\t\t\tblocksRecovered++;\n\t\t\t\temitBlockUpdate(block);\n\t\t\t}\n\t\t\temitRecoveredSymbols(sbn, recovered, block);\n\t\t}\n\t\tsynchronizeDecoderHorizon();\n\t}\n\nfunction handleFeedRepair(\n\tcmd: Extract<FecWorkerCommand, { type: \"feedRepair\" }>,\n): void {\n\tif (!wasmDecoder) {\n\t\tthrow new Error(\"[FecWorker] feedRepair: not configured\");\n\t}\n\tif (!config) {\n\t\tthrow new Error(\"[FecWorker] feedRepair: config missing\");\n\t}\n\tif (\n\t\t!isUint32(cmd.ssStart) ||\n\t\t!isUint32(cmd.ssbLength) ||\n\t\tcmd.ssbLength <= 0 ||\n\t\tcmd.ssbLength > config.k ||\n\t\tcmd.ssStart % config.k !== 0 ||\n\t\t!isUint32(cmd.rsId) ||\n\t\tconfig.k + cmd.rsId > UINT24_MAX ||\n\t\t!(cmd.data instanceof ArrayBuffer)\n\t) {\n\t\tthrow new Error(\"[FecWorker] feedRepair: invalid repair symbol\");\n\t}\n\n\tconst view = new Uint8Array(cmd.data);\n\tif (config.symbolSize > 0 && view.byteLength !== config.symbolSize) {\n\t\trepairSymbolSizeMismatch++;\n\t\tif (repairSymbolSizeMismatch <= 5 || repairSymbolSizeMismatch % 100 === 0) {\n\t\t\temit({\n\t\t\t\ttype: \"error\",\n\t\t\t\tcode: \"repair-symbol-size-mismatch\",\n\t\t\t\tmessage: `[FecWorker] Dropping repair symbol with invalid size: got ${view.byteLength}B, expected ${config.symbolSize}B (ssStart=${cmd.ssStart}, ssbLength=${cmd.ssbLength}, rsId=${cmd.rsId})`,\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\tconst sbn = Math.floor(cmd.ssStart / config.k);\n\tconst repairEsi = config.k + cmd.rsId;\n\tif (sbn <= retiredSbnFrontier) return;\n\tconst existingBlock = blocks.get(sbn);\n\tif (existingBlock && isTerminalBlock(existingBlock)) return;\n\tif (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;\n\t// One block has one authored SSB_length; a repair symbol naming another\n\t// is dropped before it reaches the decoder (which rejects it too).\n\tif (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"repair-ssb-length-conflict\",\n\t\t\tmessage: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,\n\t\t});\n\t\treturn;\n\t}\n\n\tconst result = wasmDecoder.add_repair(\n\t\tcmd.ssStart,\n\t\tcmd.ssbLength,\n\t\tcmd.rsId,\n\t\tview,\n\t\tcmd.ts,\n\t);\n\tadvanceMediaHorizon(sbn);\n\tlet repairEsis = acceptedRepairEsis.get(sbn);\n\tif (!repairEsis) {\n\t\trepairEsis = new Set<number>();\n\t\tacceptedRepairEsis.set(sbn, repairEsis);\n\t}\n\trepairEsis.add(repairEsi);\n\tconst block = getOrCreateBlock(sbn);\n\tblock.ssbLength = cmd.ssbLength;\n\tblock.repairReceived++;\n\tblock.lastSymbolTs = performance.now();\n\trepairSymbols++;\n\t\t// Every authored source symbol already arrived: knowing SSB_length makes\n\t\t// the block clean. The decoder's return is the source already routed.\n\t\t// Otherwise, returned data is the recovered source block: its SSB_length\n\t\t// fixed-width authored symbols concatenated. Split it before emitting so\n\t\t// MmtpRouter gets the same one-symbol shape as a direct source packet.\n\t\tif (block.sourceReceived >= cmd.ssbLength && block.state === \"collecting\") {\n\t\t\tmarkBlockTerminal(block, \"complete\");\n\t\t\tblocksComplete++;\n\t\t\temitBlockUpdate(block);\n\t\t} else if (result && result.byteLength > 0) {\n\t\t\tconst recovered = new Uint8Array(toOwnedArrayBuffer(result));\n\t\t\tconst recoveryMs = performance.now() - block.firstSymbolTs;\n\t\t\trecoveryTimes.push(recoveryMs);\n\t\t// Cap recovery times array at 1000 entries\n\t\tif (recoveryTimes.length > 1000) recoveryTimes.shift();\n\n\t\tif (block.state === \"collecting\") {\n\t\t\tmarkBlockTerminal(block, \"recovered\");\n\t\t\tblocksRecovered++;\n\t\t\t\temitBlockUpdate(block);\n\t\t\t}\n\t\t\temitRecoveredSymbols(sbn, recovered, block);\n\t\t}\n\t\tsynchronizeDecoderHorizon();\n\t}\n\nfunction handleRelayBlockSig(\n\tcmd: Extract<FecWorkerCommand, { type: \"relayBlockSig\" }>,\n): void {\n\t// No-op AUTH stub: store sig on block, mark as not-yet-verified\n\tconst block = blocks.get(cmd.sbn);\n\tif (block) {\n\t\t// Real verification deferred to ALTA milestone\n\t\t// For now, mark as null (not verified) \u2014 will be wired when ALTA is implemented\n\t\tblock.relayBlockSigValid = null;\n\t\temitBlockUpdate(block);\n\t}\n}\n\nfunction handleCleanup(\n\tcmd: Extract<FecWorkerCommand, { type: \"cleanup\" }>,\n): void {\n\tif (\n\t\t!Array.isArray(cmd.sbns) ||\n\t\tcmd.sbns.length === 0 ||\n\t\t!cmd.sbns.every(isUint32)\n\t) {\n\t\tthrow new Error(\"[FecWorker] cleanup: invalid SBNs\");\n\t}\n\tconst sbns = [...new Set(cmd.sbns)].sort((a, b) => a - b);\n\tconst pendingSbns = sbns.filter((sbn) => sbn > retiredSbnFrontier);\n\ttry {\n\t\tif (!wasmDecoder) {\n\t\t\tthrow new Error(\"[FecWorker] cleanup: not configured\");\n\t\t}\n\n\t\t// Call WASM cleanup only after all earlier symbol commands have drained,\n\t\t// and only for the exact SBNs the manager's deadlines named; command\n\t\t// coalescing must not infer ownership from the largest timed-out SBN.\n\t\tif (pendingSbns.length > 0) {\n\t\t\twasmDecoder.cleanup_blocks_detailed(Uint32Array.from(pendingSbns));\n\t\t}\n\t\t// Manager deadlines are block-scoped and may expire a newer SBN while an\n\t\t// older interleaved block is still live. Only accepted media progression\n\t\t// advances the shared JS/Rust horizon; cleanup creates exact tombstones.\n\n\t\t// Rust already finalized every SBN behind the media frontier. For each\n\t\t// remaining exact request, mirror the block-scoped WASM boundary immediately\n\t\t// so JS never retains state for a decoder block that is gone.\n\t\tfor (const sbn of pendingSbns) {\n\t\t\tlet currentBlock = blocks.get(sbn);\n\t\t\tif (currentBlock && isTerminalBlock(currentBlock)) {\n\t\t\t\tretireAcceptedIds(sbn);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcurrentBlock ??= getOrCreateBlock(sbn);\n\t\t\tif (currentBlock.sourceReceived >= currentBlock.k) {\n\t\t\t\tmarkBlockTerminal(currentBlock, \"complete\");\n\t\t\t\tblocksComplete++;\n\t\t\t} else {\n\t\t\t\tmarkBlockTerminal(currentBlock, \"failed\");\n\t\t\t\tblocksFailed++;\n\t\t\t}\n\t\t\temitBlockUpdate(currentBlock);\n\t\t}\n\t} finally {\n\t\t// Message ordering makes this an acknowledgement that every source/repair\n\t\t// command posted before cleanup has completed. The main thread must not\n\t\t// commit failure accounting until it observes this event.\n\t\temit({ type: \"cleanupComplete\", sbns });\n\t}\n}\n\nfunction handleDispose(): void {\n\t// Clear snapshot timer (MUST clear to prevent leaks)\n\tif (snapshotTimer !== null) {\n\t\tclearInterval(snapshotTimer);\n\t\tsnapshotTimer = null;\n\t}\n\n\t// Free ALTA verifier WASM object (MUST call to release WASM memory \u2014\n\t// Worker.terminate() fires immediately after dispose, so the FinalizationRegistry\n\t// callback has no chance to run and this memory would leak without explicit free)\n\tif (altaVerifier) {\n\t\ttry {\n\t\t\taltaVerifier.free();\n\t\t} catch (_e) {\n\t\t\t// Best-effort \u2014 verifier may already be freed\n\t\t}\n\t\taltaVerifier = null;\n\t}\n\n\t// Free WASM decoder (MUST call to release WASM memory)\n\tif (wasmDecoder) {\n\t\twasmDecoder.free();\n\t\twasmDecoder = null;\n\t}\n\n\t// Clear deferred-verify queue so pinned ArrayBuffers drop.\n\tpendingVerify.clear();\n\tpendingVerifyCap = 0;\n\tpendingVerifyTtlMs = 0;\n\n\tconfig = null;\n\tclearBlockTracking();\n}\n\nfunction handleReset(): void {\n\ttry {\n\t\twasmDecoder?.flush();\n\t\twasmDecoder?.reset_stats();\n\t} catch (err) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"reset-failed\",\n\t\t\tmessage:\n\t\t\t\terr instanceof Error\n\t\t\t\t\t? `[FecWorker] reset failed: ${err.message}`\n\t\t\t\t\t: `[FecWorker] reset failed: ${String(err)}`,\n\t\t});\n\t\treturn;\n\t}\n\n\tclearBlockTracking();\n\tsourceSymbols = 0;\n\trepairSymbols = 0;\n\tblocksComplete = 0;\n\tblocksRecovered = 0;\n\tblocksFailed = 0;\n\taltaVerified = 0;\n\taltaFailed = 0;\n\taltaFailedDetails.length = 0;\n\taltaPending = 0;\n\taltaPendingEvictedCap = 0;\n\taltaPendingEvictedTtl = 0;\n\taltaError = 0;\n\trelayBlocksVerified = 0;\n\trelayBlocksFailed = 0;\n\trepairSymbolSizeMismatch = 0;\n\temit({ type: \"resetComplete\" });\n\tif (config) emitSnapshot();\n}\n\n/**\n * A batch of symbols fed in one page task (BLO-36599). Each record is\n * handled exactly as its single-symbol command would be, in order, and a\n * failing record reports the same `command-failed` error that command would\n * without stopping the records after it.\n */\nfunction handleFeedSymbols(\n\tcmd: Extract<FecWorkerCommand, { type: \"feedSymbols\" }>,\n): void {\n\tif (!Array.isArray(cmd.symbols)) {\n\t\tthrow new Error(\"[FecWorker] feedSymbols: symbols must be an array\");\n\t}\n\tfor (const symbol of cmd.symbols) {\n\t\ttry {\n\t\t\tif (symbol?.type === \"feedSource\") handleFeedSource(symbol);\n\t\t\telse if (symbol?.type === \"feedRepair\") handleFeedRepair(symbol);\n\t\t\telse {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[FecWorker] feedSymbols: unknown symbol type ${String((symbol as { type?: unknown } | null)?.type)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\temit({\n\t\t\t\ttype: \"error\",\n\t\t\t\tcode: \"command-failed\",\n\t\t\t\tmessage:\n\t\t\t\t\terr instanceof Error ? err.message : `[FecWorker] ${String(err)}`,\n\t\t\t});\n\t\t}\n\t}\n}\n\n// --- Main message handler ---\n\nasync function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {\n\ttry {\n\t\tswitch (cmd.type) {\n\t\t\tcase \"configure\":\n\t\t\t\t// handleConfigure is async (ALTA WASM init uses dynamic import).\n\t\t\t\t// Await it so feed/cleanup messages cannot run against a partially\n\t\t\t\t// configured decoder.\n\t\t\t\tawait handleConfigure(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"feedSource\":\n\t\t\t\thandleFeedSource(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"feedRepair\":\n\t\t\t\thandleFeedRepair(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"feedSymbols\":\n\t\t\t\thandleFeedSymbols(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"relayBlockSig\":\n\t\t\t\thandleRelayBlockSig(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"cleanup\":\n\t\t\t\thandleCleanup(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"reset\":\n\t\t\t\thandleReset();\n\t\t\t\tbreak;\n\t\t\tcase \"subscribeBlockUpdates\":\n\t\t\t\tblockUpdatesSubscribed = true;\n\t\t\t\tbreak;\n\t\t\tcase \"dispose\":\n\t\t\t\thandleDispose();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\temit({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tcode: \"unknown-command\",\n\t\t\t\t\tmessage: `Unknown command type: ${(cmd as Record<string, unknown>).type}`,\n\t\t\t\t});\n\t\t}\n\t} catch (err) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"command-failed\",\n\t\t\tmessage:\n\t\t\t\terr instanceof Error ? err.message : `[FecWorker] ${String(err)}`,\n\t\t});\n\t}\n}\n\nlet commandQueue: Promise<void> = Promise.resolve();\n\nself.onmessage = (e: MessageEvent<FecWorkerCommand>) => {\n\tconst cmd = e.data;\n\tcommandQueue = commandQueue.then(() => dispatchCommand(cmd));\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Return a fresh ArrayBuffer containing exactly the bytes in `view`.\n *\n * Worker outputs must not transfer buffers that might be owned by a WASM\n * binding or by a larger shared packet. A fresh JS-owned buffer gives\n * postMessage() exclusive ownership without detaching unrelated memory.\n */\nexport function toOwnedArrayBuffer(view: Uint8Array): ArrayBuffer {\n\tconst copy = new Uint8Array(view.byteLength);\n\tcopy.set(view);\n\treturn copy.buffer;\n}\n", "/**\n * @blockcast/fec-worker \u2014 Worker Entry Point\n *\n * Runs inside DedicatedWorkerGlobalScope. Receives FecWorkerCommand messages,\n * drives the MmtFecDecoder WASM module, emits FecWorkerEvent messages back.\n *\n * WASM bindings are resolved via:\n * 1. (self as any).__MMT_WASM_BINDINGS__ \u2014 set by consumer's loader/importmap\n * 2. Falls back to error if not available (no defaults)\n *\n * This file must NOT import DOM globals incompatible with Worker scope.\n */\n\nimport type {\n\tFecWorkerCommand,\n\tFecWorkerEvent,\n\tFecTrackConfig,\n\tFecTrackStats,\n\tFecBlockSnapshot,\n\tFrameMeta,\n} from \"./fec-worker-types.js\";\nimport { toOwnedArrayBuffer } from \"./transfer.js\";\n\n// --- WASM binding types (from mmt-wasm) ---\n// Signatures MUST match a freshly generated external mmt_wasm.d.ts exactly.\n// The decoder derives SBN/ESI internally \u2014 do not pre-compute.\ninterface WasmFecDecoder {\n\t/** ISO 23008-1 \u00A7C.5.2: ss_id is the flat 32-bit Source Symbol ID. */\n\tadd_source(\n\t\tss_id: number,\n\t\tdata: Uint8Array,\n\t\ttimestamp: number,\n\t): Uint8Array | undefined;\n\t/** ISO 23008-1 \u00A7C.5.3: repair ESI = configured K + rs_id, SBN = ss_start / configured K. */\n\tadd_repair(\n\t\tss_start: number,\n\t\tssb_length: number,\n\t\trs_id: number,\n\t\tdata: Uint8Array,\n\t\ttimestamp: number,\n\t): Uint8Array | undefined;\n\tconfigure_params(k: number, symbolSize: number): void;\n\t/**\n\t * Retire the named blocks (last-chance decode + stats), driven by the\n\t * manager's exact deadlines. Nothing is synthesised for a lost block:\n\t * concealment is the sink's hold-last-good-frame.\n\t */\n\tcleanup_blocks_detailed(sbns: Uint32Array): void;\n\t/** Remove decoder blocks below its configured live-SBN interleave horizon. */\n\tcleanup_by_sbn(current_sbn: number): Uint32Array;\n\tflush(): void;\n\tpending_blocks(): number;\n\treset_stats(): void;\n\tget_stats(): {\n\t\tsource_packets: bigint;\n\t\trepair_packets: bigint;\n\t\tblocks_complete: bigint;\n\t\tblocks_recovered: bigint;\n\t\tblocks_failed: bigint;\n\t\tbytes_recovered: bigint;\n\t\trecovery_rate(): number;\n\t};\n\tfree(): void;\n}\n\ninterface WasmBindings {\n\tinitSync(module: { module: WebAssembly.Module }): unknown;\n\tMmtFecDecoder: new (interleaveDepth: number) => WasmFecDecoder;\n}\n\n// --- Worker state (local to this Worker instance) ---\n\nlet wasmDecoder: WasmFecDecoder | null = null;\nlet config: FecTrackConfig | null = null;\nlet altaEnabled = false;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet altaVerifier: any = null; // JsAltaVerifier instance (loaded from ALTA WASM)\nlet snapshotTimer: ReturnType<typeof setInterval> | null = null;\n// blockUpdate events are posted only after a client subscribes (BLO-36599).\nlet blockUpdatesSubscribed = false;\n\n// Block tracking\nconst blocks = new Map<number, FecBlockSnapshot>();\nconst acceptedSourceEsis = new Map<number, Set<number>>();\nconst acceptedRepairEsis = new Map<number, Set<number>>();\nconst UINT32_MAX = 0xffff_ffff;\nconst UINT24_MAX = 0xff_ffff;\n// ISO/IEC 23008-1 AL-FEC signaling carries interleave_depth in one byte.\nconst MAX_INTERLEAVE_DEPTH = 0xff;\n// Rust stores T as u16 and this decoder profile fixes RFC 6330 Al=8.\nconst MAX_SYMBOL_SIZE = 0xfff8;\n// RFC 6330 systematic table limit, enforced by the vendored raptorq decoder.\nconst MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK = 56_403;\n\n// Aggregate counters\nlet sourceSymbols = 0;\nlet repairSymbols = 0;\nlet blocksComplete = 0;\nlet blocksRecovered = 0;\nlet blocksFailed = 0;\nlet altaVerified = 0;\nlet altaFailed = 0; // Crypto-confirmed tamper (MAC / sig / hash chain mismatch)\nconst altaFailedDetails: Array<{\n\tseq: number;\n\terror: string;\n\tauthLen: number;\n\tpayloadLen: number;\n\tpath: string; // \"direct\" | \"recovered\" | \"pending-retry\"\n}> = [];\nlet altaPending = 0; // Unverifiable but not tamper (no trailer, verifier not ready, anchor missing)\nlet altaPendingEvictedCap = 0; // Incoming \"pending\" packets rejected because the queue was at cap after stale-sweep (flood indicator)\nlet altaPendingEvictedTtl = 0; // Deferred-queue entries dropped by TTL (retry window expired)\nlet altaError = 0; // verifyDetached threw \u2014 infrastructure flake, not tamper\nlet altaInitFailed = false; // ALTA configured but WASM init failed; altaEnabled was forced off\nlet relayBlocksVerified = 0;\nlet relayBlocksFailed = 0;\nlet liveSbn = 0;\n// Highest SBN that has fallen strictly below the decoder's retained\n// interleave window. Numeric ordering is per decoder epoch; reset/configure\n// starts a new epoch before the 32-bit source-symbol counter can wrap.\nlet retiredSbnFrontier = -1;\nlet repairSymbolSizeMismatch = 0;\n// Source symbols dropped before WASM, by cause. All three sites below were\n// silent, which left the \"block settles at exactly K-1 source symbols\" shape\n// (BLO-35597 defect 2) with no evidence to attribute it to. A retired drop\n// means a late symbol past retention; a terminal drop means the block had\n// already settled \u2014 `state !== \"collecting\"`, so complete OR recovered OR\n// FAILED, and the failed arm is the one this issue is chasing (a block aged out\n// by synchronizeDecoderHorizon stays in `blocks` until its SBN retires, so a\n// late symbol for a short block lands here); a duplicate drop means a repeated\n// (SBN, ESI), which on this wire is usually a plain duplicate packet \u2014 the\n// router's ledger has deliberate gaps \u2014 and only sometimes an SBN/ESI\n// derivation collision.\n//\n// These narrow the cause; they do not uniquely attribute it. A symbol LOST IN\n// TRANSIT never reaches handleFeedSource and increments nothing, so all three\n// reading zero on a short block is NOT evidence of a publisher defect \u2014 wire\n// loss is the null hypothesis here, and it produces an identical shape. The\n// discriminator for that residual arm is `block.ssbLength` (set at the repair\n// symbol's declared SSB_length, conflict-checked on arrival):\n// ssbLength < k \u2192 genuinely short authored block\n// ssbLength === k, sourceReceived < k \u2192 loss or a still-late symbol\n// With no repair symbol yet seen, ssbLength is undefined and both stay open.\nlet sourceDroppedRetired = 0;\nlet sourceDroppedTerminal = 0;\nlet sourceDroppedDuplicate = 0;\n\n// Recovery timing\nconst recoveryTimes: number[] = [];\n\n// \u2500\u2500 Deferred ALTA verification queue (08-07 Path 3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// When an ALTA packet arrives whose MAC references point at sequences not yet\n// in the verifier's ring buffer, we can't anchor trust \u2014 verify returns \"No\n// trust anchor\". Typical cause: a predecessor packet was lost and hasn't been\n// FEC-recovered yet. Rather than drop these, hold them in a bounded FIFO and\n// retry verification after every successful verify (direct or FEC-recovered)\n// that seeds the ring buffer with new raw bytes.\n//\n// Zero-copy (1B-B): the queue entry stores a Uint8Array view into the\n// original `data` ArrayBuffer. JS GC keeps the buffer alive via the view\n// reference; no bytes are copied on queue.\n//\n// Timeout entries (TTL exceeded) and cap-rejections count as `altaPending`\n// (2C-B): they're not tamper indicators, they're just packets that couldn't\n// be authenticated within the receive window \u2014 either loss or, in the cap\n// case, a flood of unanchorable arrivals (real or attacker-driven) that\n// exceeded the queue budget. See enqueuePendingVerify for the rate-limit\n// protocol.\ninterface PendingVerify {\n\tseq: number;\n\tauth: Uint8Array;\n\tpayload: Uint8Array;\n\tarrivalTs: number;\n}\nconst pendingVerify = new Map<number, PendingVerify>();\nlet pendingVerifyCap = 0;\nlet pendingVerifyTtlMs = 0;\n\n// --- Helpers ---\n\nfunction emit(event: FecWorkerEvent, transfer?: Transferable[]): void {\n\tself.postMessage(event, transfer ?? []);\n}\n\nfunction isUint32(value: number): boolean {\n\treturn Number.isInteger(value) && value >= 0 && value <= UINT32_MAX;\n}\n\nfunction isTerminalBlock(block: FecBlockSnapshot): boolean {\n\treturn block.state !== \"collecting\";\n}\n\nfunction retireAcceptedIds(sbn: number): void {\n\tacceptedSourceEsis.delete(sbn);\n\tacceptedRepairEsis.delete(sbn);\n}\n\nfunction clearBlockTracking(): void {\n\tblocks.clear();\n\tacceptedSourceEsis.clear();\n\tacceptedRepairEsis.clear();\n\tpendingVerify.clear();\n\trecoveryTimes.length = 0;\n\tliveSbn = 0;\n\tretiredSbnFrontier = -1;\n}\n\n/**\n * Retire terminal diagnostics only after the decoder's monotonic interleave\n * horizon proves that the SBN can no longer be valid out-of-order media.\n * Admission keeps the compact frontier as the durable tombstone, so deletion\n * never reopens a finalized block.\n */\nfunction trimBlockRetention(): void {\n\tfor (const [sbn, block] of blocks) {\n\t\tif (!isTerminalBlock(block) || sbn > retiredSbnFrontier) continue;\n\t\tblocks.delete(sbn);\n\t\tretireAcceptedIds(sbn);\n\t}\n}\n\nfunction advanceMediaHorizon(sbn: number): void {\n\tif (!config || sbn <= liveSbn) return;\n\tliveSbn = sbn;\n\tconst firstRetainedSbn = Math.max(0, liveSbn - config.interleaveDepth);\n\tretiredSbnFrontier = Math.max(retiredSbnFrontier, firstRetainedSbn - 1);\n\ttrimBlockRetention();\n}\n\nfunction markBlockTerminal(\n\tblock: FecBlockSnapshot,\n\tstate: \"complete\" | \"recovered\" | \"failed\",\n): void {\n\tblock.state = state;\n\tblock.completedTs = performance.now();\n\tretireAcceptedIds(block.sbn);\n\ttrimBlockRetention();\n}\n\nfunction synchronizeDecoderHorizon(): void {\n\tif (!wasmDecoder) return;\n\tconst expired = wasmDecoder.cleanup_by_sbn(liveSbn);\n\tfor (const sbn of expired) {\n\t\tconst block = blocks.get(sbn);\n\t\tif (!block || isTerminalBlock(block)) {\n\t\t\tretireAcceptedIds(sbn);\n\t\t\tcontinue;\n\t\t}\n\t\tmarkBlockTerminal(block, \"failed\");\n\t\tblocksFailed++;\n\t\temitBlockUpdate(block);\n\t}\n\ttrimBlockRetention();\n}\n\n/**\n * Run detached ALTA verify against the current verifier. Four-state result:\n * - \"valid\": crypto passed, packet authentic. Caller increments altaVerified.\n * - \"failed\": crypto confirmed tamper (MAC/sig mismatch, hash chain mismatch).\n * Caller increments altaFailed. Not retryable.\n * - \"pending\": anchor missing (MAC refs not in buffer, no signature, not\n * early-warmup). Caller may enqueue for retry after the next\n * successful verify seeds the ring buffer.\n * - \"error\": verifier threw \u2014 WASM trap, unmarshal panic, OOM. Caller\n * increments altaError (infrastructure flake, not tamper).\n * The distinction matters for ops alerting: a spike in\n * altaError indicates a runtime problem, not an attack.\n */\nlet _lastVerifyError = \"\";\nfunction runDetachedVerify(\n\tauth: Uint8Array,\n\tpayload: Uint8Array,\n): \"valid\" | \"failed\" | \"pending\" | \"error\" {\n\tif (!altaVerifier) return \"pending\";\n\ttry {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst r: any = altaVerifier.verifyDetached(auth, payload);\n\t\tif (r.valid) {\n\t\t\t_lastVerifyError = \"\";\n\t\t\treturn \"valid\";\n\t\t}\n\t\tconst err = (r.error as string | undefined) ?? \"\";\n\t\t_lastVerifyError = err;\n\t\t// The bootstrap-friendly verifier returns this specific error string when\n\t\t// the only obstacle to verification is a missing anchor (see alta-rs\n\t\t// verifier.rs verify()). Distinguishing this from tamper cases keeps us\n\t\t// from queuing packets that are definitively invalid.\n\t\tif (err.startsWith(\"No trust anchor\")) return \"pending\";\n\t\treturn \"failed\";\n\t} catch (e) {\n\t\t// Ratelimit: first ~N occurrences get logged. A sustained exception\n\t\t// stream indicates a real problem \u2014 the altaError counter is the\n\t\t// durable signal.\n\t\tif (altaError < 5) {\n\t\t\tconsole.warn(\"[FecWorker] verifyDetached threw:\", e);\n\t\t}\n\t\treturn \"error\";\n\t}\n}\n\n/** Extract u32 BE sequence number from the ALTA authenticator header (offset 0..3). */\nfunction seqFromAuth(auth: Uint8Array): number {\n\treturn (\n\t\t((auth[0]! << 24) | (auth[1]! << 16) | (auth[2]! << 8) | auth[3]!) >>> 0\n\t);\n}\n\n/**\n * Enqueue a packet whose ALTA verification returned \"pending\" (anchor\n * missing). Dedupes by sequence number.\n *\n * Flood resistance (08-08 review): when the queue is at cap we do NOT\n * evict the oldest entry. The oldest entry is typically the most\n * load-bearing one \u2014 it's the packet whose anchor everything else in the\n * queue is waiting on. Evict-oldest-on-insert let an attacker emitting a\n * stream of \"No trust anchor\" packets flood out legitimate entries and\n * prevent them from ever anchoring. Instead:\n * 1. First try to free stale TTL'd entries (they'd be evicted on next\n * drain anyway \u2014 pulling that forward is free).\n * 2. If still full, reject the NEW entry. altaPendingEvictedCap counts\n * these rejections (rate-limit trigger).\n *\n * Stores Uint8Array views into the original data + auth buffers \u2014 no copy.\n * The views keep their underlying ArrayBuffers alive via GC references.\n */\nfunction enqueuePendingVerify(\n\tauth: Uint8Array,\n\tpayload: Uint8Array,\n): void {\n\tconst seq = seqFromAuth(auth);\n\tif (pendingVerify.has(seq)) return;\n\n\tif (pendingVerify.size >= pendingVerifyCap) {\n\t\t// Step 1: sweep stale entries so legitimate traffic isn't penalized\n\t\t// by TTL'd junk that happens to still occupy slots.\n\t\tconst now = performance.now();\n\t\tfor (const [staleSeq, entry] of pendingVerify) {\n\t\t\tif (now - entry.arrivalTs > pendingVerifyTtlMs) {\n\t\t\t\tpendingVerify.delete(staleSeq);\n\t\t\t\taltaPendingEvictedTtl++;\n\t\t\t}\n\t\t}\n\t\t// Step 2: if the queue is still at cap it's a genuine flood \u2014\n\t\t// reject the new entry rather than evict an existing one. The\n\t\t// rejected packet counts as altaPendingEvictedCap (which is\n\t\t// surfaced up through stats for rate-limit alerting).\n\t\tif (pendingVerify.size >= pendingVerifyCap) {\n\t\t\taltaPendingEvictedCap++;\n\t\t\treturn;\n\t\t}\n\t}\n\n\tpendingVerify.set(seq, {\n\t\tseq,\n\t\tauth,\n\t\tpayload,\n\t\tarrivalTs: performance.now(),\n\t});\n}\n\n/**\n * Retry every queued entry. Called after every successful verify (direct\n * source or FEC-recovered) \u2014 new raw bytes in the verifier's ring buffer may\n * unblock previously-pending MAC references.\n *\n * Three outcomes per entry:\n * - \"valid\" \u2192 remove from queue, altaVerified++\n * - \"failed\" \u2192 remove from queue, altaFailed++ (tamper on retry)\n * - \"pending\" \u2192 keep in queue, unless TTL expired (then evict as altaPending)\n *\n * Iteration order: Map preserves insertion order, so we drain oldest-first.\n * No-op when the queue is empty or the verifier isn't ready.\n */\nfunction drainPendingVerify(): void {\n\tif (pendingVerify.size === 0 || !altaVerifier) return;\n\tconst now = performance.now();\n\t// Collect keys to avoid mutating Map during iteration\n\tconst keys = Array.from(pendingVerify.keys());\n\tfor (const seq of keys) {\n\t\tconst entry = pendingVerify.get(seq);\n\t\tif (!entry) continue;\n\t\tif (now - entry.arrivalTs > pendingVerifyTtlMs) {\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaPendingEvictedTtl++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst result = runDetachedVerify(entry.auth, entry.payload);\n\t\tif (result === \"valid\") {\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaVerified++;\n\t\t} else if (result === \"failed\") {\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaFailed++;\n\t\t\tif (altaFailedDetails.length < 16) {\n\t\t\t\taltaFailedDetails.push({\n\t\t\t\t\tseq: entry.seq,\n\t\t\t\t\terror: _lastVerifyError,\n\t\t\t\t\tauthLen: entry.auth.length,\n\t\t\t\t\tpayloadLen: entry.payload.length,\n\t\t\t\t\tpath: \"pending-retry\",\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (result === \"error\") {\n\t\t\t// Infrastructure failure \u2014 remove from queue, count as altaError.\n\t\t\t// Keeping it around would just re-throw on every subsequent drain.\n\t\t\tpendingVerify.delete(seq);\n\t\t\taltaError++;\n\t\t}\n\t\t// \"pending\" \u2192 leave in queue for next drain\n\t}\n}\n\nfunction getOrCreateBlock(sbn: number): FecBlockSnapshot {\n\tlet block = blocks.get(sbn);\n\tif (!block) {\n\t\tif (!config) throw new Error(\"[FecWorker] not configured\");\n\t\tconst now = performance.now();\n\t\tblock = {\n\t\t\tsbn,\n\t\t\tstate: \"collecting\",\n\t\t\tsourceReceived: 0,\n\t\t\trepairReceived: 0,\n\t\t\tk: config.k,\n\t\t\trepairCount: config.repairCount,\n\t\t\tfirstSymbolTs: now,\n\t\t\tlastSymbolTs: now,\n\t\t\tdeadlineTs: now + config.deliveryWindowMs,\n\t\t\taltaVerified: 0,\n\t\t\trelayBlockSigValid: null,\n\t\t};\n\t\tblocks.set(sbn, block);\n\t\ttrimBlockRetention();\n\t}\n\treturn block;\n}\n\nfunction buildStatsSnapshot(): FecTrackStats {\n\tif (!config) throw new Error(\"[FecWorker] not configured\");\n\tconst decoderStats = wasmDecoder?.get_stats();\n\tconst decoder = decoderStats\n\t\t? {\n\t\t\tsourcePackets: Number(decoderStats.source_packets),\n\t\t\trepairPackets: Number(decoderStats.repair_packets),\n\t\t\tblocksComplete: Number(decoderStats.blocks_complete),\n\t\t\tblocksRecovered: Number(decoderStats.blocks_recovered),\n\t\t\tblocksFailed: Number(decoderStats.blocks_failed),\n\t\t\tbytesRecovered: Number(decoderStats.bytes_recovered),\n\t\t\tpendingBlocks: wasmDecoder?.pending_blocks() ?? 0,\n\t\t}\n\t\t: undefined;\n\n\tconst avgRecoveryMs =\n\t\trecoveryTimes.length > 0\n\t\t\t? recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length\n\t\t\t: 0;\n\tconst maxRecoveryMs =\n\t\trecoveryTimes.length > 0 ? Math.max(...recoveryTimes) : 0;\n\n\t// Cap block array to interleave window\n\tconst maxBlocks = Math.max(config.interleaveDepth * 3, 24);\n\tconst blockArray = Array.from(blocks.values())\n\t\t.sort((a, b) => b.sbn - a.sbn)\n\t\t.slice(0, maxBlocks);\n\n\treturn {\n\t\tconfig,\n\t\tsourceSymbols,\n\t\trepairSymbols,\n\t\tblocksComplete,\n\t\tblocksRecovered,\n\t\tblocksFailed,\n\t\trecoveryRate:\n\t\t\tblocksComplete + blocksRecovered > 0\n\t\t\t\t? Math.round(\n\t\t\t\t\t\t(blocksRecovered / (blocksRecovered + blocksFailed)) * 100,\n\t\t\t\t\t) || 0\n\t\t\t\t: 0,\n\t\taltaVerified,\n\t\taltaFailed,\n\t\taltaPending,\n\t\taltaPendingEvictedCap,\n\t\taltaPendingEvictedTtl,\n\t\taltaError,\n\t\taltaInitFailed,\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\taltaFailedDetails: altaFailedDetails.slice() as any,\n\t\trelayBlocksVerified,\n\t\trelayBlocksFailed,\n\t\tliveSbn,\n\t\tblocks: blockArray,\n\t\tinterleaveWindowBlocks: config.interleaveDepth,\n\t\tavgRecoveryMs: Math.round(avgRecoveryMs * 100) / 100,\n\t\tmaxRecoveryMs: Math.round(maxRecoveryMs * 100) / 100,\n\t\trepairSymbolSizeMismatch,\n\t\tsourceDroppedRetired,\n\t\tsourceDroppedTerminal,\n\t\tsourceDroppedDuplicate,\n\t\tdecoder,\n\t};\n}\n\nfunction emitSnapshot(): void {\n\tif (!config) return;\n\temit({ type: \"snapshot\", stats: buildStatsSnapshot() });\n}\n\nfunction emitBlockUpdate(block: FecBlockSnapshot): void {\n\tif (!blockUpdatesSubscribed) return;\n\temit({ type: \"blockUpdate\", block: { ...block } });\n}\n\nfunction emitRecoveredSymbols(\n\tsbn: number,\n\trecoveredBlock: Uint8Array,\n\tblock: FecBlockSnapshot,\n): void {\n\tif (!config) return;\n\tconst symbolSize = config.symbolSize;\n\tif (symbolSize <= 0) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"recovered-symbol-size-unconfigured\",\n\t\t\tmessage: \"[FecWorker] recovered block has no configured symbolSize\",\n\t\t});\n\t\treturn;\n\t}\n\t// The decoder returns only the block's authored source symbols\n\t// (SSB_length x T); the encoder's zero padding is not media.\n\tconst ssbLength = block.ssbLength ?? config.k;\n\tif (recoveredBlock.byteLength !== symbolSize * ssbLength) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"recovered-block-too-short\",\n\t\t\tmessage: `[FecWorker] recovered block has the wrong length: got ${recoveredBlock.byteLength}B, expected ${symbolSize * ssbLength}B (SSB_length=${ssbLength})`,\n\t\t});\n\t\treturn;\n\t}\n\n\tfor (let i = 0; i < ssbLength; i++) {\n\t\tconst symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);\n\n\t\t// Source authentication now runs once at router admission in\n\t\t// @blockcast/mmt-manifest-verify. The worker only records that this\n\t\t// legacy ALTA-enabled symbol bypassed hot-path verification.\n\t\tconst recoveredAltaVerified: boolean | null = null;\n\t\tif (altaEnabled) altaPending++;\n\n\t\tconst buf = toOwnedArrayBuffer(symbol);\n\t\tconst meta: FrameMeta = {\n\t\t\taltaVerified: recoveredAltaVerified,\n\t\t\trecovered: true,\n\t\t\tsbn,\n\t\t\tesi: i,\n\t\t\tssbLength,\n\t\t};\n\t\temit({ type: \"frame\", data: buf, meta }, [buf]);\n\t}\n}\n\nfunction startSnapshotTimer(): void {\n\tif (snapshotTimer !== null) clearInterval(snapshotTimer);\n\tsnapshotTimer = setInterval(() => emitSnapshot(), 200);\n}\n\nfunction resolveWasmBindings(): WasmBindings {\n\tconst bindings = (self as unknown as Record<string, unknown>)\n\t\t.__MMT_WASM_BINDINGS__ as WasmBindings | undefined;\n\tif (!bindings) {\n\t\tthrow new Error(\n\t\t\t\"[FecWorker] WASM bindings not available. Set self.__MMT_WASM_BINDINGS__ = { initSync, MmtFecDecoder } before configure.\",\n\t\t);\n\t}\n\treturn bindings;\n}\n\n// --- Command handlers ---\n\nasync function handleConfigure(\n\tcmd: Extract<FecWorkerCommand, { type: \"configure\" }>,\n): Promise<void> {\n\tif (!cmd.wasmModule) {\n\t\tthrow new Error(\"[FecWorker] configure: wasmModule required\");\n\t}\n\tif (!cmd.config) {\n\t\tthrow new Error(\"[FecWorker] configure: config required\");\n\t}\n\tif (\n\t\t!Number.isFinite(cmd.config.interleaveDepth) ||\n\t\t!Number.isInteger(cmd.config.interleaveDepth) ||\n\t\tcmd.config.interleaveDepth <= 0 ||\n\t\tcmd.config.interleaveDepth > MAX_INTERLEAVE_DEPTH\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: interleaveDepth must be an integer in 1..${MAX_INTERLEAVE_DEPTH} (got ${cmd.config.interleaveDepth})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isFinite(cmd.config.interleaveMs) ||\n\t\tcmd.config.interleaveMs <= 0\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: interleaveMs must be finite and > 0 (got ${cmd.config.interleaveMs})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isFinite(cmd.config.deliveryWindowMs) ||\n\t\tcmd.config.deliveryWindowMs <= 0\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: deliveryWindowMs must be > 0 (got ${cmd.config.deliveryWindowMs})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isInteger(cmd.config.k) ||\n\t\tcmd.config.k <= 0 ||\n\t\tcmd.config.k > MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: k must be an integer in 1..${MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK} (got ${cmd.config.k})`,\n\t\t);\n\t}\n\tif (\n\t\t!Number.isInteger(cmd.config.symbolSize) ||\n\t\tcmd.config.symbolSize <= 0 ||\n\t\tcmd.config.symbolSize > MAX_SYMBOL_SIZE ||\n\t\tcmd.config.symbolSize % 8 !== 0\n\t) {\n\t\tthrow new Error(\n\t\t\t`[FecWorker] configure: symbolSize must be an 8-byte-aligned integer in 8..${MAX_SYMBOL_SIZE} (got ${cmd.config.symbolSize})`,\n\t\t);\n\t}\n\n\tconst wasm = resolveWasmBindings();\n\n\t// Initialize WASM from pre-compiled Module (no fetch, no re-compile)\n\twasm.initSync({ module: cmd.wasmModule });\n\n\t// A configure command starts a new decoder epoch. No terminal tombstone,\n\t// accepted-symbol ID, or active block from the previous decoder may affect it.\n\twasmDecoder?.free();\n\twasmDecoder = null;\n\tclearBlockTracking();\n\n\t// Decoder policy and geometry are explicit at the epoch boundary. The WASM\n\t// module has no guessed K/T/interleave slow path.\n\twasmDecoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth);\n\tconfig = cmd.config;\n\taltaEnabled = !!cmd.altaPublicKey;\n\n\t// Size the deferred-verify queue strictly from catalog-derived FEC\n\t// parameters. CLAUDE.md bans Math.max(x, floor) magic \u2014 if catalog\n\t// values are zero/missing, throw at init rather than silently applying\n\t// a hardcoded floor. Consumers of an undersized queue cap would see\n\t// altaPendingEvictedCap spike with no actionable signal.\n\t// Cap: 1.5\u00D7 interleave depth \u2014 wide enough to hold one full FEC recovery\n\t// window plus jitter, narrow enough to bound memory.\n\tpendingVerifyCap = Math.ceil(config.interleaveDepth * 1.5);\n\t// TTL: 1.5\u00D7 block duration (K symbols at one per interleave tick) \u2014\n\t// after this many ms, any unresolved entry has missed its FEC window.\n\tpendingVerifyTtlMs = Math.ceil(config.interleaveMs * config.k * 1.5);\n\n\t// Initialize ALTA WASM verifier if public key and WASM module provided (D-05, D-09)\n\taltaVerifier = null;\n\taltaInitFailed = false;\n\tif (cmd.altaPublicKey && cmd.altaWasmModule) {\n\t\ttry {\n\t\t\t// Load ALTA JS glue (wasm-bindgen output from wasm-pack build).\n\t\t\t// Mirrors the RaptorQ WASM pattern: initSync({ module }) then construct.\n\t\t\t// Dynamic import path matches the IWA static asset layout.\n\t\t\t// Cast via string to prevent TypeScript from attempting static resolution\n\t\t\t// of a runtime-only URL (same pattern as fec-worker-entry.js loading).\n\t\t\tconst altaGlueUrl = '/wasm/alta/alta_rs.js' as string;\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\t\tconst altaGlue: any = await import(/* webpackIgnore: true */ altaGlueUrl);\n\t\t\taltaGlue.initSync({ module: cmd.altaWasmModule });\n\t\t\tconst keyBytes = new Uint8Array(cmd.altaPublicKey);\n\t\t\taltaVerifier = new altaGlue.JsAltaVerifier(keyBytes);\n\t\t} catch (e) {\n\t\t\t// T-08-15: WASM init failure must NOT silently look like \"not-ready-yet\".\n\t\t\t// Force altaEnabled = false so handleFeedSource takes the disabled\n\t\t\t// branch instead of continuously bumping altaPending under the\n\t\t\t// misleading \"verifier not yet ready\" comment. altaInitFailed\n\t\t\t// flag in the snapshot tells consumers this is an error state, not\n\t\t\t// an opt-out.\n\t\t\tconsole.warn('[FecWorker] ALTA WASM init failed, verification disabled:', e);\n\t\t\taltaVerifier = null;\n\t\t\taltaEnabled = false;\n\t\t\taltaInitFailed = true;\n\t\t}\n\t}\n\n\twasmDecoder.configure_params(config.k, config.symbolSize);\n\n\t// Tell the client which optional commands this Worker accepts, so it can\n\t// batch symbols without risking a Worker that predates the batch command.\n\temit({ type: \"capabilities\", commands: [\"feedSymbols\", \"subscribeBlockUpdates\"] });\n\n\t// Start periodic snapshot emission (200ms floor)\n\tstartSnapshotTimer();\n\n\t// Emit initial snapshot\n\temitSnapshot();\n}\n\nfunction handleFeedSource(\n\tcmd: Extract<FecWorkerCommand, { type: \"feedSource\" }>,\n): void {\n\tif (!wasmDecoder) {\n\t\tthrow new Error(\"[FecWorker] feedSource: not configured\");\n\t}\n\tif (!config) {\n\t\tthrow new Error(\"[FecWorker] feedSource: config missing\");\n\t}\n\tif (\n\t\t!isUint32(cmd.ssId) ||\n\t\t!(cmd.data instanceof ArrayBuffer)\n\t) {\n\t\tthrow new Error(\"[FecWorker] feedSource: invalid source symbol\");\n\t}\n\n\tconst view = new Uint8Array(cmd.data);\n\n\t// Derive SBN locally for block-level accounting. WASM derives the same\n\t// value internally (ISO 23008-1 \u00A7C.5.2: SBN = floor(SS_ID / K)).\n\tconst sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;\n\tconst esi = cmd.ssId % config.k;\n\tif (sbn <= retiredSbnFrontier) {\n\t\tsourceDroppedRetired++;\n\t\treturn;\n\t}\n\tconst existingBlock = blocks.get(sbn);\n\tif (existingBlock && isTerminalBlock(existingBlock)) {\n\t\tsourceDroppedTerminal++;\n\t\treturn;\n\t}\n\tif (acceptedSourceEsis.get(sbn)?.has(esi)) {\n\t\tsourceDroppedDuplicate++;\n\t\treturn;\n\t}\n\n\tconst result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);\n\tadvanceMediaHorizon(sbn);\n\tlet sourceEsis = acceptedSourceEsis.get(sbn);\n\tif (!sourceEsis) {\n\t\tsourceEsis = new Set<number>();\n\t\tacceptedSourceEsis.set(sbn, sourceEsis);\n\t}\n\tsourceEsis.add(esi);\n\n\t// Update block state\n\tconst block = getOrCreateBlock(sbn);\n\tblock.sourceReceived++;\n\tblock.lastSymbolTs = performance.now();\n\tsourceSymbols++;\n\n\t// The FEC worker must remain decode-only on the per-symbol hot path. ALTA\n\t// WASM is retained temporarily for low-rate signaling users, but media is\n\t// authenticated by the canonical manifest verifier at router admission.\n\tif (altaEnabled) altaPending++;\n\n\t// Check if block is complete (all authored source symbols received:\n\t// SSB_length once a repair symbol declared it, otherwise K)\n\tconst authoredLength = block.ssbLength ?? block.k;\n\tif (\n\t\tblock.sourceReceived >= authoredLength &&\n\t\tblock.state === \"collecting\"\n\t) {\n\t\tmarkBlockTerminal(block, \"complete\");\n\t\tblocksComplete++;\n\t\temitBlockUpdate(block);\n\t}\n\n\t\t// A source-triggered return can be either clean block completion or recovery\n\t\t// of earlier missing symbols after repair arrived. Clean completion is\n\t\t// already routed by the source path, so only emit when the block did not\n\t\t// reach K source arrivals.\n\t\tif (result && result.byteLength > 0 && block.sourceReceived < authoredLength) {\n\t\t\tconst recovered = new Uint8Array(toOwnedArrayBuffer(result));\n\t\t\tconst recoveryMs = performance.now() - block.firstSymbolTs;\n\t\t\trecoveryTimes.push(recoveryMs);\n\t\t\tif (recoveryTimes.length > 1000) recoveryTimes.shift();\n\n\t\t\tif (block.state === \"collecting\") {\n\t\t\t\tmarkBlockTerminal(block, \"recovered\");\n\t\t\t\tblocksRecovered++;\n\t\t\t\temitBlockUpdate(block);\n\t\t\t}\n\t\t\temitRecoveredSymbols(sbn, recovered, block);\n\t\t}\n\t\tsynchronizeDecoderHorizon();\n\t}\n\nfunction handleFeedRepair(\n\tcmd: Extract<FecWorkerCommand, { type: \"feedRepair\" }>,\n): void {\n\tif (!wasmDecoder) {\n\t\tthrow new Error(\"[FecWorker] feedRepair: not configured\");\n\t}\n\tif (!config) {\n\t\tthrow new Error(\"[FecWorker] feedRepair: config missing\");\n\t}\n\tif (\n\t\t!isUint32(cmd.ssStart) ||\n\t\t!isUint32(cmd.ssbLength) ||\n\t\tcmd.ssbLength <= 0 ||\n\t\tcmd.ssbLength > config.k ||\n\t\tcmd.ssStart % config.k !== 0 ||\n\t\t!isUint32(cmd.rsId) ||\n\t\tconfig.k + cmd.rsId > UINT24_MAX ||\n\t\t!(cmd.data instanceof ArrayBuffer)\n\t) {\n\t\tthrow new Error(\"[FecWorker] feedRepair: invalid repair symbol\");\n\t}\n\n\tconst view = new Uint8Array(cmd.data);\n\tif (config.symbolSize > 0 && view.byteLength !== config.symbolSize) {\n\t\trepairSymbolSizeMismatch++;\n\t\tif (repairSymbolSizeMismatch <= 5 || repairSymbolSizeMismatch % 100 === 0) {\n\t\t\temit({\n\t\t\t\ttype: \"error\",\n\t\t\t\tcode: \"repair-symbol-size-mismatch\",\n\t\t\t\tmessage: `[FecWorker] Dropping repair symbol with invalid size: got ${view.byteLength}B, expected ${config.symbolSize}B (ssStart=${cmd.ssStart}, ssbLength=${cmd.ssbLength}, rsId=${cmd.rsId})`,\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\tconst sbn = Math.floor(cmd.ssStart / config.k);\n\tconst repairEsi = config.k + cmd.rsId;\n\t// The repair path's mirror drops stay silent and uncounted: BLO-35597\n\t// defect 2 is a source-symbol shortfall, so only the source side needed\n\t// evidence. The asymmetry is deliberate, not an oversight \u2014 extend these\n\t// the same way if a repair-side shortfall ever needs attributing.\n\tif (sbn <= retiredSbnFrontier) return;\n\tconst existingBlock = blocks.get(sbn);\n\tif (existingBlock && isTerminalBlock(existingBlock)) return;\n\tif (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;\n\t// One block has one authored SSB_length; a repair symbol naming another\n\t// is dropped before it reaches the decoder (which rejects it too).\n\tif (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"repair-ssb-length-conflict\",\n\t\t\tmessage: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,\n\t\t});\n\t\treturn;\n\t}\n\n\tconst result = wasmDecoder.add_repair(\n\t\tcmd.ssStart,\n\t\tcmd.ssbLength,\n\t\tcmd.rsId,\n\t\tview,\n\t\tcmd.ts,\n\t);\n\tadvanceMediaHorizon(sbn);\n\tlet repairEsis = acceptedRepairEsis.get(sbn);\n\tif (!repairEsis) {\n\t\trepairEsis = new Set<number>();\n\t\tacceptedRepairEsis.set(sbn, repairEsis);\n\t}\n\trepairEsis.add(repairEsi);\n\tconst block = getOrCreateBlock(sbn);\n\tblock.ssbLength = cmd.ssbLength;\n\tblock.repairReceived++;\n\tblock.lastSymbolTs = performance.now();\n\trepairSymbols++;\n\t\t// Every authored source symbol already arrived: knowing SSB_length makes\n\t\t// the block clean. The decoder's return is the source already routed.\n\t\t// Otherwise, returned data is the recovered source block: its SSB_length\n\t\t// fixed-width authored symbols concatenated. Split it before emitting so\n\t\t// MmtpRouter gets the same one-symbol shape as a direct source packet.\n\t\tif (block.sourceReceived >= cmd.ssbLength && block.state === \"collecting\") {\n\t\t\tmarkBlockTerminal(block, \"complete\");\n\t\t\tblocksComplete++;\n\t\t\temitBlockUpdate(block);\n\t\t} else if (result && result.byteLength > 0) {\n\t\t\tconst recovered = new Uint8Array(toOwnedArrayBuffer(result));\n\t\t\tconst recoveryMs = performance.now() - block.firstSymbolTs;\n\t\t\trecoveryTimes.push(recoveryMs);\n\t\t// Cap recovery times array at 1000 entries\n\t\tif (recoveryTimes.length > 1000) recoveryTimes.shift();\n\n\t\tif (block.state === \"collecting\") {\n\t\t\tmarkBlockTerminal(block, \"recovered\");\n\t\t\tblocksRecovered++;\n\t\t\t\temitBlockUpdate(block);\n\t\t\t}\n\t\t\temitRecoveredSymbols(sbn, recovered, block);\n\t\t}\n\t\tsynchronizeDecoderHorizon();\n\t}\n\nfunction handleRelayBlockSig(\n\tcmd: Extract<FecWorkerCommand, { type: \"relayBlockSig\" }>,\n): void {\n\t// No-op AUTH stub: store sig on block, mark as not-yet-verified\n\tconst block = blocks.get(cmd.sbn);\n\tif (block) {\n\t\t// Real verification deferred to ALTA milestone\n\t\t// For now, mark as null (not verified) \u2014 will be wired when ALTA is implemented\n\t\tblock.relayBlockSigValid = null;\n\t\temitBlockUpdate(block);\n\t}\n}\n\nfunction handleCleanup(\n\tcmd: Extract<FecWorkerCommand, { type: \"cleanup\" }>,\n): void {\n\tif (\n\t\t!Array.isArray(cmd.sbns) ||\n\t\tcmd.sbns.length === 0 ||\n\t\t!cmd.sbns.every(isUint32)\n\t) {\n\t\tthrow new Error(\"[FecWorker] cleanup: invalid SBNs\");\n\t}\n\tconst sbns = [...new Set(cmd.sbns)].sort((a, b) => a - b);\n\tconst pendingSbns = sbns.filter((sbn) => sbn > retiredSbnFrontier);\n\ttry {\n\t\tif (!wasmDecoder) {\n\t\t\tthrow new Error(\"[FecWorker] cleanup: not configured\");\n\t\t}\n\n\t\t// Call WASM cleanup only after all earlier symbol commands have drained,\n\t\t// and only for the exact SBNs the manager's deadlines named; command\n\t\t// coalescing must not infer ownership from the largest timed-out SBN.\n\t\tif (pendingSbns.length > 0) {\n\t\t\twasmDecoder.cleanup_blocks_detailed(Uint32Array.from(pendingSbns));\n\t\t}\n\t\t// Manager deadlines are block-scoped and may expire a newer SBN while an\n\t\t// older interleaved block is still live. Only accepted media progression\n\t\t// advances the shared JS/Rust horizon; cleanup creates exact tombstones.\n\n\t\t// Rust already finalized every SBN behind the media frontier. For each\n\t\t// remaining exact request, mirror the block-scoped WASM boundary immediately\n\t\t// so JS never retains state for a decoder block that is gone.\n\t\tfor (const sbn of pendingSbns) {\n\t\t\tlet currentBlock = blocks.get(sbn);\n\t\t\tif (currentBlock && isTerminalBlock(currentBlock)) {\n\t\t\t\tretireAcceptedIds(sbn);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcurrentBlock ??= getOrCreateBlock(sbn);\n\t\t\tif (currentBlock.sourceReceived >= currentBlock.k) {\n\t\t\t\tmarkBlockTerminal(currentBlock, \"complete\");\n\t\t\t\tblocksComplete++;\n\t\t\t} else {\n\t\t\t\tmarkBlockTerminal(currentBlock, \"failed\");\n\t\t\t\tblocksFailed++;\n\t\t\t}\n\t\t\temitBlockUpdate(currentBlock);\n\t\t}\n\t} finally {\n\t\t// Message ordering makes this an acknowledgement that every source/repair\n\t\t// command posted before cleanup has completed. The main thread must not\n\t\t// commit failure accounting until it observes this event.\n\t\temit({ type: \"cleanupComplete\", sbns });\n\t}\n}\n\nfunction handleDispose(): void {\n\t// Clear snapshot timer (MUST clear to prevent leaks)\n\tif (snapshotTimer !== null) {\n\t\tclearInterval(snapshotTimer);\n\t\tsnapshotTimer = null;\n\t}\n\n\t// Free ALTA verifier WASM object (MUST call to release WASM memory \u2014\n\t// Worker.terminate() fires immediately after dispose, so the FinalizationRegistry\n\t// callback has no chance to run and this memory would leak without explicit free)\n\tif (altaVerifier) {\n\t\ttry {\n\t\t\taltaVerifier.free();\n\t\t} catch (_e) {\n\t\t\t// Best-effort \u2014 verifier may already be freed\n\t\t}\n\t\taltaVerifier = null;\n\t}\n\n\t// Free WASM decoder (MUST call to release WASM memory)\n\tif (wasmDecoder) {\n\t\twasmDecoder.free();\n\t\twasmDecoder = null;\n\t}\n\n\t// Clear deferred-verify queue so pinned ArrayBuffers drop.\n\tpendingVerify.clear();\n\tpendingVerifyCap = 0;\n\tpendingVerifyTtlMs = 0;\n\n\tconfig = null;\n\tclearBlockTracking();\n}\n\nfunction handleReset(): void {\n\ttry {\n\t\twasmDecoder?.flush();\n\t\twasmDecoder?.reset_stats();\n\t} catch (err) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"reset-failed\",\n\t\t\tmessage:\n\t\t\t\terr instanceof Error\n\t\t\t\t\t? `[FecWorker] reset failed: ${err.message}`\n\t\t\t\t\t: `[FecWorker] reset failed: ${String(err)}`,\n\t\t});\n\t\treturn;\n\t}\n\n\tclearBlockTracking();\n\tsourceSymbols = 0;\n\trepairSymbols = 0;\n\tblocksComplete = 0;\n\tblocksRecovered = 0;\n\tblocksFailed = 0;\n\taltaVerified = 0;\n\taltaFailed = 0;\n\taltaFailedDetails.length = 0;\n\taltaPending = 0;\n\taltaPendingEvictedCap = 0;\n\taltaPendingEvictedTtl = 0;\n\taltaError = 0;\n\trelayBlocksVerified = 0;\n\trelayBlocksFailed = 0;\n\trepairSymbolSizeMismatch = 0;\n\tsourceDroppedRetired = 0;\n\tsourceDroppedTerminal = 0;\n\tsourceDroppedDuplicate = 0;\n\temit({ type: \"resetComplete\" });\n\tif (config) emitSnapshot();\n}\n\n/**\n * A batch of symbols fed in one page task (BLO-36599). Each record is\n * handled exactly as its single-symbol command would be, in order, and a\n * failing record reports the same `command-failed` error that command would\n * without stopping the records after it.\n */\nfunction handleFeedSymbols(\n\tcmd: Extract<FecWorkerCommand, { type: \"feedSymbols\" }>,\n): void {\n\tif (!Array.isArray(cmd.symbols)) {\n\t\tthrow new Error(\"[FecWorker] feedSymbols: symbols must be an array\");\n\t}\n\tfor (const symbol of cmd.symbols) {\n\t\ttry {\n\t\t\tif (symbol?.type === \"feedSource\") handleFeedSource(symbol);\n\t\t\telse if (symbol?.type === \"feedRepair\") handleFeedRepair(symbol);\n\t\t\telse {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[FecWorker] feedSymbols: unknown symbol type ${String((symbol as { type?: unknown } | null)?.type)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\temit({\n\t\t\t\ttype: \"error\",\n\t\t\t\tcode: \"command-failed\",\n\t\t\t\tmessage:\n\t\t\t\t\terr instanceof Error ? err.message : `[FecWorker] ${String(err)}`,\n\t\t\t});\n\t\t}\n\t}\n}\n\n// --- Main message handler ---\n\nasync function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {\n\ttry {\n\t\tswitch (cmd.type) {\n\t\t\tcase \"configure\":\n\t\t\t\t// handleConfigure is async (ALTA WASM init uses dynamic import).\n\t\t\t\t// Await it so feed/cleanup messages cannot run against a partially\n\t\t\t\t// configured decoder.\n\t\t\t\tawait handleConfigure(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"feedSource\":\n\t\t\t\thandleFeedSource(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"feedRepair\":\n\t\t\t\thandleFeedRepair(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"feedSymbols\":\n\t\t\t\thandleFeedSymbols(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"relayBlockSig\":\n\t\t\t\thandleRelayBlockSig(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"cleanup\":\n\t\t\t\thandleCleanup(cmd);\n\t\t\t\tbreak;\n\t\t\tcase \"reset\":\n\t\t\t\thandleReset();\n\t\t\t\tbreak;\n\t\t\tcase \"subscribeBlockUpdates\":\n\t\t\t\tblockUpdatesSubscribed = true;\n\t\t\t\tbreak;\n\t\t\tcase \"dispose\":\n\t\t\t\thandleDispose();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\temit({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tcode: \"unknown-command\",\n\t\t\t\t\tmessage: `Unknown command type: ${(cmd as Record<string, unknown>).type}`,\n\t\t\t\t});\n\t\t}\n\t} catch (err) {\n\t\temit({\n\t\t\ttype: \"error\",\n\t\t\tcode: \"command-failed\",\n\t\t\tmessage:\n\t\t\t\terr instanceof Error ? err.message : `[FecWorker] ${String(err)}`,\n\t\t});\n\t}\n}\n\nlet commandQueue: Promise<void> = Promise.resolve();\n\nself.onmessage = (e: MessageEvent<FecWorkerCommand>) => {\n\tconst cmd = e.data;\n\tcommandQueue = commandQueue.then(() => dispatchCommand(cmd));\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockcast/fec-worker",
3
- "version": "0.1.0-main.10a8b91bb731",
3
+ "version": "0.1.0-main.144feb8d235c",
4
4
  "description": "Worker-per-track FEC decode with zero-copy ArrayBuffer transfer and ALTA auth plumbing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "test:watch": "vitest"
36
36
  },
37
37
  "dependencies": {
38
- "@blockcast/mmt-alta-parse": "0.1.0-main.10a8b91bb731"
38
+ "@blockcast/mmt-alta-parse": "0.1.0-main.144feb8d235c"
39
39
  },
40
40
  "devDependencies": {
41
41
  "esbuild": "^0.28.0",
@@ -33,7 +33,11 @@ export interface FecTrackConfig {
33
33
  repairCount: number;
34
34
  /** Bytes per symbol */
35
35
  symbolSize: number;
36
- /** Interleave depth in frames (D) */
36
+ /** Retained decoder horizon as a BLOCK COUNT (SBN distance), not D.
37
+ * `advanceMediaHorizon` does `liveSbn - interleaveDepth` and
38
+ * `MmtFecDecoder::new` documents it as "the number of interleaved blocks".
39
+ * Never assign `deriveFecInterleaveDepth()` here — that returns media units
40
+ * per block, which is a different quantity (BLO-35597). */
37
41
  interleaveDepth: number;
38
42
  /** Interleave depth in milliseconds (D * frameDurationMs) */
39
43
  interleaveMs: number;
@@ -152,6 +156,43 @@ export interface FecTrackStats {
152
156
  maxRecoveryMs: number;
153
157
  /** Repair symbols rejected before WASM because their byte length did not match T. */
154
158
  repairSymbolSizeMismatch?: number;
159
+ /**
160
+ * Source symbols whose SBN had already fallen below the retained window.
161
+ *
162
+ * This and its two peers below are dropped before WASM and narrow the
163
+ * explanations for a block that settles at K-1 source symbols (BLO-35597
164
+ * defect 2): a late symbol past retention, a symbol for a block that had
165
+ * already settled (complete, recovered *or failed*), or a repeated
166
+ * (SBN, ESI).
167
+ *
168
+ * They narrow; they do not uniquely attribute. A symbol lost in transit
169
+ * never reaches the worker and increments nothing, so all three reading
170
+ * zero on a short block means only that no *receiver-side* drop was
171
+ * recorded — wire loss and a short authored block both remain open. Use
172
+ * `FecBlockSnapshot.ssbLength` to separate them: below `k` is a genuinely
173
+ * short authored block, equal to `k` with fewer accepted is loss-or-late.
174
+ * `ssbLength` is undefined until a repair symbol declares it, in which case
175
+ * both stay open.
176
+ *
177
+ * All three are cumulative across decoder epochs: `configure` clears block
178
+ * tracking but none of them, so they stay comparable against the
179
+ * `sourceSymbols` denominator. Only a reset zeroes them.
180
+ */
181
+ sourceDroppedRetired?: number;
182
+ /** Source symbols for a block that had already completed, recovered or failed. */
183
+ sourceDroppedTerminal?: number;
184
+ /**
185
+ * Source symbols repeating an (SBN, ESI) already accepted in the current
186
+ * epoch; `configure` clears the accepted-ESI sets.
187
+ *
188
+ * Unlike the other two, this one cannot fire once a block settles — the
189
+ * terminal check returns first, and marking a block terminal retires its
190
+ * accepted-ESI set anyway — so a non-zero value means the repeat reached
191
+ * the worker while the block was **still collecting**. It attributes the
192
+ * timing, not the origin: usually a duplicate packet on the wire,
193
+ * sometimes an SBN/ESI derivation collision.
194
+ */
195
+ sourceDroppedDuplicate?: number;
155
196
  /** Native decoder counters, converted to numbers so browser diagnostics remain JSON-safe. */
156
197
  decoder?: {
157
198
  sourcePackets: number;
package/src/fec-worker.ts CHANGED
@@ -120,6 +120,30 @@ let liveSbn = 0;
120
120
  // starts a new epoch before the 32-bit source-symbol counter can wrap.
121
121
  let retiredSbnFrontier = -1;
122
122
  let repairSymbolSizeMismatch = 0;
123
+ // Source symbols dropped before WASM, by cause. All three sites below were
124
+ // silent, which left the "block settles at exactly K-1 source symbols" shape
125
+ // (BLO-35597 defect 2) with no evidence to attribute it to. A retired drop
126
+ // means a late symbol past retention; a terminal drop means the block had
127
+ // already settled — `state !== "collecting"`, so complete OR recovered OR
128
+ // FAILED, and the failed arm is the one this issue is chasing (a block aged out
129
+ // by synchronizeDecoderHorizon stays in `blocks` until its SBN retires, so a
130
+ // late symbol for a short block lands here); a duplicate drop means a repeated
131
+ // (SBN, ESI), which on this wire is usually a plain duplicate packet — the
132
+ // router's ledger has deliberate gaps — and only sometimes an SBN/ESI
133
+ // derivation collision.
134
+ //
135
+ // These narrow the cause; they do not uniquely attribute it. A symbol LOST IN
136
+ // TRANSIT never reaches handleFeedSource and increments nothing, so all three
137
+ // reading zero on a short block is NOT evidence of a publisher defect — wire
138
+ // loss is the null hypothesis here, and it produces an identical shape. The
139
+ // discriminator for that residual arm is `block.ssbLength` (set at the repair
140
+ // symbol's declared SSB_length, conflict-checked on arrival):
141
+ // ssbLength < k → genuinely short authored block
142
+ // ssbLength === k, sourceReceived < k → loss or a still-late symbol
143
+ // With no repair symbol yet seen, ssbLength is undefined and both stay open.
144
+ let sourceDroppedRetired = 0;
145
+ let sourceDroppedTerminal = 0;
146
+ let sourceDroppedDuplicate = 0;
123
147
 
124
148
  // Recovery timing
125
149
  const recoveryTimes: number[] = [];
@@ -469,6 +493,9 @@ function buildStatsSnapshot(): FecTrackStats {
469
493
  avgRecoveryMs: Math.round(avgRecoveryMs * 100) / 100,
470
494
  maxRecoveryMs: Math.round(maxRecoveryMs * 100) / 100,
471
495
  repairSymbolSizeMismatch,
496
+ sourceDroppedRetired,
497
+ sourceDroppedTerminal,
498
+ sourceDroppedDuplicate,
472
499
  decoder,
473
500
  };
474
501
  }
@@ -698,10 +725,19 @@ function handleFeedSource(
698
725
  // value internally (ISO 23008-1 §C.5.2: SBN = floor(SS_ID / K)).
699
726
  const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
700
727
  const esi = cmd.ssId % config.k;
701
- if (sbn <= retiredSbnFrontier) return;
728
+ if (sbn <= retiredSbnFrontier) {
729
+ sourceDroppedRetired++;
730
+ return;
731
+ }
702
732
  const existingBlock = blocks.get(sbn);
703
- if (existingBlock && isTerminalBlock(existingBlock)) return;
704
- if (acceptedSourceEsis.get(sbn)?.has(esi)) return;
733
+ if (existingBlock && isTerminalBlock(existingBlock)) {
734
+ sourceDroppedTerminal++;
735
+ return;
736
+ }
737
+ if (acceptedSourceEsis.get(sbn)?.has(esi)) {
738
+ sourceDroppedDuplicate++;
739
+ return;
740
+ }
705
741
 
706
742
  const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
707
743
  advanceMediaHorizon(sbn);
@@ -791,6 +827,10 @@ function handleFeedRepair(
791
827
  }
792
828
  const sbn = Math.floor(cmd.ssStart / config.k);
793
829
  const repairEsi = config.k + cmd.rsId;
830
+ // The repair path's mirror drops stay silent and uncounted: BLO-35597
831
+ // defect 2 is a source-symbol shortfall, so only the source side needed
832
+ // evidence. The asymmetry is deliberate, not an oversight — extend these
833
+ // the same way if a repair-side shortfall ever needs attributing.
794
834
  if (sbn <= retiredSbnFrontier) return;
795
835
  const existingBlock = blocks.get(sbn);
796
836
  if (existingBlock && isTerminalBlock(existingBlock)) return;
@@ -984,6 +1024,9 @@ function handleReset(): void {
984
1024
  relayBlocksVerified = 0;
985
1025
  relayBlocksFailed = 0;
986
1026
  repairSymbolSizeMismatch = 0;
1027
+ sourceDroppedRetired = 0;
1028
+ sourceDroppedTerminal = 0;
1029
+ sourceDroppedDuplicate = 0;
987
1030
  emit({ type: "resetComplete" });
988
1031
  if (config) emitSnapshot();
989
1032
  }