@blockcast/fec-worker 0.3.0-main.d9354616fdf2 → 0.3.0-main.dd5b3a8dcbc8

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.
@@ -1,46 +1,12 @@
1
1
  /**
2
- * Order of K-stride FEC blocks on the 32-bit SS_ID (ISO/IEC 23008-1:2023
3
- * C.5.2; RFC 1982 serial arithmetic, SERIAL_BITS = 32; BLO-35597 iso:5).
4
- *
5
- * Block `sbn` starts at SS_ID `sbn * K mod 2^32`, and blocks are ordered by
6
- * serial comparison of their starts. That is identical to comparing SBNs
7
- * with < for any two blocks less than 2^31 SS_IDs apart, so only the order
8
- * across the SS_ID wrap changes.
9
- *
10
- * The canonical helpers are @blockcast/mmt-base `kStrideBlockPrecedes` /
11
- * `kStrideBlocksBehind`. This worker bundle does not depend on mmt-base, so
12
- * it carries these two, and block-order.test.ts pins them to the same wrap
13
- * vectors.
2
+ * Order of iso-ssbg1-v1 FEC blocks on the 32-bit SS_ID (ISO/IEC 23008-1:2023
3
+ * C.5.2/C.5.3; RFC 1982 serial arithmetic, SERIAL_BITS = 32; BLO-35597 iso:5).
4
+ * The K-stride (SBN * K) order of the retired k-padded-v1 profile is gone.
14
5
  */
15
6
 
16
7
  const SPAN = 0x1_0000_0000;
17
8
  const HALF = 0x8000_0000;
18
9
 
19
- function blockStart(sbn: number, k: number): number {
20
- // sbn < 2^32 and k <= 56 403 keep the product below 2^48: exact.
21
- return (sbn * k) % SPAN;
22
- }
23
-
24
- /** Forward SS_ID distance from block `a`'s start to block `b`'s, or null when `a` does not precede `b`. */
25
- function precedingDistance(a: number, b: number, k: number): number | null {
26
- const distance = (blockStart(b, k) - blockStart(a, k) + SPAN) % SPAN;
27
- return distance !== 0 && distance < HALF ? distance : null;
28
- }
29
-
30
- /** Whether block `a` precedes block `b`. */
31
- export function blockPrecedes(a: number, b: number, k: number): boolean {
32
- return precedingDistance(a, b, k) !== null;
33
- }
34
-
35
- /**
36
- * How many blocks `a` lies behind `b`: 0 unless `a` precedes `b`. Across the
37
- * wrap the short pre-wrap block (2^32 mod K symbols) counts as one block.
38
- */
39
- export function blocksBehind(a: number, b: number, k: number): number {
40
- const distance = precedingDistance(a, b, k);
41
- return distance === null ? 0 : Math.ceil(distance / k);
42
- }
43
-
44
10
  // --- iso-ssbg1-v1 (ISO/IEC 23008-1 C.3.3 ssbg_mode1; C.5.2/C.5.3) ---------
45
11
  //
46
12
  // Under the ISO profile SS_ID increments by one per FEC source packet and
