@zmdb/protobuf 1.0.0-beta.1

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/src/index.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Encode `value` as protobuf at build time.
3
+ *
4
+ * The type argument and its field tags do not exist at runtime, so the build plugin
5
+ * replaces this call with a generated encoder. An unreplaced call cannot guess a wire
6
+ * contract and fails by name.
7
+ */
8
+ export function protoEncode<T>(_value: T): Uint8Array {
9
+ throw new Error(
10
+ 'protoEncode<T>(value) was not replaced at build time. It is compiled away by @zmdb/compiler ' +
11
+ '(the unplugin, Metro adapter, or project compiler), which did not run over this file — a type argument cannot ' +
12
+ 'be read at runtime, so there is no protobuf wire contract to fall back to.',
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Decode protobuf `bytes` as `T` at build time.
18
+ *
19
+ * The type argument and its field tags do not exist at runtime, so the build plugin
20
+ * replaces this call with a generated decoder. An unreplaced call cannot guess a wire
21
+ * contract and fails by name.
22
+ */
23
+ export function protoDecode<T>(_bytes: Uint8Array): T {
24
+ throw new Error(
25
+ 'protoDecode<T>(bytes) was not replaced at build time. It is compiled away by @zmdb/compiler ' +
26
+ '(the unplugin, Metro adapter, or project compiler), which did not run over this file — a type argument cannot ' +
27
+ 'be read at runtime, so there is no protobuf wire contract to fall back to.',
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Emit the proto3 descriptor for `T` at build time.
33
+ *
34
+ * A type argument does not exist at runtime, so an untransformed call cannot provide
35
+ * a partial fallback. The build plugin replaces this call with a string literal.
36
+ */
37
+ export function protoDescriptor<_T>(): string {
38
+ throw new Error(
39
+ 'protoDescriptor<T>() was not replaced at build time. It is compiled away by @zmdb/compiler ' +
40
+ '(the unplugin, Metro adapter, or project compiler), which did not run over this file — a type argument cannot ' +
41
+ 'be read at runtime, so there is no descriptor to fall back to.',
42
+ );
43
+ }
44
+
45
+ /**
46
+ * One gRPC method declaration. Request and response types are reflected by the
47
+ * AOT transformer; the stream flags are present-or-absent so there is one
48
+ * spelling for each call shape.
49
+ */
50
+ export interface GrpcMethodDef {
51
+ readonly request: unknown;
52
+ readonly response: unknown;
53
+ readonly requestStream?: true;
54
+ readonly responseStream?: true;
55
+ }
56
+
57
+ /** A closed service declaration. Concrete services must be type aliases. */
58
+ export type GrpcServiceDef = { readonly [method: string]: GrpcMethodDef };
59
+
60
+ /** Generated codecs and validators for one method. */
61
+ export interface GrpcLoadedMethod<D extends GrpcMethodDef> {
62
+ readonly path: string;
63
+ readonly requestStream: boolean;
64
+ readonly responseStream: boolean;
65
+ validateRequest(value: unknown): D['request'];
66
+ serializeRequest(value: D['request']): Uint8Array;
67
+ deserializeRequest(bytes: Uint8Array): D['request'];
68
+ validateResponse(value: unknown): D['response'];
69
+ serializeResponse(value: D['response']): Uint8Array;
70
+ deserializeResponse(bytes: Uint8Array): D['response'];
71
+ }
72
+
73
+ /**
74
+ * A build-time service artifact. No descriptor is parsed at runtime: the AOT
75
+ * transformer emits this object and its straight-line protobuf codecs.
76
+ */
77
+ export interface GrpcLoadedService<S extends GrpcServiceDef> {
78
+ readonly name: string;
79
+ readonly descriptor: string;
80
+ readonly methods: { readonly [M in keyof S]: GrpcLoadedMethod<S[M]> };
81
+ }
82
+
83
+ /**
84
+ * Emit a complete proto3 file for `S` at build time.
85
+ *
86
+ * An untransformed call cannot recover `S`, so it fails by name.
87
+ */
88
+ export function grpcDescriptor<_S extends GrpcServiceDef>(_service: string, _package: string): string {
89
+ throw new Error(
90
+ 'grpcDescriptor<S>(service, package) was not replaced at build time. It is compiled away by ' +
91
+ '@zmdb/compiler (the unplugin, Metro adapter, or project compiler), which did not run over this file — a type argument ' +
92
+ 'cannot be read at runtime, so there is no gRPC descriptor to fall back to.',
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Load a typed gRPC service from `S` at build time.
98
+ *
99
+ * "Load" means emit the descriptor, codecs and validators from TypeScript. It
100
+ * never reads or parses a `.proto` file.
101
+ */
102
+ export function loadGrpcService<S extends GrpcServiceDef>(_service: string, _package: string): GrpcLoadedService<S> {
103
+ throw new Error(
104
+ 'loadGrpcService<S>(service, package) was not replaced at build time. It is compiled away by ' +
105
+ '@zmdb/compiler (the unplugin, Metro adapter, or project compiler), which did not run over this file — a type argument ' +
106
+ 'cannot be read at runtime, so there is no gRPC service definition to fall back to.',
107
+ );
108
+ }
package/src/wire.ts ADDED
@@ -0,0 +1,333 @@
1
+ // The small runtime the AOT protobuf codecs target.
2
+ //
3
+ // Message shape, field numbers and scalar choices have already been compiled into
4
+ // straight-line JavaScript. These classes own only byte-level wire primitives; neither
5
+ // receives a descriptor or performs a field lookup.
6
+
7
+ const INITIAL_CAPACITY = 64;
8
+ const UTF8 = new TextEncoder();
9
+ const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
10
+ const EMPTY_BYTES = new Uint8Array();
11
+ const MAX_FIELD_NUMBER = 536_870_911;
12
+
13
+ /** A growable protobuf output buffer. `finish()` returns an exact-sized copy. */
14
+ export class ProtoWriter {
15
+ #buffer = new Uint8Array(INITIAL_CAPACITY);
16
+ #view = new DataView(this.#buffer.buffer);
17
+ #length = 0;
18
+
19
+ /** A field key: `(field number << 3) | wire type`, without 32-bit bitwise truncation. */
20
+ tag(fieldNumber: number, wireType: 0 | 1 | 2 | 5): void {
21
+ this.uint32(fieldNumber * 8 + wireType);
22
+ }
23
+
24
+ uint32(value: number): void {
25
+ this.#varint(BigInt.asUintN(32, BigInt(value)));
26
+ }
27
+
28
+ int32(value: number): void {
29
+ this.#varint(BigInt.asUintN(64, BigInt(value)));
30
+ }
31
+
32
+ sint32(value: number): void {
33
+ const signed = BigInt.asIntN(32, BigInt(value));
34
+ this.#varint(BigInt.asUintN(32, (signed << 1n) ^ (signed >> 31n)));
35
+ }
36
+
37
+ uint64(value: bigint): void {
38
+ this.#varint(BigInt.asUintN(64, value));
39
+ }
40
+
41
+ int64(value: bigint): void {
42
+ this.#varint(BigInt.asUintN(64, value));
43
+ }
44
+
45
+ sint64(value: bigint): void {
46
+ const signed = BigInt.asIntN(64, value);
47
+ this.#varint(BigInt.asUintN(64, (signed << 1n) ^ (signed >> 63n)));
48
+ }
49
+
50
+ fixed32(value: number): void {
51
+ this.#reserve(4);
52
+ this.#view.setUint32(this.#length, value, true);
53
+ this.#length += 4;
54
+ }
55
+
56
+ sfixed32(value: number): void {
57
+ this.#reserve(4);
58
+ this.#view.setInt32(this.#length, value, true);
59
+ this.#length += 4;
60
+ }
61
+
62
+ fixed64(value: bigint): void {
63
+ this.#reserve(8);
64
+ this.#view.setBigUint64(this.#length, BigInt.asUintN(64, value), true);
65
+ this.#length += 8;
66
+ }
67
+
68
+ sfixed64(value: bigint): void {
69
+ this.#reserve(8);
70
+ this.#view.setBigInt64(this.#length, BigInt.asIntN(64, value), true);
71
+ this.#length += 8;
72
+ }
73
+
74
+ float(value: number): void {
75
+ this.#reserve(4);
76
+ this.#view.setFloat32(this.#length, value, true);
77
+ this.#length += 4;
78
+ }
79
+
80
+ double(value: number): void {
81
+ this.#reserve(8);
82
+ this.#view.setFloat64(this.#length, value, true);
83
+ this.#length += 8;
84
+ }
85
+
86
+ bool(value: boolean): void {
87
+ this.uint32(value ? 1 : 0);
88
+ }
89
+
90
+ string(value: string): void {
91
+ this.bytes(UTF8.encode(value));
92
+ }
93
+
94
+ bytes(value: Uint8Array): void {
95
+ this.uint32(value.byteLength);
96
+ this.#reserve(value.byteLength);
97
+ this.#buffer.set(value, this.#length);
98
+ this.#length += value.byteLength;
99
+ }
100
+
101
+ /** Detach the written prefix from spare capacity. */
102
+ finish(): Uint8Array {
103
+ return this.#buffer.slice(0, this.#length);
104
+ }
105
+
106
+ #varint(value: bigint): void {
107
+ let remaining = value;
108
+ while (remaining >= 0x80n) {
109
+ this.#byte(Number((remaining & 0x7fn) | 0x80n));
110
+ remaining >>= 7n;
111
+ }
112
+ this.#byte(Number(remaining));
113
+ }
114
+
115
+ #byte(value: number): void {
116
+ this.#reserve(1);
117
+ this.#buffer[this.#length] = value;
118
+ this.#length += 1;
119
+ }
120
+
121
+ #reserve(extra: number): void {
122
+ const needed = this.#length + extra;
123
+ if (needed <= this.#buffer.byteLength) return;
124
+
125
+ let capacity = this.#buffer.byteLength;
126
+ while (capacity < needed) capacity *= 2;
127
+ const grown = new Uint8Array(capacity);
128
+ grown.set(this.#buffer);
129
+ this.#buffer = grown;
130
+ this.#view = new DataView(grown.buffer);
131
+ }
132
+ }
133
+
134
+ /** A bounded protobuf input cursor. Every length is checked before a view is made. */
135
+ export class ProtoReader {
136
+ readonly #bytes: Uint8Array;
137
+ readonly #view: DataView;
138
+ #offset = 0;
139
+
140
+ constructor(bytes: Uint8Array = EMPTY_BYTES) {
141
+ this.#bytes = bytes;
142
+ this.#view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
143
+ }
144
+
145
+ get done(): boolean {
146
+ return this.#offset === this.#bytes.byteLength;
147
+ }
148
+
149
+ get offset(): number {
150
+ return this.#offset;
151
+ }
152
+
153
+ /** Read and validate a field key, returning `(field number << 3) | wire type`. */
154
+ key(): number {
155
+ const at = this.#offset;
156
+ const value = this.#varint();
157
+ if (value > 0xffff_ffffn) {
158
+ throw new RangeError(`protobuf field key at offset ${at} exceeds 32 bits`);
159
+ }
160
+ const key = Number(value);
161
+ const fieldNumber = Math.floor(key / 8);
162
+ const wireType = key % 8;
163
+ if (fieldNumber < 1 || fieldNumber > MAX_FIELD_NUMBER) {
164
+ throw new RangeError(`invalid protobuf field number ${fieldNumber} at offset ${at}`);
165
+ }
166
+ if (wireType === 6 || wireType === 7) {
167
+ throw new RangeError(`invalid protobuf wire type ${wireType} at offset ${at}`);
168
+ }
169
+ return key;
170
+ }
171
+
172
+ uint32(): number {
173
+ return Number(BigInt.asUintN(32, this.#varint()));
174
+ }
175
+
176
+ int32(): number {
177
+ return Number(BigInt.asIntN(32, this.#varint()));
178
+ }
179
+
180
+ sint32(): number {
181
+ const value = BigInt.asUintN(32, this.#varint());
182
+ return Number(BigInt.asIntN(32, (value >> 1n) ^ -(value & 1n)));
183
+ }
184
+
185
+ uint64(): bigint {
186
+ return BigInt.asUintN(64, this.#varint());
187
+ }
188
+
189
+ int64(): bigint {
190
+ return BigInt.asIntN(64, this.#varint());
191
+ }
192
+
193
+ sint64(): bigint {
194
+ const value = BigInt.asUintN(64, this.#varint());
195
+ return BigInt.asIntN(64, (value >> 1n) ^ -(value & 1n));
196
+ }
197
+
198
+ fixed32(): number {
199
+ this.#require(4, 'fixed32');
200
+ const value = this.#view.getUint32(this.#offset, true);
201
+ this.#offset += 4;
202
+ return value;
203
+ }
204
+
205
+ sfixed32(): number {
206
+ this.#require(4, 'sfixed32');
207
+ const value = this.#view.getInt32(this.#offset, true);
208
+ this.#offset += 4;
209
+ return value;
210
+ }
211
+
212
+ fixed64(): bigint {
213
+ this.#require(8, 'fixed64');
214
+ const value = this.#view.getBigUint64(this.#offset, true);
215
+ this.#offset += 8;
216
+ return value;
217
+ }
218
+
219
+ sfixed64(): bigint {
220
+ this.#require(8, 'sfixed64');
221
+ const value = this.#view.getBigInt64(this.#offset, true);
222
+ this.#offset += 8;
223
+ return value;
224
+ }
225
+
226
+ float(): number {
227
+ this.#require(4, 'float');
228
+ const value = this.#view.getFloat32(this.#offset, true);
229
+ this.#offset += 4;
230
+ return value;
231
+ }
232
+
233
+ double(): number {
234
+ this.#require(8, 'double');
235
+ const value = this.#view.getFloat64(this.#offset, true);
236
+ this.#offset += 8;
237
+ return value;
238
+ }
239
+
240
+ bool(): boolean {
241
+ return this.#varint() !== 0n;
242
+ }
243
+
244
+ string(): string {
245
+ const at = this.#offset;
246
+ const bytes = this.#lengthDelimited();
247
+ try {
248
+ return UTF8_DECODER.decode(bytes);
249
+ } catch (error) {
250
+ const detail = error instanceof Error ? error.message : String(error);
251
+ throw new TypeError(`invalid protobuf UTF-8 string at offset ${at}: ${detail}`, { cause: error });
252
+ }
253
+ }
254
+
255
+ /** A cursor bounded to one length-delimited payload. */
256
+ message(): ProtoReader {
257
+ return new ProtoReader(this.#lengthDelimited());
258
+ }
259
+
260
+ /** Discard one unknown occurrence without trusting its length. Groups are refused. */
261
+ skip(wireType: number): void {
262
+ const at = this.#offset;
263
+ switch (wireType) {
264
+ case 0:
265
+ this.#varint();
266
+ return;
267
+ case 1:
268
+ this.#advance(8, 'fixed64 unknown field');
269
+ return;
270
+ case 2:
271
+ this.#lengthDelimited();
272
+ return;
273
+ case 3:
274
+ case 4:
275
+ throw new RangeError(`deprecated protobuf group wire type ${wireType} at offset ${at} is not supported`);
276
+ case 5:
277
+ this.#advance(4, 'fixed32 unknown field');
278
+ return;
279
+ default:
280
+ throw new RangeError(`invalid protobuf wire type ${wireType} at offset ${at}`);
281
+ }
282
+ }
283
+
284
+ #varint(): bigint {
285
+ const start = this.#offset;
286
+ let value = 0n;
287
+ for (let index = 0; index < 10; index += 1) {
288
+ if (this.#offset >= this.#bytes.byteLength) {
289
+ throw new RangeError(`truncated protobuf varint at offset ${start}; input ended at offset ${this.#offset}`);
290
+ }
291
+ const byte = this.#bytes[this.#offset];
292
+ if (byte === undefined) {
293
+ throw new RangeError(`truncated protobuf varint at offset ${start}; input ended at offset ${this.#offset}`);
294
+ }
295
+ this.#offset += 1;
296
+ if (index === 9 && byte > 1) {
297
+ throw new RangeError(`protobuf varint at offset ${start} exceeds 64 bits`);
298
+ }
299
+ value |= BigInt(byte & 0x7f) << BigInt(index * 7);
300
+ if ((byte & 0x80) === 0) return value;
301
+ }
302
+ throw new RangeError(`protobuf varint at offset ${start} exceeds 10 bytes`);
303
+ }
304
+
305
+ #lengthDelimited(): Uint8Array {
306
+ const prefix = this.#offset;
307
+ const encoded = this.#varint();
308
+ const remaining = BigInt(this.#bytes.byteLength - this.#offset);
309
+ if (encoded > remaining) {
310
+ throw new RangeError(
311
+ `protobuf length ${encoded} at offset ${prefix} exceeds ${remaining} remaining byte(s) at offset ${this.#offset}`,
312
+ );
313
+ }
314
+ const length = Number(encoded);
315
+ const start = this.#offset;
316
+ this.#offset += length;
317
+ return this.#bytes.subarray(start, this.#offset);
318
+ }
319
+
320
+ #advance(length: number, what: string): void {
321
+ this.#require(length, what);
322
+ this.#offset += length;
323
+ }
324
+
325
+ #require(length: number, what: string): void {
326
+ const remaining = this.#bytes.byteLength - this.#offset;
327
+ if (length > remaining) {
328
+ throw new RangeError(
329
+ `truncated protobuf ${what} at offset ${this.#offset}: needs ${length} byte(s), ${remaining} remaining`,
330
+ );
331
+ }
332
+ }
333
+ }