@blockcast/fec-worker 0.1.0-main.c71423b8a4e4 → 0.1.0-main.ce3c8aeb7f68

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/src/fec-worker.ts CHANGED
@@ -75,6 +75,8 @@ let altaEnabled = false;
75
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
76
  let altaVerifier: any = null; // JsAltaVerifier instance (loaded from ALTA WASM)
77
77
  let snapshotTimer: ReturnType<typeof setInterval> | null = null;
78
+ // blockUpdate events are posted only after a client subscribes (BLO-36599).
79
+ let blockUpdatesSubscribed = false;
78
80
 
79
81
  // Block tracking
80
82
  const blocks = new Map<number, FecBlockSnapshot>();
@@ -478,6 +480,7 @@ function emitSnapshot(): void {
478
480
  }
479
481
 
480
482
  function emitBlockUpdate(block: FecBlockSnapshot): void {
483
+ if (!blockUpdatesSubscribed) return;
481
484
  emit({ type: "blockUpdate", block: { ...block } });
482
485
  }
483
486
 
@@ -496,16 +499,19 @@ function emitRecoveredSymbols(
496
499
  });
497
500
  return;
498
501
  }
499
- if (recoveredBlock.byteLength < symbolSize * config.k) {
502
+ // The decoder returns only the block's authored source symbols
503
+ // (SSB_length x T); the encoder's zero padding is not media.
504
+ const ssbLength = block.ssbLength ?? config.k;
505
+ if (recoveredBlock.byteLength !== symbolSize * ssbLength) {
500
506
  emit({
501
507
  type: "error",
502
508
  code: "recovered-block-too-short",
503
- message: `[FecWorker] recovered block too short: got ${recoveredBlock.byteLength}B, expected at least ${symbolSize * config.k}B`,
509
+ message: `[FecWorker] recovered block has the wrong length: got ${recoveredBlock.byteLength}B, expected ${symbolSize * ssbLength}B (SSB_length=${ssbLength})`,
504
510
  });
505
511
  return;
506
512
  }
507
513
 
508
- for (let i = 0; i < config.k; i++) {
514
+ for (let i = 0; i < ssbLength; i++) {
509
515
  const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
510
516
 
511
517
  // Source authentication now runs once at router admission in
@@ -520,6 +526,7 @@ function emitRecoveredSymbols(
520
526
  recovered: true,
521
527
  sbn,
522
528
  esi: i,
529
+ ssbLength,
523
530
  };
524
531
  emit({ type: "frame", data: buf, meta }, [buf]);
525
532
  }
@@ -662,6 +669,10 @@ async function handleConfigure(
662
669
 
663
670
  wasmDecoder.configure_params(config.k, config.symbolSize);
664
671
 
672
+ // Tell the client which optional commands this Worker accepts, so it can
673
+ // batch symbols without risking a Worker that predates the batch command.
674
+ emit({ type: "capabilities", commands: ["feedSymbols", "subscribeBlockUpdates"] });
675
+
665
676
  // Start periodic snapshot emission (200ms floor)
666
677
  startSnapshotTimer();
667
678
 
@@ -716,9 +727,11 @@ function handleFeedSource(
716
727
  // authenticated by the canonical manifest verifier at router admission.
717
728
  if (altaEnabled) altaPending++;
718
729
 
719
- // Check if block is complete (all K source symbols received)
730
+ // Check if block is complete (all authored source symbols received:
731
+ // SSB_length once a repair symbol declared it, otherwise K)
732
+ const authoredLength = block.ssbLength ?? block.k;
720
733
  if (
721
- block.sourceReceived >= block.k &&
734
+ block.sourceReceived >= authoredLength &&
722
735
  block.state === "collecting"
723
736
  ) {
724
737
  markBlockTerminal(block, "complete");
@@ -730,7 +743,7 @@ function handleFeedSource(
730
743
  // of earlier missing symbols after repair arrived. Clean completion is
731
744
  // already routed by the source path, so only emit when the block did not
732
745
  // reach K source arrivals.
733
- if (result && result.byteLength > 0 && block.sourceReceived < block.k) {
746
+ if (result && result.byteLength > 0 && block.sourceReceived < authoredLength) {
734
747
  const recovered = new Uint8Array(toOwnedArrayBuffer(result));
735
748
  const recoveryMs = performance.now() - block.firstSymbolTs;
736
749
  recoveryTimes.push(recoveryMs);
@@ -786,6 +799,16 @@ function handleFeedRepair(
786
799
  const existingBlock = blocks.get(sbn);
787
800
  if (existingBlock && isTerminalBlock(existingBlock)) return;
788
801
  if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
802
+ // One block has one authored SSB_length; a repair symbol naming another
803
+ // is dropped before it reaches the decoder (which rejects it too).
804
+ if (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {
805
+ emit({
806
+ type: "error",
807
+ code: "repair-ssb-length-conflict",
808
+ message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,
809
+ });
810
+ return;
811
+ }
789
812
 
790
813
  const result = wasmDecoder.add_repair(
791
814
  cmd.ssStart,
@@ -802,13 +825,20 @@ function handleFeedRepair(
802
825
  }
803
826
  repairEsis.add(repairEsi);
804
827
  const block = getOrCreateBlock(sbn);
828
+ block.ssbLength = cmd.ssbLength;
805
829
  block.repairReceived++;
806
830
  block.lastSymbolTs = performance.now();
807
831
  repairSymbols++;
808
- // If WASM returned data, it is the recovered source block: K fixed-width
809
- // source symbols concatenated. Split it before emitting so MmtpRouter gets
810
- // the same one-symbol shape as a direct source packet.
811
- if (result && result.byteLength > 0) {
832
+ // Every authored source symbol already arrived: knowing SSB_length makes
833
+ // the block clean. The decoder's return is the source already routed.
834
+ // Otherwise, returned data is the recovered source block: its SSB_length
835
+ // fixed-width authored symbols concatenated. Split it before emitting so
836
+ // MmtpRouter gets the same one-symbol shape as a direct source packet.
837
+ if (block.sourceReceived >= cmd.ssbLength && block.state === "collecting") {
838
+ markBlockTerminal(block, "complete");
839
+ blocksComplete++;
840
+ emitBlockUpdate(block);
841
+ } else if (result && result.byteLength > 0) {
812
842
  const recovered = new Uint8Array(toOwnedArrayBuffer(result));
813
843
  const recoveryMs = performance.now() - block.firstSymbolTs;
814
844
  recoveryTimes.push(recoveryMs);
@@ -994,6 +1024,38 @@ function handleReset(): void {
994
1024
  if (config) emitSnapshot();
995
1025
  }
996
1026
 
1027
+ /**
1028
+ * A batch of symbols fed in one page task (BLO-36599). Each record is
1029
+ * handled exactly as its single-symbol command would be, in order, and a
1030
+ * failing record reports the same `command-failed` error that command would
1031
+ * without stopping the records after it.
1032
+ */
1033
+ function handleFeedSymbols(
1034
+ cmd: Extract<FecWorkerCommand, { type: "feedSymbols" }>,
1035
+ ): void {
1036
+ if (!Array.isArray(cmd.symbols)) {
1037
+ throw new Error("[FecWorker] feedSymbols: symbols must be an array");
1038
+ }
1039
+ for (const symbol of cmd.symbols) {
1040
+ try {
1041
+ if (symbol?.type === "feedSource") handleFeedSource(symbol);
1042
+ else if (symbol?.type === "feedRepair") handleFeedRepair(symbol);
1043
+ else {
1044
+ throw new Error(
1045
+ `[FecWorker] feedSymbols: unknown symbol type ${String((symbol as { type?: unknown } | null)?.type)}`,
1046
+ );
1047
+ }
1048
+ } catch (err) {
1049
+ emit({
1050
+ type: "error",
1051
+ code: "command-failed",
1052
+ message:
1053
+ err instanceof Error ? err.message : `[FecWorker] ${String(err)}`,
1054
+ });
1055
+ }
1056
+ }
1057
+ }
1058
+
997
1059
  // --- Main message handler ---
998
1060
 
999
1061
  async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
@@ -1011,6 +1073,9 @@ async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
1011
1073
  case "feedRepair":
1012
1074
  handleFeedRepair(cmd);
1013
1075
  break;
1076
+ case "feedSymbols":
1077
+ handleFeedSymbols(cmd);
1078
+ break;
1014
1079
  case "relayBlockSig":
1015
1080
  handleRelayBlockSig(cmd);
1016
1081
  break;
@@ -1020,6 +1085,9 @@ async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
1020
1085
  case "reset":
1021
1086
  handleReset();
1022
1087
  break;
1088
+ case "subscribeBlockUpdates":
1089
+ blockUpdatesSubscribed = true;
1090
+ break;
1023
1091
  case "dispose":
1024
1092
  handleDispose();
1025
1093
  break;
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  export type {
8
8
  FecWorkerCommand,
9
+ FecSymbolCommand,
10
+ FecWorkerOptionalCommand,
9
11
  FecWorkerEvent,
10
12
  FecWorkerErrorCode,
11
13
  FecTrackConfig,