@zero-server/grpc 0.9.0 → 0.9.2

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,1221 @@
1
+ /**
2
+ * @module grpc/codec
3
+ * @description Zero-dependency Protocol Buffers wire-format encoder/decoder.
4
+ * Implements the proto3 binary encoding for all scalar types,
5
+ * nested messages, repeated fields, maps, oneofs, and enums.
6
+ *
7
+ * Wire types:
8
+ * - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
9
+ * - 1: 64-bit fixed (fixed64, sfixed64, double)
10
+ * - 2: Length-delimited (string, bytes, nested message, packed repeated)
11
+ * - 5: 32-bit fixed (fixed32, sfixed32, float)
12
+ *
13
+ * @see https://protobuf.dev/programming-guides/encoding/
14
+ */
15
+
16
+ const log = require('../debug')('zero:grpc');
17
+
18
+ // -- Constants ---------------------------------------------
19
+
20
+ /**
21
+ * Protobuf wire types.
22
+ * @enum {number}
23
+ */
24
+ const WIRE_TYPE = {
25
+ VARINT: 0,
26
+ FIXED64: 1,
27
+ LENGTH_DELIMITED: 2,
28
+ /** @deprecated Not used in proto3. */
29
+ START_GROUP: 3,
30
+ /** @deprecated Not used in proto3. */
31
+ END_GROUP: 4,
32
+ FIXED32: 5,
33
+ };
34
+
35
+ /**
36
+ * Maximum varint size in bytes (10 bytes for 64-bit values).
37
+ * @type {number}
38
+ */
39
+ const MAX_VARINT_SIZE = 10;
40
+
41
+ /**
42
+ * Maximum message size (4 MB default, configurable per-call).
43
+ * @type {number}
44
+ */
45
+ const DEFAULT_MAX_MESSAGE_SIZE = 4 * 1024 * 1024;
46
+
47
+ /**
48
+ * Maximum recursion depth for nested messages (prevents stack overflow from malicious payloads).
49
+ * @type {number}
50
+ */
51
+ const MAX_RECURSION_DEPTH = 64;
52
+
53
+ /**
54
+ * Maps proto3 type names to wire types and read/write helpers.
55
+ * @private
56
+ */
57
+ const TYPE_INFO = {
58
+ double: { wire: WIRE_TYPE.FIXED64, size: 8 },
59
+ float: { wire: WIRE_TYPE.FIXED32, size: 4 },
60
+ int32: { wire: WIRE_TYPE.VARINT },
61
+ int64: { wire: WIRE_TYPE.VARINT },
62
+ uint32: { wire: WIRE_TYPE.VARINT },
63
+ uint64: { wire: WIRE_TYPE.VARINT },
64
+ sint32: { wire: WIRE_TYPE.VARINT },
65
+ sint64: { wire: WIRE_TYPE.VARINT },
66
+ fixed32: { wire: WIRE_TYPE.FIXED32, size: 4 },
67
+ fixed64: { wire: WIRE_TYPE.FIXED64, size: 8 },
68
+ sfixed32: { wire: WIRE_TYPE.FIXED32, size: 4 },
69
+ sfixed64: { wire: WIRE_TYPE.FIXED64, size: 8 },
70
+ bool: { wire: WIRE_TYPE.VARINT },
71
+ string: { wire: WIRE_TYPE.LENGTH_DELIMITED },
72
+ bytes: { wire: WIRE_TYPE.LENGTH_DELIMITED },
73
+ // enum and message are handled dynamically
74
+ };
75
+
76
+ // -- Writer ------------------------------------------------
77
+
78
+ /**
79
+ * Protobuf binary writer — encodes JavaScript objects into wire-format bytes.
80
+ *
81
+ * @class
82
+ *
83
+ * @example
84
+ * const writer = new Writer();
85
+ * writer.writeVarint(1 << 3 | 0, 150); // field 1, varint = 150
86
+ * const bytes = writer.finish();
87
+ */
88
+ class Writer
89
+ {
90
+ constructor()
91
+ {
92
+ /** @private */
93
+ this._chunks = [];
94
+ /** @private */
95
+ this._size = 0;
96
+ }
97
+
98
+ // -- Low-Level Primitives ------------------------------
99
+
100
+ /**
101
+ * Write raw bytes.
102
+ *
103
+ * @param {Buffer} buf
104
+ * @returns {Writer} `this` for chaining.
105
+ */
106
+ writeRaw(buf)
107
+ {
108
+ this._chunks.push(buf);
109
+ this._size += buf.length;
110
+ return this;
111
+ }
112
+
113
+ /**
114
+ * Write a varint (variable-length integer) using LEB128 encoding.
115
+ * Handles values up to 2^53 safely (JavaScript number precision limit).
116
+ *
117
+ * @param {number} value - Non-negative integer.
118
+ * @returns {Writer} `this` for chaining.
119
+ */
120
+ writeVarint(value)
121
+ {
122
+ const buf = Buffer.alloc(MAX_VARINT_SIZE);
123
+ let offset = 0;
124
+
125
+ // Handle negative numbers as unsigned 64-bit
126
+ if (value < 0)
127
+ {
128
+ // Two's complement for 64-bit
129
+ const lo = (value & 0xFFFFFFFF) >>> 0;
130
+ const hi = (Math.floor(value / 0x100000000) & 0xFFFFFFFF) >>> 0;
131
+ return this._writeVarint64(lo, hi);
132
+ }
133
+
134
+ while (value > 0x7F)
135
+ {
136
+ buf[offset++] = (value & 0x7F) | 0x80;
137
+ value >>>= 7;
138
+ }
139
+ buf[offset++] = value & 0x7F;
140
+ this._chunks.push(buf.subarray(0, offset));
141
+ this._size += offset;
142
+ return this;
143
+ }
144
+
145
+ /**
146
+ * Write a 64-bit varint as two 32-bit halves.
147
+ * @private
148
+ * @param {number} lo - Low 32 bits.
149
+ * @param {number} hi - High 32 bits.
150
+ * @returns {Writer}
151
+ */
152
+ _writeVarint64(lo, hi)
153
+ {
154
+ const buf = Buffer.alloc(MAX_VARINT_SIZE);
155
+ let offset = 0;
156
+
157
+ while (hi > 0 || lo > 0x7F)
158
+ {
159
+ buf[offset++] = (lo & 0x7F) | 0x80;
160
+ lo = ((lo >>> 7) | (hi << 25)) >>> 0;
161
+ hi >>>= 7;
162
+ }
163
+ buf[offset++] = lo & 0x7F;
164
+ this._chunks.push(buf.subarray(0, offset));
165
+ this._size += offset;
166
+ return this;
167
+ }
168
+
169
+ /**
170
+ * Write a signed varint using ZigZag encoding (sint32/sint64).
171
+ *
172
+ * @param {number} value - Signed integer.
173
+ * @returns {Writer}
174
+ */
175
+ writeSVarint(value)
176
+ {
177
+ // ZigZag: (n << 1) ^ (n >> 31) for 32-bit
178
+ return this.writeVarint(((value << 1) ^ (value >> 31)) >>> 0);
179
+ }
180
+
181
+ /**
182
+ * Write a 32-bit fixed integer (little-endian).
183
+ *
184
+ * @param {number} value
185
+ * @returns {Writer}
186
+ */
187
+ writeFixed32(value)
188
+ {
189
+ const buf = Buffer.alloc(4);
190
+ buf.writeUInt32LE(value >>> 0, 0);
191
+ return this.writeRaw(buf);
192
+ }
193
+
194
+ /**
195
+ * Write a 32-bit signed fixed integer (little-endian).
196
+ *
197
+ * @param {number} value
198
+ * @returns {Writer}
199
+ */
200
+ writeSFixed32(value)
201
+ {
202
+ const buf = Buffer.alloc(4);
203
+ buf.writeInt32LE(value, 0);
204
+ return this.writeRaw(buf);
205
+ }
206
+
207
+ /**
208
+ * Write a 64-bit fixed integer (little-endian) from a BigInt or number.
209
+ *
210
+ * @param {number|BigInt} value
211
+ * @returns {Writer}
212
+ */
213
+ writeFixed64(value)
214
+ {
215
+ const buf = Buffer.alloc(8);
216
+ if (typeof value === 'bigint')
217
+ {
218
+ buf.writeBigUInt64LE(value, 0);
219
+ }
220
+ else
221
+ {
222
+ buf.writeUInt32LE(value >>> 0, 0);
223
+ buf.writeUInt32LE((value / 0x100000000) >>> 0, 4);
224
+ }
225
+ return this.writeRaw(buf);
226
+ }
227
+
228
+ /**
229
+ * Write a 64-bit signed fixed integer (little-endian).
230
+ *
231
+ * @param {number|BigInt} value
232
+ * @returns {Writer}
233
+ */
234
+ writeSFixed64(value)
235
+ {
236
+ const buf = Buffer.alloc(8);
237
+ if (typeof value === 'bigint')
238
+ {
239
+ buf.writeBigInt64LE(value, 0);
240
+ }
241
+ else
242
+ {
243
+ buf.writeInt32LE(value & 0xFFFFFFFF, 0);
244
+ buf.writeInt32LE(Math.floor(value / 0x100000000), 4);
245
+ }
246
+ return this.writeRaw(buf);
247
+ }
248
+
249
+ /**
250
+ * Write a 32-bit IEEE 754 float.
251
+ *
252
+ * @param {number} value
253
+ * @returns {Writer}
254
+ */
255
+ writeFloat(value)
256
+ {
257
+ const buf = Buffer.alloc(4);
258
+ buf.writeFloatLE(value, 0);
259
+ return this.writeRaw(buf);
260
+ }
261
+
262
+ /**
263
+ * Write a 64-bit IEEE 754 double.
264
+ *
265
+ * @param {number} value
266
+ * @returns {Writer}
267
+ */
268
+ writeDouble(value)
269
+ {
270
+ const buf = Buffer.alloc(8);
271
+ buf.writeDoubleLE(value, 0);
272
+ return this.writeRaw(buf);
273
+ }
274
+
275
+ /**
276
+ * Write a boolean as a single-byte varint.
277
+ *
278
+ * @param {boolean} value
279
+ * @returns {Writer}
280
+ */
281
+ writeBool(value)
282
+ {
283
+ return this.writeVarint(value ? 1 : 0);
284
+ }
285
+
286
+ /**
287
+ * Write a UTF-8 string (length-prefixed).
288
+ *
289
+ * @param {string} value
290
+ * @returns {Writer}
291
+ */
292
+ writeString(value)
293
+ {
294
+ const strBuf = Buffer.from(value, 'utf8');
295
+ this.writeVarint(strBuf.length);
296
+ return this.writeRaw(strBuf);
297
+ }
298
+
299
+ /**
300
+ * Write raw bytes (length-prefixed).
301
+ *
302
+ * @param {Buffer} value
303
+ * @returns {Writer}
304
+ */
305
+ writeBytes(value)
306
+ {
307
+ if (!Buffer.isBuffer(value)) value = Buffer.from(value);
308
+ this.writeVarint(value.length);
309
+ return this.writeRaw(value);
310
+ }
311
+
312
+ /**
313
+ * Write a field tag (field number + wire type).
314
+ *
315
+ * @param {number} fieldNumber - Protobuf field number.
316
+ * @param {number} wireType - Wire type (0-5).
317
+ * @returns {Writer}
318
+ */
319
+ writeTag(fieldNumber, wireType)
320
+ {
321
+ return this.writeVarint((fieldNumber << 3) | wireType);
322
+ }
323
+
324
+ /**
325
+ * Finalize and return the complete encoded buffer.
326
+ *
327
+ * @returns {Buffer} Concatenated protobuf binary.
328
+ */
329
+ finish()
330
+ {
331
+ if (this._chunks.length === 0) return Buffer.alloc(0);
332
+ if (this._chunks.length === 1) return this._chunks[0];
333
+ return Buffer.concat(this._chunks, this._size);
334
+ }
335
+
336
+ /**
337
+ * Get the total byte size of all written data.
338
+ *
339
+ * @returns {number}
340
+ */
341
+ get length()
342
+ {
343
+ return this._size;
344
+ }
345
+
346
+ // -- Field-level convenience methods (tag + value) ------
347
+
348
+ /**
349
+ * Write a string field (tag + length-delimited string).
350
+ * @param {number} fieldNumber
351
+ * @param {string} value
352
+ * @returns {Writer}
353
+ */
354
+ string(fieldNumber, value)
355
+ {
356
+ this.writeTag(fieldNumber, 2);
357
+ return this.writeString(value);
358
+ }
359
+
360
+ /**
361
+ * Write a bytes/embedded-message field (tag + length-delimited bytes).
362
+ * @param {number} fieldNumber
363
+ * @param {Buffer} value
364
+ * @returns {Writer}
365
+ */
366
+ bytes(fieldNumber, value)
367
+ {
368
+ this.writeTag(fieldNumber, 2);
369
+ return this.writeBytes(value);
370
+ }
371
+
372
+ /**
373
+ * Write an int32/enum field (tag + varint).
374
+ * @param {number} fieldNumber
375
+ * @param {number} value
376
+ * @returns {Writer}
377
+ */
378
+ int32(fieldNumber, value)
379
+ {
380
+ this.writeTag(fieldNumber, 0);
381
+ return this.writeVarint(value);
382
+ }
383
+
384
+ /**
385
+ * Write a bool field (tag + varint 0/1).
386
+ * @param {number} fieldNumber
387
+ * @param {boolean} value
388
+ * @returns {Writer}
389
+ */
390
+ bool(fieldNumber, value)
391
+ {
392
+ this.writeTag(fieldNumber, 0);
393
+ return this.writeBool(value);
394
+ }
395
+ }
396
+
397
+ // -- Reader ------------------------------------------------
398
+
399
+ /**
400
+ * Protobuf binary reader — decodes wire-format bytes into JavaScript values.
401
+ *
402
+ * @class
403
+ *
404
+ * @param {Buffer} buffer - Protobuf binary data to read.
405
+ *
406
+ * @example
407
+ * const reader = new Reader(buffer);
408
+ * while (reader.remaining > 0) {
409
+ * const { fieldNumber, wireType } = reader.readTag();
410
+ * // read value based on wireType...
411
+ * }
412
+ */
413
+ class Reader
414
+ {
415
+ /**
416
+ * @constructor
417
+ * @param {Buffer} buffer - Protobuf wire-format data.
418
+ */
419
+ constructor(buffer)
420
+ {
421
+ if (!Buffer.isBuffer(buffer))
422
+ throw new TypeError('Reader requires a Buffer');
423
+
424
+ /** @private */
425
+ this._buf = buffer;
426
+ /** @private */
427
+ this._pos = 0;
428
+ /** @private */
429
+ this._end = buffer.length;
430
+ }
431
+
432
+ /**
433
+ * Number of bytes remaining to be read.
434
+ *
435
+ * @returns {number}
436
+ */
437
+ get remaining()
438
+ {
439
+ return this._end - this._pos;
440
+ }
441
+
442
+ /**
443
+ * Whether all bytes have been consumed.
444
+ *
445
+ * @returns {boolean}
446
+ */
447
+ get done()
448
+ {
449
+ return this._pos >= this._end;
450
+ }
451
+
452
+ /**
453
+ * Current read position (byte offset).
454
+ *
455
+ * @returns {number}
456
+ */
457
+ get position()
458
+ {
459
+ return this._pos;
460
+ }
461
+
462
+ // -- Low-Level Primitives ------------------------------
463
+
464
+ /**
465
+ * Read a varint (LEB128-encoded variable-length integer).
466
+ * Returns a JavaScript number (safe for values up to 2^53).
467
+ *
468
+ * @returns {number}
469
+ */
470
+ readVarint()
471
+ {
472
+ let result = 0;
473
+ let shift = 0;
474
+
475
+ for (let i = 0; i < MAX_VARINT_SIZE; i++)
476
+ {
477
+ if (this._pos >= this._end)
478
+ throw new RangeError('Varint extends past end of buffer');
479
+
480
+ const byte = this._buf[this._pos++];
481
+ result |= (byte & 0x7F) << shift;
482
+
483
+ if ((byte & 0x80) === 0)
484
+ {
485
+ // For values that might overflow 32-bit, reconstruct using multiplication
486
+ if (shift >= 28)
487
+ {
488
+ return this._readVarintSlow(result, shift, byte);
489
+ }
490
+ return result >>> 0;
491
+ }
492
+ shift += 7;
493
+ if (shift >= 28)
494
+ {
495
+ return this._readVarintHigh(result, shift);
496
+ }
497
+ }
498
+
499
+ throw new RangeError('Varint too long (> 10 bytes)');
500
+ }
501
+
502
+ /**
503
+ * Handle high-bit varint continuation.
504
+ * @private
505
+ */
506
+ _readVarintHigh(lo, shift)
507
+ {
508
+ let hi = 0;
509
+ let hiShift = 0;
510
+
511
+ if (shift === 28)
512
+ {
513
+ // We've read 4 bytes (28 bits). The 5th byte contributes to both lo and hi.
514
+ if (this._pos >= this._end)
515
+ throw new RangeError('Varint extends past end of buffer');
516
+ const byte = this._buf[this._pos++];
517
+ lo |= (byte & 0x0F) << 28;
518
+ hi = (byte & 0x7F) >> 4;
519
+ if ((byte & 0x80) === 0)
520
+ return (hi * 0x100000000 + (lo >>> 0));
521
+ hiShift = 3;
522
+ }
523
+
524
+ for (let i = 0; i < 5; i++)
525
+ {
526
+ if (this._pos >= this._end)
527
+ throw new RangeError('Varint extends past end of buffer');
528
+ const byte = this._buf[this._pos++];
529
+ hi |= (byte & 0x7F) << hiShift;
530
+ hiShift += 7;
531
+ if ((byte & 0x80) === 0)
532
+ return (hi * 0x100000000 + (lo >>> 0));
533
+ }
534
+
535
+ throw new RangeError('Varint too long');
536
+ }
537
+
538
+ /**
539
+ * Reconstruct large varint value.
540
+ * @private
541
+ */
542
+ _readVarintSlow(lo, shift, lastByte)
543
+ {
544
+ return lo >>> 0;
545
+ }
546
+
547
+ /**
548
+ * Read a signed varint using ZigZag decoding (sint32/sint64).
549
+ *
550
+ * @returns {number}
551
+ */
552
+ readSVarint()
553
+ {
554
+ const n = this.readVarint();
555
+ return ((n >>> 1) ^ -(n & 1)) | 0;
556
+ }
557
+
558
+ /**
559
+ * Read a 32-bit fixed unsigned integer (little-endian).
560
+ *
561
+ * @returns {number}
562
+ */
563
+ readFixed32()
564
+ {
565
+ this._checkBounds(4);
566
+ const val = this._buf.readUInt32LE(this._pos);
567
+ this._pos += 4;
568
+ return val;
569
+ }
570
+
571
+ /**
572
+ * Read a 32-bit fixed signed integer (little-endian).
573
+ *
574
+ * @returns {number}
575
+ */
576
+ readSFixed32()
577
+ {
578
+ this._checkBounds(4);
579
+ const val = this._buf.readInt32LE(this._pos);
580
+ this._pos += 4;
581
+ return val;
582
+ }
583
+
584
+ /**
585
+ * Read a 64-bit fixed unsigned integer (little-endian).
586
+ * Returns a number (precision loss above 2^53).
587
+ *
588
+ * @returns {number}
589
+ */
590
+ readFixed64()
591
+ {
592
+ this._checkBounds(8);
593
+ const lo = this._buf.readUInt32LE(this._pos);
594
+ const hi = this._buf.readUInt32LE(this._pos + 4);
595
+ this._pos += 8;
596
+ return hi * 0x100000000 + lo;
597
+ }
598
+
599
+ /**
600
+ * Read a 64-bit fixed signed integer (little-endian).
601
+ *
602
+ * @returns {number}
603
+ */
604
+ readSFixed64()
605
+ {
606
+ this._checkBounds(8);
607
+ const lo = this._buf.readUInt32LE(this._pos);
608
+ const hi = this._buf.readInt32LE(this._pos + 4);
609
+ this._pos += 8;
610
+ return hi * 0x100000000 + lo;
611
+ }
612
+
613
+ /**
614
+ * Read a 32-bit IEEE 754 float.
615
+ *
616
+ * @returns {number}
617
+ */
618
+ readFloat()
619
+ {
620
+ this._checkBounds(4);
621
+ const val = this._buf.readFloatLE(this._pos);
622
+ this._pos += 4;
623
+ return val;
624
+ }
625
+
626
+ /**
627
+ * Read a 64-bit IEEE 754 double.
628
+ *
629
+ * @returns {number}
630
+ */
631
+ readDouble()
632
+ {
633
+ this._checkBounds(8);
634
+ const val = this._buf.readDoubleLE(this._pos);
635
+ this._pos += 8;
636
+ return val;
637
+ }
638
+
639
+ /**
640
+ * Read a boolean varint.
641
+ *
642
+ * @returns {boolean}
643
+ */
644
+ readBool()
645
+ {
646
+ return this.readVarint() !== 0;
647
+ }
648
+
649
+ /**
650
+ * Read a length-prefixed UTF-8 string.
651
+ *
652
+ * @returns {string}
653
+ */
654
+ readString()
655
+ {
656
+ const len = this.readVarint();
657
+ this._checkBounds(len);
658
+ const str = this._buf.toString('utf8', this._pos, this._pos + len);
659
+ this._pos += len;
660
+ return str;
661
+ }
662
+
663
+ /**
664
+ * Read length-prefixed raw bytes.
665
+ *
666
+ * @returns {Buffer}
667
+ */
668
+ readBytes()
669
+ {
670
+ const len = this.readVarint();
671
+ this._checkBounds(len);
672
+ const buf = this._buf.subarray(this._pos, this._pos + len);
673
+ this._pos += len;
674
+ return Buffer.from(buf); // copy to detach from source
675
+ }
676
+
677
+ /**
678
+ * Read a field tag and decode field number + wire type.
679
+ *
680
+ * @returns {{ fieldNumber: number, wireType: number }}
681
+ */
682
+ readTag()
683
+ {
684
+ const tag = this.readVarint();
685
+ return {
686
+ fieldNumber: tag >>> 3,
687
+ wireType: tag & 0x07,
688
+ };
689
+ }
690
+
691
+ /**
692
+ * Skip a field value based on its wire type (for unknown fields).
693
+ *
694
+ * @param {number} wireType - Wire type to skip.
695
+ */
696
+ skipField(wireType)
697
+ {
698
+ switch (wireType)
699
+ {
700
+ case WIRE_TYPE.VARINT:
701
+ this.readVarint();
702
+ break;
703
+ case WIRE_TYPE.FIXED64:
704
+ this._checkBounds(8);
705
+ this._pos += 8;
706
+ break;
707
+ case WIRE_TYPE.LENGTH_DELIMITED:
708
+ {
709
+ const len = this.readVarint();
710
+ this._checkBounds(len);
711
+ this._pos += len;
712
+ break;
713
+ }
714
+ case WIRE_TYPE.FIXED32:
715
+ this._checkBounds(4);
716
+ this._pos += 4;
717
+ break;
718
+ default:
719
+ throw new Error(`Unknown wire type: ${wireType}`);
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Create a sub-reader for a length-delimited embedded message.
725
+ *
726
+ * @returns {Reader} A new Reader limited to the embedded message bytes.
727
+ */
728
+ readSubReader()
729
+ {
730
+ const len = this.readVarint();
731
+ this._checkBounds(len);
732
+ const sub = new Reader(this._buf.subarray(this._pos, this._pos + len));
733
+ this._pos += len;
734
+ return sub;
735
+ }
736
+
737
+ /**
738
+ * Bounds check helper.
739
+ * @private
740
+ */
741
+ _checkBounds(needed)
742
+ {
743
+ if (this._pos + needed > this._end)
744
+ throw new RangeError(`Not enough data: need ${needed} bytes at offset ${this._pos}, have ${this._end - this._pos}`);
745
+ }
746
+ }
747
+
748
+ // -- Message Codec -----------------------------------------
749
+
750
+ /**
751
+ * Encode a JavaScript object to protobuf binary using a message descriptor.
752
+ *
753
+ * @param {object} obj - The JavaScript object to encode.
754
+ * @param {object} messageDesc - Message descriptor from the proto parser.
755
+ * @param {Object<string, object>} allMessages - Map of all message descriptors (for nested types).
756
+ * @param {number} [depth=0] - Current recursion depth (stack overflow protection).
757
+ * @returns {Buffer} Encoded protobuf binary.
758
+ *
759
+ * @example
760
+ * const buf = encode({ name: 'Alice', age: 30 }, personDesc, allMessages);
761
+ */
762
+ function encode(obj, messageDesc, allMessages, depth = 0)
763
+ {
764
+ if (depth > MAX_RECURSION_DEPTH)
765
+ throw new Error(`Maximum encoding depth (${MAX_RECURSION_DEPTH}) exceeded — possible circular reference`);
766
+
767
+ if (!obj || typeof obj !== 'object')
768
+ return Buffer.alloc(0);
769
+
770
+ const writer = new Writer();
771
+ const fields = messageDesc.fields;
772
+
773
+ for (const field of fields)
774
+ {
775
+ const value = obj[field.name];
776
+
777
+ // Proto3: skip fields with default/zero values
778
+ if (value === undefined || value === null) continue;
779
+
780
+ if (field.map)
781
+ {
782
+ _encodeMap(writer, field, value, allMessages, depth);
783
+ }
784
+ else if (field.repeated)
785
+ {
786
+ _encodeRepeated(writer, field, value, allMessages, depth);
787
+ }
788
+ else
789
+ {
790
+ _encodeField(writer, field, value, allMessages, depth);
791
+ }
792
+ }
793
+
794
+ return writer.finish();
795
+ }
796
+
797
+ /**
798
+ * Decode protobuf binary into a JavaScript object using a message descriptor.
799
+ *
800
+ * @param {Buffer} buffer - Protobuf wire-format data.
801
+ * @param {object} messageDesc - Message descriptor from the proto parser.
802
+ * @param {Object<string, object>} allMessages - Map of all message descriptors.
803
+ * @param {number} [depth=0] - Current recursion depth.
804
+ * @returns {object} Decoded JavaScript object.
805
+ *
806
+ * @example
807
+ * const person = decode(buffer, personDesc, allMessages);
808
+ */
809
+ function decode(buffer, messageDesc, allMessages, depth = 0)
810
+ {
811
+ if (depth > MAX_RECURSION_DEPTH)
812
+ throw new Error(`Maximum decoding depth (${MAX_RECURSION_DEPTH}) exceeded — possible circular reference`);
813
+
814
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0)
815
+ return _defaultObject(messageDesc);
816
+
817
+ const reader = new Reader(buffer);
818
+ const result = _defaultObject(messageDesc);
819
+ const fieldMap = {};
820
+
821
+ for (const f of messageDesc.fields)
822
+ {
823
+ fieldMap[f.number] = f;
824
+ }
825
+
826
+ while (reader.remaining > 0)
827
+ {
828
+ const { fieldNumber, wireType } = reader.readTag();
829
+ const field = fieldMap[fieldNumber];
830
+
831
+ if (!field)
832
+ {
833
+ // Unknown field — skip it (forward compatibility)
834
+ reader.skipField(wireType);
835
+ continue;
836
+ }
837
+
838
+ if (field.map)
839
+ {
840
+ _decodeMapEntry(reader, field, result, allMessages, depth);
841
+ }
842
+ else if (field.repeated && wireType === WIRE_TYPE.LENGTH_DELIMITED && isPackable(field.type))
843
+ {
844
+ // Packed repeated field
845
+ _decodePackedRepeated(reader, field, result);
846
+ }
847
+ else if (field.repeated)
848
+ {
849
+ // Non-packed repeated (one element per tag)
850
+ const val = _readFieldValue(reader, field, wireType, allMessages, depth);
851
+ result[field.name].push(val);
852
+ }
853
+ else
854
+ {
855
+ result[field.name] = _readFieldValue(reader, field, wireType, allMessages, depth);
856
+ }
857
+ }
858
+
859
+ return result;
860
+ }
861
+
862
+ // -- Private Encoding Helpers ------------------------------
863
+
864
+ /** @private */
865
+ function _encodeField(writer, field, value, allMessages, depth)
866
+ {
867
+ const typeInfo = TYPE_INFO[field.type];
868
+
869
+ if (typeInfo)
870
+ {
871
+ // Scalar type — skip default values in proto3
872
+ if (_isDefaultValue(field.type, value)) return;
873
+
874
+ writer.writeTag(field.number, typeInfo.wire);
875
+ _writeScalar(writer, field.type, value);
876
+ }
877
+ else if (field.enumDef)
878
+ {
879
+ // Enum field
880
+ const numVal = typeof value === 'string' ? (field.enumDef.values[value] || 0) : Number(value);
881
+ if (numVal === 0) return; // default enum value
882
+ writer.writeTag(field.number, WIRE_TYPE.VARINT);
883
+ writer.writeVarint(numVal);
884
+ }
885
+ else
886
+ {
887
+ // Nested message
888
+ const msgDesc = allMessages[field.type];
889
+ if (!msgDesc) throw new Error(`Unknown message type: ${field.type}`);
890
+
891
+ const nested = encode(value, msgDesc, allMessages, depth + 1);
892
+ writer.writeTag(field.number, WIRE_TYPE.LENGTH_DELIMITED);
893
+ writer.writeBytes(nested);
894
+ }
895
+ }
896
+
897
+ /** @private */
898
+ function _encodeRepeated(writer, field, values, allMessages, depth)
899
+ {
900
+ if (!Array.isArray(values) || values.length === 0) return;
901
+
902
+ const typeInfo = TYPE_INFO[field.type];
903
+
904
+ // Pack scalars (proto3 default)
905
+ if (typeInfo && isPackable(field.type))
906
+ {
907
+ const inner = new Writer();
908
+ for (const v of values) _writeScalar(inner, field.type, v);
909
+ writer.writeTag(field.number, WIRE_TYPE.LENGTH_DELIMITED);
910
+ const packed = inner.finish();
911
+ writer.writeVarint(packed.length);
912
+ writer.writeRaw(packed);
913
+ }
914
+ else
915
+ {
916
+ // Non-packable (strings, bytes, messages) — one tag per element
917
+ for (const v of values) _encodeField(writer, field, v, allMessages, depth);
918
+ }
919
+ }
920
+
921
+ /** @private */
922
+ function _encodeMap(writer, field, mapObj, allMessages, depth)
923
+ {
924
+ if (!mapObj || typeof mapObj !== 'object') return;
925
+
926
+ // Maps are encoded as repeated message { key = 1; value = 2; }
927
+ const entries = mapObj instanceof Map ? mapObj.entries() : Object.entries(mapObj);
928
+
929
+ for (const [k, v] of entries)
930
+ {
931
+ const entryWriter = new Writer();
932
+
933
+ // Encode key (field 1)
934
+ const keyField = { number: 1, type: field.keyType, name: 'key' };
935
+ entryWriter.writeTag(1, TYPE_INFO[field.keyType].wire);
936
+ _writeScalar(entryWriter, field.keyType, k);
937
+
938
+ // Encode value (field 2)
939
+ const valField = { number: 2, type: field.valueType, name: 'value', enumDef: field.enumDef };
940
+ _encodeField(entryWriter, valField, v, allMessages, depth);
941
+
942
+ const entryBuf = entryWriter.finish();
943
+ writer.writeTag(field.number, WIRE_TYPE.LENGTH_DELIMITED);
944
+ writer.writeVarint(entryBuf.length);
945
+ writer.writeRaw(entryBuf);
946
+ }
947
+ }
948
+
949
+ // -- Private Decoding Helpers ------------------------------
950
+
951
+ /** @private */
952
+ function _readFieldValue(reader, field, wireType, allMessages, depth)
953
+ {
954
+ const typeInfo = TYPE_INFO[field.type];
955
+
956
+ if (typeInfo)
957
+ {
958
+ return _readScalar(reader, field.type, wireType);
959
+ }
960
+ else if (field.enumDef)
961
+ {
962
+ const val = reader.readVarint();
963
+ // Reverse lookup: number → name
964
+ const reverseEnum = field.enumDef._reverse || _buildReverseEnum(field.enumDef);
965
+ return reverseEnum[val] || val;
966
+ }
967
+ else
968
+ {
969
+ // Nested message
970
+ const msgDesc = allMessages[field.type];
971
+ if (!msgDesc) throw new Error(`Unknown message type: ${field.type}`);
972
+ const sub = reader.readSubReader();
973
+ return decode(sub._buf, msgDesc, allMessages, depth + 1);
974
+ }
975
+ }
976
+
977
+ /** @private */
978
+ function _decodeMapEntry(reader, field, result, allMessages, depth)
979
+ {
980
+ const sub = reader.readSubReader();
981
+ let key, value;
982
+
983
+ while (sub.remaining > 0)
984
+ {
985
+ const { fieldNumber, wireType } = sub.readTag();
986
+ if (fieldNumber === 1)
987
+ {
988
+ key = _readScalar(sub, field.keyType, wireType);
989
+ }
990
+ else if (fieldNumber === 2)
991
+ {
992
+ const valField = { type: field.valueType, enumDef: field.enumDef };
993
+ value = _readFieldValue(sub, valField, wireType, allMessages, depth);
994
+ }
995
+ else
996
+ {
997
+ sub.skipField(wireType);
998
+ }
999
+ }
1000
+
1001
+ if (key !== undefined)
1002
+ {
1003
+ result[field.name][key] = value;
1004
+ }
1005
+ }
1006
+
1007
+ /** @private */
1008
+ function _decodePackedRepeated(reader, field, result)
1009
+ {
1010
+ const sub = reader.readSubReader();
1011
+
1012
+ while (sub.remaining > 0)
1013
+ {
1014
+ const val = _readScalar(sub, field.type);
1015
+ result[field.name].push(val);
1016
+ }
1017
+ }
1018
+
1019
+ // -- Scalar Helpers ----------------------------------------
1020
+
1021
+ /** @private */
1022
+ function _writeScalar(writer, type, value)
1023
+ {
1024
+ switch (type)
1025
+ {
1026
+ case 'int32':
1027
+ case 'int64':
1028
+ case 'uint32':
1029
+ case 'uint64':
1030
+ case 'enum':
1031
+ writer.writeVarint(Number(value));
1032
+ break;
1033
+ case 'sint32':
1034
+ case 'sint64':
1035
+ writer.writeSVarint(Number(value));
1036
+ break;
1037
+ case 'bool':
1038
+ writer.writeBool(!!value);
1039
+ break;
1040
+ case 'fixed32':
1041
+ writer.writeFixed32(Number(value));
1042
+ break;
1043
+ case 'sfixed32':
1044
+ writer.writeSFixed32(Number(value));
1045
+ break;
1046
+ case 'fixed64':
1047
+ writer.writeFixed64(value);
1048
+ break;
1049
+ case 'sfixed64':
1050
+ writer.writeSFixed64(value);
1051
+ break;
1052
+ case 'float':
1053
+ writer.writeFloat(Number(value));
1054
+ break;
1055
+ case 'double':
1056
+ writer.writeDouble(Number(value));
1057
+ break;
1058
+ case 'string':
1059
+ writer.writeString(String(value));
1060
+ break;
1061
+ case 'bytes':
1062
+ writer.writeBytes(Buffer.isBuffer(value) ? value : Buffer.from(value));
1063
+ break;
1064
+ default:
1065
+ throw new Error(`Unknown scalar type: ${type}`);
1066
+ }
1067
+ }
1068
+
1069
+ /** @private */
1070
+ function _readScalar(reader, type, wireType)
1071
+ {
1072
+ switch (type)
1073
+ {
1074
+ case 'int32':
1075
+ {
1076
+ const v = reader.readVarint();
1077
+ return v > 0x7FFFFFFF ? v - 0x100000000 : v;
1078
+ }
1079
+ case 'int64':
1080
+ return reader.readVarint();
1081
+ case 'uint32':
1082
+ case 'uint64':
1083
+ return reader.readVarint();
1084
+ case 'sint32':
1085
+ case 'sint64':
1086
+ return reader.readSVarint();
1087
+ case 'bool':
1088
+ return reader.readBool();
1089
+ case 'fixed32':
1090
+ return reader.readFixed32();
1091
+ case 'sfixed32':
1092
+ return reader.readSFixed32();
1093
+ case 'fixed64':
1094
+ return reader.readFixed64();
1095
+ case 'sfixed64':
1096
+ return reader.readSFixed64();
1097
+ case 'float':
1098
+ return reader.readFloat();
1099
+ case 'double':
1100
+ return reader.readDouble();
1101
+ case 'string':
1102
+ return reader.readString();
1103
+ case 'bytes':
1104
+ return reader.readBytes();
1105
+ case 'enum':
1106
+ return reader.readVarint();
1107
+ default:
1108
+ throw new Error(`Unknown scalar type: ${type}`);
1109
+ }
1110
+ }
1111
+
1112
+ /** @private */
1113
+ function _isDefaultValue(type, value)
1114
+ {
1115
+ switch (type)
1116
+ {
1117
+ case 'string': return value === '';
1118
+ case 'bytes': return Buffer.isBuffer(value) && value.length === 0;
1119
+ case 'bool': return value === false;
1120
+ case 'float':
1121
+ case 'double':
1122
+ case 'int32':
1123
+ case 'int64':
1124
+ case 'uint32':
1125
+ case 'uint64':
1126
+ case 'sint32':
1127
+ case 'sint64':
1128
+ case 'fixed32':
1129
+ case 'fixed64':
1130
+ case 'sfixed32':
1131
+ case 'sfixed64':
1132
+ return value === 0;
1133
+ default:
1134
+ return false;
1135
+ }
1136
+ }
1137
+
1138
+ /** @private */
1139
+ function _defaultObject(messageDesc)
1140
+ {
1141
+ const obj = {};
1142
+ for (const field of messageDesc.fields)
1143
+ {
1144
+ if (field.map)
1145
+ {
1146
+ obj[field.name] = {};
1147
+ }
1148
+ else if (field.repeated)
1149
+ {
1150
+ obj[field.name] = [];
1151
+ }
1152
+ else
1153
+ {
1154
+ obj[field.name] = _defaultScalar(field);
1155
+ }
1156
+ }
1157
+ return obj;
1158
+ }
1159
+
1160
+ /** @private */
1161
+ function _defaultScalar(field)
1162
+ {
1163
+ if (field.enumDef) return 0;
1164
+ switch (field.type)
1165
+ {
1166
+ case 'string': return '';
1167
+ case 'bytes': return Buffer.alloc(0);
1168
+ case 'bool': return false;
1169
+ case 'float':
1170
+ case 'double':
1171
+ case 'int32':
1172
+ case 'int64':
1173
+ case 'uint32':
1174
+ case 'uint64':
1175
+ case 'sint32':
1176
+ case 'sint64':
1177
+ case 'fixed32':
1178
+ case 'fixed64':
1179
+ case 'sfixed32':
1180
+ case 'sfixed64':
1181
+ return 0;
1182
+ default:
1183
+ return null; // nested message default
1184
+ }
1185
+ }
1186
+
1187
+ /**
1188
+ * Check if a protobuf type can be packed (numeric/bool scalars only).
1189
+ *
1190
+ * @param {string} type - Protobuf type name.
1191
+ * @returns {boolean}
1192
+ */
1193
+ function isPackable(type)
1194
+ {
1195
+ return type !== 'string' && type !== 'bytes' && TYPE_INFO[type] !== undefined;
1196
+ }
1197
+
1198
+ /** @private */
1199
+ function _buildReverseEnum(enumDef)
1200
+ {
1201
+ const rev = {};
1202
+ for (const [name, val] of Object.entries(enumDef.values))
1203
+ {
1204
+ rev[val] = name;
1205
+ }
1206
+ enumDef._reverse = rev;
1207
+ return rev;
1208
+ }
1209
+
1210
+ module.exports = {
1211
+ Writer,
1212
+ Reader,
1213
+ WIRE_TYPE,
1214
+ TYPE_INFO,
1215
+ MAX_VARINT_SIZE,
1216
+ MAX_RECURSION_DEPTH,
1217
+ DEFAULT_MAX_MESSAGE_SIZE,
1218
+ encode,
1219
+ decode,
1220
+ isPackable,
1221
+ };