@blockcast/fec-worker 0.1.0 → 0.3.0-main.2515e5ca4858
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 +19 -9
- package/dist/block-order.d.ts +22 -0
- package/dist/block-order.d.ts.map +1 -0
- package/dist/block-order.js +48 -0
- package/dist/block-order.js.map +1 -0
- package/dist/fec-worker-client.d.ts +19 -7
- package/dist/fec-worker-client.d.ts.map +1 -1
- package/dist/fec-worker-client.js +105 -30
- package/dist/fec-worker-client.js.map +1 -1
- package/dist/fec-worker-types.d.ts +146 -25
- package/dist/fec-worker-types.d.ts.map +1 -1
- package/dist/fec-worker.js +326 -127
- package/dist/fec-worker.js.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/shred-fec-worker.js +23 -2
- package/dist/shred-fec-worker.js.map +2 -2
- package/dist/shred-worker-types.d.ts +16 -1
- package/dist/shred-worker-types.d.ts.map +1 -1
- package/dist/shred-worker-types.js +16 -1
- package/dist/shred-worker-types.js.map +1 -1
- package/package.json +8 -3
- package/src/block-order.ts +59 -0
- package/src/fec-worker-client.ts +114 -32
- package/src/fec-worker-types.ts +145 -21
- package/src/fec-worker.ts +518 -180
- package/src/index.ts +3 -0
- package/src/shred-fec-worker.ts +34 -3
- package/src/shred-worker-types.ts +16 -1
- package/src/__tests__/fec-worker-alta.test.ts +0 -231
- package/src/__tests__/fec-worker-cleanup-integration.test.ts +0 -250
- package/src/__tests__/fec-worker-pending-queue.test.ts +0 -315
- package/src/__tests__/fec-worker-repair-size.test.ts +0 -1029
- package/src/__tests__/fec-worker-wasm-contract.test.ts +0 -84
- package/src/__tests__/shred-fec-worker.test.ts +0 -448
- package/src/fec-worker-client.test.ts +0 -530
- package/src/fec-worker-types.test.ts +0 -400
- package/src/shred-worker-client.test.ts +0 -182
- package/src/transfer.test.ts +0 -24
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* path is bit-identical to the proven on-thread path.
|
|
26
26
|
*
|
|
27
27
|
* v3 (28-byte header):
|
|
28
|
-
* [0] version u8 (
|
|
28
|
+
* [0] version u8 (3 or 4; any other version is dropped, not misframed)
|
|
29
29
|
* [1-8] slot u64 LE
|
|
30
30
|
* [9-12] fecSetIndex u32 LE
|
|
31
31
|
* [13-16] localIndex u32 LE (data: 0..numData; coding: numData+position)
|
|
@@ -38,8 +38,23 @@
|
|
|
38
38
|
* DATA_COMPLETE (last data shred in the set) is passed to `add_shred` as its
|
|
39
39
|
* `lastInSet` argument — the WASM uses it to infer numData for a set that
|
|
40
40
|
* received only data shreds (no coding).
|
|
41
|
+
*
|
|
42
|
+
* v4 has the same 28-byte header, but its body is the full canonical Agave
|
|
43
|
+
* shred (signature, headers, erasure shard, chained Merkle root, proof, and
|
|
44
|
+
* the retransmitter signature on resigned variants) — the form a validator TVU
|
|
45
|
+
* accepts (BLO-41383). The worker recovers the exact v3 erasure-shard view
|
|
46
|
+
* from it with the forwarder's own `erasure_shard()` slice, so both versions
|
|
47
|
+
* reach `add_shred` byte-identical:
|
|
48
|
+
* variant = shred[64]; data 0x9_/0xb_, coding 0x6_/0x7_ (chained merkle;
|
|
49
|
+
* 0xb_/0x7_ resigned), must agree with IS_CODING_SHRED
|
|
50
|
+
* length = 1203 (data) / 1228 (coding), exactly
|
|
51
|
+
* shard start = 64 (data: after the signature) / 89 (coding headers)
|
|
52
|
+
* shard end = length - (32 root + 20 * (variant & 0x0f) proof
|
|
53
|
+
* + 64 retransmitter signature if resigned)
|
|
41
54
|
*/
|
|
42
55
|
export declare const SHRED_WIRE_VERSION = 3;
|
|
56
|
+
/** Wire version whose body is the full canonical shred (see above). */
|
|
57
|
+
export declare const SHRED_WIRE_VERSION_FULL = 4;
|
|
43
58
|
export declare const SHRED_HEADER_LEN = 28;
|
|
44
59
|
export declare const SHRED_FLAG_DATA_COMPLETE = 1;
|
|
45
60
|
export declare const SHRED_FLAG_IS_CODING = 2;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shred-worker-types.d.ts","sourceRoot":"","sources":["../src/shred-worker-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH
|
|
1
|
+
{"version":3,"file":"shred-worker-types.d.ts","sourceRoot":"","sources":["../src/shred-worker-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* path is bit-identical to the proven on-thread path.
|
|
27
27
|
*
|
|
28
28
|
* v3 (28-byte header):
|
|
29
|
-
* [0] version u8 (
|
|
29
|
+
* [0] version u8 (3 or 4; any other version is dropped, not misframed)
|
|
30
30
|
* [1-8] slot u64 LE
|
|
31
31
|
* [9-12] fecSetIndex u32 LE
|
|
32
32
|
* [13-16] localIndex u32 LE (data: 0..numData; coding: numData+position)
|
|
@@ -39,8 +39,23 @@
|
|
|
39
39
|
* DATA_COMPLETE (last data shred in the set) is passed to `add_shred` as its
|
|
40
40
|
* `lastInSet` argument — the WASM uses it to infer numData for a set that
|
|
41
41
|
* received only data shreds (no coding).
|
|
42
|
+
*
|
|
43
|
+
* v4 has the same 28-byte header, but its body is the full canonical Agave
|
|
44
|
+
* shred (signature, headers, erasure shard, chained Merkle root, proof, and
|
|
45
|
+
* the retransmitter signature on resigned variants) — the form a validator TVU
|
|
46
|
+
* accepts (BLO-41383). The worker recovers the exact v3 erasure-shard view
|
|
47
|
+
* from it with the forwarder's own `erasure_shard()` slice, so both versions
|
|
48
|
+
* reach `add_shred` byte-identical:
|
|
49
|
+
* variant = shred[64]; data 0x9_/0xb_, coding 0x6_/0x7_ (chained merkle;
|
|
50
|
+
* 0xb_/0x7_ resigned), must agree with IS_CODING_SHRED
|
|
51
|
+
* length = 1203 (data) / 1228 (coding), exactly
|
|
52
|
+
* shard start = 64 (data: after the signature) / 89 (coding headers)
|
|
53
|
+
* shard end = length - (32 root + 20 * (variant & 0x0f) proof
|
|
54
|
+
* + 64 retransmitter signature if resigned)
|
|
42
55
|
*/
|
|
43
56
|
export const SHRED_WIRE_VERSION = 3;
|
|
57
|
+
/** Wire version whose body is the full canonical shred (see above). */
|
|
58
|
+
export const SHRED_WIRE_VERSION_FULL = 4;
|
|
44
59
|
export const SHRED_HEADER_LEN = 28;
|
|
45
60
|
export const SHRED_FLAG_DATA_COMPLETE = 0x01;
|
|
46
61
|
export const SHRED_FLAG_IS_CODING = 0x02;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shred-worker-types.js","sourceRoot":"","sources":["../src/shred-worker-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,+EAA+E;AAE/E
|
|
1
|
+
{"version":3,"file":"shred-worker-types.js","sourceRoot":"","sources":["../src/shred-worker-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,uEAAuE;AACvE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/fec-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-main.2515e5ca4858",
|
|
4
4
|
"description": "Worker-per-track FEC decode with zero-copy ArrayBuffer transfer and ALTA auth plumbing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
-
"src"
|
|
22
|
+
"src",
|
|
23
|
+
"!src/**/*.test.ts",
|
|
24
|
+
"!src/**/*.spec.ts",
|
|
25
|
+
"!src/**/__tests__",
|
|
26
|
+
"!dist/**/*.test.*",
|
|
27
|
+
"!dist/**/*.spec.*"
|
|
23
28
|
],
|
|
24
29
|
"scripts": {
|
|
25
30
|
"build": "tsc && node build.mjs",
|
|
@@ -30,7 +35,7 @@
|
|
|
30
35
|
"test:watch": "vitest"
|
|
31
36
|
},
|
|
32
37
|
"dependencies": {
|
|
33
|
-
"@blockcast/mmt-alta-parse": "
|
|
38
|
+
"@blockcast/mmt-alta-parse": "0.3.0-main.2515e5ca4858"
|
|
34
39
|
},
|
|
35
40
|
"devDependencies": {
|
|
36
41
|
"esbuild": "^0.28.0",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SPAN = 0x1_0000_0000;
|
|
8
|
+
const HALF = 0x8000_0000;
|
|
9
|
+
|
|
10
|
+
// --- iso-ssbg1-v1 (ISO/IEC 23008-1 C.3.3 ssbg_mode1; C.5.2/C.5.3) ---------
|
|
11
|
+
//
|
|
12
|
+
// Under the ISO profile SS_ID increments by one per FEC source packet and
|
|
13
|
+
// wraps at 2^32, and a block is the SSB_length consecutive SS_IDs
|
|
14
|
+
// `[SS_Start, SS_Start + SSB_length)`. Blocks are keyed and ordered by their
|
|
15
|
+
// SS_Start, never by an SBN derived from K: the catalog K is only the maximum
|
|
16
|
+
// block length (kMax). The canonical helpers are @blockcast/mmt-base
|
|
17
|
+
// `serial32Distance` / `serial32Precedes` / `isoBlockContains` /
|
|
18
|
+
// `isoBlocksBehindLowerBound`; this bundle carries its own copies (it does not
|
|
19
|
+
// depend on mmt-base), and block-order.test.ts pins them to the same vectors.
|
|
20
|
+
|
|
21
|
+
/** Forward RFC 1982 distance from `from` to `to` on the 32-bit SS_ID, in 0..2^32-1. */
|
|
22
|
+
export function serialDistance(from: number, to: number): number {
|
|
23
|
+
return (to - from + SPAN) % SPAN;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether `a` precedes `b` under RFC 1982 serial comparison (SERIAL_BITS = 32). */
|
|
27
|
+
export function serialPrecedes(a: number, b: number): boolean {
|
|
28
|
+
const distance = serialDistance(a, b);
|
|
29
|
+
return distance !== 0 && distance < HALF;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether `ssId` is one of the `ssbLength` consecutive SS_IDs starting at `ssStart`, mod 2^32. */
|
|
33
|
+
export function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {
|
|
34
|
+
return serialDistance(ssStart, ssId) < ssbLength;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether two SS_ID ranges `[start, start + length)` share any SS_ID, mod 2^32. */
|
|
38
|
+
export function isoBlocksOverlap(
|
|
39
|
+
aStart: number,
|
|
40
|
+
aLength: number,
|
|
41
|
+
bStart: number,
|
|
42
|
+
bLength: number,
|
|
43
|
+
): boolean {
|
|
44
|
+
return isoBlockContains(aStart, aLength, bStart) || isoBlockContains(bStart, bLength, aStart);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How many blocks SS_ID `a` lies behind SS_ID `b` (0 unless `a` precedes `b`):
|
|
49
|
+
* a lower bound, because an iso-ssbg1-v1 block holds at most `kMax` source
|
|
50
|
+
* symbols, so `n * kMax` SS_IDs always span at least `n` blocks. This is the
|
|
51
|
+
* one interleave-horizon metric every ISO receiver layer shares (decoder
|
|
52
|
+
* retention, worker retirement, ordered-relay settlement).
|
|
53
|
+
*/
|
|
54
|
+
export function isoBlocksBehind(a: number, b: number, kMax: number): number {
|
|
55
|
+
if (!Number.isSafeInteger(kMax) || kMax <= 0) {
|
|
56
|
+
throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`);
|
|
57
|
+
}
|
|
58
|
+
return serialPrecedes(a, b) ? Math.floor(serialDistance(a, b) / kMax) : 0;
|
|
59
|
+
}
|
package/src/fec-worker-client.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
FecWorkerCommand,
|
|
13
|
+
FecSymbolCommand,
|
|
13
14
|
FecWorkerEvent,
|
|
14
15
|
FecTrackConfig,
|
|
15
16
|
FecTrackStats,
|
|
@@ -23,7 +24,6 @@ export class FecWorkerClient {
|
|
|
23
24
|
#onFrame: ((data: ArrayBuffer, meta: FrameMeta) => void) | null = null;
|
|
24
25
|
#onSnapshot: ((stats: FecTrackStats) => void) | null = null;
|
|
25
26
|
#onBlockUpdate: ((block: FecBlockSnapshot) => void) | null = null;
|
|
26
|
-
#onGreenFill: ((sbn: number, data: ArrayBuffer) => void) | null = null;
|
|
27
27
|
#onCleanupComplete: ((sbn: number) => void) | null = null;
|
|
28
28
|
#onError: ((message: string, code?: FecWorkerErrorCode) => void) | null = null;
|
|
29
29
|
#cleanupInFlight: Set<number> | null = null;
|
|
@@ -31,6 +31,20 @@ export class FecWorkerClient {
|
|
|
31
31
|
#pendingResets = 0;
|
|
32
32
|
#resetFailed = false;
|
|
33
33
|
#disposed = false;
|
|
34
|
+
// Symbols fed during the current task, sent as one feedSymbols message
|
|
35
|
+
// when the task's microtasks run (BLO-36599). A packet stream delivers many
|
|
36
|
+
// symbols per task, and each postMessage is a structured-clone round trip
|
|
37
|
+
// on the page thread; the batch boundary is the task, not a size or timer.
|
|
38
|
+
#pendingSymbols: FecSymbolCommand[] = [];
|
|
39
|
+
#pendingTransfer: Transferable[] = [];
|
|
40
|
+
#flushQueued = false;
|
|
41
|
+
// Batching starts only once the Worker announced it accepts feedSymbols.
|
|
42
|
+
#batchSymbols = false;
|
|
43
|
+
// The Worker posts blockUpdate events only to a client that subscribed,
|
|
44
|
+
// which this client does once a callback is registered and the Worker
|
|
45
|
+
// announced the command (BLO-36599).
|
|
46
|
+
#blockUpdatesAnnounced = false;
|
|
47
|
+
#blockUpdatesSubscribed = false;
|
|
34
48
|
|
|
35
49
|
/**
|
|
36
50
|
* Create a FecWorkerClient.
|
|
@@ -66,6 +80,7 @@ export class FecWorkerClient {
|
|
|
66
80
|
altaWasmModule?: WebAssembly.Module,
|
|
67
81
|
): void {
|
|
68
82
|
if (this.#disposed) throw new Error("[FecWorkerClient] disposed");
|
|
83
|
+
this.#flushSymbols();
|
|
69
84
|
const cmd: FecWorkerCommand = {
|
|
70
85
|
type: "configure",
|
|
71
86
|
config,
|
|
@@ -84,7 +99,8 @@ export class FecWorkerClient {
|
|
|
84
99
|
|
|
85
100
|
/**
|
|
86
101
|
* Feed a source symbol to the Worker. data ArrayBuffer is transferred (zero-copy).
|
|
87
|
-
* Caller must not access data after this call — it is
|
|
102
|
+
* Caller must not access data after this call — it is transferred with the
|
|
103
|
+
* task's symbol batch, before any later command of this client.
|
|
88
104
|
*
|
|
89
105
|
* Optional `alta` carries the detached ALTA authenticator bytes and the
|
|
90
106
|
* byte offset in `data` where the signed payload ends. The auth buffer is
|
|
@@ -97,19 +113,23 @@ export class FecWorkerClient {
|
|
|
97
113
|
ts: number,
|
|
98
114
|
alta?: { auth: ArrayBuffer; payloadEnd: number },
|
|
99
115
|
): void {
|
|
100
|
-
if (this.#disposed)
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
if (this.#disposed) throw new Error("[FecWorkerClient] feedSource on a disposed client");
|
|
117
|
+
if (alta) {
|
|
118
|
+
this.#queueSymbol(
|
|
119
|
+
{
|
|
103
120
|
type: "feedSource",
|
|
104
121
|
ssId,
|
|
105
122
|
data,
|
|
106
123
|
ts,
|
|
107
124
|
altaAuth: alta.auth,
|
|
108
125
|
altaPayloadEnd: alta.payloadEnd,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
},
|
|
127
|
+
data,
|
|
128
|
+
alta.auth,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
this.#queueSymbol({ type: "feedSource", ssId, data, ts }, data);
|
|
132
|
+
}
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
/**
|
|
@@ -117,8 +137,8 @@ export class FecWorkerClient {
|
|
|
117
137
|
* Caller must not access data after this call — it is detached.
|
|
118
138
|
*
|
|
119
139
|
* Carries the wire-level Repair FEC Payload ID fields (ISO 23008-1 §C.5.3):
|
|
120
|
-
* - ssStart: SS_ID of first source symbol in the block (
|
|
121
|
-
* - ssbLength:
|
|
140
|
+
* - ssStart: SS_ID of first source symbol in the block (any uint32; the block key)
|
|
141
|
+
* - ssbLength: authored source-symbol count (1..configured K)
|
|
122
142
|
* - rsId: repair symbol index within the block
|
|
123
143
|
*/
|
|
124
144
|
feedRepair(
|
|
@@ -128,17 +148,39 @@ export class FecWorkerClient {
|
|
|
128
148
|
data: ArrayBuffer,
|
|
129
149
|
ts: number,
|
|
130
150
|
): void {
|
|
131
|
-
if (this.#disposed)
|
|
151
|
+
if (this.#disposed) throw new Error("[FecWorkerClient] feedRepair on a disposed client");
|
|
152
|
+
this.#queueSymbol({ type: "feedRepair", ssStart, ssbLength, rsId, data, ts }, data);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#queueSymbol(symbol: FecSymbolCommand, data: ArrayBuffer, auth?: ArrayBuffer): void {
|
|
156
|
+
if (!this.#batchSymbols) {
|
|
157
|
+
this.#worker.postMessage(symbol, auth ? [data, auth] : [data]);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#pendingSymbols.push(symbol);
|
|
161
|
+
this.#pendingTransfer.push(data);
|
|
162
|
+
if (auth) this.#pendingTransfer.push(auth);
|
|
163
|
+
if (this.#flushQueued) return;
|
|
164
|
+
this.#flushQueued = true;
|
|
165
|
+
queueMicrotask(() => {
|
|
166
|
+
this.#flushQueued = false;
|
|
167
|
+
this.#flushSymbols();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Send the symbols fed so far. Every other command calls this first, so a
|
|
173
|
+
* cleanup barrier, reset or signature still follows all earlier symbols.
|
|
174
|
+
*/
|
|
175
|
+
#flushSymbols(): void {
|
|
176
|
+
if (this.#pendingSymbols.length === 0 || this.#disposed) return;
|
|
177
|
+
const symbols = this.#pendingSymbols;
|
|
178
|
+
const transfer = this.#pendingTransfer;
|
|
179
|
+
this.#pendingSymbols = [];
|
|
180
|
+
this.#pendingTransfer = [];
|
|
132
181
|
this.#worker.postMessage(
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
ssStart,
|
|
136
|
-
ssbLength,
|
|
137
|
-
rsId,
|
|
138
|
-
data,
|
|
139
|
-
ts,
|
|
140
|
-
} satisfies FecWorkerCommand,
|
|
141
|
-
[data],
|
|
182
|
+
{ type: "feedSymbols", symbols } satisfies FecWorkerCommand,
|
|
183
|
+
transfer,
|
|
142
184
|
);
|
|
143
185
|
}
|
|
144
186
|
|
|
@@ -146,7 +188,8 @@ export class FecWorkerClient {
|
|
|
146
188
|
* Submit relay block signature for verification. sig and hash are transferred.
|
|
147
189
|
*/
|
|
148
190
|
relayBlockSig(sbn: number, sig: ArrayBuffer, hash: ArrayBuffer): void {
|
|
149
|
-
if (this.#disposed)
|
|
191
|
+
if (this.#disposed) throw new Error("[FecWorkerClient] relayBlockSig on a disposed client");
|
|
192
|
+
this.#flushSymbols();
|
|
150
193
|
this.#worker.postMessage(
|
|
151
194
|
{ type: "relayBlockSig", sbn, sig, hash } satisfies FecWorkerCommand,
|
|
152
195
|
[sig, hash],
|
|
@@ -154,13 +197,14 @@ export class FecWorkerClient {
|
|
|
154
197
|
}
|
|
155
198
|
|
|
156
199
|
/**
|
|
157
|
-
* Trigger cleanup after a block's inactivity deadline.
|
|
200
|
+
* Trigger cleanup after a block's inactivity deadline. `currentSbn` is the
|
|
201
|
+
* block key, the block's SS_Start.
|
|
158
202
|
*
|
|
159
203
|
* While one queue-drain barrier is in flight, later SBNs are coalesced into
|
|
160
204
|
* the next command without losing their individual acknowledgement identity.
|
|
161
205
|
*/
|
|
162
206
|
cleanup(currentSbn: number): void {
|
|
163
|
-
if (this.#disposed)
|
|
207
|
+
if (this.#disposed) throw new Error("[FecWorkerClient] cleanup on a disposed client");
|
|
164
208
|
if (
|
|
165
209
|
!Number.isInteger(currentSbn) ||
|
|
166
210
|
currentSbn < 0 ||
|
|
@@ -177,7 +221,8 @@ export class FecWorkerClient {
|
|
|
177
221
|
|
|
178
222
|
/** Reset this track's decoder epoch while preserving Worker configuration. */
|
|
179
223
|
reset(): void {
|
|
180
|
-
if (this.#disposed)
|
|
224
|
+
if (this.#disposed) throw new Error("[FecWorkerClient] reset on a disposed client");
|
|
225
|
+
this.#flushSymbols();
|
|
181
226
|
this.#pendingResets++;
|
|
182
227
|
this.#cleanupInFlight = null;
|
|
183
228
|
this.#queuedCleanupSbns.clear();
|
|
@@ -185,6 +230,9 @@ export class FecWorkerClient {
|
|
|
185
230
|
}
|
|
186
231
|
|
|
187
232
|
#postCleanup(sbns: Set<number>): void {
|
|
233
|
+
// The cleanup is a queue-drain barrier: every symbol fed before it is
|
|
234
|
+
// posted must reach the Worker first.
|
|
235
|
+
this.#flushSymbols();
|
|
188
236
|
this.#cleanupInFlight = sbns;
|
|
189
237
|
const orderedSbns = [...sbns].sort((a, b) => a - b);
|
|
190
238
|
this.#worker.postMessage(
|
|
@@ -202,14 +250,28 @@ export class FecWorkerClient {
|
|
|
202
250
|
this.#onSnapshot = cb;
|
|
203
251
|
}
|
|
204
252
|
|
|
205
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Register callback for individual block state changes. The Worker posts
|
|
255
|
+
* them only once asked, so registering subscribes (after the Worker
|
|
256
|
+
* announces `subscribeBlockUpdates`). An older Worker that does not
|
|
257
|
+
* announce it posts them unasked.
|
|
258
|
+
*/
|
|
206
259
|
onBlockUpdate(cb: (block: FecBlockSnapshot) => void): void {
|
|
207
260
|
this.#onBlockUpdate = cb;
|
|
261
|
+
this.#subscribeBlockUpdates();
|
|
208
262
|
}
|
|
209
263
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
264
|
+
#subscribeBlockUpdates(): void {
|
|
265
|
+
if (
|
|
266
|
+
this.#disposed ||
|
|
267
|
+
this.#blockUpdatesSubscribed ||
|
|
268
|
+
!this.#blockUpdatesAnnounced ||
|
|
269
|
+
!this.#onBlockUpdate
|
|
270
|
+
) return;
|
|
271
|
+
this.#blockUpdatesSubscribed = true;
|
|
272
|
+
this.#worker.postMessage(
|
|
273
|
+
{ type: "subscribeBlockUpdates" } satisfies FecWorkerCommand,
|
|
274
|
+
);
|
|
213
275
|
}
|
|
214
276
|
|
|
215
277
|
/** Register callback fired after the Worker drains all commands queued before cleanup. */
|
|
@@ -222,6 +284,16 @@ export class FecWorkerClient {
|
|
|
222
284
|
this.#onError = cb;
|
|
223
285
|
}
|
|
224
286
|
|
|
287
|
+
/**
|
|
288
|
+
* True once dispose() ran. A disposed client cannot decode, fill, or
|
|
289
|
+
* acknowledge anything, so routing symbols to it discards them; every
|
|
290
|
+
* command method (feedSource, feedRepair, relayBlockSig, cleanup, reset)
|
|
291
|
+
* throws rather than pretend otherwise. Callers check this first.
|
|
292
|
+
*/
|
|
293
|
+
get disposed(): boolean {
|
|
294
|
+
return this.#disposed;
|
|
295
|
+
}
|
|
296
|
+
|
|
225
297
|
/**
|
|
226
298
|
* Dispose the Worker. Sends dispose command then terminates.
|
|
227
299
|
* Idempotent — safe to call multiple times.
|
|
@@ -229,6 +301,9 @@ export class FecWorkerClient {
|
|
|
229
301
|
dispose(): void {
|
|
230
302
|
if (this.#disposed) return;
|
|
231
303
|
this.#disposed = true;
|
|
304
|
+
// A terminated Worker decodes nothing; symbols still batched are dropped.
|
|
305
|
+
this.#pendingSymbols = [];
|
|
306
|
+
this.#pendingTransfer = [];
|
|
232
307
|
this.#cleanupInFlight = null;
|
|
233
308
|
this.#queuedCleanupSbns.clear();
|
|
234
309
|
this.#worker.postMessage({ type: "dispose" } satisfies FecWorkerCommand);
|
|
@@ -236,6 +311,16 @@ export class FecWorkerClient {
|
|
|
236
311
|
}
|
|
237
312
|
|
|
238
313
|
#handleEvent(event: FecWorkerEvent): void {
|
|
314
|
+
if (event.type === "capabilities") {
|
|
315
|
+
if (Array.isArray(event.commands) && event.commands.includes("feedSymbols")) {
|
|
316
|
+
this.#batchSymbols = true;
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(event.commands) && event.commands.includes("subscribeBlockUpdates")) {
|
|
319
|
+
this.#blockUpdatesAnnounced = true;
|
|
320
|
+
this.#subscribeBlockUpdates();
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
239
324
|
if (event.type === "resetComplete") {
|
|
240
325
|
this.#pendingResets = Math.max(0, this.#pendingResets - 1);
|
|
241
326
|
this.#resetFailed = false;
|
|
@@ -261,9 +346,6 @@ export class FecWorkerClient {
|
|
|
261
346
|
case "blockUpdate":
|
|
262
347
|
this.#onBlockUpdate?.(event.block);
|
|
263
348
|
break;
|
|
264
|
-
case "greenFill":
|
|
265
|
-
this.#onGreenFill?.(event.sbn, event.data);
|
|
266
|
-
break;
|
|
267
349
|
case "cleanupComplete":
|
|
268
350
|
for (const sbn of event.sbns) {
|
|
269
351
|
this.#onCleanupComplete?.(sbn);
|