@pbwire/pbwire 0.0.0-stage → 0.1.7

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,525 @@
1
+ 'use strict';
2
+
3
+ /* pbwire compat runtime: the verify/fromObject/toObject engine every generated
4
+ * facade delegates to. Schemas are cached per embedded descriptor set, so any
5
+ * number of generated files share this module with no global-state surprises:
6
+ * one entry per distinct FileDescriptorSet, keyed by its own bytes.
7
+ */
8
+
9
+ const { Schema } = require('./pbwire.js');
10
+
11
+ const _schemas = new Map();
12
+ function _type(descB64, full) {
13
+ let byName = _schemas.get(descB64);
14
+ if (byName === undefined) {
15
+ byName = new Map();
16
+ byName.set('', Schema.fromBytes(Buffer.from(descB64, 'base64')));
17
+ _schemas.set(descB64, byName);
18
+ }
19
+ let t = byName.get(full);
20
+ if (t === undefined) {
21
+ t = byName.get('').message(full);
22
+ byName.set(full, t);
23
+ }
24
+ return t;
25
+ }
26
+
27
+ function _isLongLike(v) {
28
+ return typeof v === 'object' && v !== null &&
29
+ Number.isInteger(v.low) && Number.isInteger(v.high);
30
+ }
31
+
32
+ function _longToBigInt(v) {
33
+ return (BigInt(v.high >>> 0) << 32n) | BigInt(v.low >>> 0);
34
+ }
35
+
36
+ // _toBigInt normalizes an int64-ish value for the encoder. BigInt passes
37
+ // through, numbers pass through (the addon casts them, same as plain
38
+ // objects), numeric strings and Long-like {low, high} become BigInt.
39
+ function _toBigInt(v, path, unsigned) {
40
+ if (typeof v === 'bigint') {
41
+ // fall through to the range check below
42
+ } else if (typeof v === 'number') {
43
+ if (!Number.isInteger(v)) {
44
+ throw new TypeError('pbwire: ' + path + ' is not an integer');
45
+ }
46
+ return v;
47
+ } else if (typeof v === 'string') {
48
+ if (!/^-?\d+$/.test(v)) {
49
+ throw new TypeError('pbwire: ' + path + ' is not an integer string');
50
+ }
51
+ v = BigInt(v);
52
+ } else if (_isLongLike(v)) {
53
+ v = _longToBigInt(v);
54
+ } else {
55
+ throw new TypeError('pbwire: ' + path + ' is not an int64 value');
56
+ }
57
+ if (unsigned) {
58
+ if (v < 0n || v > 18446744073709551615n) {
59
+ throw new TypeError('pbwire: ' + path + ' out of uint64 range');
60
+ }
61
+ } else if (v < -9223372036854775808n || v > 9223372036854775807n) {
62
+ throw new TypeError('pbwire: ' + path + ' out of int64 range');
63
+ }
64
+ return v;
65
+ }
66
+
67
+ // Shape check only (any BigInt passes); range is enforced when the value
68
+ // is normalized for the encoder in _toBigInt.
69
+ function _checkInt64(v) {
70
+ if (typeof v === 'bigint') {
71
+ return true;
72
+ }
73
+ if (typeof v === 'number') {
74
+ return Number.isInteger(v);
75
+ }
76
+ if (typeof v === 'string') {
77
+ return /^-?\d+$/.test(v);
78
+ }
79
+ return _isLongLike(v);
80
+ }
81
+
82
+ // _verifyField returns null when v is valid, else a reason string.
83
+ function _verifyField(fd, v, path, enums, classes) {
84
+ if (fd.rep) {
85
+ if (!Array.isArray(v)) {
86
+ return path + ': array expected';
87
+ }
88
+ for (let i = 0; i < v.length; i++) {
89
+ const bad = _verifyOne(fd, v[i], path + '[' + i + ']', enums, classes);
90
+ if (bad !== null) {
91
+ return bad;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ return _verifyOne(fd, v, path, enums, classes);
97
+ }
98
+
99
+ function _verifyOne(fd, v, path, enums, classes) {
100
+ switch (fd.k) {
101
+ // Reason strings mirror protobufjs verify() verbatim so mixed code reads
102
+ // the same errors from either library. Like protobufjs, 32-bit checks are
103
+ // integer-only (range truncation happens in the encoder, identically).
104
+ case 'i32':
105
+ case 's32':
106
+ case 'sfx32':
107
+ case 'u32':
108
+ case 'fx32':
109
+ return Number.isInteger(v) ? null : path + ': integer expected';
110
+ case 'i64':
111
+ case 's64':
112
+ case 'sfx64':
113
+ case 'u64':
114
+ case 'fx64':
115
+ return _checkInt64(v) ? null : path + ': integer|Long expected';
116
+ case 'f32':
117
+ case 'f64':
118
+ return (typeof v === 'number') ? null : path + ': number expected';
119
+ case 'bool':
120
+ return (typeof v === 'boolean') ? null : path + ': boolean expected';
121
+ case 'str':
122
+ return (typeof v === 'string') ? null : path + ': string expected';
123
+ case 'bytes':
124
+ return (typeof v === 'object' && v !== null &&
125
+ (Buffer.isBuffer(v) || v instanceof Uint8Array))
126
+ ? null : path + ': buffer expected';
127
+ case 'enum': {
128
+ const names = enums[fd.en];
129
+ if (typeof v === 'number' && Number.isInteger(v)) {
130
+ // Open enums (proto3) carry unknown values through the round trip.
131
+ if (!fd.closed || names === undefined) {
132
+ return null;
133
+ }
134
+ for (const key in names) {
135
+ if (names[key] === v) {
136
+ return null;
137
+ }
138
+ }
139
+ return path + ': enum value expected';
140
+ }
141
+ return path + ': enum value expected';
142
+ }
143
+ case 'msg':
144
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
145
+ return path + ': object expected';
146
+ }
147
+ return classes[fd.msg].verify(v);
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function _fromOne(fd, v, path, enums, classes) {
154
+ switch (fd.k) {
155
+ case 'i64':
156
+ case 's64':
157
+ case 'sfx64':
158
+ return _toBigInt(v, path, false);
159
+ case 'u64':
160
+ case 'fx64':
161
+ return _toBigInt(v, path, true);
162
+ case 'bytes':
163
+ if (Buffer.isBuffer(v)) {
164
+ return v;
165
+ }
166
+ if (v instanceof Uint8Array) {
167
+ return Buffer.from(v);
168
+ }
169
+ throw new TypeError('pbwire: ' + path + ' is not bytes');
170
+ case 'enum': {
171
+ if (typeof v === 'string') {
172
+ const names = enums[fd.en];
173
+ if (names !== undefined && names[v] !== undefined) {
174
+ return names[v];
175
+ }
176
+ throw new TypeError('pbwire: ' + path + ' unknown enum name ' + v);
177
+ }
178
+ if (typeof v === 'number' && Number.isInteger(v)) {
179
+ return v;
180
+ }
181
+ throw new TypeError('pbwire: ' + path + ' is not an enum value');
182
+ }
183
+ case 'msg':
184
+ if (v !== null && typeof v === 'object' && !(v instanceof classes[fd.msg])) {
185
+ return classes[fd.msg].fromObject(v);
186
+ }
187
+ return v;
188
+ default:
189
+ return v;
190
+ }
191
+ }
192
+
193
+ function _fromValue(fd, v, path, enums, classes) {
194
+ if (v === null || v === undefined) {
195
+ return fd.rep ? [] : null;
196
+ }
197
+ if (fd.rep) {
198
+ if (!Array.isArray(v)) {
199
+ throw new TypeError('pbwire: ' + path + ' is not an array');
200
+ }
201
+ return v.map((item, i) => _fromOne(fd, item, path + '[' + i + ']', enums, classes));
202
+ }
203
+ return _fromOne(fd, v, path, enums, classes);
204
+ }
205
+
206
+ function _toInt64(v, mode) {
207
+ const big = typeof v === 'bigint' ? v
208
+ : typeof v === 'number' ? BigInt(Math.trunc(v))
209
+ : _longToBigInt(v);
210
+ if (mode === 'String') {
211
+ return big.toString();
212
+ }
213
+ if (mode === 'Number') {
214
+ return Number(big);
215
+ }
216
+ return big;
217
+ }
218
+
219
+ function _toOne(fd, v, opts, enums, revEnums) {
220
+ switch (fd.k) {
221
+ case 'i64':
222
+ case 's64':
223
+ case 'sfx64':
224
+ case 'u64':
225
+ case 'fx64':
226
+ return _toInt64(v, opts.longs || 'BigInt');
227
+ case 'enum':
228
+ if ((opts.enums || 'Number') === 'String') {
229
+ const rev = revEnums[fd.en];
230
+ if (rev !== undefined && rev[v] !== undefined) {
231
+ return rev[v];
232
+ }
233
+ }
234
+ return v;
235
+ case 'bytes':
236
+ return (opts.bytes || 'Buffer') === 'base64' ? Buffer.from(v).toString('base64') : v;
237
+ case 'msg':
238
+ return v.toObject(opts);
239
+ default:
240
+ return v;
241
+ }
242
+ }
243
+
244
+ // Output keys are camelCase like protobufjs toObject; keepCase:true keeps
245
+ // the proto names (the dynamic API's convention).
246
+ function _toPlain(cls, msg, opts, enums, revEnums) {
247
+ const out = {};
248
+ const keyOf = (fd) => (opts.keepCase ? fd.n : fd.js);
249
+ const marked = msg._present instanceof Set ? msg._present : null;
250
+ for (const fd of cls._FIELDS) {
251
+ const v = msg[fd.js];
252
+ if (v === null || v === undefined) {
253
+ continue;
254
+ }
255
+ if (fd.rep && v.length === 0) {
256
+ continue;
257
+ }
258
+ // Unmarked fields are constructor defaults or direct post-hoc writes:
259
+ // include non-default values (direct post-hoc writes work), drop default
260
+ // ones (so create({}) encodes empty). Marked fields always go on the wire.
261
+ if (!opts.defaults && !fd.req && marked !== null &&
262
+ !marked.has(fd.js) && _isDefault(fd, v)) {
263
+ continue;
264
+ }
265
+ out[keyOf(fd)] = fd.rep
266
+ ? v.map((item) => _toOne(fd, item, opts, enums, revEnums))
267
+ : _toOne(fd, v, opts, enums, revEnums);
268
+ }
269
+ if (opts.defaults) {
270
+ for (const fd of cls._FIELDS) {
271
+ if (out[keyOf(fd)] !== undefined) {
272
+ continue;
273
+ }
274
+ if (fd.rep) {
275
+ out[keyOf(fd)] = [];
276
+ } else if (fd.k === 'msg') {
277
+ out[keyOf(fd)] = null;
278
+ } else {
279
+ out[keyOf(fd)] = _toOne(fd, _defaultFor(fd), opts, enums, revEnums);
280
+ }
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+
286
+ function _isDefault(fd, v) {
287
+ if (fd.rep) {
288
+ return v.length === 0;
289
+ }
290
+ switch (fd.k) {
291
+ case 'i64':
292
+ case 's64':
293
+ case 'sfx64':
294
+ case 'u64':
295
+ case 'fx64':
296
+ return v === 0 || v === 0n;
297
+ case 'bool':
298
+ return v === false;
299
+ case 'str':
300
+ return v === '';
301
+ case 'bytes':
302
+ return v.length === 0;
303
+ case 'msg':
304
+ return v === null || v === undefined;
305
+ default:
306
+ return v === 0;
307
+ }
308
+ }
309
+
310
+ function _defaultFor(fd, classes) {
311
+ if (fd.rep) {
312
+ return [];
313
+ }
314
+ // Presence fields (explicit presence or sub-message) default to absent.
315
+ if (fd.has) {
316
+ return null;
317
+ }
318
+ switch (fd.k) {
319
+ case 'i64':
320
+ case 's64':
321
+ case 'sfx64':
322
+ case 'u64':
323
+ case 'fx64':
324
+ return 0n;
325
+ case 'f32':
326
+ case 'f64':
327
+ case 'i32':
328
+ case 's32':
329
+ case 'sfx32':
330
+ case 'u32':
331
+ case 'fx32':
332
+ case 'enum':
333
+ return 0;
334
+ case 'bool':
335
+ return false;
336
+ case 'str':
337
+ return '';
338
+ case 'bytes':
339
+ return Buffer.alloc(0);
340
+ case 'msg':
341
+ return null;
342
+ default:
343
+ return null;
344
+ }
345
+ }
346
+
347
+ function _encodeVarint(n) {
348
+ const parts = [];
349
+ while (n >= 128) {
350
+ parts.push((n % 128) | 128);
351
+ n = Math.floor(n / 128);
352
+ }
353
+ parts.push(n);
354
+ return Buffer.from(parts);
355
+ }
356
+
357
+ function _decodeVarint(buf, pos) {
358
+ let n = 0;
359
+ let shift = 0;
360
+ while (pos < buf.length) {
361
+ const b = buf[pos++];
362
+ n += (b & 127) * 2 ** shift;
363
+ shift += 7;
364
+ if ((b & 128) === 0) {
365
+ return [n, pos];
366
+ }
367
+ if (shift >= 64) {
368
+ throw new Error('pbwire: varint too long');
369
+ }
370
+ }
371
+ throw new Error('pbwire: truncated varint');
372
+ }
373
+
374
+ // _makeClass builds one message class over a MessageType. cls._FIELDS is the
375
+ // table the engine above reads; wire bytes always come from the C core.
376
+ // _fromWire converts C-validated wire values into an instance. Unlike
377
+ // fromObject it performs no validation and no coercion: the decoder has
378
+ // already checked every value against the schema, so nested plains only
379
+ // need wrapping and every present field only needs marking. The encode
380
+ // boundary (addon tree build) remains the loudness net for bad data.
381
+ function _fromWire(cls, plain) {
382
+ // new keeps V8's hidden-class shape (measured faster than Object.create
383
+ // + defineProperty here); the constructor's defaults stand for absent
384
+ // fields, present ones are converted and marked below.
385
+ const inst = new cls();
386
+ const classes = cls._CLASSES;
387
+ for (const fd of cls._FIELDS) {
388
+ const v = plain[fd.n];
389
+ if (v === undefined) {
390
+ continue;
391
+ }
392
+ if (fd.k === 'msg') {
393
+ if (fd.rep) {
394
+ inst[fd.js] = v.map((item) => _fromWire(classes[fd.msg], item));
395
+ } else {
396
+ inst[fd.js] = _fromWire(classes[fd.msg], v);
397
+ }
398
+ } else if (fd.rep) {
399
+ inst[fd.js] = v.slice();
400
+ } else {
401
+ inst[fd.js] = v;
402
+ }
403
+ inst._present.add(fd.js);
404
+ }
405
+ return inst;
406
+ }
407
+
408
+ function _makeClass(full, descB64, fields) {
409
+ class Compat {
410
+ constructor(props) {
411
+ props = props || {};
412
+ Object.defineProperty(this, '_present', { value: new Set(), enumerable: false, writable: true });
413
+ for (const fd of fields) {
414
+ const v = props[fd.js] !== undefined ? props[fd.js] : props[fd.n];
415
+ if (v === undefined) {
416
+ this[fd.js] = _defaultFor(fd);
417
+ } else {
418
+ this[fd.js] = v;
419
+ // fromObject normalizes below; direct construction marks as set.
420
+ this._present.add(fd.js);
421
+ }
422
+ }
423
+ }
424
+ static create(props) {
425
+ return this.fromObject(props || {});
426
+ }
427
+ static encode(message) {
428
+ const inst = (message instanceof this) ? message : this.fromObject(message);
429
+ // The dynamic encoder takes proto names; toObject() shows camelCase.
430
+ const plain = _toPlain(this, inst, { keepCase: true }, this._ENUMS, this._REVENUMS);
431
+ const wire = _type(descB64, full).encode(plain);
432
+ return { finish: () => wire };
433
+ }
434
+ static encodeDelimited(message) {
435
+ const wire = this.encode(message).finish();
436
+ const out = Buffer.concat([_encodeVarint(wire.length), wire]);
437
+ return { finish: () => out };
438
+ }
439
+ static decode(bytes) {
440
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
441
+ const m = _type(descB64, full).decode(buf);
442
+ const plain = {};
443
+ for (const fd of fields) {
444
+ const v = m.get(fd.n);
445
+ if (v !== undefined) {
446
+ plain[fd.n] = v;
447
+ }
448
+ }
449
+ return _fromWire(Compat, plain);
450
+ }
451
+ static decodeDelimited(bytes) {
452
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
453
+ const pair = _decodeVarint(buf, 0);
454
+ return this.decode(buf.subarray(pair[1], pair[1] + pair[0]));
455
+ }
456
+ static verify(obj) {
457
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
458
+ return 'object expected';
459
+ }
460
+ const seen = new Map();
461
+ for (const fd of fields) {
462
+ let v = obj[fd.js];
463
+ if (v === undefined) {
464
+ v = obj[fd.n];
465
+ }
466
+ if (v === null || v === undefined) {
467
+ if (fd.req) {
468
+ return fd.n + ': required field missing';
469
+ }
470
+ continue;
471
+ }
472
+ const bad = _verifyField(fd, v, fd.n, this._ENUMS, this._CLASSES);
473
+ if (bad !== null) {
474
+ return bad;
475
+ }
476
+ if (fd.oneof !== null) {
477
+ if (seen.has(fd.oneof)) {
478
+ return fd.oneof + ': multiple values';
479
+ }
480
+ seen.set(fd.oneof, true);
481
+ }
482
+ }
483
+ return null;
484
+ }
485
+ static fromObject(obj) {
486
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
487
+ throw new TypeError('pbwire: fromObject expects an object');
488
+ }
489
+ const inst = new this();
490
+ for (const fd of fields) {
491
+ let v = obj[fd.js] !== undefined ? obj[fd.js] : obj[fd.n];
492
+ if (v === undefined) {
493
+ continue;
494
+ }
495
+ if (v === null) {
496
+ inst[fd.js] = fd.rep ? [] : null;
497
+ continue;
498
+ }
499
+ // One validation logic for verify() and fromObject(): normalize,
500
+ // then reject with the same reason verify() would give.
501
+ const nv = _fromValue(fd, v, fd.n, this._ENUMS, this._CLASSES);
502
+ const bad = _verifyField(fd, nv, fd.n, this._ENUMS, this._CLASSES);
503
+ if (bad !== null) {
504
+ throw new TypeError('pbwire: ' + bad);
505
+ }
506
+ inst[fd.js] = nv;
507
+ inst._present.add(fd.js);
508
+ }
509
+ return inst;
510
+ }
511
+ static toObject(message, opts) {
512
+ const inst = (message instanceof this) ? message : this.fromObject(message);
513
+ return _toPlain(this, inst, opts || {}, this._ENUMS, this._REVENUMS);
514
+ }
515
+ toObject(opts) {
516
+ return this.constructor.toObject(this, opts);
517
+ }
518
+ toJSON() {
519
+ return this.constructor.toObject(this, { enums: 'String', longs: 'String', bytes: 'base64' });
520
+ }
521
+ }
522
+ Compat._FIELDS = fields;
523
+ return Compat;
524
+ }
525
+ module.exports = { _makeClass };
package/lib/pbwire.js ADDED
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ /* Node.js binding: a FileDescriptorSet becomes a schema at runtime (no code
4
+ * generation, no protoc plugin), and messages are read and written by name.
5
+ *
6
+ * const { Schema } = require('./lib/pbwire');
7
+ * const schema = Schema.fromFile('demo.desc');
8
+ * const person = schema.message('demo.Person');
9
+ *
10
+ * const msg = person.newMessage();
11
+ * msg.set('name', 'alice');
12
+ * msg.set('inner', { x: 1, tag: 'kid' });
13
+ * const wire = msg.encode(); // Buffer
14
+ * msg.free();
15
+ *
16
+ * const back = person.decode(wire); // Message
17
+ * back.get('inner').x; // 1
18
+ * back.encode().equals(wire); // true
19
+ *
20
+ * A message is one of two things:
21
+ *
22
+ * A built message (newMessage()/set()) is a value tree in C. Writing to it
23
+ * is cheap after the first field, and encode() runs the core encoder into a
24
+ * reusable buffer.
25
+ *
26
+ * A decoded message is the bytes plus one slice per wire field: decode()
27
+ * validates the payload once, a value is materialized only when it is read,
28
+ * and encode() hands the bytes back, so a round trip is byte-exact (unknown
29
+ * fields, field order and non-canonical encodings included).
30
+ *
31
+ * Writing to a decoded message turns it into a tree (one decode of the retained
32
+ * bytes), so the API is the same either way.
33
+ *
34
+ * Values map per field type:
35
+ *
36
+ * int32/uint32/sint32/fixed32/sfixed32/enum number
37
+ * int64/uint64/sint64/fixed64/sfixed64 BigInt
38
+ * float/double number
39
+ * bool boolean
40
+ * string string
41
+ * bytes Buffer
42
+ * message / repeated object / array
43
+ *
44
+ * There is no int64 precision loss: 64-bit fields are BigInt, and a JS number
45
+ * is accepted when setting (rounded the way Number->int64 rounds).
46
+ */
47
+
48
+ const fs = require('node:fs');
49
+ const path = require('node:path');
50
+
51
+ const addon = require(path.join(__dirname, '..', 'build', 'pbwire_node.node'));
52
+
53
+ class Schema {
54
+ constructor(handle) {
55
+ this._handle = handle;
56
+ }
57
+
58
+ /** Load a FileDescriptorSet (protoc --descriptor_set_out, buf, ...). */
59
+ static fromBytes(bytes) {
60
+ return new Schema(addon.loadDescriptorSet(bytes));
61
+ }
62
+
63
+ static fromFile(file) {
64
+ return Schema.fromBytes(fs.readFileSync(file));
65
+ }
66
+
67
+ /** A message type of this schema; throws when the name is not a message. */
68
+ message(name) {
69
+ const handle = addon.descriptor(this._handle, name);
70
+ if (handle === null) {
71
+ throw new Error(`pbwire: ${name} is not a message in this schema`);
72
+ }
73
+ return new MessageType(this, name, handle);
74
+ }
75
+ }
76
+
77
+ class MessageType {
78
+ constructor(schema, name, handle) {
79
+ this.schema = schema;
80
+ this.name = name;
81
+ this._handle = handle;
82
+ // A proto2 `required` field has to hold in the merged message, which is
83
+ // what the tree decoder checks; such a type decodes eagerly.
84
+ this._eager = addon.isEager(handle);
85
+ }
86
+
87
+ /** An empty message of this type. */
88
+ newMessage() {
89
+ const message = new Message(this);
90
+ message._handle = addon.messageNew(this._handle);
91
+ return message;
92
+ }
93
+
94
+ /** Encode a plain object or a Message built by this type. */
95
+ encode(value) {
96
+ if (value instanceof Message) {
97
+ if (value.type !== this) {
98
+ throw new TypeError(`pbwire: ${value.type.name} is not ${this.name}`);
99
+ }
100
+ return value.encode();
101
+ }
102
+ if (value === null || typeof value !== 'object') {
103
+ throw new TypeError('pbwire: encode expects an object or a Message');
104
+ }
105
+ // The addon builds the tree, encodes it into the type's scratch buffer and
106
+ // frees the tree; only the bytes cross back.
107
+ return this._copy(addon.encodeObject(this._handle, value));
108
+ }
109
+
110
+ /** Decode wire bytes into a Message; throws on malformed input. */
111
+ decode(bytes) {
112
+ if (!Buffer.isBuffer(bytes)) {
113
+ throw new TypeError('pbwire: decode expects a Buffer');
114
+ }
115
+ // A private copy: the caller may reuse its Buffer, and a decoded message
116
+ // keeps the bytes it came from.
117
+ const wire = Buffer.from(bytes);
118
+ const message = new Message(this);
119
+ if (this._eager) {
120
+ message._handle = addon.decodeEager(this._handle, wire);
121
+ } else {
122
+ message._wire = wire;
123
+ message._slices = addon.decode(this._handle, wire);
124
+ }
125
+ return message;
126
+ }
127
+
128
+ /** Copies the scratch an encode just filled into a pooled Buffer. */
129
+ _copy(len) {
130
+ const out = Buffer.allocUnsafe(len);
131
+ addon.copyEncoded(this._handle, out);
132
+ return out;
133
+ }
134
+ }
135
+
136
+ class Message {
137
+ constructor(type) {
138
+ this.type = type;
139
+ this._handle = null; // tree form (newMessage/set)
140
+ this._wire = null; // wire form (decode): the bytes
141
+ this._slices = null; // wire form: (field index, offset, length) triples
142
+ }
143
+
144
+ /** Field value by name, or undefined when the field is absent. */
145
+ get(name) {
146
+ this._live();
147
+ if (this._wire !== null) {
148
+ return addon.wireGet(this.type._handle, this._wire, this._slices, name);
149
+ }
150
+ return addon.get(this._handle, name);
151
+ }
152
+
153
+ /** Store a field value by name; `undefined`/`null` clears the field. */
154
+ set(name, value) {
155
+ this._live();
156
+ if (this._wire !== null) {
157
+ // Writing needs a mutable tree: decode the retained bytes once.
158
+ this._handle = addon.promote(this.type._handle, this._wire);
159
+ this._wire = null;
160
+ this._slices = null;
161
+ }
162
+ addon.set(this._handle, name, value);
163
+ return this;
164
+ }
165
+
166
+ /** Wire bytes of this message. */
167
+ encode() {
168
+ this._live();
169
+ if (this._wire !== null) {
170
+ // Untouched since decode: the bytes are the encoding.
171
+ return Buffer.from(this._wire);
172
+ }
173
+ return this.type._copy(addon.encode(this._handle));
174
+ }
175
+
176
+ /** Release the tree early; calling it twice is a no-op. A decoded message
177
+ * only drops its bytes, which the garbage collector would do anyway. */
178
+ free() {
179
+ if (this._handle !== null) {
180
+ addon.free(this._handle);
181
+ this._handle = null;
182
+ }
183
+ this._wire = null;
184
+ this._slices = null;
185
+ }
186
+
187
+ _live() {
188
+ if (this._handle === null && this._wire === null) {
189
+ throw new TypeError('pbwire: message was freed');
190
+ }
191
+ }
192
+ }
193
+
194
+ module.exports = { Schema, MessageType, Message, abiVersion: addon.abiVersion };