@pbwire/pbwire 0.0.0-stage → 0.1.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.
- package/README.md +204 -2
- package/build.sh +49 -0
- package/lib/pbwire.js +194 -0
- package/package.json +19 -4
- package/runtime/addon.c +2566 -0
package/README.md
CHANGED
|
@@ -1,3 +1,205 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pbwire Node.js binding
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A `FileDescriptorSet` (`protoc --descriptor_set_out`) becomes a schema at
|
|
4
|
+
runtime; no code generation, no `protoc` plugin, no npm dependencies. The
|
|
5
|
+
runtime is `runtime/addon.c`, an N-API addon in C over `libpbwire.a`; the
|
|
6
|
+
`Schema`/`Message` surface is `lib/pbwire.js`.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
const { Schema } = require('./lib/pbwire');
|
|
10
|
+
|
|
11
|
+
const schema = Schema.fromFile('tests/data/demo.desc');
|
|
12
|
+
const person = schema.message('demo.Person');
|
|
13
|
+
|
|
14
|
+
const wire = person.encode({name: 'alice', id: 7, inner: {x: 42}});
|
|
15
|
+
// or, by hand:
|
|
16
|
+
const msg = person.newMessage();
|
|
17
|
+
msg.set('name', 'alice').set('inner', {x: 42});
|
|
18
|
+
msg.encode(); // Buffer
|
|
19
|
+
msg.free();
|
|
20
|
+
|
|
21
|
+
const back = person.decode(wire);
|
|
22
|
+
back.get('name'); // 'alice'
|
|
23
|
+
back.get('inner').x; // 42
|
|
24
|
+
back.encode().equals(wire); // true: byte-exact re-encode
|
|
25
|
+
back.free();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
A message is one of two things, and both live behind the same API:
|
|
31
|
+
|
|
32
|
+
- **A built message** (`newMessage()`/`set()`) is a `pbw_message` value tree in
|
|
33
|
+
C. Field writes go straight into it, and `encode()` runs the core encoder
|
|
34
|
+
into a per-type reusable buffer, so re-encoding a message never crosses the
|
|
35
|
+
boundary again.
|
|
36
|
+
- **A decoded message** (`decode()`) is the bytes it came from plus one 12-byte
|
|
37
|
+
slice per wire field: decode validates the payload once while recording those
|
|
38
|
+
slices, `get()` materializes a single field on demand, and `encode()` hands
|
|
39
|
+
the same bytes back. A round trip is therefore byte-exact — unknown fields,
|
|
40
|
+
field order and non-canonical encodings included — and nothing is built for
|
|
41
|
+
fields that are never read.
|
|
42
|
+
|
|
43
|
+
The wire form holds no native state: the bytes are a JS `Buffer` and the slices
|
|
44
|
+
a `Uint32Array`, both owned by the message object. That is deliberate. Attaching
|
|
45
|
+
a finalizer to native memory costs N-API ~250 ns per object, which is more than
|
|
46
|
+
the entire decode of a small message, so only the built form (which has no
|
|
47
|
+
JS-visible storage) pays for one.
|
|
48
|
+
|
|
49
|
+
Writing to a decoded message promotes it to a tree (one `pbw_decode` of the
|
|
50
|
+
retained bytes), so `set()` works on either form and only callers that actually
|
|
51
|
+
edit a parsed message pay for the tree.
|
|
52
|
+
|
|
53
|
+
Field facts (number, type, wire type, label, oneof, closed enum, sub-message)
|
|
54
|
+
are resolved once per descriptor into a cached table with a name hash and a
|
|
55
|
+
number index, so `get()`/`set()` and the wire scan never walk the descriptor.
|
|
56
|
+
|
|
57
|
+
## Build and run
|
|
58
|
+
|
|
59
|
+
Needs Node (headers ship with it) and a C compiler. No `npm install`, no
|
|
60
|
+
`node-gyp`.
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
bindings/node/build.sh # build the addon, then run the tests
|
|
64
|
+
bindings/node/build.sh build # build only
|
|
65
|
+
node bindings/node/test/test_pbwire.js # the tests alone (node:test)
|
|
66
|
+
node bindings/node/examples/roundtrip.js # the example
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`build.sh` builds `build/libpbwire.a` first when it is missing (the root `make`
|
|
70
|
+
default goal is the shared library, so it asks for the archive by name), then
|
|
71
|
+
compiles the addon with clang against `$(node -p ...)/include/node` and links
|
|
72
|
+
the static core. Set `NODE_INCLUDE` if a packaging splits the Node headers away
|
|
73
|
+
from the binary. Output: `bindings/node/build/pbwire_node.node`.
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
`Schema`
|
|
78
|
+
| member | meaning |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `Schema.fromBytes(buffer)` | load a `FileDescriptorSet`; throws on malformed input |
|
|
81
|
+
| `Schema.fromFile(path)` | same, from a file |
|
|
82
|
+
| `schema.message(name)` | the message type `name` (`"pkg.Msg"`); throws when it is not a message |
|
|
83
|
+
|
|
84
|
+
`MessageType`
|
|
85
|
+
| member | meaning |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `type.name` / `type.schema` | dotted full name and owning schema |
|
|
88
|
+
| `type.newMessage()` | empty `Message` (the mutable tree form) |
|
|
89
|
+
| `type.encode(object \| Message)` | encode a plain object or an existing `Message` |
|
|
90
|
+
| `type.decode(buffer)` | decode wire bytes into a `Message`; throws on malformed input |
|
|
91
|
+
|
|
92
|
+
`Message`
|
|
93
|
+
| member | meaning |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `msg.get(name)` | field value, `undefined` when the field is absent; throws for an unknown name |
|
|
96
|
+
| `msg.set(name, value)` | store a value, replacing what was there; `undefined`/`null` clears the field |
|
|
97
|
+
| `msg.encode()` | wire bytes as a `Buffer` |
|
|
98
|
+
| `msg.free()` | release early; a second call is a no-op, any other call afterwards throws |
|
|
99
|
+
|
|
100
|
+
Values map per field type:
|
|
101
|
+
|
|
102
|
+
| field type | JS value |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `int32`, `uint32`, `sint32`, `fixed32`, `sfixed32`, `enum` | `number` |
|
|
105
|
+
| `int64`, `uint64`, `sint64`, `fixed64`, `sfixed64` | `BigInt` (a `number` is accepted on `set`) |
|
|
106
|
+
| `float`, `double` | `number` |
|
|
107
|
+
| `bool` | `boolean` |
|
|
108
|
+
| `string` | `string` |
|
|
109
|
+
| `bytes` | `Buffer` |
|
|
110
|
+
| message | plain `object` (nested `get`/`set` by field name) |
|
|
111
|
+
| repeated | `array` |
|
|
112
|
+
|
|
113
|
+
Presence is explicit, as in the C core: a field set to its zero value is
|
|
114
|
+
written to the wire instead of being skipped the way a generated proto3
|
|
115
|
+
serializer would skip it. A message field is replaced on `set`, not merged, and
|
|
116
|
+
a repeated write replaces the whole list. Reading follows the wire: a repeated
|
|
117
|
+
field that is not on the wire reads `undefined`, a oneof reads only its last
|
|
118
|
+
member, and a singular sub-message that appears twice merges (the payloads
|
|
119
|
+
concatenate, which is what parsing them as one message means).
|
|
120
|
+
|
|
121
|
+
Two proto2 behaviours are worth naming:
|
|
122
|
+
|
|
123
|
+
- **Closed enums.** A number the enum declaration does not list is not a value
|
|
124
|
+
of the field (the reference parsers move it to the unknown fields), so `get()`
|
|
125
|
+
skips it, and `encode()` passes the original bytes on untouched.
|
|
126
|
+
- **`required` fields.** Whether a required field is present is a property of
|
|
127
|
+
the *merged* message, which is what the tree decoder checks after parsing. A
|
|
128
|
+
type that reaches a `required` field therefore decodes through the tree
|
|
129
|
+
(`decodeEager`) instead of the wire view; everything else decodes lazily.
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
`test/test_pbwire.js` covers descriptor-set loading, every field kind, oneof,
|
|
134
|
+
packed and unpacked repetition, groups, closed enums, required fields,
|
|
135
|
+
byte-exact re-encode (including unknown field pass-through), editing a decoded
|
|
136
|
+
message, absent fields, schema/wire errors, nesting caps, and double-free
|
|
137
|
+
safety. The last two tests are differential: one decodes a corpus of truncated
|
|
138
|
+
and bit-flipped payloads, asserts that a decoded message is always its own
|
|
139
|
+
bytes, and compares every field against the tree decoder's answer on the same
|
|
140
|
+
bytes (so the two materializers cannot drift apart); the other feeds the addon a
|
|
141
|
+
slice array it did not produce and expects an error rather than a read past the
|
|
142
|
+
buffer.
|
|
143
|
+
|
|
144
|
+
The fixtures are `tests/data/{demo,shapes,legacy}.desc`, generated from the
|
|
145
|
+
`.proto` files next to them:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
protoc -Ibindings/node/tests/data \
|
|
149
|
+
--descriptor_set_out=bindings/node/tests/data/demo.desc \
|
|
150
|
+
bindings/node/tests/data/demo.proto
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Benchmark
|
|
154
|
+
|
|
155
|
+
`benchmarks/bench.sh` generates `matrix3_pb.js` with `protoc-gen-js`, installs
|
|
156
|
+
`google-protobuf` (the jspb runtime), `protoc-gen-js` and [protobufjs](https://www.npmjs.com/package/protobufjs) (the
|
|
157
|
+
fastest widely used JS runtime) into `benchmarks/node_modules` on first run,
|
|
158
|
+
and prints the four standard tiers from [`BENCHMARKS.md`](../../BENCHMARKS.md)
|
|
159
|
+
for `pbwire-node`, jspb and protobufjs side by side:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
./benchmarks/bench.sh # or: make node-bench
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| payload | op | pbwire-node (MB/s, x jspb) | jspb (MB/s) | protobufjs (MB/s) |
|
|
166
|
+
|---|---|---|---|---|
|
|
167
|
+
| 136 B | encode | 608.5 (10.9x) | 56.0 | 243.3 |
|
|
168
|
+
| 136 B | decode | 375.3 (3.1x) | 119.5 | 395.7 |
|
|
169
|
+
| 174 B | encode | 568.6 (9.0x) | 62.9 | 300.8 |
|
|
170
|
+
| 174 B | decode | 431.9 (3.8x) | 112.7 | 391.1 |
|
|
171
|
+
| 2469 B | encode | 1201.6 (20.8x) | 57.9 | 445.6 |
|
|
172
|
+
| 2469 B | decode | 1122.9 (14.0x) | 80.3 | 375.4 |
|
|
173
|
+
| 74913 B | encode | 1350.2 (22.3x) | 60.6 | 496.5 |
|
|
174
|
+
| 74913 B | decode | 1655.0 (20.0x) | 82.9 | 402.0 |
|
|
175
|
+
|
|
176
|
+
jspb's throughput does not move with payload size: its writer resolves a
|
|
177
|
+
code/type table per field and hands every repeated element to a closure, where
|
|
178
|
+
the C core writes one tag byte and one inline value, or memcpys an already
|
|
179
|
+
validated slice. Two boundary costs decide the small tiers: N-API's own
|
|
180
|
+
`Buffer` creation costs ~200 ns, so `encode()` encodes into the type's reusable
|
|
181
|
+
buffer and copies into a JS-allocated (pooled) `Buffer` of the exact size, and
|
|
182
|
+
decode copies the payload once with `Buffer.from` instead of paying for a
|
|
183
|
+
per-message finalizer.
|
|
184
|
+
|
|
185
|
+
## Deliberate skips
|
|
186
|
+
|
|
187
|
+
- **No protoc plugin / code generation.** The schema is always loaded at run
|
|
188
|
+
time, like the Python binding: a `FileDescriptorSet` is the only schema
|
|
189
|
+
source, and there is no generated layout to keep in sync.
|
|
190
|
+
- **No conformance testee.** The official suite is covered through the C core
|
|
191
|
+
and `conformance/`, not through this binding. The differential test above is
|
|
192
|
+
what keeps the addon's wire reader honest against the core's.
|
|
193
|
+
|
|
194
|
+
Also out of scope here: JSON/text format (`pbw_json_print`/`pbw_text_print`),
|
|
195
|
+
field presence introspection (`pbw_message_field_count`), and delimited framing.
|
|
196
|
+
Each is one addon export away when a consumer needs it.
|
|
197
|
+
|
|
198
|
+
## Typed models
|
|
199
|
+
|
|
200
|
+
`protoc-gen-pbwire-typed` (in `../typed`) generates `matrix3.d.ts` interfaces
|
|
201
|
+
(for TypeScript) and a JSDoc/`@ts-check` `.js` with the same shapes plus the enum
|
|
202
|
+
constants, describing exactly the plain objects `MessageType.encode()` accepts
|
|
203
|
+
and `Message.get()` returns. The committed output is `tests/gen/` and
|
|
204
|
+
`examples/gen/`; the typed test type-checks with `tsc --strict --checkJs`.
|
|
205
|
+
See `../typed/README.md`.
|
package/build.sh
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Build and check the Node.js binding: compile the N-API addon against
|
|
3
|
+
# build/libpbwire.a (building it first when missing), then run the self-check.
|
|
4
|
+
# clang on purpose: no node-gyp, no npm, no dependencies.
|
|
5
|
+
#
|
|
6
|
+
# bindings/node/build.sh build the addon, then run the tests
|
|
7
|
+
# bindings/node/build.sh build build only
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
here=$(cd "$(dirname "$0")" && pwd)
|
|
11
|
+
root=$(cd "$here/../.." && pwd)
|
|
12
|
+
cc=${CC:-clang}
|
|
13
|
+
|
|
14
|
+
if [ ! -f "$root/build/libpbwire.a" ]; then
|
|
15
|
+
# The default make goal is the shared library; the addon links the static
|
|
16
|
+
# archive, so name it explicitly.
|
|
17
|
+
make -C "$root" build/libpbwire.a
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Node ships its headers next to the binary: <prefix>/bin/node and
|
|
21
|
+
# <prefix>/include/node. Override with NODE_INCLUDE if a packaging split them.
|
|
22
|
+
: "${NODE_INCLUDE:=$(node -p 'require("path").join(require("path").dirname(require("path").dirname(process.execPath)), "include", "node")')}"
|
|
23
|
+
if [ ! -f "$NODE_INCLUDE/node_api.h" ]; then
|
|
24
|
+
echo "build.sh: node_api.h not found in $NODE_INCLUDE (set NODE_INCLUDE)" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# A .node module is a loadable module, not a library: resolve the node symbols
|
|
29
|
+
# at load time on macOS, at link time from libc elsewhere.
|
|
30
|
+
case $(uname -s) in
|
|
31
|
+
Darwin) link="-dynamiclib -undefined dynamic_lookup" ;;
|
|
32
|
+
*) link="-shared" ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
mkdir -p "$here/build"
|
|
36
|
+
# shellcheck disable=SC2086 # $link is a flag list
|
|
37
|
+
$cc -std=c99 -O2 -fPIC -Wall -Wextra \
|
|
38
|
+
-I"$NODE_INCLUDE" -I"$root/include" \
|
|
39
|
+
$link -o "$here/build/pbwire_node.node" \
|
|
40
|
+
"$here/runtime/addon.c" "$root/build/libpbwire.a"
|
|
41
|
+
|
|
42
|
+
echo "built $here/build/pbwire_node.node"
|
|
43
|
+
|
|
44
|
+
if [ "${1:-all}" = build ]; then
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# --expose-gc so the finalizer test can force the collections it checks.
|
|
49
|
+
exec node --expose-gc --test "$here/test/test_pbwire.js" "$here/tests/typed_test.js"
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pbwire/pbwire",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dynamic protobuf over the pbwire C ABI, schema at runtime, no generated code",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/jinganix/pbwire.git",
|
|
9
|
+
"directory": "bindings/node"
|
|
10
|
+
},
|
|
11
|
+
"author": "jinganix",
|
|
12
|
+
"main": "lib/pbwire.js",
|
|
13
|
+
"files": ["lib/", "runtime/", "build.sh", "README.md"],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"install": "./build.sh build",
|
|
16
|
+
"test": "./build.sh"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
}
|
|
21
|
+
}
|