/** * @blockcast/fec-worker — Worker Entry Point * * Runs inside DedicatedWorkerGlobalScope. Receives FecWorkerCommand messages, * drives the MmtFecDecoder WASM module, emits FecWorkerEvent messages back. * * WASM bindings are resolved via: * 1. (self as any).__MMT_WASM_BINDINGS__ — set by consumer's loader/importmap * 2. Falls back to error if not available (no defaults) * * This file must NOT import DOM globals incompatible with Worker scope. */ import type { FecWorkerCommand, FecWorkerEvent, FecTrackConfig, FecTrackStats, FecBlockSnapshot, FrameMeta, FecBlockProfile, } from "./fec-worker-types.js"; import { toOwnedArrayBuffer } from "./transfer.js"; import { isoBlockContains, isoBlocksBehind, isoBlocksOverlap, serialDistance, serialPrecedes, } from "./block-order.js"; // --- WASM binding types (from mmt-wasm) --- // Signatures MUST match a freshly generated external mmt_wasm.d.ts exactly. // The decoder derives SBN/ESI internally — do not pre-compute. interface WasmFecDecoder { /** * ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID; data is * the protected packet, unpadded. The decoder builds the ssbg_mode1 symbol * and holds it until a repair names the block containing it. */ add_source( ss_id: number, data: Uint8Array, timestamp: number, ): Uint8Array | undefined; /** * ISO 23008-1 §C.5.3: the block is `[ss_start, ss_start + ssb_length)` * with K' = ssb_length and repair ESI = ssb_length + rs_id; a recovered * block is ssb_length x T bytes of ssbg_mode1 symbols. */ add_repair( ss_start: number, ssb_length: number, rs_id: number, data: Uint8Array, timestamp: number, ): Uint8Array | undefined; configure_params(k: number, symbolSize: number): void; /** * Retire the named blocks (last-chance decode + stats), driven by the * manager's exact deadlines. Nothing is synthesised for a lost block: * concealment is the sink's hold-last-good-frame. */ cleanup_blocks_detailed(sbns: Uint32Array): void; /** * Remove decoder blocks, and held unplaced sources, more than * interleaveDepth `isoBlocksBehind` the live SS_ID; returns the SS_Starts * of the blocks it removed. It runs the decoder's `retire_unplaced` sweep * itself, so the Worker never calls that separately: synchronizeDecoderHorizon * calls this after every source and repair it accepts, which is what keeps * the decoder's unplaced pool bounded when a block's repairs never arrive. */ cleanup_by_ss_start(current: number): Uint32Array; /** The block profile the decoder was constructed with. */ get_block_profile(): string; flush(): void; pending_blocks(): number; reset_stats(): void; get_stats(): { source_packets: bigint; repair_packets: bigint; blocks_complete: bigint; blocks_recovered: bigint; blocks_failed: bigint; bytes_recovered: bigint; recovery_rate(): number; }; free(): void; } interface WasmBindings { initSync(module: { module: WebAssembly.Module }): unknown; MmtFecDecoder: new (interleaveDepth: number, blockProfile: FecBlockProfile) => WasmFecDecoder; } // --- Worker state (local to this Worker instance) --- let wasmDecoder: WasmFecDecoder | null = null; let config: FecTrackConfig | null = null; let snapshotTimer: ReturnType | null = null; // blockUpdate events are posted only after a client subscribes (BLO-36599). let blockUpdatesSubscribed = false; // Block tracking const blocks = new Map(); const acceptedSourceEsis = new Map>(); const acceptedRepairEsis = new Map>(); // iso-ssbg1-v1: sources fed to the decoder before any repair named a block // containing them, keyed by SS_ID with their arrival time. Map order is // arrival order, which the horizon sweep relies on (retireUnplacedSources). const unplacedSources = new Map(); const UINT32_MAX = 0xffff_ffff; const UINT24_MAX = 0xff_ffff; const SS_ID_SPAN = 0x1_0000_0000; // ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet protected-packet length field. const SSBG1_LENGTH_FIELD_BYTES = 2; // ISO/IEC 23008-1 AL-FEC signaling carries interleave_depth in one byte. const MAX_INTERLEAVE_DEPTH = 0xff; // Rust stores T as u16 and this decoder profile fixes RFC 6330 Al=8. const MAX_SYMBOL_SIZE = 0xfff8; // RFC 6330 systematic table limit, enforced by the vendored raptorq decoder. const MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK = 56_403; // Aggregate counters let sourceSymbols = 0; let repairSymbols = 0; let blocksComplete = 0; let blocksRecovered = 0; let blocksFailed = 0; let relayBlocksVerified = 0; let relayBlocksFailed = 0; let liveSbn = 0; // Whether liveSbn has been set by a symbol of this decoder epoch. Block order // is serial on the 32-bit SS_ID (block-order.ts, BLO-35597 iso:5), where 0 is // a real position, so "nothing seen yet" needs its own state. let liveSbnKnown = false; let repairSymbolSizeMismatch = 0; // Source symbols dropped before WASM, by cause. All three sites below were // silent, which left the "block settles at exactly K-1 source symbols" shape // (BLO-35597 defect 2) with no evidence to attribute it to. A retired drop // means a late symbol past retention; a terminal drop means the block had // already settled — `state !== "collecting"`, so complete OR recovered OR // FAILED, and the failed arm is the one this issue is chasing (a block aged out // by synchronizeDecoderHorizon stays in `blocks` until its SBN retires, so a // late symbol for a short block lands here); a duplicate drop means a repeated // (SBN, ESI), which on this wire is usually a plain duplicate packet — the // router's ledger has deliberate gaps — and only sometimes an SBN/ESI // derivation collision. // // These narrow the cause; they do not uniquely attribute it. A symbol LOST IN // TRANSIT never reaches handleFeedSource and increments nothing, so all three // reading zero on a short block is NOT evidence of a publisher defect — wire // loss is the null hypothesis here, and it produces an identical shape. The // discriminator for that residual arm is `block.ssbLength` (set at the repair // symbol's declared SSB_length, conflict-checked on arrival): // ssbLength < k → genuinely short authored block // ssbLength === k, sourceReceived < k → loss or a still-late symbol // With no repair symbol yet seen, ssbLength is undefined and both stay open. let sourceDroppedRetired = 0; let sourceDroppedTerminal = 0; let sourceDroppedDuplicate = 0; let sourceUnplacedRetired = 0; // Recovery timing const recoveryTimes: number[] = []; // --- Helpers --- function emit(event: FecWorkerEvent, transfer?: Transferable[]): void { self.postMessage(event, transfer ?? []); } function isUint32(value: number): boolean { return Number.isInteger(value) && value >= 0 && value <= UINT32_MAX; } function isTerminalBlock(block: FecBlockSnapshot): boolean { return block.state !== "collecting"; } function retireAcceptedIds(sbn: number): void { acceptedSourceEsis.delete(sbn); acceptedRepairEsis.delete(sbn); } function clearBlockTracking(): void { blocks.clear(); acceptedSourceEsis.clear(); acceptedRepairEsis.clear(); unplacedSources.clear(); recoveryTimes.length = 0; liveSbn = 0; liveSbnKnown = false; } /** * Whether `sbn` has fallen behind the decoder's retained interleave window: * more than `interleaveDepth` blocks behind the live SBN. liveSbn only moves * forward within a decoder epoch and configure() resets both, so this is the * monotonic retirement frontier the decoder horizon implies. */ function isRetiredSbn(sbn: number): boolean { if (config === null || !liveSbnKnown) return false; // `sbn` is an SS_ID (a block's SS_Start or a source's own SS_ID) and // liveSbn the live SS_ID; K is only the maximum block length. return isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth; } /** * iso-ssbg1-v1: the SS_IDs a known block certainly covers. Every block a * repair created has its SSB_length; a cleanup tombstone for a block no * repair declared certainly holds only its own SS_Start. */ function isoKnownLength(block: FecBlockSnapshot): number { return block.ssbLength ?? 1; } /** iso-ssbg1-v1: the known block whose SS_ID range contains `ssId` (ranges never overlap). */ function findIsoBlock(ssId: number): FecBlockSnapshot | undefined { for (const block of blocks.values()) { if (isoBlockContains(block.sbn, isoKnownLength(block), ssId)) return block; } return undefined; } /** * Whether a terminal block's diagnostics may be deleted. Under iso-ssbg1-v1 a * source carries no block identity, so a deleted block cannot refuse a late * source of its own: that source would be admitted as unplaced and reopen the * block in the decoder. The tombstone therefore stays until its LAST SS_ID is * retired, after which the SS_ID retirement check refuses every member. */ function isRetiredBlock(key: number, block: FecBlockSnapshot): boolean { return isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN); } /** * Retire terminal diagnostics only after the decoder's monotonic interleave * horizon proves that the SBN can no longer be valid out-of-order media. * Admission keeps the compact frontier as the durable tombstone, so deletion * never reopens a finalized block. */ function trimBlockRetention(): void { for (const [sbn, block] of blocks) { if (!isTerminalBlock(block) || !isRetiredBlock(sbn, block)) continue; blocks.delete(sbn); retireAcceptedIds(sbn); } } /** Advance the live position: an SS_ID / SS_Start, in serial order. */ function advanceMediaHorizon(sbn: number): void { if (!config) return; if (liveSbnKnown && !serialPrecedes(liveSbn, sbn)) return; liveSbn = sbn; liveSbnKnown = true; trimBlockRetention(); } function markBlockTerminal( block: FecBlockSnapshot, state: "complete" | "recovered" | "failed", ): void { block.state = state; block.completedTs = performance.now(); retireAcceptedIds(block.sbn); trimBlockRetention(); } function synchronizeDecoderHorizon(): void { if (!wasmDecoder || !liveSbnKnown) return; const expired = wasmDecoder.cleanup_by_ss_start(liveSbn); for (const sbn of expired) { const block = blocks.get(sbn); if (!block || isTerminalBlock(block)) { retireAcceptedIds(sbn); continue; } markBlockTerminal(block, "failed"); blocksFailed++; emitBlockUpdate(block); } retireUnplacedSources(); trimBlockRetention(); } /** * iso-ssbg1-v1: retire unplaced sources behind the horizon, by the same * isoBlocksBehind metric as a block start, sweeping in arrival order and * stopping at the first live one (amortised O(1) per symbol). A source that * arrived out of order behind a newer live one is retired when it reaches the * front; until then nothing can place it, because any repair range holding it * starts at or before it and is itself retired. One the live position moved * at least half the SS_ID space past no longer precedes it serially; it is * retired too rather than pinning the sweep. */ function retireUnplacedSources(): void { for (const ssId of unplacedSources.keys()) { const unreachable = ssId !== liveSbn && !serialPrecedes(ssId, liveSbn); if (!unreachable && !isRetiredSbn(ssId)) break; unplacedSources.delete(ssId); sourceUnplacedRetired++; } } /** * Look up or create a block. `isoSsbLength` creates an iso-ssbg1-v1 block, * whose K' is its SSB_length (C.5.3); without it the block has the catalog K. */ function getOrCreateBlock(sbn: number, isoSsbLength?: number): FecBlockSnapshot { let block = blocks.get(sbn); if (!block) { if (!config) throw new Error("[FecWorker] not configured"); const now = performance.now(); block = { sbn, state: "collecting", sourceReceived: 0, repairReceived: 0, k: isoSsbLength ?? config.k, repairCount: config.repairCount, firstSymbolTs: now, lastSymbolTs: now, // A positive interleaveMs makes this the catalog completion deadline; // zero makes deliveryWindowMs a receiver-local cleanup bound. deadlineTs: now + config.deliveryWindowMs, relayBlockSigValid: null, }; if (isoSsbLength !== undefined) block.ssbLength = isoSsbLength; blocks.set(sbn, block); trimBlockRetention(); } return block; } function buildStatsSnapshot(): FecTrackStats { if (!config) throw new Error("[FecWorker] not configured"); const decoderStats = wasmDecoder?.get_stats(); const decoder = decoderStats ? { sourcePackets: Number(decoderStats.source_packets), repairPackets: Number(decoderStats.repair_packets), blocksComplete: Number(decoderStats.blocks_complete), blocksRecovered: Number(decoderStats.blocks_recovered), blocksFailed: Number(decoderStats.blocks_failed), bytesRecovered: Number(decoderStats.bytes_recovered), pendingBlocks: wasmDecoder?.pending_blocks() ?? 0, } : undefined; const avgRecoveryMs = recoveryTimes.length > 0 ? recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length : 0; const maxRecoveryMs = recoveryTimes.length > 0 ? Math.max(...recoveryTimes) : 0; // Cap block array to interleave window const maxBlocks = Math.max(config.interleaveDepth * 3, 24); // Newest first in SS_ID serial order, so the cap keeps the newest blocks // across the 2^32 wrap too: every block start precedes the live SS_ID. const blockArray = Array.from(blocks.values()) .sort((a, b) => serialDistance(a.sbn, liveSbn) - serialDistance(b.sbn, liveSbn)) .slice(0, maxBlocks); return { config, sourceSymbols, repairSymbols, blocksComplete, blocksRecovered, blocksFailed, recoveryRate: blocksComplete + blocksRecovered > 0 ? Math.round( (blocksRecovered / (blocksRecovered + blocksFailed)) * 100, ) || 0 : 0, relayBlocksVerified, relayBlocksFailed, liveSbn, blocks: blockArray, interleaveWindowBlocks: config.interleaveDepth, avgRecoveryMs: Math.round(avgRecoveryMs * 100) / 100, maxRecoveryMs: Math.round(maxRecoveryMs * 100) / 100, repairSymbolSizeMismatch, sourceDroppedRetired, sourceDroppedTerminal, sourceDroppedDuplicate, sourceUnplacedRetired, decoder, }; } function emitSnapshot(): void { if (!config) return; emit({ type: "snapshot", stats: buildStatsSnapshot() }); } function emitBlockUpdate(block: FecBlockSnapshot): void { if (!blockUpdatesSubscribed) return; emit({ type: "blockUpdate", block: { ...block } }); } function emitRecoveredSymbols( sbn: number, recoveredBlock: Uint8Array, block: FecBlockSnapshot, ): void { if (!config) return; const symbolSize = config.symbolSize; if (symbolSize <= 0) { emit({ type: "error", code: "recovered-symbol-size-unconfigured", message: "[FecWorker] recovered block has no configured symbolSize", }); return; } // The decoder returns only the block's authored source symbols // (SSB_length x T); the encoder's zero padding is not media. const ssbLength = block.ssbLength ?? config.k; if (recoveredBlock.byteLength !== symbolSize * ssbLength) { emit({ type: "error", code: "recovered-block-too-short", message: `[FecWorker] recovered block has the wrong length: got ${recoveredBlock.byteLength}B, expected ${symbolSize * ssbLength}B (SSB_length=${ssbLength})`, }); return; } for (let i = 0; i < ssbLength; i++) { const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize); // ssbg_mode1 symbols are emitted whole: the router trims them by their // length field. A field past T-2 or non-zero padding means the // recovered symbol is not what was sent, so it is refused, not trimmed. const invalid = invalidSsbg1Symbol(symbol, symbolSize); if (invalid !== null) { emit({ type: "error", code: "recovered-ssbg1-symbol-invalid", message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`, }); continue; } const buf = toOwnedArrayBuffer(symbol); const meta: FrameMeta = { recovered: true, sbn, esi: i, ssbLength, }; emit({ type: "frame", data: buf, meta }, [buf]); } } /** Why a T-byte ssbg_mode1 symbol is malformed (C.3.3), or null when it is well formed. */ function invalidSsbg1Symbol(symbol: Uint8Array, symbolSize: number): string | null { const length = (symbol[0]! << 8) | symbol[1]!; const maxLength = symbolSize - SSBG1_LENGTH_FIELD_BYTES; if (length > maxLength) { return `length field ${length} exceeds T-2 = ${maxLength}`; } for (let j = SSBG1_LENGTH_FIELD_BYTES + length; j < symbolSize; j++) { if (symbol[j] !== 0) return `non-zero padding at byte ${j} after a ${length}-byte packet`; } return null; } function startSnapshotTimer(): void { if (snapshotTimer !== null) clearInterval(snapshotTimer); snapshotTimer = setInterval(() => emitSnapshot(), 200); } function resolveWasmBindings(): WasmBindings { const bindings = (self as unknown as Record) .__MMT_WASM_BINDINGS__ as WasmBindings | undefined; if (!bindings) { throw new Error( "[FecWorker] WASM bindings not available. Set self.__MMT_WASM_BINDINGS__ = { initSync, MmtFecDecoder } before configure.", ); } return bindings; } /** * Refuse a decoder that does not run the configured block profile: one that * ignored or predates the constructor argument would apply one profile's * block math to the other's wire. */ function verifyDecoderProfile(decoder: WasmFecDecoder, expected: FecBlockProfile): void { let actual: unknown; try { actual = decoder.get_block_profile(); } catch (err) { decoder.free(); throw new Error( `[FecWorker] configure: decoder cannot report its block profile (${err instanceof Error ? err.message : String(err)})`, ); } if (actual !== expected) { decoder.free(); throw new Error( `[FecWorker] configure: decoder runs block profile ${JSON.stringify(actual)}, not the configured "${expected}"`, ); } } function acceptId(ids: Map>, key: number, id: number): void { let set = ids.get(key); if (!set) { set = new Set(); ids.set(key, set); } set.add(id); } /** A block the decoder returned: record its recovery and emit its source symbols. */ function recordRecovered(key: number, block: FecBlockSnapshot, result: Uint8Array): void { const recovered = new Uint8Array(toOwnedArrayBuffer(result)); const recoveryMs = performance.now() - block.firstSymbolTs; recoveryTimes.push(recoveryMs); // Cap recovery times array at 1000 entries if (recoveryTimes.length > 1000) recoveryTimes.shift(); if (block.state === "collecting") { markBlockTerminal(block, "recovered"); blocksRecovered++; emitBlockUpdate(block); } emitRecoveredSymbols(key, recovered, block); } /** Settle a block after one of its source symbols was accepted. */ function settleSourceArrival( key: number, block: FecBlockSnapshot, result: Uint8Array | undefined, ): void { // Check if block is complete (all authored source symbols received: // SSB_length once a repair symbol declared it, otherwise K) const authoredLength = block.ssbLength ?? block.k; if ( block.sourceReceived >= authoredLength && block.state === "collecting" ) { markBlockTerminal(block, "complete"); blocksComplete++; emitBlockUpdate(block); } // A source-triggered return can be either clean block completion or recovery // of earlier missing symbols after repair arrived. Clean completion is // already routed by the source path, so only emit when the block did not // reach K source arrivals. if (result && result.byteLength > 0 && block.sourceReceived < authoredLength) { recordRecovered(key, block, result); } synchronizeDecoderHorizon(); } /** Settle a block after one of its repair symbols was accepted. */ function settleRepairArrival( key: number, block: FecBlockSnapshot, ssbLength: number, result: Uint8Array | undefined, ): void { // Every authored source symbol already arrived: knowing SSB_length makes // the block clean. The decoder's return is the source already routed. // Otherwise, returned data is the recovered source block: its SSB_length // fixed-width authored symbols concatenated. Split it before emitting so // MmtpRouter gets the same one-symbol shape as a direct source packet. if (block.sourceReceived >= ssbLength && block.state === "collecting") { markBlockTerminal(block, "complete"); blocksComplete++; emitBlockUpdate(block); } else if (result && result.byteLength > 0) { recordRecovered(key, block, result); } synchronizeDecoderHorizon(); } /** * iso-ssbg1-v1 source admission. A source inside a known block is accounted * at ESI = SS_ID - SS_Start. One no repair has placed yet is held unplaced, * deduplicated by SS_ID, and still fed to the decoder, which holds it too, * until a repair names a range containing it. */ function feedIsoSource( cmd: Extract, view: Uint8Array, decoder: WasmFecDecoder, cfg: FecTrackConfig, ): void { const maxPacketLength = cfg.symbolSize - SSBG1_LENGTH_FIELD_BYTES; if (view.byteLength > maxPacketLength) { throw new Error( `[FecWorker] feedSource: iso-ssbg1-v1 protected packet of ${view.byteLength}B exceeds T-2 = ${maxPacketLength}B`, ); } const block = findIsoBlock(cmd.ssId); if (!block) { if (isRetiredSbn(cmd.ssId)) { sourceDroppedRetired++; return; } if (unplacedSources.has(cmd.ssId)) { sourceDroppedDuplicate++; return; } const result = decoder.add_source(cmd.ssId, view, cmd.ts); advanceMediaHorizon(cmd.ssId); unplacedSources.set(cmd.ssId, performance.now()); sourceSymbols++; // No repair has named this source's block, so the decoder cannot have // one to recover; data here is a Worker/decoder disagreement that // cannot be attributed to a block, and is refused rather than guessed. if (result && result.byteLength > 0) { emit({ type: "error", code: "recovered-block-unplaced", message: `[FecWorker] Dropping a ${result.byteLength}B recovered block returned for SS_ID ${cmd.ssId}, which no known block contains`, }); } synchronizeDecoderHorizon(); return; } const key = block.sbn; if (isRetiredSbn(key)) { sourceDroppedRetired++; return; } if (isTerminalBlock(block)) { sourceDroppedTerminal++; return; } const esi = serialDistance(key, cmd.ssId); if (acceptedSourceEsis.get(key)?.has(esi)) { sourceDroppedDuplicate++; return; } const result = decoder.add_source(cmd.ssId, view, cmd.ts); advanceMediaHorizon(cmd.ssId); acceptId(acceptedSourceEsis, key, esi); block.sourceReceived++; block.lastSymbolTs = performance.now(); sourceSymbols++; settleSourceArrival(key, block, result); } /** iso-ssbg1-v1: a known block sharing an SS_ID with `[ssStart, ssStart + ssbLength)`. */ function findOverlappingIsoBlock( ssStart: number, ssbLength: number, ): FecBlockSnapshot | undefined { for (const block of blocks.values()) { if (isoBlocksOverlap(block.sbn, isoKnownLength(block), ssStart, ssbLength)) return block; } return undefined; } /** * iso-ssbg1-v1: create the block a repair first named, with K' = SSB_length, * and move every unplaced source in its range into its accounting. */ function placeIsoBlock(ssStart: number, ssbLength: number): FecBlockSnapshot { const block = getOrCreateBlock(ssStart, ssbLength); for (let esi = 0; esi < ssbLength; esi++) { const ssId = (ssStart + esi) % SS_ID_SPAN; const arrivalTs = unplacedSources.get(ssId); if (arrivalTs === undefined) continue; unplacedSources.delete(ssId); acceptId(acceptedSourceEsis, ssStart, esi); block.sourceReceived++; if (arrivalTs < block.firstSymbolTs) block.firstSymbolTs = arrivalTs; } return block; } /** * iso-ssbg1-v1 repair admission. The block is `[SS_Start, SS_Start + * SSB_length)` keyed by SS_Start, with repair ESI = SSB_length + RS_ID. A * known block's SSB_length is checked before deduplication because the * dedup key depends on it. */ function feedIsoRepair( cmd: Extract, view: Uint8Array, decoder: WasmFecDecoder, ): void { const key = cmd.ssStart; if (isRetiredSbn(key)) return; const existingBlock = blocks.get(key); if (existingBlock && isTerminalBlock(existingBlock)) return; if (existingBlock) { if (existingBlock.ssbLength !== cmd.ssbLength) { emit({ type: "error", code: "repair-ssb-length-conflict", message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (ssStart=${key})`, }); return; } } else { const overlapped = findOverlappingIsoBlock(key, cmd.ssbLength); if (overlapped) { emit({ type: "error", code: "repair-block-overlap", message: `[FecWorker] Dropping repair symbol whose block (ssStart=${key}, SSB_length=${cmd.ssbLength}) overlaps known block (ssStart=${overlapped.sbn}, SSB_length=${isoKnownLength(overlapped)})`, }); return; } } const repairEsi = cmd.ssbLength + cmd.rsId; if (acceptedRepairEsis.get(key)?.has(repairEsi)) return; const result = decoder.add_repair(key, cmd.ssbLength, cmd.rsId, view, cmd.ts); advanceMediaHorizon(key); const block = existingBlock ?? placeIsoBlock(key, cmd.ssbLength); acceptId(acceptedRepairEsis, key, repairEsi); block.repairReceived++; block.lastSymbolTs = performance.now(); repairSymbols++; settleRepairArrival(key, block, cmd.ssbLength, result); } // --- Command handlers --- async function handleConfigure( cmd: Extract, ): Promise { if (!cmd.wasmModule) { throw new Error("[FecWorker] configure: wasmModule required"); } if (!cmd.config) { throw new Error("[FecWorker] configure: config required"); } // The block profile is the one discriminator for two wire-identical block // maths, so it is required and never defaulted (BLO-35597). const blockProfile: unknown = cmd.config.blockProfile; if (blockProfile === undefined) { throw new Error( '[FecWorker] configure: blockProfile is required: name the FEC block profile ("iso-ssbg1-v1")', ); } if (blockProfile !== "iso-ssbg1-v1") { throw new Error( `[FecWorker] configure: blockProfile must be "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`, ); } if ( !Number.isFinite(cmd.config.interleaveDepth) || !Number.isInteger(cmd.config.interleaveDepth) || cmd.config.interleaveDepth <= 0 || cmd.config.interleaveDepth > MAX_INTERLEAVE_DEPTH ) { throw new Error( `[FecWorker] configure: interleaveDepth must be an integer in 1..${MAX_INTERLEAVE_DEPTH} (got ${cmd.config.interleaveDepth})`, ); } if ( !Number.isFinite(cmd.config.interleaveMs) || !Number.isSafeInteger(cmd.config.interleaveMs) || cmd.config.interleaveMs < 0 ) { throw new Error( `[FecWorker] configure: interleaveMs must be a finite non-negative integer (got ${cmd.config.interleaveMs})`, ); } if ( !Number.isFinite(cmd.config.deliveryWindowMs) || cmd.config.deliveryWindowMs <= 0 ) { throw new Error( `[FecWorker] configure: deliveryWindowMs must be > 0 (got ${cmd.config.deliveryWindowMs})`, ); } if ( !Number.isInteger(cmd.config.k) || cmd.config.k <= 0 || cmd.config.k > MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK ) { throw new Error( `[FecWorker] configure: k must be an integer in 1..${MAX_RAPTORQ_SOURCE_SYMBOLS_PER_BLOCK} (got ${cmd.config.k})`, ); } if ( !Number.isInteger(cmd.config.symbolSize) || cmd.config.symbolSize <= 0 || cmd.config.symbolSize > MAX_SYMBOL_SIZE || cmd.config.symbolSize % 8 !== 0 ) { throw new Error( `[FecWorker] configure: symbolSize must be an 8-byte-aligned integer in 8..${MAX_SYMBOL_SIZE} (got ${cmd.config.symbolSize})`, ); } const wasm = resolveWasmBindings(); // Initialize WASM from pre-compiled Module (no fetch, no re-compile) wasm.initSync({ module: cmd.wasmModule }); // A configure command starts a new decoder epoch. No terminal tombstone, // accepted-symbol ID, or active block from the previous decoder may affect it. wasmDecoder?.free(); wasmDecoder = null; clearBlockTracking(); // Decoder policy and geometry are explicit at the epoch boundary. The WASM // module has no guessed K/T/interleave slow path. const decoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth, blockProfile); verifyDecoderProfile(decoder, blockProfile); wasmDecoder = decoder; config = cmd.config; wasmDecoder.configure_params(config.k, config.symbolSize); // Tell the client which optional commands this Worker accepts, so it can // batch symbols without risking a Worker that predates the batch command. emit({ type: "capabilities", commands: ["feedSymbols", "subscribeBlockUpdates"] }); // Start periodic snapshot emission (200ms floor) startSnapshotTimer(); // Emit initial snapshot emitSnapshot(); } function handleFeedSource( cmd: Extract, ): void { if (!wasmDecoder) { throw new Error("[FecWorker] feedSource: not configured"); } if (!config) { throw new Error("[FecWorker] feedSource: config missing"); } if ( !isUint32(cmd.ssId) || !(cmd.data instanceof ArrayBuffer) ) { throw new Error("[FecWorker] feedSource: invalid source symbol"); } feedIsoSource(cmd, new Uint8Array(cmd.data), wasmDecoder, config); } function handleFeedRepair( cmd: Extract, ): void { if (!wasmDecoder) { throw new Error("[FecWorker] feedRepair: not configured"); } if (!config) { throw new Error("[FecWorker] feedRepair: config missing"); } // iso-ssbg1-v1 blocks start at any SS_ID and repair ESI = SSB_length + RS_ID. if ( !isUint32(cmd.ssStart) || !isUint32(cmd.ssbLength) || cmd.ssbLength <= 0 || cmd.ssbLength > config.k || !isUint32(cmd.rsId) || cmd.ssbLength + cmd.rsId > UINT24_MAX || !(cmd.data instanceof ArrayBuffer) ) { throw new Error("[FecWorker] feedRepair: invalid repair symbol"); } const view = new Uint8Array(cmd.data); if (config.symbolSize > 0 && view.byteLength !== config.symbolSize) { repairSymbolSizeMismatch++; if (repairSymbolSizeMismatch <= 5 || repairSymbolSizeMismatch % 100 === 0) { emit({ type: "error", code: "repair-symbol-size-mismatch", message: `[FecWorker] Dropping repair symbol with invalid size: got ${view.byteLength}B, expected ${config.symbolSize}B (ssStart=${cmd.ssStart}, ssbLength=${cmd.ssbLength}, rsId=${cmd.rsId})`, }); } return; } feedIsoRepair(cmd, view, wasmDecoder); } function handleRelayBlockSig( cmd: Extract, ): void { // No-op AUTH stub: store sig on block, mark as not-yet-verified const block = blocks.get(cmd.sbn); if (block) { // Relay block signatures are not verified here; source provenance is the // signed per-group manifest checked at router admission. block.relayBlockSigValid = null; emitBlockUpdate(block); } } function handleCleanup( cmd: Extract, ): void { if ( !Array.isArray(cmd.sbns) || cmd.sbns.length === 0 || !cmd.sbns.every(isUint32) ) { throw new Error("[FecWorker] cleanup: invalid SBNs"); } const sbns = [...new Set(cmd.sbns)].sort((a, b) => a - b); const pendingSbns = sbns.filter((sbn) => !isRetiredSbn(sbn)); try { if (!wasmDecoder) { throw new Error("[FecWorker] cleanup: not configured"); } // Call WASM cleanup only after all earlier symbol commands have drained, // and only for the exact SBNs the manager's deadlines named; command // coalescing must not infer ownership from the largest timed-out SBN. if (pendingSbns.length > 0) { wasmDecoder.cleanup_blocks_detailed(Uint32Array.from(pendingSbns)); } // Manager deadlines are block-scoped and may expire a newer SBN while an // older interleaved block is still live. Only accepted media progression // advances the shared JS/Rust horizon; cleanup creates exact tombstones. // Rust already finalized every SBN behind the media frontier. For each // remaining exact request, mirror the block-scoped WASM boundary immediately // so JS never retains state for a decoder block that is gone. for (const sbn of pendingSbns) { let currentBlock = blocks.get(sbn); if (currentBlock && isTerminalBlock(currentBlock)) { retireAcceptedIds(sbn); continue; } currentBlock ??= getOrCreateBlock(sbn); // Compare with the block's SSB_length; a key no repair declared has // none, so a block the Worker never knew can only have failed. const authoredLength = currentBlock.ssbLength; if (authoredLength !== undefined && currentBlock.sourceReceived >= authoredLength) { markBlockTerminal(currentBlock, "complete"); blocksComplete++; } else { markBlockTerminal(currentBlock, "failed"); blocksFailed++; } emitBlockUpdate(currentBlock); } } finally { // Message ordering makes this an acknowledgement that every source/repair // command posted before cleanup has completed. The main thread must not // commit failure accounting until it observes this event. emit({ type: "cleanupComplete", sbns }); } } function handleDispose(): void { // Clear snapshot timer (MUST clear to prevent leaks) if (snapshotTimer !== null) { clearInterval(snapshotTimer); snapshotTimer = null; } // Free WASM decoder (MUST call to release WASM memory) if (wasmDecoder) { wasmDecoder.free(); wasmDecoder = null; } config = null; clearBlockTracking(); } function handleReset(): void { try { wasmDecoder?.flush(); wasmDecoder?.reset_stats(); } catch (err) { emit({ type: "error", code: "reset-failed", message: err instanceof Error ? `[FecWorker] reset failed: ${err.message}` : `[FecWorker] reset failed: ${String(err)}`, }); return; } clearBlockTracking(); sourceSymbols = 0; repairSymbols = 0; blocksComplete = 0; blocksRecovered = 0; blocksFailed = 0; relayBlocksVerified = 0; relayBlocksFailed = 0; repairSymbolSizeMismatch = 0; sourceDroppedRetired = 0; sourceDroppedTerminal = 0; sourceDroppedDuplicate = 0; sourceUnplacedRetired = 0; emit({ type: "resetComplete" }); if (config) emitSnapshot(); } /** * A batch of symbols fed in one page task (BLO-36599). Each record is * handled exactly as its single-symbol command would be, in order, and a * failing record reports the same `command-failed` error that command would * without stopping the records after it. */ function handleFeedSymbols( cmd: Extract, ): void { if (!Array.isArray(cmd.symbols)) { throw new Error("[FecWorker] feedSymbols: symbols must be an array"); } for (const symbol of cmd.symbols) { try { if (symbol?.type === "feedSource") handleFeedSource(symbol); else if (symbol?.type === "feedRepair") handleFeedRepair(symbol); else { throw new Error( `[FecWorker] feedSymbols: unknown symbol type ${String((symbol as { type?: unknown } | null)?.type)}`, ); } } catch (err) { emit({ type: "error", code: "command-failed", message: err instanceof Error ? err.message : `[FecWorker] ${String(err)}`, }); } } } // --- Main message handler --- async function dispatchCommand(cmd: FecWorkerCommand): Promise { try { switch (cmd.type) { case "configure": // Await handleConfigure so feed/cleanup messages cannot run against a partially // configured decoder. await handleConfigure(cmd); break; case "feedSource": handleFeedSource(cmd); break; case "feedRepair": handleFeedRepair(cmd); break; case "feedSymbols": handleFeedSymbols(cmd); break; case "relayBlockSig": handleRelayBlockSig(cmd); break; case "cleanup": handleCleanup(cmd); break; case "reset": handleReset(); break; case "subscribeBlockUpdates": blockUpdatesSubscribed = true; break; case "dispose": handleDispose(); break; default: emit({ type: "error", code: "unknown-command", message: `Unknown command type: ${(cmd as Record).type}`, }); } } catch (err) { emit({ type: "error", code: "command-failed", message: err instanceof Error ? err.message : `[FecWorker] ${String(err)}`, }); } } let commandQueue: Promise = Promise.resolve(); self.onmessage = (e: MessageEvent) => { const cmd = e.data; commandQueue = commandQueue.then(() => dispatchCommand(cmd)); };