@blockcast/fec-worker 0.1.0 → 0.3.0

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.
Files changed (40) hide show
  1. package/README.md +17 -7
  2. package/dist/block-order.d.ts +38 -0
  3. package/dist/block-order.d.ts.map +1 -0
  4. package/dist/block-order.js +78 -0
  5. package/dist/block-order.js.map +1 -0
  6. package/dist/fec-worker-client.d.ts +20 -7
  7. package/dist/fec-worker-client.d.ts.map +1 -1
  8. package/dist/fec-worker-client.js +106 -30
  9. package/dist/fec-worker-client.js.map +1 -1
  10. package/dist/fec-worker-types.d.ts +153 -25
  11. package/dist/fec-worker-types.d.ts.map +1 -1
  12. package/dist/fec-worker.js +382 -88
  13. package/dist/fec-worker.js.map +4 -4
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/shred-fec-worker.js +23 -2
  18. package/dist/shred-fec-worker.js.map +2 -2
  19. package/dist/shred-worker-types.d.ts +16 -1
  20. package/dist/shred-worker-types.d.ts.map +1 -1
  21. package/dist/shred-worker-types.js +16 -1
  22. package/dist/shred-worker-types.js.map +1 -1
  23. package/package.json +8 -3
  24. package/src/block-order.ts +93 -0
  25. package/src/fec-worker-client.ts +115 -32
  26. package/src/fec-worker-types.ts +152 -21
  27. package/src/fec-worker.ts +583 -129
  28. package/src/index.ts +3 -0
  29. package/src/shred-fec-worker.ts +34 -3
  30. package/src/shred-worker-types.ts +16 -1
  31. package/src/__tests__/fec-worker-alta.test.ts +0 -231
  32. package/src/__tests__/fec-worker-cleanup-integration.test.ts +0 -250
  33. package/src/__tests__/fec-worker-pending-queue.test.ts +0 -315
  34. package/src/__tests__/fec-worker-repair-size.test.ts +0 -1029
  35. package/src/__tests__/fec-worker-wasm-contract.test.ts +0 -84
  36. package/src/__tests__/shred-fec-worker.test.ts +0 -448
  37. package/src/fec-worker-client.test.ts +0 -530
  38. package/src/fec-worker-types.test.ts +0 -400
  39. package/src/shred-worker-client.test.ts +0 -182
  40. 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 (== 3; non-3 frames are dropped, not misframed)
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":"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"}
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 (== 3; non-3 frames are dropped, not misframed)
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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC"}
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.1.0",
3
+ "version": "0.3.0",
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": "^0.1.0"
38
+ "@blockcast/mmt-alta-parse": "^0.3.0"
34
39
  },
