@bcts/spqr 1.0.0-alpha.21

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.
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Copyright © 2025 Signal Messenger, LLC
3
+ * Copyright © 2026 Parity Technologies
4
+ *
5
+ * Polynomial erasure coding over GF(2^16).
6
+ * Ported from the Rust SPQR implementation.
7
+ *
8
+ * The encoder splits a message into 16 parallel polynomials and can produce
9
+ * an unlimited number of coded chunks. Any sufficient subset of chunks
10
+ * allows the decoder to reconstruct the original message via Lagrange
11
+ * interpolation.
12
+ */
13
+
14
+ import { GF16, parallelMult } from "./gf.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Error type
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export class PolynomialError extends Error {
21
+ constructor(message: string) {
22
+ super(message);
23
+ this.name = "PolynomialError";
24
+ }
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** A point on a polynomial in GF(2^16). */
32
+ export interface Pt {
33
+ x: GF16;
34
+ y: GF16;
35
+ }
36
+
37
+ /** A coded chunk: an index plus exactly 32 bytes of data. */
38
+ export interface Chunk {
39
+ index: number; // u16
40
+ data: Uint8Array; // exactly 32 bytes
41
+ }
42
+
43
+ /** Encoder interface. */
44
+ export interface Encoder {
45
+ nextChunk(): Chunk;
46
+ }
47
+
48
+ /** Decoder interface. */
49
+ export interface Decoder {
50
+ addChunk(chunk: Chunk): void;
51
+ decodedMessage(): Uint8Array | null;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Number of polynomials used for interleaving
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const NUM_POLYS = 16;
59
+ const CHUNK_DATA_SIZE = 32; // 16 GF16 values * 2 bytes each
60
+ const MAX_MESSAGE_LENGTH = (1 << 16) * NUM_POLYS; // 1,048,576 bytes (matches Rust)
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Poly class
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * A polynomial over GF(2^16) in coefficient form.
68
+ *
69
+ * Coefficients are stored in little-endian order:
70
+ * coefficients[0] = constant term (x^0)
71
+ * coefficients[1] = linear term (x^1)
72
+ * ...
73
+ */
74
+ export class Poly {
75
+ coefficients: GF16[];
76
+
77
+ constructor(coefficients: GF16[]) {
78
+ this.coefficients = coefficients;
79
+ }
80
+
81
+ /** Create a zero polynomial of a given length. */
82
+ static zeros(len: number): Poly {
83
+ const coeffs: GF16[] = new Array<GF16>(len);
84
+ for (let i = 0; i < len; i++) {
85
+ coeffs[i] = GF16.ZERO;
86
+ }
87
+ return new Poly(coeffs);
88
+ }
89
+
90
+ /** Number of coefficients. */
91
+ get length(): number {
92
+ return this.coefficients.length;
93
+ }
94
+
95
+ // -- Evaluation -----------------------------------------------------------
96
+
97
+ /**
98
+ * Evaluate the polynomial at a given point using a divide-and-conquer
99
+ * approach for computing powers of x, then a dot product.
100
+ *
101
+ * xs[0] = 1, xs[1] = x, xs[i] = xs[floor(i/2)] * xs[floor(i/2) + (i%2)]
102
+ */
103
+ computeAt(x: GF16): GF16 {
104
+ const n = this.coefficients.length;
105
+ if (n === 0) return GF16.ZERO;
106
+ if (n === 1) return this.coefficients[0];
107
+
108
+ // Build powers of x
109
+ const xs: GF16[] = new Array<GF16>(n);
110
+ xs[0] = GF16.ONE;
111
+ if (n > 1) xs[1] = x;
112
+
113
+ for (let i = 2; i < n; i++) {
114
+ const half = i >>> 1;
115
+ const rem = i & 1;
116
+ xs[i] = xs[half].mul(xs[half + rem]);
117
+ }
118
+
119
+ // Dot product: sum(coefficients[i] * xs[i])
120
+ let result = GF16.ZERO;
121
+ for (let i = 0; i < n; i++) {
122
+ result = result.add(this.coefficients[i].mul(xs[i]));
123
+ }
124
+ return result;
125
+ }
126
+
127
+ // -- In-place arithmetic --------------------------------------------------
128
+
129
+ /** Add another polynomial to this one in-place. */
130
+ addAssign(other: Poly): void {
131
+ // Extend if necessary
132
+ while (this.coefficients.length < other.coefficients.length) {
133
+ this.coefficients.push(GF16.ZERO);
134
+ }
135
+ for (let i = 0; i < other.coefficients.length; i++) {
136
+ this.coefficients[i] = this.coefficients[i].add(other.coefficients[i]);
137
+ }
138
+ }
139
+
140
+ /** Multiply all coefficients by a scalar in-place. */
141
+ multAssign(m: GF16): void {
142
+ parallelMult(m, this.coefficients);
143
+ }
144
+
145
+ // -- Serialization --------------------------------------------------------
146
+
147
+ /** Serialize coefficients as big-endian u16 pairs. */
148
+ serialize(): Uint8Array {
149
+ const out = new Uint8Array(this.coefficients.length * 2);
150
+ for (let i = 0; i < this.coefficients.length; i++) {
151
+ const v = this.coefficients[i].value;
152
+ out[i * 2] = (v >>> 8) & 0xff;
153
+ out[i * 2 + 1] = v & 0xff;
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /** Deserialize from big-endian u16 pairs. */
159
+ static deserialize(data: Uint8Array): Poly {
160
+ if (data.length % 2 !== 0) {
161
+ throw new PolynomialError("Poly data length must be even");
162
+ }
163
+ const n = data.length / 2;
164
+ const coeffs: GF16[] = new Array<GF16>(n);
165
+ for (let i = 0; i < n; i++) {
166
+ coeffs[i] = new GF16((data[i * 2] << 8) | data[i * 2 + 1]);
167
+ }
168
+ return new Poly(coeffs);
169
+ }
170
+
171
+ // -- Lagrange interpolation -----------------------------------------------
172
+
173
+ /**
174
+ * Lagrange interpolation over a set of points.
175
+ *
176
+ * Given N points (x_i, y_i), produces the unique polynomial of degree < N
177
+ * passing through all of them.
178
+ */
179
+ static lagrangeInterpolate(pts: Pt[]): Poly {
180
+ const n = pts.length;
181
+ if (n === 0) return new Poly([]);
182
+
183
+ // Step 1: Compute the "master" product polynomial
184
+ // P(x) = PRODUCT_{i=0}^{n-1} (x - x_i)
185
+ //
186
+ // In GF(2^n), (x - x_i) = (x + x_i) since subtraction = addition.
187
+ // We represent (x + x_i) as [x_i, 1] (constant, linear).
188
+ let product = new Poly([pts[0].x, GF16.ONE]);
189
+ for (let i = 1; i < n; i++) {
190
+ product = polyMultiply(product, new Poly([pts[i].x, GF16.ONE]));
191
+ }
192
+
193
+ // Step 2: For each point, compute the Lagrange basis polynomial
194
+ // L_i(x) = y_i * PRODUCT_{j != i} (x - x_j) / PRODUCT_{j != i} (x_i - x_j)
195
+ //
196
+ // We obtain PRODUCT_{j != i} (x - x_j) by dividing the master product by
197
+ // (x - x_i) using synthetic division.
198
+ const result = Poly.zeros(n);
199
+
200
+ for (let i = 0; i < n; i++) {
201
+ // Synthetic division of `product` by (x - x_i) = (x + x_i)
202
+ const basis = syntheticDivide(product, pts[i].x);
203
+
204
+ // Compute the denominator: PRODUCT_{j != i} (x_i - x_j)
205
+ // This is just basis.computeAt(x_i) since basis = product / (x - x_i)
206
+ // and product(x_i) = 0, so we evaluate the quotient at x_i.
207
+ const denom = basis.computeAt(pts[i].x);
208
+
209
+ // Scale: L_i(x) = basis(x) * y_i / denom
210
+ const scale = pts[i].y.div(denom);
211
+ const scaled = clonePoly(basis);
212
+ scaled.multAssign(scale);
213
+
214
+ result.addAssign(scaled);
215
+ }
216
+
217
+ return result;
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Polynomial helpers (private)
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /** Multiply two polynomials (convolution over GF(2^16)). */
226
+ function polyMultiply(a: Poly, b: Poly): Poly {
227
+ if (a.length === 0 || b.length === 0) return new Poly([]);
228
+ const result = Poly.zeros(a.length + b.length - 1);
229
+ for (let i = 0; i < a.length; i++) {
230
+ for (let j = 0; j < b.length; j++) {
231
+ result.coefficients[i + j] = result.coefficients[i + j].add(
232
+ a.coefficients[i].mul(b.coefficients[j]),
233
+ );
234
+ }
235
+ }
236
+ return result;
237
+ }
238
+
239
+ /**
240
+ * Synthetic division: divide poly by (x - root) = (x + root) in GF(2^n).
241
+ *
242
+ * Returns the quotient polynomial (degree one less than input).
243
+ */
244
+ function syntheticDivide(poly: Poly, root: GF16): Poly {
245
+ const n = poly.length;
246
+ if (n <= 1) return new Poly([]);
247
+
248
+ // Work from high degree down
249
+ const quotient: GF16[] = new Array<GF16>(n - 1);
250
+ let carry = GF16.ZERO;
251
+
252
+ for (let i = n - 1; i >= 1; i--) {
253
+ const coeff = poly.coefficients[i].add(carry);
254
+ quotient[i - 1] = coeff;
255
+ // In GF(2^n), multiply by root (same as negated root since -root = root)
256
+ carry = coeff.mul(root);
257
+ }
258
+
259
+ return new Poly(quotient);
260
+ }
261
+
262
+ /** Clone a polynomial (shallow copy of coefficient array). */
263
+ function clonePoly(p: Poly): Poly {
264
+ return new Poly(p.coefficients.slice());
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // SortedPtSet - maintains points sorted by x.value, no duplicate x's
269
+ // ---------------------------------------------------------------------------
270
+
271
+ class SortedPtSet {
272
+ private readonly pts: Pt[] = [];
273
+
274
+ get length(): number {
275
+ return this.pts.length;
276
+ }
277
+
278
+ /** Insert a point, maintaining sorted order. Returns false if duplicate x. */
279
+ insert(pt: Pt): boolean {
280
+ const idx = this.findIndex(pt.x.value);
281
+ if (idx < this.pts.length && this.pts[idx].x.value === pt.x.value) {
282
+ return false; // duplicate x
283
+ }
284
+ this.pts.splice(idx, 0, pt);
285
+ return true;
286
+ }
287
+
288
+ /** Binary search for the insertion point of a given x value. */
289
+ private findIndex(xVal: number): number {
290
+ let lo = 0;
291
+ let hi = this.pts.length;
292
+ while (lo < hi) {
293
+ const mid = (lo + hi) >>> 1;
294
+ if (this.pts[mid].x.value < xVal) {
295
+ lo = mid + 1;
296
+ } else {
297
+ hi = mid;
298
+ }
299
+ }
300
+ return lo;
301
+ }
302
+
303
+ /** Look up a point by x value. Returns undefined if not found. */
304
+ findByX(xVal: number): Pt | undefined {
305
+ const idx = this.findIndex(xVal);
306
+ if (idx < this.pts.length && this.pts[idx].x.value === xVal) {
307
+ return this.pts[idx];
308
+ }
309
+ return undefined;
310
+ }
311
+
312
+ /** Return all points as an array (for interpolation). */
313
+ toArray(): Pt[] {
314
+ return this.pts.slice();
315
+ }
316
+
317
+ /** Serialize all points as big-endian u16 pairs (x then y). */
318
+ serialize(): Uint8Array {
319
+ const out = new Uint8Array(this.pts.length * 4);
320
+ for (let i = 0; i < this.pts.length; i++) {
321
+ const pt = this.pts[i];
322
+ out[i * 4] = (pt.x.value >>> 8) & 0xff;
323
+ out[i * 4 + 1] = pt.x.value & 0xff;
324
+ out[i * 4 + 2] = (pt.y.value >>> 8) & 0xff;
325
+ out[i * 4 + 3] = pt.y.value & 0xff;
326
+ }
327
+ return out;
328
+ }
329
+
330
+ /** Deserialize from big-endian u16 pairs (x then y). */
331
+ static deserialize(data: Uint8Array): SortedPtSet {
332
+ if (data.length % 4 !== 0) {
333
+ throw new PolynomialError("SortedPtSet data length must be multiple of 4");
334
+ }
335
+ const set = new SortedPtSet();
336
+ const n = data.length / 4;
337
+ for (let i = 0; i < n; i++) {
338
+ const x = new GF16((data[i * 4] << 8) | data[i * 4 + 1]);
339
+ const y = new GF16((data[i * 4 + 2] << 8) | data[i * 4 + 3]);
340
+ set.insert({ x, y });
341
+ }
342
+ return set;
343
+ }
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Encoder state
348
+ // ---------------------------------------------------------------------------
349
+
350
+ /** Points-based state: raw GF16 values for each of 16 polynomials. */
351
+ interface PointsState {
352
+ kind: "points";
353
+ /** 16 arrays of GF16 y-values (x is implicit: 0, 1, 2, ...). */
354
+ points: GF16[][];
355
+ }
356
+
357
+ /** Polys-based state: Lagrange-interpolated polynomials. */
358
+ interface PolysState {
359
+ kind: "polys";
360
+ polys: Poly[];
361
+ }
362
+
363
+ type EncoderState = PointsState | PolysState;
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // PolyEncoder
367
+ // ---------------------------------------------------------------------------
368
+
369
+ export class PolyEncoder implements Encoder {
370
+ private idx: number;
371
+ private state: EncoderState;
372
+
373
+ private constructor(idx: number, state: EncoderState) {
374
+ this.idx = idx;
375
+ this.state = state;
376
+ }
377
+
378
+ /**
379
+ * Create an encoder from a message byte array.
380
+ *
381
+ * The message is split into 16 interleaved streams of GF16 values.
382
+ * Each pair of consecutive bytes becomes one GF16 element. If the message
383
+ * length is odd, it is padded with a zero byte.
384
+ */
385
+ static encodeBytes(msg: Uint8Array): PolyEncoder {
386
+ if (msg.length % 2 !== 0) {
387
+ throw new PolynomialError("Message length must be even");
388
+ }
389
+ if (msg.length > MAX_MESSAGE_LENGTH) {
390
+ throw new PolynomialError("Message length exceeds maximum");
391
+ }
392
+
393
+ // Total number of GF16 values
394
+ const totalValues = msg.length / 2;
395
+
396
+ // Initialize 16 point arrays
397
+ const points: GF16[][] = new Array<GF16[]>(NUM_POLYS);
398
+ for (let p = 0; p < NUM_POLYS; p++) {
399
+ points[p] = [];
400
+ }
401
+
402
+ // Distribute values across 16 polynomials round-robin
403
+ for (let i = 0; i < totalValues; i++) {
404
+ const poly = i % NUM_POLYS;
405
+ const value = (msg[i * 2] << 8) | msg[i * 2 + 1];
406
+ points[poly].push(new GF16(value));
407
+ }
408
+
409
+ return new PolyEncoder(0, { kind: "points", points });
410
+ }
411
+
412
+ /** Return the next chunk and advance the index. */
413
+ nextChunk(): Chunk {
414
+ const chunk = this.chunkAt(this.idx);
415
+ this.idx++;
416
+ return chunk;
417
+ }
418
+
419
+ /** Compute the chunk at a specific index. */
420
+ chunkAt(idx: number): Chunk {
421
+ const data = new Uint8Array(CHUNK_DATA_SIZE);
422
+
423
+ for (let i = 0; i < NUM_POLYS; i++) {
424
+ const totalIdx = idx * NUM_POLYS + i;
425
+ const poly = totalIdx % NUM_POLYS;
426
+ const polyIdx = Math.floor(totalIdx / NUM_POLYS);
427
+ const val = this.pointAt(poly, polyIdx);
428
+ data[i * 2] = (val.value >>> 8) & 0xff;
429
+ data[i * 2 + 1] = val.value & 0xff;
430
+ }
431
+
432
+ return { index: idx & 0xffff, data };
433
+ }
434
+
435
+ /**
436
+ * Get the GF16 value for polynomial `poly` at index `idx`.
437
+ *
438
+ * If we are in Points state and the index is within range, return directly.
439
+ * Otherwise, convert to Polys state and evaluate.
440
+ */
441
+ private pointAt(poly: number, idx: number): GF16 {
442
+ if (this.state.kind === "points") {
443
+ const pts = this.state.points[poly];
444
+ if (idx < pts.length) {
445
+ return pts[idx];
446
+ }
447
+ // Need to convert to polys for extrapolation
448
+ this.convertToPolys();
449
+ }
450
+
451
+ // State is now "polys"
452
+ const polys = (this.state as PolysState).polys;
453
+ return polys[poly].computeAt(new GF16(idx));
454
+ }
455
+
456
+ /** Convert from Points state to Polys state via Lagrange interpolation. */
457
+ private convertToPolys(): void {
458
+ if (this.state.kind === "polys") return;
459
+
460
+ const { points } = this.state;
461
+ const polys: Poly[] = new Array<Poly>(NUM_POLYS);
462
+
463
+ for (let p = 0; p < NUM_POLYS; p++) {
464
+ const pts: Pt[] = points[p].map((y, i) => ({
465
+ x: new GF16(i),
466
+ y,
467
+ }));
468
+ polys[p] = Poly.lagrangeInterpolate(pts);
469
+ }
470
+
471
+ this.state = { kind: "polys", polys };
472
+ }
473
+
474
+ /** Serialize encoder state for protobuf transport. */
475
+ toProto(): { idx: number; pts: Uint8Array[]; polys: Uint8Array[] } {
476
+ if (this.state.kind === "points") {
477
+ const pts: Uint8Array[] = this.state.points.map((arr) => {
478
+ const buf = new Uint8Array(arr.length * 2);
479
+ for (let i = 0; i < arr.length; i++) {
480
+ buf[i * 2] = (arr[i].value >>> 8) & 0xff;
481
+ buf[i * 2 + 1] = arr[i].value & 0xff;
482
+ }
483
+ return buf;
484
+ });
485
+ return { idx: this.idx, pts, polys: [] };
486
+ }
487
+ const polys: Uint8Array[] = this.state.polys.map((p) => p.serialize());
488
+ return { idx: this.idx, pts: [], polys };
489
+ }
490
+
491
+ /** Restore encoder from protobuf data. */
492
+ static fromProto(data: { idx: number; pts: Uint8Array[]; polys: Uint8Array[] }): PolyEncoder {
493
+ if (data.polys.length > 0) {
494
+ const polys = data.polys.map((buf) => Poly.deserialize(buf));
495
+ return new PolyEncoder(data.idx, { kind: "polys", polys });
496
+ }
497
+ const points: GF16[][] = data.pts.map((buf) => {
498
+ const arr: GF16[] = [];
499
+ for (let i = 0; i < buf.length; i += 2) {
500
+ arr.push(new GF16((buf[i] << 8) | buf[i + 1]));
501
+ }
502
+ return arr;
503
+ });
504
+ return new PolyEncoder(data.idx, { kind: "points", points });
505
+ }
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // PolyDecoder
510
+ // ---------------------------------------------------------------------------
511
+
512
+ export class PolyDecoder implements Decoder {
513
+ /** Total number of GF16 values needed (= message byte length / 2, rounded up). */
514
+ ptsNeeded: number;
515
+ private readonly pts: SortedPtSet[];
516
+ private _isComplete: boolean;
517
+
518
+ private constructor(ptsNeeded: number, pts: SortedPtSet[], isComplete: boolean) {
519
+ this.ptsNeeded = ptsNeeded;
520
+ this.pts = pts;
521
+ this._isComplete = isComplete;
522
+ }
523
+
524
+ /**
525
+ * Create a decoder for a message of `lenBytes` bytes.
526
+ *
527
+ * The caller must know the original message length to know when enough
528
+ * chunks have been received.
529
+ */
530
+ static create(lenBytes: number): PolyDecoder {
531
+ if (lenBytes % 2 !== 0) {
532
+ throw new PolynomialError("Message length must be even");
533
+ }
534
+ const ptsNeeded = lenBytes / 2;
535
+
536
+ const pts: SortedPtSet[] = new Array<SortedPtSet>(NUM_POLYS);
537
+ for (let i = 0; i < NUM_POLYS; i++) {
538
+ pts[i] = new SortedPtSet();
539
+ }
540
+
541
+ return new PolyDecoder(ptsNeeded, pts, false);
542
+ }
543
+
544
+ /** Whether all polynomial sets have enough points to decode. */
545
+ get isComplete(): boolean {
546
+ return this._isComplete;
547
+ }
548
+
549
+ /**
550
+ * Number of points necessary for polynomial `poly`.
551
+ *
552
+ * The total points (ptsNeeded) are distributed across 16 polynomials
553
+ * round-robin, so some may need one more point than others.
554
+ */
555
+ private necessaryPoints(poly: number): number {
556
+ const base = Math.floor(this.ptsNeeded / NUM_POLYS);
557
+ return poly < this.ptsNeeded % NUM_POLYS ? base + 1 : base;
558
+ }
559
+
560
+ /** Add a chunk to the decoder. */
561
+ addChunk(chunk: Chunk): void {
562
+ if (this._isComplete) return;
563
+
564
+ for (let i = 0; i < NUM_POLYS; i++) {
565
+ const totalIdx = chunk.index * NUM_POLYS + i;
566
+ const poly = totalIdx % NUM_POLYS;
567
+ const polyIdx = Math.floor(totalIdx / NUM_POLYS);
568
+
569
+ const needed = this.necessaryPoints(poly);
570
+ if (this.pts[poly].length >= needed) {
571
+ continue; // Already have enough points for this polynomial
572
+ }
573
+
574
+ const y = new GF16((chunk.data[i * 2] << 8) | chunk.data[i * 2 + 1]);
575
+ const pt: Pt = { x: new GF16(polyIdx), y };
576
+ this.pts[poly].insert(pt);
577
+ }
578
+
579
+ // Check completeness
580
+ this._isComplete = this.checkComplete();
581
+ }
582
+
583
+ /** Check whether all 16 polynomial sets have enough points. */
584
+ private checkComplete(): boolean {
585
+ for (let p = 0; p < NUM_POLYS; p++) {
586
+ if (this.pts[p].length < this.necessaryPoints(p)) {
587
+ return false;
588
+ }
589
+ }
590
+ return true;
591
+ }
592
+
593
+ /**
594
+ * Attempt to decode the message.
595
+ *
596
+ * Returns the decoded byte array if enough chunks have been received,
597
+ * or null if more chunks are needed.
598
+ */
599
+ decodedMessage(): Uint8Array | null {
600
+ if (!this._isComplete) return null;
601
+
602
+ const result = new Uint8Array(this.ptsNeeded * 2);
603
+
604
+ for (let i = 0; i < this.ptsNeeded; i++) {
605
+ const poly = i % NUM_POLYS;
606
+ const polyIdx = Math.floor(i / NUM_POLYS);
607
+ const xVal = polyIdx;
608
+
609
+ // Try direct lookup first (fast path)
610
+ let val: GF16;
611
+ const direct = this.pts[poly].findByX(xVal);
612
+ if (direct !== undefined) {
613
+ val = direct.y;
614
+ } else {
615
+ // Fall back to Lagrange interpolation
616
+ const allPts = this.pts[poly].toArray();
617
+ const interpolated = Poly.lagrangeInterpolate(allPts);
618
+ val = interpolated.computeAt(new GF16(xVal));
619
+ }
620
+
621
+ result[i * 2] = (val.value >>> 8) & 0xff;
622
+ result[i * 2 + 1] = val.value & 0xff;
623
+ }
624
+
625
+ return result;
626
+ }
627
+
628
+ /** Serialize decoder state for protobuf transport. */
629
+ toProto(): {
630
+ ptsNeeded: number;
631
+ polys: number;
632
+ pts: Uint8Array[];
633
+ isComplete: boolean;
634
+ } {
635
+ return {
636
+ ptsNeeded: this.ptsNeeded,
637
+ polys: NUM_POLYS,
638
+ pts: this.pts.map((s) => s.serialize()),
639
+ isComplete: this._isComplete,
640
+ };
641
+ }
642
+
643
+ /** Restore decoder from protobuf data. */
644
+ static fromProto(data: {
645
+ ptsNeeded: number;
646
+ polys: number;
647
+ pts: Uint8Array[];
648
+ isComplete: boolean;
649
+ }): PolyDecoder {
650
+ const pts: SortedPtSet[] = data.pts.map((buf) => SortedPtSet.deserialize(buf));
651
+ // Ensure we always have exactly 16 sets
652
+ while (pts.length < NUM_POLYS) {
653
+ pts.push(new SortedPtSet() as unknown as SortedPtSet);
654
+ }
655
+ return new PolyDecoder(data.ptsNeeded, pts, data.isComplete);
656
+ }
657
+ }