@@ -137,8 +137,7 @@ export class FecWorkerClient {
137
137
  * Caller must not access data after this call — it is detached.
138
138
  *
139
139
  * Carries the wire-level Repair FEC Payload ID fields (ISO 23008-1 §C.5.3):
140
- * - ssStart: SS_ID of first source symbol in the block (= SBN * configured K
141
- * under `k-padded-v1`; any uint32, the block key, under `iso-ssbg1-v1`)
140
+ * - ssStart: SS_ID of first source symbol in the block (any uint32; the block key)
142
141
  * - ssbLength: authored source-symbol count (1..configured K)
143
142
  * - rsId: repair symbol index within the block
144
143
  */
@@ -199,7 +198,7 @@ export class FecWorkerClient {
199
198
 
200
199
  /**
201
200
  * Trigger cleanup after a block's inactivity deadline. `currentSbn` is the
202
- * block key: the SBN under `k-padded-v1`, the SS_Start under `iso-ssbg1-v1`.
201
+ * block key, the block's SS_Start.
203
202
  *
204
203
  * While one queue-drain barrier is in flight, later SBNs are coalesced into
205
204
  * the next command without losing their individual acknowledgement identity.
@@ -12,19 +12,16 @@
12
12
 
13
13
  /**
14
14
  * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C): the catalog
15
- * `fec.blockProfile` token. Both profiles carry identical wire tuples whose
16
- * block math differs, so it is required and never inferred.
17
- *
18
- * - `k-padded-v1` (legacy): SBN = floor(SS_ID / K), ESI = SS_ID % K;
19
- * SS_Start = SBN * K; repair ESI = K + RS_ID.
20
- * - `iso-ssbg1-v1` (C.3.3 ssbg_mode1): SS_ID increments by one per source
21
- * packet mod 2^32; a block is `[SS_Start, SS_Start + SSB_length)` keyed by
22
- * SS_Start; `k` is only the maximum SSB_length; repair ESI =
23
- * SSB_length + RS_ID.
15
+ * `fec.blockProfile` token. Required and never inferred; the only profile is
16
+ * `iso-ssbg1-v1` (C.3.3 ssbg_mode1): SS_ID increments by one per source
17
+ * packet mod 2^32; a block is `[SS_Start, SS_Start + SSB_length)` keyed by
18
+ * SS_Start; `k` is only the maximum SSB_length; repair ESI =
19
+ * SSB_length + RS_ID. The retired `k-padded-v1` token is refused like any
20
+ * unknown one.
24
21
  *
25
22
  * Mirrors @blockcast/mmt-base `FecBlockProfile`.
26
23
  */
27
- export type FecBlockProfile = "k-padded-v1" | "iso-ssbg1-v1";
24
+ export type FecBlockProfile = "iso-ssbg1-v1";
28
25
 
29
26
  /** Per-track FEC configuration. Matches design spec section 3.2 FecTrackConfig. */
30
27
  export interface FecTrackConfig {
@@ -51,12 +48,11 @@ export interface FecTrackConfig {
51
48
  repairCount: number;
52
49
  /** Bytes per symbol */
53
50
  symbolSize: number;
54
- /** Retained decoder horizon as a BLOCK COUNT (SBN distance), not D.
55
- * `isRetiredSbn` retires blocks more than this many blocks behind
56
- * `liveSbn` (serial order on SS_ID, BLO-35597 iso:5) and
57
- * `MmtFecDecoder::new` documents it as "the number of interleaved blocks".
58
- * Under `iso-ssbg1-v1` the distance is `isoBlocksBehind(SS_ID, live SS_ID, k)`
59
- * (block-order.ts), the lower bound every ISO receiver layer shares.
51
+ /** Retained decoder horizon as a BLOCK COUNT, not D. `isRetiredSbn`
52
+ * retires blocks more than this many blocks behind the live SS_ID, by
53
+ * `isoBlocksBehind(SS_ID, live SS_ID, k)` (block-order.ts), the lower
54
+ * bound every ISO receiver layer shares, and `MmtFecDecoder::new`
55
+ * documents it as "the number of interleaved blocks".
60
56
  * Never assign `deriveFecInterleaveDepth()` here — that returns media units
61
57
  * per block, which is a different quantity (BLO-35597). */
62
58
  interleaveDepth: number;
@@ -79,8 +75,7 @@ export interface FecTrackConfig {
79
75
 
80
76
  /** Per-block snapshot for live block visualization and stats. */
81
77
  export interface FecBlockSnapshot {
82
- /** Block key: the source block number under `k-padded-v1`, the block's
83
- * SS_Start under `iso-ssbg1-v1`. */
78
+ /** Block key: the block's SS_Start. */
84
79
  sbn: number;
85
80
  /** Current block state */
86
81
  state: "collecting" | "complete" | "recovered" | "failed";
@@ -168,8 +163,7 @@ export interface FecTrackStats {
168
163
  /** Blocks with invalid relay signature */
169
164
  relayBlocksFailed: number;
170
165
 
171
- /** Live media position: the newest source block number under
172
- * `k-padded-v1`, the newest accepted SS_ID / SS_Start under `iso-ssbg1-v1`. */
166
+ /** Live media position: the newest accepted SS_ID / SS_Start. */
173
167
  liveSbn: number;
174
168
  /** Active block snapshots */
175
169
  blocks: FecBlockSnapshot[];
@@ -220,13 +214,14 @@ export interface FecTrackStats {
220
214
  */
221
215
  sourceDroppedDuplicate?: number;
222
216
  /**
223
- * `iso-ssbg1-v1` only: accepted source symbols whose block never became
224
- * known (no repair symbol named a range containing them) before their
225
- * SS_ID fell behind the retained horizon. They were already fed to the
226
- * decoder and routed directly; this counts the bookkeeping retired with
227
- * them, i.e. evidence that a block's repair symbols never arrived.
217
+ * Accepted source symbols whose block never became known (no repair
218
+ * symbol named a range containing them) before their SS_ID fell behind
219
+ * the retained horizon. They were already fed to the decoder and routed
220
+ * directly; this counts the bookkeeping retired with them, i.e. evidence
221
+ * that a block's repair symbols never arrived. The decoder retires its
222
+ * own copies by the same horizon inside `cleanup_by_ss_start`.
228
223
  */
229
- sourceUnplacedRetired?: number;
224
+ sourceUnplacedRetired: number;
230
225
  /** Native decoder counters, converted to numbers so browser diagnostics remain JSON-safe. */
231
226
  decoder?: {
232
227
  sourcePackets: number;
@@ -266,7 +261,7 @@ export interface FrameMeta {
266
261
  altaVerified: boolean | null;
267
262
  /** Whether this frame was recovered via FEC (not received directly) */
268
263
  recovered: boolean;
269
- /** Block key this frame belongs to: SBN (`k-padded-v1`) or SS_Start (`iso-ssbg1-v1`) */
264
+ /** Block key this frame belongs to: the block's SS_Start */
270
265
  sbn: number;
271
266
  /** Encoding Symbol ID within the recovered source block (0..ssbLength-1).
272
267
  * Under `iso-ssbg1-v1` a recovered frame is the full T-byte ssbg_mode1
@@ -301,13 +296,12 @@ export type FecWorkerCommand =
301
296
  | {
302
297
  type: "feedSource";
303
298
  /** Flat 32-bit Source Symbol ID from the MMTP Source FEC Payload ID
304
- * (ISO 23008-1 §C.5.2). Under `k-padded-v1` the WASM decoder derives
305
- * SBN = floor(ssId/K) and ESI = ssId % K internally — do NOT
306
- * pre-compute. Under `iso-ssbg1-v1` SS_IDs are consecutive per source
307
- * packet mod 2^32 and the block comes from a repair's range. */
299
+ * (ISO 23008-1 §C.5.2). SS_IDs are consecutive per source packet mod
300
+ * 2^32 and the block comes from a repair's range — do NOT
301
+ * pre-compute a block. */
308
302
  ssId: number;
309
- /** `k-padded-v1`: the source symbol. `iso-ssbg1-v1`: the protected
310
- * packet exactly, unpadded and without its SS_ID (at most T-2 bytes). */
303
+ /** The protected packet exactly, unpadded and without its SS_ID (at
304
+ * most T-2 bytes). */
311
305
  data: ArrayBuffer;
312
306
  ts: number;
313
307
  /** Optional ALTA authenticator bytes (detached mode — 08-07 MMTP
@@ -326,14 +320,13 @@ export type FecWorkerCommand =
326
320
  | {
327
321
  type: "feedRepair";
328
322
  /** SS_Start: SS_ID of the first source symbol in the block, from the
329
- * MMTP Repair FEC Payload ID (ISO 23008-1 §C.5.3). `k-padded-v1`:
330
- * = SBN * configured K. `iso-ssbg1-v1`: any uint32; the block key. */
323
+ * MMTP Repair FEC Payload ID (ISO 23008-1 §C.5.3): any uint32; the
324
+ * block key. */
331
325
  ssStart: number;
332
326
  /** Authored source-symbol count for this block; 1..configured K. */
333
327
  ssbLength: number;
334
328
  /** RS_ID: repair symbol index within the block; WASM maps to repair
335
- * ESI = configured K + RS_ID (`k-padded-v1`) or SSB_length + RS_ID
336
- * (`iso-ssbg1-v1`). */
329
+ * ESI = SSB_length + RS_ID. */
337
330
  rsId: number;
338
331
  data: ArrayBuffer;
339
332
  ts: number;
@@ -358,7 +351,7 @@ export type FecWorkerCommand =
358
351
  | {
359
352
  type: "cleanup";
360
353
  /** Every timed-out block covered by this coalesced queue-drain barrier,
361
- * by block key: SBN (`k-padded-v1`) or SS_Start (`iso-ssbg1-v1`). */
354
+ * by block key (SS_Start). */
362
355
  sbns: number[];
363
356
  }
364
357
  | { type: "reset" }
package/src/fec-worker.ts CHANGED
@@ -22,8 +22,6 @@ import type {
22
22
  } from "./fec-worker-types.js";
23
23
  import { toOwnedArrayBuffer } from "./transfer.js";
24
24
  import {
25
- blockPrecedes,
26
- blocksBehind,
27
25
  isoBlockContains,
28
26
  isoBlocksBehind,
29
27
  isoBlocksOverlap,
@@ -36,10 +34,9 @@ import {
36
34
  // The decoder derives SBN/ESI internally — do not pre-compute.
37
35
  interface WasmFecDecoder {
38
36
  /**
39
- * ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID.
40
- * `k-padded-v1`: data is the source symbol. `iso-ssbg1-v1`: data is the
41
- * protected packet, unpadded; the decoder builds the ssbg_mode1 symbol and
42
- * holds it until a repair names the block containing it.
37
+ * ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID; data is
38
+ * the protected packet, unpadded. The decoder builds the ssbg_mode1 symbol
39
+ * and holds it until a repair names the block containing it.
43
40
  */
44
41
  add_source(
45
42
  ss_id: number,
@@ -47,11 +44,9 @@ interface WasmFecDecoder {
47
44
  timestamp: number,
48
45
  ): Uint8Array | undefined;
49
46
  /**
50
- * ISO 23008-1 §C.5.3. `k-padded-v1`: repair ESI = configured K + rs_id,
51
- * SBN = ss_start / configured K. `iso-ssbg1-v1`: the block is
52
- * `[ss_start, ss_start + ssb_length)` with K' = ssb_length and repair
53
- * ESI = ssb_length + rs_id; a recovered block is ssb_length x T bytes of
54
- * ssbg_mode1 symbols.
47
+ * ISO 23008-1 §C.5.3: the block is `[ss_start, ss_start + ssb_length)`
48
+ * with K' = ssb_length and repair ESI = ssb_length + rs_id; a recovered
49
+ * block is ssb_length x T bytes of ssbg_mode1 symbols.
55
50
  */
56
51
  add_repair(
57
52
  ss_start: number,
@@ -68,14 +63,12 @@ interface WasmFecDecoder {
68
63
  */
69
64
  cleanup_blocks_detailed(sbns: Uint32Array): void;
70
65
  /**
71
- * `k-padded-v1` only (throws under `iso-ssbg1-v1`): remove decoder blocks
72
- * below its configured live-SBN interleave horizon; returns their SBNs.
73
- */
74
- cleanup_by_sbn(current_sbn: number): Uint32Array;
75
- /**
76
- * `iso-ssbg1-v1` only: remove decoder blocks, and held unplaced sources,
77
- * more than interleaveDepth `isoBlocksBehind` the live SS_ID; returns the
78
- * SS_Starts of the blocks it removed.
66
+ * Remove decoder blocks, and held unplaced sources, more than
67
+ * interleaveDepth `isoBlocksBehind` the live SS_ID; returns the SS_Starts
68
+ * of the blocks it removed. It runs the decoder's `retire_unplaced` sweep
69
+ * itself, so the Worker never calls that separately: synchronizeDecoderHorizon
70
+ * calls this after every source and repair it accepts, which is what keeps
71
+ * the decoder's unplaced pool bounded when a block's repairs never arrive.
79
72
  */
80
73
  cleanup_by_ss_start(current: number): Uint32Array;
81
74
  /** The block profile the decoder was constructed with. */
@@ -255,15 +248,9 @@ function clearBlockTracking(): void {
255
248
  */
256
249
  function isRetiredSbn(sbn: number): boolean {
257
250
  if (config === null || !liveSbnKnown) return false;
258
- // iso-ssbg1-v1: `sbn` is an SS_ID (a block's SS_Start or a source's own
259
- // SS_ID) and liveSbn the live SS_ID; K is only the maximum block length.
260
- return config.blockProfile === "iso-ssbg1-v1"
261
- ? isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth
262
- : blocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
263
- }
264
-
265
- function isIsoProfile(): boolean {
266
- return config?.blockProfile === "iso-ssbg1-v1";
251
+ // `sbn` is an SS_ID (a block's SS_Start or a source's own SS_ID) and
252
+ // liveSbn the live SS_ID; K is only the maximum block length.
253
+ return isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
267
254
  }
268
255
 
269
256
  /**
@@ -291,9 +278,7 @@ function findIsoBlock(ssId: number): FecBlockSnapshot | undefined {
291
278
  * retired, after which the SS_ID retirement check refuses every member.
292
279
  */
293
280
  function isRetiredBlock(key: number, block: FecBlockSnapshot): boolean {
294
- return isIsoProfile()
295
- ? isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN)
296
- : isRetiredSbn(key);
281
+ return isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN);
297
282
  }
298
283
 
299
284
  /**
@@ -310,15 +295,10 @@ function trimBlockRetention(): void {
310
295
  }
311
296
  }
312
297
 
313
- /** Advance the live position: an SBN (k-padded-v1) or an SS_ID / SS_Start (iso-ssbg1-v1). */
298
+ /** Advance the live position: an SS_ID / SS_Start, in serial order. */
314
299
  function advanceMediaHorizon(sbn: number): void {
315
300
  if (!config) return;
316
- if (liveSbnKnown) {
317
- const ahead = config.blockProfile === "iso-ssbg1-v1"
318
- ? serialPrecedes(liveSbn, sbn)
319
- : blockPrecedes(liveSbn, sbn, config.k);
320
- if (!ahead) return;
321
- }
301
+ if (liveSbnKnown && !serialPrecedes(liveSbn, sbn)) return;
322
302
  liveSbn = sbn;
323
303
  liveSbnKnown = true;
324
304
  trimBlockRetention();
@@ -336,10 +316,7 @@ function markBlockTerminal(
336
316
 
337
317
  function synchronizeDecoderHorizon(): void {
338
318
  if (!wasmDecoder || !liveSbnKnown) return;
339
- const iso = isIsoProfile();
340
- const expired = iso
341
- ? wasmDecoder.cleanup_by_ss_start(liveSbn)
342
- : wasmDecoder.cleanup_by_sbn(liveSbn);
319
+ const expired = wasmDecoder.cleanup_by_ss_start(liveSbn);
343
320
  for (const sbn of expired) {
344
321
  const block = blocks.get(sbn);
345
322
  if (!block || isTerminalBlock(block)) {
@@ -350,7 +327,7 @@ function synchronizeDecoderHorizon(): void {
350
327
  blocksFailed++;
351
328
  emitBlockUpdate(block);
352
329
  }
353
- if (iso) retireUnplacedSources();
330
+ retireUnplacedSources();
354
331
  trimBlockRetention();
355
332
  }
356
333
 
@@ -583,8 +560,10 @@ function buildStatsSnapshot(): FecTrackStats {
583
560
 
584
561
  // Cap block array to interleave window
585
562
  const maxBlocks = Math.max(config.interleaveDepth * 3, 24);
563
+ // Newest first in SS_ID serial order, so the cap keeps the newest blocks
564
+ // across the 2^32 wrap too: every block start precedes the live SS_ID.
586
565
  const blockArray = Array.from(blocks.values())
587
- .sort((a, b) => b.sbn - a.sbn)
566
+ .sort((a, b) => serialDistance(a.sbn, liveSbn) - serialDistance(b.sbn, liveSbn))
588
567
  .slice(0, maxBlocks);
589
568
 
590
569
  return {
@@ -620,7 +599,7 @@ function buildStatsSnapshot(): FecTrackStats {
620
599
  sourceDroppedRetired,
621
600
  sourceDroppedTerminal,
622
601
  sourceDroppedDuplicate,
623
- ...(config.blockProfile === "iso-ssbg1-v1" ? { sourceUnplacedRetired } : {}),
602
+ sourceUnplacedRetired,
624
603
  decoder,
625
604
  };
626
605
  }
@@ -662,22 +641,19 @@ function emitRecoveredSymbols(
662
641
  return;
663
642
  }
664
643
 
665
- const iso = config.blockProfile === "iso-ssbg1-v1";
666
644
  for (let i = 0; i < ssbLength; i++) {
667
645
  const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
668
646
  // ssbg_mode1 symbols are emitted whole: the router trims them by their
669
647
  // length field. A field past T-2 or non-zero padding means the
670
648
  // recovered symbol is not what was sent, so it is refused, not trimmed.
671
- if (iso) {
672
- const invalid = invalidSsbg1Symbol(symbol, symbolSize);
673
- if (invalid !== null) {
674
- emit({
675
- type: "error",
676
- code: "recovered-ssbg1-symbol-invalid",
677
- message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`,
678
- });
679
- continue;
680
- }
649
+ const invalid = invalidSsbg1Symbol(symbol, symbolSize);
650
+ if (invalid !== null) {
651
+ emit({
652
+ type: "error",
653
+ code: "recovered-ssbg1-symbol-invalid",
654
+ message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`,
655
+ });
656
+ continue;
681
657
  }
682
658
 
683
659
  // Source authentication now runs once at router admission in
@@ -990,12 +966,12 @@ async function handleConfigure(
990
966
  const blockProfile: unknown = cmd.config.blockProfile;
991
967
  if (blockProfile === undefined) {
992
968
  throw new Error(
993
- '[FecWorker] configure: blockProfile is required: name the FEC block profile ("k-padded-v1" or "iso-ssbg1-v1")',
969
+ '[FecWorker] configure: blockProfile is required: name the FEC block profile ("iso-ssbg1-v1")',
994
970
  );
995
971
  }
996
- if (blockProfile !== "k-padded-v1" && blockProfile !== "iso-ssbg1-v1") {
972
+ if (blockProfile !== "iso-ssbg1-v1") {
997
973
  throw new Error(
998
- `[FecWorker] configure: blockProfile must be "k-padded-v1" or "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`,
974
+ `[FecWorker] configure: blockProfile must be "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`,
999
975
  );
1000
976
  }
1001
977
  if (
@@ -1134,51 +1110,7 @@ function handleFeedSource(
1134
1110
  throw new Error("[FecWorker] feedSource: invalid source symbol");
1135
1111
  }
1136
1112
 
1137
- const view = new Uint8Array(cmd.data);
1138
- if (config.blockProfile === "iso-ssbg1-v1") {
1139
- feedIsoSource(cmd, view, wasmDecoder, config);
1140
- return;
1141
- }
1142
-
1143
- // Derive SBN locally for block-level accounting. WASM derives the same
1144
- // value internally (ISO 23008-1 §C.5.2: SBN = floor(SS_ID / K)).
1145
- const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
1146
- const esi = cmd.ssId % config.k;
1147
- if (isRetiredSbn(sbn)) {
1148
- sourceDroppedRetired++;
1149
- return;
1150
- }
1151
- const existingBlock = blocks.get(sbn);
1152
- if (existingBlock && isTerminalBlock(existingBlock)) {
1153
- sourceDroppedTerminal++;
1154
- return;
1155
- }
1156
- if (acceptedSourceEsis.get(sbn)?.has(esi)) {
1157
- sourceDroppedDuplicate++;
1158
- return;
1159
- }
1160
-
1161
- const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
1162
- advanceMediaHorizon(sbn);
1163
- let sourceEsis = acceptedSourceEsis.get(sbn);
1164
- if (!sourceEsis) {
1165
- sourceEsis = new Set<number>();
1166
- acceptedSourceEsis.set(sbn, sourceEsis);
1167
- }
1168
- sourceEsis.add(esi);
1169
-
1170
- // Update block state
1171
- const block = getOrCreateBlock(sbn);
1172
- block.sourceReceived++;
1173
- block.lastSymbolTs = performance.now();
1174
- sourceSymbols++;
1175
-
1176
- // The FEC worker must remain decode-only on the per-symbol hot path. ALTA
1177
- // WASM is retained temporarily for low-rate signaling users, but media is
1178
- // authenticated by the canonical manifest verifier at router admission.
1179
- if (altaEnabled) altaPending++;
1180
-
1181
- settleSourceArrival(sbn, block, result);
1113
+ feedIsoSource(cmd, new Uint8Array(cmd.data), wasmDecoder, config);
1182
1114
  }
1183
1115
 
1184
1116
  function handleFeedRepair(
@@ -1190,17 +1122,14 @@ function handleFeedRepair(
1190
1122
  if (!config) {
1191
1123
  throw new Error("[FecWorker] feedRepair: config missing");
1192
1124
  }
1193
- // k-padded-v1 blocks start at multiples of K and repair ESI = K + RS_ID;
1194
1125
  // iso-ssbg1-v1 blocks start at any SS_ID and repair ESI = SSB_length + RS_ID.
1195
- const iso = config.blockProfile === "iso-ssbg1-v1";
1196
1126
  if (
1197
1127
  !isUint32(cmd.ssStart) ||
1198
1128
  !isUint32(cmd.ssbLength) ||
1199
1129
  cmd.ssbLength <= 0 ||
1200
1130
  cmd.ssbLength > config.k ||
1201
- (!iso && cmd.ssStart % config.k !== 0) ||
1202
1131
  !isUint32(cmd.rsId) ||
1203
- (iso ? cmd.ssbLength : config.k) + cmd.rsId > UINT24_MAX ||
1132
+ cmd.ssbLength + cmd.rsId > UINT24_MAX ||
1204
1133
  !(cmd.data instanceof ArrayBuffer)
1205
1134
  ) {
1206
1135
  throw new Error("[FecWorker] feedRepair: invalid repair symbol");
@@ -1218,51 +1147,7 @@ function handleFeedRepair(
1218
1147
  }
1219
1148
  return;
1220
1149
  }
1221
- if (iso) {
1222
- feedIsoRepair(cmd, view, wasmDecoder);
1223
- return;
1224
- }
1225
- const sbn = Math.floor(cmd.ssStart / config.k);
1226
- const repairEsi = config.k + cmd.rsId;
1227
- // The repair path's mirror drops stay silent and uncounted: BLO-35597
1228
- // defect 2 is a source-symbol shortfall, so only the source side needed
1229
- // evidence. The asymmetry is deliberate, not an oversight — extend these
1230
- // the same way if a repair-side shortfall ever needs attributing.
1231
- if (isRetiredSbn(sbn)) return;
1232
- const existingBlock = blocks.get(sbn);
1233
- if (existingBlock && isTerminalBlock(existingBlock)) return;
1234
- if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
1235
- // One block has one authored SSB_length; a repair symbol naming another
1236
- // is dropped before it reaches the decoder (which rejects it too).
1237
- if (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {
1238
- emit({
1239
- type: "error",
1240
- code: "repair-ssb-length-conflict",
1241
- message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,
1242
- });
1243
- return;
1244
- }
1245
-
1246
- const result = wasmDecoder.add_repair(
1247
- cmd.ssStart,
1248
- cmd.ssbLength,
1249
- cmd.rsId,
1250
- view,
1251
- cmd.ts,
1252
- );
1253
- advanceMediaHorizon(sbn);
1254
- let repairEsis = acceptedRepairEsis.get(sbn);
1255
- if (!repairEsis) {
1256
- repairEsis = new Set<number>();
1257
- acceptedRepairEsis.set(sbn, repairEsis);
1258
- }
1259
- repairEsis.add(repairEsi);
1260
- const block = getOrCreateBlock(sbn);
1261
- block.ssbLength = cmd.ssbLength;
1262
- block.repairReceived++;
1263
- block.lastSymbolTs = performance.now();
1264
- repairSymbols++;
1265
- settleRepairArrival(sbn, block, cmd.ssbLength, result);
1150
+ feedIsoRepair(cmd, view, wasmDecoder);
1266
1151
  }
1267
1152
 
1268
1153
  function handleRelayBlockSig(
@@ -1315,10 +1200,9 @@ function handleCleanup(
1315
1200
  continue;
1316
1201
  }
1317
1202
  currentBlock ??= getOrCreateBlock(sbn);
1318
- // k-padded-v1 compares with K, unchanged. iso-ssbg1-v1 compares with
1319
- // the block's SSB_length; a key no repair declared has none, so a
1320
- // block the Worker never knew can only have failed.
1321
- const authoredLength = isIsoProfile() ? currentBlock.ssbLength : currentBlock.k;
1203
+ // Compare with the block's SSB_length; a key no repair declared has
1204
+ // none, so a block the Worker never knew can only have failed.
1205
+ const authoredLength = currentBlock.ssbLength;
1322
1206
  if (authoredLength !== undefined && currentBlock.sourceReceived >= authoredLength) {
1323
1207
  markBlockTerminal(currentBlock, "complete");
1324
1208
  blocksComplete++;