35
40
  "devDependencies": {
36
41
  "esbuild": "^0.28.0",
@@ -0,0 +1,93 @@
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.
14
+ */
15
+
16
+ const SPAN = 0x1_0000_0000;
17
+ const HALF = 0x8000_0000;
18
+
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
+ // --- iso-ssbg1-v1 (ISO/IEC 23008-1 C.3.3 ssbg_mode1; C.5.2/C.5.3) ---------
45
+ //
46
+ // Under the ISO profile SS_ID increments by one per FEC source packet and
47
+ // wraps at 2^32, and a block is the SSB_length consecutive SS_IDs
48
+ // `[SS_Start, SS_Start + SSB_length)`. Blocks are keyed and ordered by their
49
+ // SS_Start, never by an SBN derived from K: the catalog K is only the maximum
50
+ // block length (kMax). The canonical helpers are @blockcast/mmt-base
51
+ // `serial32Distance` / `serial32Precedes` / `isoBlockContains` /
52
+ // `isoBlocksBehindLowerBound`; this bundle carries its own copies (it does not
53
+ // depend on mmt-base), and block-order.test.ts pins them to the same vectors.
54
+
55
+ /** Forward RFC 1982 distance from `from` to `to` on the 32-bit SS_ID, in 0..2^32-1. */
56
+ export function serialDistance(from: number, to: number): number {
57
+ return (to - from + SPAN) % SPAN;
58
+ }
59
+
60
+ /** Whether `a` precedes `b` under RFC 1982 serial comparison (SERIAL_BITS = 32). */
61
+ export function serialPrecedes(a: number, b: number): boolean {
62
+ const distance = serialDistance(a, b);
63
+ return distance !== 0 && distance < HALF;
64
+ }
65
+
66
+ /** Whether `ssId` is one of the `ssbLength` consecutive SS_IDs starting at `ssStart`, mod 2^32. */
67
+ export function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {
68
+ return serialDistance(ssStart, ssId) < ssbLength;
69
+ }
70
+
71
+ /** Whether two SS_ID ranges `[start, start + length)` share any SS_ID, mod 2^32. */
72
+ export function isoBlocksOverlap(
73
+ aStart: number,
74
+ aLength: number,
75
+ bStart: number,
76
+ bLength: number,
77
+ ): boolean {
78
+ return isoBlockContains(aStart, aLength, bStart) || isoBlockContains(bStart, bLength, aStart);
79
+ }
80
+
81
+ /**
82
+ * How many blocks SS_ID `a` lies behind SS_ID `b` (0 unless `a` precedes `b`):
83
+ * a lower bound, because an iso-ssbg1-v1 block holds at most `kMax` source
84
+ * symbols, so `n * kMax` SS_IDs always span at least `n` blocks. This is the
85
+ * one interleave-horizon metric every ISO receiver layer shares (decoder
86
+ * retention, worker retirement, ordered-relay settlement).
87
+ */
88
+ export function isoBlocksBehind(a: number, b: number, kMax: number): number {
89
+ if (!Number.isSafeInteger(kMax) || kMax <= 0) {
90
+ throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`);
91
+ }
92
+ return serialPrecedes(a, b) ? Math.floor(serialDistance(a, b) / kMax) : 0;
93
+ }
@@ -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 detached.
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) return;
101
- const cmd: FecWorkerCommand = alta
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
- : { type: "feedSource", ssId, data, ts };
111
- const transfer: Transferable[] = alta ? [data, alta.auth] : [data];
112
- this.#worker.postMessage(cmd, transfer);
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,9 @@ 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 (= SBN * SSB_length)
121
- * - ssbLength: K for this block
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`)
142
+ * - ssbLength: authored source-symbol count (1..configured K)
122
143
  * - rsId: repair symbol index within the block
123
144
  */
124
145
  feedRepair(
@@ -128,17 +149,39 @@ export class FecWorkerClient {
128
149
  data: ArrayBuffer,
129
150
  ts: number,
130
151
  ): void {
131
- if (this.#disposed) return;
152
+ if (this.#disposed) throw new Error("[FecWorkerClient] feedRepair on a disposed client");
153
+ this.#queueSymbol({ type: "feedRepair", ssStart, ssbLength, rsId, data, ts }, data);
154
+ }
155
+
156
+ #queueSymbol(symbol: FecSymbolCommand, data: ArrayBuffer, auth?: ArrayBuffer): void {
157
+ if (!this.#batchSymbols) {
158
+ this.#worker.postMessage(symbol, auth ? [data, auth] : [data]);
159
+ return;
160
+ }
161
+ this.#pendingSymbols.push(symbol);
162
+ this.#pendingTransfer.push(data);
163
+ if (auth) this.#pendingTransfer.push(auth);
164
+ if (this.#flushQueued) return;
165
+ this.#flushQueued = true;
166
+ queueMicrotask(() => {
167
+ this.#flushQueued = false;
168
+ this.#flushSymbols();
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Send the symbols fed so far. Every other command calls this first, so a
174
+ * cleanup barrier, reset or signature still follows all earlier symbols.
175
+ */
176
+ #flushSymbols(): void {
177
+ if (this.#pendingSymbols.length === 0 || this.#disposed) return;
178
+ const symbols = this.#pendingSymbols;
179
+ const transfer = this.#pendingTransfer;
180
+ this.#pendingSymbols = [];
181
+ this.#pendingTransfer = [];
132
182
  this.#worker.postMessage(
133
- {
134
- type: "feedRepair",
135
- ssStart,
136
- ssbLength,
137
- rsId,
138
- data,
139
- ts,
140
- } satisfies FecWorkerCommand,
141
- [data],
183
+ { type: "feedSymbols", symbols } satisfies FecWorkerCommand,
184
+ transfer,
142
185
  );
143
186
  }
144
187
 
@@ -146,7 +189,8 @@ export class FecWorkerClient {
146
189
  * Submit relay block signature for verification. sig and hash are transferred.
147
190
  */
148
191
  relayBlockSig(sbn: number, sig: ArrayBuffer, hash: ArrayBuffer): void {
149
- if (this.#disposed) return;
192
+ if (this.#disposed) throw new Error("[FecWorkerClient] relayBlockSig on a disposed client");
193
+ this.#flushSymbols();
150
194
  this.#worker.postMessage(
151
195
  { type: "relayBlockSig", sbn, sig, hash } satisfies FecWorkerCommand,
152
196
  [sig, hash],
@@ -154,13 +198,14 @@ export class FecWorkerClient {
154
198
  }
155
199
 
156
200
  /**
157
- * Trigger cleanup after a block's inactivity deadline.
201
+ * 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`.
158
203
  *
159
204
  * While one queue-drain barrier is in flight, later SBNs are coalesced into
160
205
  * the next command without losing their individual acknowledgement identity.
161
206
  */
162
207
  cleanup(currentSbn: number): void {
163
- if (this.#disposed) return;
208
+ if (this.#disposed) throw new Error("[FecWorkerClient] cleanup on a disposed client");
164
209
  if (
165
210
  !Number.isInteger(currentSbn) ||
166
211
  currentSbn < 0 ||
@@ -177,7 +222,8 @@ export class FecWorkerClient {
177
222
 
178
223
  /** Reset this track's decoder epoch while preserving Worker configuration. */
179
224
  reset(): void {
180
- if (this.#disposed) return;
225
+ if (this.#disposed) throw new Error("[FecWorkerClient] reset on a disposed client");
226
+ this.#flushSymbols();
181
227
  this.#pendingResets++;
182
228
  this.#cleanupInFlight = null;
183
229
  this.#queuedCleanupSbns.clear();
@@ -185,6 +231,9 @@ export class FecWorkerClient {
185
231
  }
186
232
 
187
233
  #postCleanup(sbns: Set<number>): void {
234
+ // The cleanup is a queue-drain barrier: every symbol fed before it is
235
+ // posted must reach the Worker first.
236
+ this.#flushSymbols();
188
237
  this.#cleanupInFlight = sbns;
189
238
  const orderedSbns = [...sbns].sort((a, b) => a - b);
190
239
  this.#worker.postMessage(
@@ -202,14 +251,28 @@ export class FecWorkerClient {
202
251
  this.#onSnapshot = cb;
203
252
  }
204
253
 
205
- /** Register callback for individual block state changes. */
254
+ /**
255
+ * Register callback for individual block state changes. The Worker posts
256
+ * them only once asked, so registering subscribes (after the Worker
257
+ * announces `subscribeBlockUpdates`). An older Worker that does not
258
+ * announce it posts them unasked.
259
+ */
206
260
  onBlockUpdate(cb: (block: FecBlockSnapshot) => void): void {
207
261
  this.#onBlockUpdate = cb;
262
+ this.#subscribeBlockUpdates();
208
263
  }
209
264
 
210
- /** Register callback for green-fill frames. */
211
- onGreenFill(cb: (sbn: number, data: ArrayBuffer) => void): void {
212
- this.#onGreenFill = cb;
265
+ #subscribeBlockUpdates(): void {
266
+ if (
267
+ this.#disposed ||
268
+ this.#blockUpdatesSubscribed ||
269
+ !this.#blockUpdatesAnnounced ||
270
+ !this.#onBlockUpdate
271
+ ) return;
272
+ this.#blockUpdatesSubscribed = true;
273
+ this.#worker.postMessage(
274
+ { type: "subscribeBlockUpdates" } satisfies FecWorkerCommand,
275
+ );
213
276
  }
214
277
 
215
278
  /** Register callback fired after the Worker drains all commands queued before cleanup. */
@@ -222,6 +285,16 @@ export class FecWorkerClient {
222
285
  this.#onError = cb;
223
286
  }
224
287
 
288
+ /**
289
+ * True once dispose() ran. A disposed client cannot decode, fill, or
290
+ * acknowledge anything, so routing symbols to it discards them; every
291
+ * command method (feedSource, feedRepair, relayBlockSig, cleanup, reset)
292
+ * throws rather than pretend otherwise. Callers check this first.
293
+ */
294
+ get disposed(): boolean {
295
+ return this.#disposed;
296
+ }
297
+
225
298
  /**
226
299
  * Dispose the Worker. Sends dispose command then terminates.
227
300
  * Idempotent — safe to call multiple times.
@@ -229,6 +302,9 @@ export class FecWorkerClient {
229
302
  dispose(): void {
230
303
  if (this.#disposed) return;
231
304
  this.#disposed = true;
305
+ // A terminated Worker decodes nothing; symbols still batched are dropped.
306
+ this.#pendingSymbols = [];
307
+ this.#pendingTransfer = [];
232
308
  this.#cleanupInFlight = null;
233
309
  this.#queuedCleanupSbns.clear();
234
310
  this.#worker.postMessage({ type: "dispose" } satisfies FecWorkerCommand);
@@ -236,6 +312,16 @@ export class FecWorkerClient {
236
312
  }
237
313
 
238
314
  #handleEvent(event: FecWorkerEvent): void {
315
+ if (event.type === "capabilities") {
316
+ if (Array.isArray(event.commands) && event.commands.includes("feedSymbols")) {
317
+ this.#batchSymbols = true;
318
+ }
319
+ if (Array.isArray(event.commands) && event.commands.includes("subscribeBlockUpdates")) {
320
+ this.#blockUpdatesAnnounced = true;
321
+ this.#subscribeBlockUpdates();
322
+ }
323
+ return;
324
+ }
239
325
  if (event.type === "resetComplete") {
240
326
  this.#pendingResets = Math.max(0, this.#pendingResets - 1);
241
327
  this.#resetFailed = false;
@@ -261,9 +347,6 @@ export class FecWorkerClient {
261
347
  case "blockUpdate":
262
348
  this.#onBlockUpdate?.(event.block);
263
349
  break;
264
- case "greenFill":
265
- this.#onGreenFill?.(event.sbn, event.data);
266
- break;
267
350
  case "cleanupComplete":
268
351
  for (const sbn of event.sbns) {
269
352
  this.#onCleanupComplete?.(sbn);