@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/README.md +5 -1
- package/dist/fec-worker-client.d.ts +8 -2
- package/dist/fec-worker-client.d.ts.map +1 -1
- package/dist/fec-worker-client.js +89 -16
- package/dist/fec-worker-client.js.map +1 -1
- package/dist/fec-worker-types.d.ts +48 -3
- package/dist/fec-worker-types.d.ts.map +1 -1
- package/dist/fec-worker.js +55 -8
- package/dist/fec-worker.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/fec-worker-client.ts +97 -17
- package/src/fec-worker-types.ts +46 -2
- package/src/fec-worker.ts +78 -10
- package/src/index.ts +2 -0
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
|
-
|
|
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
|
|
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 <
|
|
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
|
|
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 >=
|
|
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 <
|
|
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
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
|
|
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;
|