@blockcast/fec-worker 0.1.0-main.64a6b356bf43 → 0.1.0-main.6598eedfb039
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 +10 -6
- package/dist/fec-worker-client.d.ts +10 -6
- package/dist/fec-worker-client.d.ts.map +1 -1
- package/dist/fec-worker-client.js +90 -25
- package/dist/fec-worker-client.js.map +1 -1
- package/dist/fec-worker-types.d.ts +53 -14
- package/dist/fec-worker-types.d.ts.map +1 -1
- package/dist/fec-worker.js +62 -44
- 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 -26
- package/src/fec-worker-types.ts +51 -10
- package/src/fec-worker.ts +99 -67
- package/src/index.ts +2 -0
package/src/fec-worker.ts
CHANGED
|
@@ -31,7 +31,7 @@ interface WasmFecDecoder {
|
|
|
31
31
|
data: Uint8Array,
|
|
32
32
|
timestamp: number,
|
|
33
33
|
): Uint8Array | undefined;
|
|
34
|
-
/** ISO 23008-1 §C.5.3: repair ESI =
|
|
34
|
+
/** ISO 23008-1 §C.5.3: repair ESI = configured K + rs_id, SBN = ss_start / configured K. */
|
|
35
35
|
add_repair(
|
|
36
36
|
ss_start: number,
|
|
37
37
|
ssb_length: number,
|
|
@@ -40,8 +40,12 @@ interface WasmFecDecoder {
|
|
|
40
40
|
timestamp: number,
|
|
41
41
|
): Uint8Array | undefined;
|
|
42
42
|
configure_params(k: number, symbolSize: number): void;
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Retire the named blocks (last-chance decode + stats), driven by the
|
|
45
|
+
* manager's exact deadlines. Nothing is synthesised for a lost block:
|
|
46
|
+
* concealment is the sink's hold-last-good-frame.
|
|
47
|
+
*/
|
|
48
|
+
cleanup_blocks_detailed(sbns: Uint32Array): void;
|
|
45
49
|
/** Remove decoder blocks below its configured live-SBN interleave horizon. */
|
|
46
50
|
cleanup_by_sbn(current_sbn: number): Uint32Array;
|
|
47
51
|
flush(): void;
|
|
@@ -61,10 +65,7 @@ interface WasmFecDecoder {
|
|
|
61
65
|
|
|
62
66
|
interface WasmBindings {
|
|
63
67
|
initSync(module: { module: WebAssembly.Module }): unknown;
|
|
64
|
-
MmtFecDecoder: new (
|
|
65
|
-
interleaveDepth: number,
|
|
66
|
-
greenFillEnabled: boolean,
|
|
67
|
-
) => WasmFecDecoder;
|
|
68
|
+
MmtFecDecoder: new (interleaveDepth: number) => WasmFecDecoder;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// --- Worker state (local to this Worker instance) ---
|
|
@@ -75,6 +76,8 @@ let altaEnabled = false;
|
|
|
75
76
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
77
|
let altaVerifier: any = null; // JsAltaVerifier instance (loaded from ALTA WASM)
|
|
77
78
|
let snapshotTimer: ReturnType<typeof setInterval> | null = null;
|
|
79
|
+
// blockUpdate events are posted only after a client subscribes (BLO-36599).
|
|
80
|
+
let blockUpdatesSubscribed = false;
|
|
78
81
|
|
|
79
82
|
// Block tracking
|
|
80
83
|
const blocks = new Map<number, FecBlockSnapshot>();
|
|
@@ -95,7 +98,6 @@ let repairSymbols = 0;
|
|
|
95
98
|
let blocksComplete = 0;
|
|
96
99
|
let blocksRecovered = 0;
|
|
97
100
|
let blocksFailed = 0;
|
|
98
|
-
let greenFillFrames = 0;
|
|
99
101
|
let altaVerified = 0;
|
|
100
102
|
let altaFailed = 0; // Crypto-confirmed tamper (MAC / sig / hash chain mismatch)
|
|
101
103
|
const altaFailedDetails: Array<{
|
|
@@ -444,7 +446,6 @@ function buildStatsSnapshot(): FecTrackStats {
|
|
|
444
446
|
blocksComplete,
|
|
445
447
|
blocksRecovered,
|
|
446
448
|
blocksFailed,
|
|
447
|
-
greenFillFrames,
|
|
448
449
|
recoveryRate:
|
|
449
450
|
blocksComplete + blocksRecovered > 0
|
|
450
451
|
? Math.round(
|
|
@@ -478,6 +479,7 @@ function emitSnapshot(): void {
|
|
|
478
479
|
}
|
|
479
480
|
|
|
480
481
|
function emitBlockUpdate(block: FecBlockSnapshot): void {
|
|
482
|
+
if (!blockUpdatesSubscribed) return;
|
|
481
483
|
emit({ type: "blockUpdate", block: { ...block } });
|
|
482
484
|
}
|
|
483
485
|
|
|
@@ -496,16 +498,19 @@ function emitRecoveredSymbols(
|
|
|
496
498
|
});
|
|
497
499
|
return;
|
|
498
500
|
}
|
|
499
|
-
|
|
501
|
+
// The decoder returns only the block's authored source symbols
|
|
502
|
+
// (SSB_length x T); the encoder's zero padding is not media.
|
|
503
|
+
const ssbLength = block.ssbLength ?? config.k;
|
|
504
|
+
if (recoveredBlock.byteLength !== symbolSize * ssbLength) {
|
|
500
505
|
emit({
|
|
501
506
|
type: "error",
|
|
502
507
|
code: "recovered-block-too-short",
|
|
503
|
-
message: `[FecWorker] recovered block
|
|
508
|
+
message: `[FecWorker] recovered block has the wrong length: got ${recoveredBlock.byteLength}B, expected ${symbolSize * ssbLength}B (SSB_length=${ssbLength})`,
|
|
504
509
|
});
|
|
505
510
|
return;
|
|
506
511
|
}
|
|
507
512
|
|
|
508
|
-
for (let i = 0; i <
|
|
513
|
+
for (let i = 0; i < ssbLength; i++) {
|
|
509
514
|
const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
|
|
510
515
|
|
|
511
516
|
// Source authentication now runs once at router admission in
|
|
@@ -520,6 +525,7 @@ function emitRecoveredSymbols(
|
|
|
520
525
|
recovered: true,
|
|
521
526
|
sbn,
|
|
522
527
|
esi: i,
|
|
528
|
+
ssbLength,
|
|
523
529
|
};
|
|
524
530
|
emit({ type: "frame", data: buf, meta }, [buf]);
|
|
525
531
|
}
|
|
@@ -611,10 +617,7 @@ async function handleConfigure(
|
|
|
611
617
|
|
|
612
618
|
// Decoder policy and geometry are explicit at the epoch boundary. The WASM
|
|
613
619
|
// module has no guessed K/T/interleave slow path.
|
|
614
|
-
wasmDecoder = new wasm.MmtFecDecoder(
|
|
615
|
-
cmd.config.interleaveDepth,
|
|
616
|
-
cmd.config.trackType === "video",
|
|
617
|
-
);
|
|
620
|
+
wasmDecoder = new wasm.MmtFecDecoder(cmd.config.interleaveDepth);
|
|
618
621
|
config = cmd.config;
|
|
619
622
|
altaEnabled = !!cmd.altaPublicKey;
|
|
620
623
|
|
|
@@ -662,6 +665,10 @@ async function handleConfigure(
|
|
|
662
665
|
|
|
663
666
|
wasmDecoder.configure_params(config.k, config.symbolSize);
|
|
664
667
|
|
|
668
|
+
// Tell the client which optional commands this Worker accepts, so it can
|
|
669
|
+
// batch symbols without risking a Worker that predates the batch command.
|
|
670
|
+
emit({ type: "capabilities", commands: ["feedSymbols", "subscribeBlockUpdates"] });
|
|
671
|
+
|
|
665
672
|
// Start periodic snapshot emission (200ms floor)
|
|
666
673
|
startSnapshotTimer();
|
|
667
674
|
|
|
@@ -716,9 +723,11 @@ function handleFeedSource(
|
|
|
716
723
|
// authenticated by the canonical manifest verifier at router admission.
|
|
717
724
|
if (altaEnabled) altaPending++;
|
|
718
725
|
|
|
719
|
-
// Check if block is complete (all
|
|
726
|
+
// Check if block is complete (all authored source symbols received:
|
|
727
|
+
// SSB_length once a repair symbol declared it, otherwise K)
|
|
728
|
+
const authoredLength = block.ssbLength ?? block.k;
|
|
720
729
|
if (
|
|
721
|
-
block.sourceReceived >=
|
|
730
|
+
block.sourceReceived >= authoredLength &&
|
|
722
731
|
block.state === "collecting"
|
|
723
732
|
) {
|
|
724
733
|
markBlockTerminal(block, "complete");
|
|
@@ -730,7 +739,7 @@ function handleFeedSource(
|
|
|
730
739
|
// of earlier missing symbols after repair arrived. Clean completion is
|
|
731
740
|
// already routed by the source path, so only emit when the block did not
|
|
732
741
|
// reach K source arrivals.
|
|
733
|
-
if (result && result.byteLength > 0 && block.sourceReceived <
|
|
742
|
+
if (result && result.byteLength > 0 && block.sourceReceived < authoredLength) {
|
|
734
743
|
const recovered = new Uint8Array(toOwnedArrayBuffer(result));
|
|
735
744
|
const recoveryMs = performance.now() - block.firstSymbolTs;
|
|
736
745
|
recoveryTimes.push(recoveryMs);
|
|
@@ -759,10 +768,10 @@ function handleFeedRepair(
|
|
|
759
768
|
!isUint32(cmd.ssStart) ||
|
|
760
769
|
!isUint32(cmd.ssbLength) ||
|
|
761
770
|
cmd.ssbLength <= 0 ||
|
|
762
|
-
cmd.ssbLength
|
|
763
|
-
cmd.ssStart %
|
|
771
|
+
cmd.ssbLength > config.k ||
|
|
772
|
+
cmd.ssStart % config.k !== 0 ||
|
|
764
773
|
!isUint32(cmd.rsId) ||
|
|
765
|
-
|
|
774
|
+
config.k + cmd.rsId > UINT24_MAX ||
|
|
766
775
|
!(cmd.data instanceof ArrayBuffer)
|
|
767
776
|
) {
|
|
768
777
|
throw new Error("[FecWorker] feedRepair: invalid repair symbol");
|
|
@@ -780,12 +789,22 @@ function handleFeedRepair(
|
|
|
780
789
|
}
|
|
781
790
|
return;
|
|
782
791
|
}
|
|
783
|
-
const sbn = Math.floor(cmd.ssStart /
|
|
784
|
-
const repairEsi =
|
|
792
|
+
const sbn = Math.floor(cmd.ssStart / config.k);
|
|
793
|
+
const repairEsi = config.k + cmd.rsId;
|
|
785
794
|
if (sbn <= retiredSbnFrontier) return;
|
|
786
795
|
const existingBlock = blocks.get(sbn);
|
|
787
796
|
if (existingBlock && isTerminalBlock(existingBlock)) return;
|
|
788
797
|
if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
|
|
798
|
+
// One block has one authored SSB_length; a repair symbol naming another
|
|
799
|
+
// is dropped before it reaches the decoder (which rejects it too).
|
|
800
|
+
if (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {
|
|
801
|
+
emit({
|
|
802
|
+
type: "error",
|
|
803
|
+
code: "repair-ssb-length-conflict",
|
|
804
|
+
message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
789
808
|
|
|
790
809
|
const result = wasmDecoder.add_repair(
|
|
791
810
|
cmd.ssStart,
|
|
@@ -802,13 +821,20 @@ function handleFeedRepair(
|
|
|
802
821
|
}
|
|
803
822
|
repairEsis.add(repairEsi);
|
|
804
823
|
const block = getOrCreateBlock(sbn);
|
|
824
|
+
block.ssbLength = cmd.ssbLength;
|
|
805
825
|
block.repairReceived++;
|
|
806
826
|
block.lastSymbolTs = performance.now();
|
|
807
827
|
repairSymbols++;
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
|
|
828
|
+
// Every authored source symbol already arrived: knowing SSB_length makes
|
|
829
|
+
// the block clean. The decoder's return is the source already routed.
|
|
830
|
+
// Otherwise, returned data is the recovered source block: its SSB_length
|
|
831
|
+
// fixed-width authored symbols concatenated. Split it before emitting so
|
|
832
|
+
// MmtpRouter gets the same one-symbol shape as a direct source packet.
|
|
833
|
+
if (block.sourceReceived >= cmd.ssbLength && block.state === "collecting") {
|
|
834
|
+
markBlockTerminal(block, "complete");
|
|
835
|
+
blocksComplete++;
|
|
836
|
+
emitBlockUpdate(block);
|
|
837
|
+
} else if (result && result.byteLength > 0) {
|
|
812
838
|
const recovered = new Uint8Array(toOwnedArrayBuffer(result));
|
|
813
839
|
const recoveryMs = performance.now() - block.firstSymbolTs;
|
|
814
840
|
recoveryTimes.push(recoveryMs);
|
|
@@ -855,21 +881,19 @@ function handleCleanup(
|
|
|
855
881
|
throw new Error("[FecWorker] cleanup: not configured");
|
|
856
882
|
}
|
|
857
883
|
|
|
858
|
-
// Call WASM cleanup only after all earlier symbol commands have drained
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
: new Uint8Array();
|
|
884
|
+
// Call WASM cleanup only after all earlier symbol commands have drained,
|
|
885
|
+
// and only for the exact SBNs the manager's deadlines named; command
|
|
886
|
+
// coalescing must not infer ownership from the largest timed-out SBN.
|
|
887
|
+
if (pendingSbns.length > 0) {
|
|
888
|
+
wasmDecoder.cleanup_blocks_detailed(Uint32Array.from(pendingSbns));
|
|
889
|
+
}
|
|
865
890
|
// Manager deadlines are block-scoped and may expire a newer SBN while an
|
|
866
891
|
// older interleaved block is still live. Only accepted media progression
|
|
867
892
|
// advances the shared JS/Rust horizon; cleanup creates exact tombstones.
|
|
868
893
|
|
|
869
894
|
// Rust already finalized every SBN behind the media frontier. For each
|
|
870
895
|
// remaining exact request, mirror the block-scoped WASM boundary immediately
|
|
871
|
-
// so JS never retains state for a decoder block that is gone
|
|
872
|
-
// the trusted cleanup framing were to fail.
|
|
896
|
+
// so JS never retains state for a decoder block that is gone.
|
|
873
897
|
for (const sbn of pendingSbns) {
|
|
874
898
|
let currentBlock = blocks.get(sbn);
|
|
875
899
|
if (currentBlock && isTerminalBlock(currentBlock)) {
|
|
@@ -886,12 +910,6 @@ function handleCleanup(
|
|
|
886
910
|
}
|
|
887
911
|
emitBlockUpdate(currentBlock);
|
|
888
912
|
}
|
|
889
|
-
|
|
890
|
-
const greenFills = decodeCleanupRecords(cleanupRecords);
|
|
891
|
-
for (const { sbn, data } of greenFills) {
|
|
892
|
-
greenFillFrames++;
|
|
893
|
-
emit({ type: "greenFill", sbn, data }, [data]);
|
|
894
|
-
}
|
|
895
913
|
} finally {
|
|
896
914
|
// Message ordering makes this an acknowledgement that every source/repair
|
|
897
915
|
// command posted before cleanup has completed. The main thread must not
|
|
@@ -900,29 +918,6 @@ function handleCleanup(
|
|
|
900
918
|
}
|
|
901
919
|
}
|
|
902
920
|
|
|
903
|
-
function decodeCleanupRecords(
|
|
904
|
-
framed: Uint8Array,
|
|
905
|
-
): Array<{ sbn: number; data: ArrayBuffer }> {
|
|
906
|
-
const records: Array<{ sbn: number; data: ArrayBuffer }> = [];
|
|
907
|
-
const view = new DataView(framed.buffer, framed.byteOffset, framed.byteLength);
|
|
908
|
-
let offset = 0;
|
|
909
|
-
while (offset < framed.byteLength) {
|
|
910
|
-
if (framed.byteLength - offset < 8) {
|
|
911
|
-
throw new Error("[FecWorker] cleanup_blocks_detailed: truncated record header");
|
|
912
|
-
}
|
|
913
|
-
const sbn = view.getUint32(offset, false);
|
|
914
|
-
const length = view.getUint32(offset + 4, false);
|
|
915
|
-
offset += 8;
|
|
916
|
-
if (length > framed.byteLength - offset) {
|
|
917
|
-
throw new Error("[FecWorker] cleanup_blocks_detailed: truncated record payload");
|
|
918
|
-
}
|
|
919
|
-
const data = toOwnedArrayBuffer(framed.subarray(offset, offset + length));
|
|
920
|
-
records.push({ sbn, data });
|
|
921
|
-
offset += length;
|
|
922
|
-
}
|
|
923
|
-
return records;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
921
|
function handleDispose(): void {
|
|
927
922
|
// Clear snapshot timer (MUST clear to prevent leaks)
|
|
928
923
|
if (snapshotTimer !== null) {
|
|
@@ -979,7 +974,6 @@ function handleReset(): void {
|
|
|
979
974
|
blocksComplete = 0;
|
|
980
975
|
blocksRecovered = 0;
|
|
981
976
|
blocksFailed = 0;
|
|
982
|
-
greenFillFrames = 0;
|
|
983
977
|
altaVerified = 0;
|
|
984
978
|
altaFailed = 0;
|
|
985
979
|
altaFailedDetails.length = 0;
|
|
@@ -994,6 +988,38 @@ function handleReset(): void {
|
|
|
994
988
|
if (config) emitSnapshot();
|
|
995
989
|
}
|
|
996
990
|
|
|
991
|
+
/**
|
|
992
|
+
* A batch of symbols fed in one page task (BLO-36599). Each record is
|
|
993
|
+
* handled exactly as its single-symbol command would be, in order, and a
|
|
994
|
+
* failing record reports the same `command-failed` error that command would
|
|
995
|
+
* without stopping the records after it.
|
|
996
|
+
*/
|
|
997
|
+
function handleFeedSymbols(
|
|
998
|
+
cmd: Extract<FecWorkerCommand, { type: "feedSymbols" }>,
|
|
999
|
+
): void {
|
|
1000
|
+
if (!Array.isArray(cmd.symbols)) {
|
|
1001
|
+
throw new Error("[FecWorker] feedSymbols: symbols must be an array");
|
|
1002
|
+
}
|
|
1003
|
+
for (const symbol of cmd.symbols) {
|
|
1004
|
+
try {
|
|
1005
|
+
if (symbol?.type === "feedSource") handleFeedSource(symbol);
|
|
1006
|
+
else if (symbol?.type === "feedRepair") handleFeedRepair(symbol);
|
|
1007
|
+
else {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`[FecWorker] feedSymbols: unknown symbol type ${String((symbol as { type?: unknown } | null)?.type)}`,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
emit({
|
|
1014
|
+
type: "error",
|
|
1015
|
+
code: "command-failed",
|
|
1016
|
+
message:
|
|
1017
|
+
err instanceof Error ? err.message : `[FecWorker] ${String(err)}`,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
997
1023
|
// --- Main message handler ---
|
|
998
1024
|
|
|
999
1025
|
async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
|
|
@@ -1011,6 +1037,9 @@ async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
|
|
|
1011
1037
|
case "feedRepair":
|
|
1012
1038
|
handleFeedRepair(cmd);
|
|
1013
1039
|
break;
|
|
1040
|
+
case "feedSymbols":
|
|
1041
|
+
handleFeedSymbols(cmd);
|
|
1042
|
+
break;
|
|
1014
1043
|
case "relayBlockSig":
|
|
1015
1044
|
handleRelayBlockSig(cmd);
|
|
1016
1045
|
break;
|
|
@@ -1020,6 +1049,9 @@ async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
|
|
|
1020
1049
|
case "reset":
|
|
1021
1050
|
handleReset();
|
|
1022
1051
|
break;
|
|
1052
|
+
case "subscribeBlockUpdates":
|
|
1053
|
+
blockUpdatesSubscribed = true;
|
|
1054
|
+
break;
|
|
1023
1055
|
case "dispose":
|
|
1024
1056
|
handleDispose();
|
|
1025
1057
|
break;
|