+4
.vscode/settings.json
+4
.vscode/settings.json
+44
.zed/settings.json
+44
.zed/settings.json
···
1
+
// Folder-specific settings
2
+
//
3
+
// For a full list of overridable settings, and general information on folder-specific settings,
4
+
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
5
+
{
6
+
"lsp": {
7
+
"deno": {
8
+
"settings": {
9
+
"deno": {
10
+
"enable": true
11
+
}
12
+
}
13
+
}
14
+
},
15
+
"languages": {
16
+
"JavaScript": {
17
+
"language_servers": [
18
+
"deno",
19
+
"!typescript-language-server",
20
+
"!vtsls",
21
+
"!eslint"
22
+
],
23
+
"formatter": "language_server"
24
+
},
25
+
"TypeScript": {
26
+
"language_servers": [
27
+
"deno",
28
+
"!typescript-language-server",
29
+
"!vtsls",
30
+
"!eslint"
31
+
],
32
+
"formatter": "language_server"
33
+
},
34
+
"TSX": {
35
+
"language_servers": [
36
+
"deno",
37
+
"!typescript-language-server",
38
+
"!vtsls",
39
+
"!eslint"
40
+
],
41
+
"formatter": "language_server"
42
+
}
43
+
}
44
+
}
+14
LICENSE
+14
LICENSE
···
1
+
BSD Zero Clause License
2
+
3
+
Copyright (c) 2025 Mary
4
+
5
+
Permission to use, copy, modify, and/or distribute this software for any
6
+
purpose with or without fee is hereby granted.
7
+
8
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+
PERFORMANCE OF THIS SOFTWARE.
+94
README.md
+94
README.md
···
1
+
# protobuf
2
+
3
+
protobuf codec with static type inference.
4
+
5
+
```typescript
6
+
import * as p from '@mary/protobuf';
7
+
8
+
// basic usage
9
+
{
10
+
const Person = p.message({
11
+
id: p.int64(),
12
+
name: p.string(),
13
+
email: p.optional(p.string()),
14
+
tags: p.repeated(p.string()),
15
+
}, {
16
+
id: 1,
17
+
name: 2,
18
+
email: 3,
19
+
tags: 4,
20
+
});
21
+
22
+
const person: p.InferInput<typeof Person> = {
23
+
id: 123n,
24
+
name: 'alice',
25
+
email: 'alice@example.com',
26
+
tags: ['developer', 'rust'],
27
+
};
28
+
29
+
const encoded = p.encode(Person, person);
30
+
const decoded = p.decode(Person, encoded);
31
+
// ^? p.InferOutput<typeof Person>
32
+
33
+
const result = p.tryDecode(Person, buffer);
34
+
35
+
if (result.ok) {
36
+
result.value;
37
+
} else {
38
+
result.message;
39
+
result.issues;
40
+
}
41
+
}
42
+
43
+
// nested and self-referential messages
44
+
{
45
+
const Address = p.message({
46
+
street: p.string(),
47
+
city: p.string(),
48
+
zipCode: p.optional(p.string()),
49
+
}, {
50
+
street: 1,
51
+
city: 2,
52
+
zipCode: 3,
53
+
});
54
+
55
+
const Place = p.message({
56
+
name: p.string(),
57
+
address: Address,
58
+
}, {
59
+
name: 1,
60
+
address: 2,
61
+
});
62
+
63
+
const Node = p.message({
64
+
value: p.int32(),
65
+
get next() {
66
+
return p.optional(Node);
67
+
},
68
+
}, {
69
+
value: 1,
70
+
next: 2,
71
+
});
72
+
}
73
+
74
+
// maps
75
+
{
76
+
const Scoreboard = p.message({
77
+
scores: p.map(p.string(), p.int32()),
78
+
}, {
79
+
scores: 1,
80
+
});
81
+
82
+
const data: p.InferInput<typeof Scoreboard> = {
83
+
scores: new Map([['alice', 100], ['bob', 95]]),
84
+
};
85
+
}
86
+
```
87
+
88
+
## non-features
89
+
90
+
- **enums support**: use `int32()` instead for open enums
91
+
- **oneof support**: complicated, breaks self-referential messages
92
+
- **extensions support**: not supported
93
+
- **groups support**: use nested messages instead
94
+
- **code generation**: no intent
+20
deno.json
+20
deno.json
···
1
+
{
2
+
"name": "@mary/protobuf",
3
+
"version": "0.1.0",
4
+
"license": "0BSD",
5
+
"exports": "./lib/mod.ts",
6
+
"imports": {
7
+
"@std/assert": "jsr:@std/assert@^1.0.13",
8
+
"nanoid": "npm:nanoid@^5.1.5"
9
+
},
10
+
"publish": {
11
+
"include": ["lib/", "LICENSE", "README.md", "deno.json"]
12
+
},
13
+
"fmt": {
14
+
"useTabs": true,
15
+
"indentWidth": 2,
16
+
"lineWidth": 110,
17
+
"semiColons": true,
18
+
"singleQuote": true
19
+
}
20
+
}
+31
deno.lock
+31
deno.lock
···
1
+
{
2
+
"version": "5",
3
+
"specifiers": {
4
+
"jsr:@std/assert@^1.0.13": "1.0.13",
5
+
"jsr:@std/internal@^1.0.6": "1.0.6",
6
+
"npm:nanoid@^5.1.5": "5.1.5"
7
+
},
8
+
"jsr": {
9
+
"@std/assert@1.0.13": {
10
+
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
11
+
"dependencies": [
12
+
"jsr:@std/internal"
13
+
]
14
+
},
15
+
"@std/internal@1.0.6": {
16
+
"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
17
+
}
18
+
},
19
+
"npm": {
20
+
"nanoid@5.1.5": {
21
+
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
22
+
"bin": true
23
+
}
24
+
},
25
+
"workspace": {
26
+
"dependencies": [
27
+
"jsr:@std/assert@^1.0.13",
28
+
"npm:nanoid@^5.1.5"
29
+
]
30
+
}
31
+
}
+26
lib/bitset.ts
+26
lib/bitset.ts
···
1
+
export type BitSet = number | number[];
2
+
3
+
export const setBit = (bits: BitSet, index: number): BitSet => {
4
+
if (typeof bits !== 'number') {
5
+
const idx = index >> 5;
6
+
7
+
for (let i = bits.length; i <= idx; i++) {
8
+
bits.push(0);
9
+
}
10
+
11
+
bits[idx] |= 1 << index % 32;
12
+
return bits;
13
+
} else if (index < 32) {
14
+
return bits | (1 << index);
15
+
} else {
16
+
return setBit([bits, 0], index);
17
+
}
18
+
};
19
+
20
+
export const getBit = (bits: BitSet, index: number): number => {
21
+
if (typeof bits === 'number') {
22
+
return index < 32 ? (bits >>> index) & 1 : 0;
23
+
} else {
24
+
return (bits[index >> 5] >>> index % 32) & 1;
25
+
}
26
+
};
+1137
lib/mod.test.ts
+1137
lib/mod.test.ts
···
1
+
import {
2
+
assert,
3
+
assertAlmostEquals,
4
+
assertArrayIncludes,
5
+
assertEquals,
6
+
assertStringIncludes,
7
+
assertThrows,
8
+
} from '@std/assert';
9
+
import { nanoid } from 'nanoid/non-secure';
10
+
11
+
import * as p from './mod.ts';
12
+
13
+
// #region Primitive types
14
+
15
+
Deno.test('string encoding/decoding', () => {
16
+
const Message = p.message({ text: p.string() }, { text: 1 });
17
+
18
+
const cases = [
19
+
'',
20
+
'hello world',
21
+
'hello 🚀',
22
+
'a'.repeat(1000),
23
+
'Café',
24
+
'おはようございます☀️',
25
+
'नमस्ते',
26
+
'Здравствуйте',
27
+
'你'.repeat(43),
28
+
'🌟'.repeat(32),
29
+
'🚀🌟💻',
30
+
'🏳️🌈🏳️⚧️',
31
+
];
32
+
33
+
for (const text of cases) {
34
+
const encoded = p.encode(Message, { text });
35
+
const decoded = p.decode(Message, encoded);
36
+
37
+
assertEquals(decoded, { text });
38
+
}
39
+
});
40
+
41
+
Deno.test('int32 encoding/decoding', () => {
42
+
const Message = p.message({ value: p.int32() }, { value: 1 });
43
+
44
+
const cases = [
45
+
0,
46
+
1,
47
+
-1,
48
+
127,
49
+
-128,
50
+
255,
51
+
-256,
52
+
32767,
53
+
-32768,
54
+
65535,
55
+
-65536,
56
+
2147483647, // max int32
57
+
-2147483648, // min int32
58
+
];
59
+
60
+
for (const value of cases) {
61
+
const encoded = p.encode(Message, { value });
62
+
const decoded = p.decode(Message, encoded);
63
+
64
+
assertEquals(decoded, { value });
65
+
}
66
+
});
67
+
68
+
Deno.test('int64 encoding/decoding', () => {
69
+
const Message = p.message({ value: p.int64() }, { value: 1 });
70
+
71
+
const cases = [
72
+
0n,
73
+
1n,
74
+
-1n,
75
+
127n,
76
+
-128n,
77
+
9223372036854775807n, // max int64
78
+
-9223372036854775808n, // min int64
79
+
];
80
+
81
+
for (const value of cases) {
82
+
const encoded = p.encode(Message, { value });
83
+
const decoded = p.decode(Message, encoded);
84
+
85
+
assertEquals(decoded, { value });
86
+
}
87
+
});
88
+
89
+
Deno.test('uint32 encoding/decoding', () => {
90
+
const Message = p.message({ value: p.uint32() }, { value: 1 });
91
+
92
+
const cases = [
93
+
0,
94
+
1,
95
+
127,
96
+
255,
97
+
32767,
98
+
65535,
99
+
2147483647,
100
+
4294967295, // max uint32
101
+
];
102
+
103
+
for (const value of cases) {
104
+
const encoded = p.encode(Message, { value });
105
+
const decoded = p.decode(Message, encoded);
106
+
107
+
assertEquals(decoded, { value });
108
+
}
109
+
});
110
+
111
+
Deno.test('uint64 encoding/decoding', () => {
112
+
const Message = p.message({ value: p.uint64() }, { value: 1 });
113
+
114
+
const cases = [
115
+
0n,
116
+
1n,
117
+
127n,
118
+
255n,
119
+
18446744073709551615n, // max uint64
120
+
];
121
+
122
+
for (const value of cases) {
123
+
const encoded = p.encode(Message, { value });
124
+
const decoded = p.decode(Message, encoded);
125
+
126
+
assertEquals(decoded, { value });
127
+
}
128
+
});
129
+
130
+
Deno.test('sint32 encoding/decoding (zigzag)', () => {
131
+
const schema = p.message({ value: p.sint32() }, { value: 1 });
132
+
133
+
const testCases = [
134
+
0,
135
+
1,
136
+
-1,
137
+
2,
138
+
-2,
139
+
127,
140
+
-128,
141
+
2147483647, // max int32
142
+
-2147483648, // min int32
143
+
];
144
+
145
+
for (const value of testCases) {
146
+
const encoded = p.encode(schema, { value });
147
+
const decoded = p.decode(schema, encoded);
148
+
149
+
assertEquals(decoded, { value });
150
+
}
151
+
});
152
+
153
+
Deno.test('sint64 encoding/decoding (zigzag)', () => {
154
+
const Message = p.message({ value: p.sint64() }, { value: 1 });
155
+
156
+
const cases = [
157
+
0n,
158
+
1n,
159
+
-1n,
160
+
2n,
161
+
-2n,
162
+
127n,
163
+
-128n,
164
+
9223372036854775807n, // max int64
165
+
-9223372036854775808n, // min int64
166
+
];
167
+
168
+
for (const value of cases) {
169
+
const encoded = p.encode(Message, { value });
170
+
const decoded = p.decode(Message, encoded);
171
+
172
+
assertEquals(decoded, { value });
173
+
}
174
+
});
175
+
176
+
Deno.test('float encoding/decoding', () => {
177
+
const Message = p.message({ value: p.float() }, { value: 1 });
178
+
179
+
const cases = [
180
+
0.0,
181
+
1.0,
182
+
-1.0,
183
+
3.14159,
184
+
-3.14159,
185
+
1.5e10,
186
+
-1.5e10,
187
+
3.4028235e38, // close to max float32
188
+
1.175494e-38, // close to min positive float32
189
+
Infinity,
190
+
-Infinity,
191
+
NaN,
192
+
];
193
+
194
+
for (const value of cases) {
195
+
const encoded = p.encode(Message, { value });
196
+
const decoded = p.decode(Message, encoded);
197
+
198
+
// Special handling for infinity values
199
+
if (Number.isFinite(value)) {
200
+
// For finite values, check they're close due to float precision
201
+
// Use a more lenient tolerance for large numbers
202
+
const tolerance = Math.abs(value) > 1e9 ? Math.abs(value) * 1e-6 : 1e-6;
203
+
204
+
assertAlmostEquals(decoded.value, value, tolerance);
205
+
} else {
206
+
assertEquals(decoded.value, value);
207
+
}
208
+
}
209
+
});
210
+
211
+
Deno.test('double encoding/decoding', () => {
212
+
const Message = p.message({ value: p.double() }, { value: 1 });
213
+
214
+
const cases = [
215
+
0.0,
216
+
1.0,
217
+
-1.0,
218
+
3.141592653589793,
219
+
-3.141592653589793,
220
+
1.7976931348623157e+308, // close to max double
221
+
2.2250738585072014e-308, // close to min positive double
222
+
Infinity,
223
+
-Infinity,
224
+
NaN,
225
+
];
226
+
227
+
for (const value of cases) {
228
+
const encoded = p.encode(Message, { value });
229
+
const decoded = p.decode(Message, encoded);
230
+
231
+
assertEquals(decoded, { value });
232
+
}
233
+
});
234
+
235
+
Deno.test('float range validation', () => {
236
+
const Message = p.message({ value: p.float() }, { value: 1 });
237
+
238
+
// Test values that should cause range errors
239
+
const invald = [
240
+
3.4028236e38, // slightly above max float32
241
+
-3.4028236e38, // slightly below min float32
242
+
1e39, // way above max float32
243
+
-1e39, // way below min float32
244
+
];
245
+
246
+
for (const value of invald) {
247
+
assertThrows(() => p.encode(Message, { value }), Error, 'invalid_range');
248
+
}
249
+
250
+
// Test edge values that should work
251
+
const valid = [
252
+
3.4028235e38, // max float32
253
+
-3.4028235e38, // min float32
254
+
1.175494e-38, // min positive float32
255
+
-1.175494e-38, // max negative float32
256
+
0, // zero
257
+
Infinity, // positive infinity
258
+
-Infinity, // negative infinity
259
+
NaN, // not a number
260
+
];
261
+
262
+
for (const value of valid) {
263
+
const encoded = p.encode(Message, { value });
264
+
void p.decode(Message, encoded);
265
+
}
266
+
});
267
+
268
+
Deno.test('double range validation', () => {
269
+
const Message = p.message({ value: p.double() }, { value: 1 });
270
+
271
+
// JavaScript's number type is already IEEE 754 double precision,
272
+
// so all numbers are valid for double. Test edge cases work correctly.
273
+
const cases = [
274
+
Number.MAX_VALUE, // max double
275
+
-Number.MAX_VALUE, // min double
276
+
Number.MIN_VALUE, // min positive double
277
+
-Number.MIN_VALUE, // max negative double
278
+
0, // zero
279
+
Infinity, // positive infinity
280
+
-Infinity, // negative infinity
281
+
NaN, // not a number
282
+
];
283
+
284
+
for (const value of cases) {
285
+
const encoded = p.encode(Message, { value });
286
+
void p.decode(Message, encoded);
287
+
}
288
+
});
289
+
290
+
Deno.test('boolean encoding/decoding', () => {
291
+
const Message = p.message({ value: p.boolean() }, { value: 1 });
292
+
293
+
const cases = [true, false];
294
+
295
+
for (const value of cases) {
296
+
const encoded = p.encode(Message, { value });
297
+
const decoded = p.decode(Message, encoded);
298
+
299
+
assertEquals(decoded, { value });
300
+
}
301
+
});
302
+
303
+
Deno.test('bytes encoding/decoding', () => {
304
+
const Message = p.message({ data: p.bytes() }, { data: 1 });
305
+
306
+
const cases = [
307
+
Uint8Array.from([]),
308
+
Uint8Array.from([0]),
309
+
Uint8Array.from([1, 2, 3, 4, 5]),
310
+
Uint8Array.from([255, 254, 253]),
311
+
new Uint8Array(Array.from({ length: 1000 }, (_, i) => i % 256)), // large array
312
+
];
313
+
314
+
for (const data of cases) {
315
+
const encoded = p.encode(Message, { data });
316
+
const decoded = p.decode(Message, encoded);
317
+
318
+
assertEquals(decoded, { data });
319
+
}
320
+
});
321
+
322
+
Deno.test('fixed32 encoding/decoding', () => {
323
+
const Message = p.message({ value: p.fixed32() }, { value: 1 });
324
+
325
+
const cases = [
326
+
0,
327
+
1,
328
+
255,
329
+
65535,
330
+
4294967295, // max uint32
331
+
];
332
+
333
+
for (const value of cases) {
334
+
const encoded = p.encode(Message, { value });
335
+
const decoded = p.decode(Message, encoded);
336
+
337
+
assertEquals(decoded, { value });
338
+
}
339
+
});
340
+
341
+
Deno.test('fixed64 encoding/decoding', () => {
342
+
const Message = p.message({ value: p.fixed64() }, { value: 1 });
343
+
344
+
const cases = [
345
+
0n,
346
+
1n,
347
+
255n,
348
+
65535n,
349
+
18446744073709551615n, // max uint64
350
+
];
351
+
352
+
for (const value of cases) {
353
+
const encoded = p.encode(Message, { value });
354
+
const decoded = p.decode(Message, encoded);
355
+
356
+
assertEquals(decoded, { value });
357
+
}
358
+
});
359
+
360
+
Deno.test('sfixed32 encoding/decoding', () => {
361
+
const Message = p.message({ value: p.sfixed32() }, { value: 1 });
362
+
363
+
const cases = [
364
+
0,
365
+
1,
366
+
-1,
367
+
2147483647, // max int32
368
+
-2147483648, // min int32
369
+
];
370
+
371
+
for (const value of cases) {
372
+
const encoded = p.encode(Message, { value });
373
+
const decoded = p.decode(Message, encoded);
374
+
375
+
assertEquals(decoded, { value });
376
+
}
377
+
});
378
+
379
+
Deno.test('sfixed64 encoding/decoding', () => {
380
+
const Message = p.message({ value: p.sfixed64() }, { value: 1 });
381
+
382
+
const cases = [
383
+
0n,
384
+
1n,
385
+
-1n,
386
+
9223372036854775807n, // max int64
387
+
-9223372036854775808n, // min int64
388
+
];
389
+
390
+
for (const value of cases) {
391
+
const encoded = p.encode(Message, { value });
392
+
const decoded = p.decode(Message, encoded);
393
+
394
+
assertEquals(decoded, { value });
395
+
}
396
+
});
397
+
398
+
// #endregion
399
+
400
+
// #region Complex types
401
+
402
+
Deno.test('repeated fields', () => {
403
+
const Message = p.message({
404
+
numbers: p.repeated(p.int32()),
405
+
strings: p.repeated(p.string()),
406
+
}, {
407
+
numbers: 1,
408
+
strings: 2,
409
+
});
410
+
411
+
const cases = [
412
+
{
413
+
numbers: [],
414
+
strings: [],
415
+
},
416
+
{
417
+
numbers: [1],
418
+
strings: ['hello'],
419
+
},
420
+
{
421
+
numbers: [1, 2, 3, -1, -2],
422
+
strings: ['hello', 'world', ''],
423
+
},
424
+
{
425
+
numbers: Array.from({ length: 100 }, (_, i) => i),
426
+
strings: Array.from({ length: 100 }, (_, i) => `item${i}`),
427
+
},
428
+
];
429
+
430
+
for (const data of cases) {
431
+
const encoded = p.encode(Message, data);
432
+
const decoded = p.decode(Message, encoded);
433
+
434
+
assertEquals(decoded, data);
435
+
}
436
+
});
437
+
438
+
Deno.test('messages with optional fields', () => {
439
+
const Message = p.message({
440
+
required: p.string(),
441
+
withDefault: p.optional(p.string(), 'default_value'),
442
+
withFunctionDefault: p.optional(p.int32(), () => 42),
443
+
withoutDefault: p.optional(p.string()),
444
+
}, {
445
+
required: 1,
446
+
withDefault: 2,
447
+
withFunctionDefault: 3,
448
+
withoutDefault: 4,
449
+
});
450
+
451
+
{
452
+
const full = {
453
+
required: 'hello',
454
+
withDefault: 'custom',
455
+
withFunctionDefault: 99,
456
+
withoutDefault: 'present',
457
+
};
458
+
459
+
const encoded = p.encode(Message, full);
460
+
const decoded = p.decode(Message, encoded);
461
+
462
+
assertEquals(decoded, full);
463
+
}
464
+
{
465
+
const minimal = { required: 'hello' };
466
+
467
+
const encoded = p.encode(Message, minimal);
468
+
const decoded = p.decode(Message, encoded);
469
+
470
+
assertEquals(decoded, {
471
+
required: 'hello',
472
+
withDefault: 'default_value',
473
+
withFunctionDefault: 42,
474
+
// withoutDefault should be undefined (not present)
475
+
});
476
+
}
477
+
478
+
{
479
+
const partial = {
480
+
required: 'hello',
481
+
withDefault: 'custom_value',
482
+
};
483
+
484
+
const encoded = p.encode(Message, partial);
485
+
const decoded = p.decode(Message, encoded);
486
+
487
+
assertEquals(decoded, {
488
+
required: 'hello',
489
+
withDefault: 'custom_value',
490
+
withFunctionDefault: 42,
491
+
// withoutDefault should be undefined (not present)
492
+
});
493
+
}
494
+
});
495
+
496
+
Deno.test('empty messages', () => {
497
+
const Message = p.message({}, {});
498
+
499
+
const encoded = p.encode(Message, {});
500
+
const decoded = p.decode(Message, encoded);
501
+
502
+
assertEquals(decoded, {});
503
+
});
504
+
505
+
Deno.test('nested messages', () => {
506
+
const Address = p.message({
507
+
street: p.string(),
508
+
city: p.string(),
509
+
zipCode: p.optional(p.string()),
510
+
}, {
511
+
street: 1,
512
+
city: 2,
513
+
zipCode: 3,
514
+
});
515
+
516
+
const Person = p.message({
517
+
name: p.string(),
518
+
age: p.int32(),
519
+
address: Address,
520
+
addresses: p.repeated(Address),
521
+
}, {
522
+
name: 1,
523
+
age: 2,
524
+
address: 3,
525
+
addresses: 4,
526
+
});
527
+
528
+
const data = {
529
+
name: 'John Doe',
530
+
age: 30,
531
+
address: {
532
+
street: '123 Main St',
533
+
city: 'Anytown',
534
+
zipCode: '12345',
535
+
},
536
+
addresses: [
537
+
{
538
+
street: '456 Oak Ave',
539
+
city: 'Other City',
540
+
},
541
+
{
542
+
street: '789 Pine Rd',
543
+
city: 'Another City',
544
+
zipCode: '67890',
545
+
},
546
+
],
547
+
};
548
+
549
+
const encoded = p.encode(Person, data);
550
+
const decoded = p.decode(Person, encoded);
551
+
552
+
assertEquals(decoded, data);
553
+
});
554
+
555
+
Deno.test('self-referential messages', () => {
556
+
const Node = p.message({
557
+
value: p.int32(),
558
+
get next() {
559
+
return p.optional(Node);
560
+
},
561
+
}, {
562
+
value: 1,
563
+
next: 2,
564
+
});
565
+
566
+
{
567
+
const encoded = p.encode(Node, { value: 42 });
568
+
const decoded = p.decode(Node, encoded);
569
+
570
+
assertEquals(decoded, { value: 42 });
571
+
}
572
+
573
+
{
574
+
const encoded = p.encode(Node, { value: 1, next: { value: 2 } });
575
+
const decoded = p.decode(Node, encoded);
576
+
577
+
assertEquals(decoded, { value: 1, next: { value: 2 } });
578
+
}
579
+
580
+
{
581
+
const complex: p.InferInput<typeof Node> = {
582
+
value: 1,
583
+
next: {
584
+
value: 2,
585
+
next: {
586
+
value: 3,
587
+
next: {
588
+
value: 4,
589
+
next: {
590
+
value: 5,
591
+
},
592
+
},
593
+
},
594
+
},
595
+
};
596
+
597
+
const encoded = p.encode(Node, complex);
598
+
const decoded = p.decode(Node, encoded);
599
+
600
+
assertEquals(decoded, complex);
601
+
}
602
+
});
603
+
604
+
Deno.test('map type', () => {
605
+
const Person = p.message({
606
+
id: p.int32(),
607
+
name: p.string(),
608
+
}, {
609
+
id: 1,
610
+
name: 2,
611
+
});
612
+
613
+
const Message = p.message({
614
+
map: p.map(p.string(), Person),
615
+
}, {
616
+
map: 1,
617
+
});
618
+
619
+
const cases = [
620
+
new Map(),
621
+
new Map([['item1', { id: 1, name: 'first' }]]),
622
+
new Map([['item1', { id: 1, name: 'first' }], ['item2', { id: 2, name: 'second' }]]),
623
+
];
624
+
625
+
for (const map of cases) {
626
+
const encoded = p.encode(Message, { map });
627
+
const decoded = p.decode(Message, encoded);
628
+
629
+
assertEquals(decoded, { map });
630
+
}
631
+
});
632
+
633
+
Deno.test('Timestamp type', () => {
634
+
{
635
+
const encoded = p.encode(p.Timestamp, {});
636
+
const decoded = p.decode(p.Timestamp, encoded);
637
+
638
+
assertEquals(decoded, { seconds: 0n, nanos: 0 });
639
+
}
640
+
641
+
{
642
+
const data: p.InferInput<typeof p.Timestamp> = {
643
+
seconds: 1609459200n, // 2021-01-01 00:00:00 UTC
644
+
nanos: 123456789,
645
+
};
646
+
647
+
const encoded = p.encode(p.Timestamp, data);
648
+
const decoded = p.decode(p.Timestamp, encoded);
649
+
650
+
assertEquals(decoded, data);
651
+
}
652
+
});
653
+
654
+
Deno.test('Duration type', () => {
655
+
{
656
+
const data: p.InferInput<typeof p.Duration> = {
657
+
seconds: 3661n, // 1 hour, 1 minute, 1 second
658
+
nanos: 500000000, // 0.5 seconds
659
+
};
660
+
661
+
const encoded = p.encode(p.Duration, data);
662
+
const decoded = p.decode(p.Duration, encoded);
663
+
664
+
assertEquals(decoded, data);
665
+
}
666
+
667
+
{
668
+
const negative: p.InferInput<typeof p.Duration> = {
669
+
seconds: -30n,
670
+
nanos: -500000000,
671
+
};
672
+
673
+
const encoded = p.encode(p.Duration, negative);
674
+
const decoded = p.decode(p.Duration, encoded);
675
+
676
+
assertEquals(decoded, negative);
677
+
}
678
+
});
679
+
680
+
Deno.test('Any type', () => {
681
+
{
682
+
const messageSchema = p.message({ value: p.string() }, { value: 1 });
683
+
684
+
const data = {
685
+
typeUrl: 'type.googleapis.com/test.Message',
686
+
value: p.encode(messageSchema, { value: 'hello world' }),
687
+
};
688
+
689
+
const encoded = p.encode(p.Any, data);
690
+
const decoded = p.decode(p.Any, encoded);
691
+
692
+
assertEquals(decoded, data);
693
+
}
694
+
695
+
{
696
+
const encoded = p.encode(p.Any, {});
697
+
const decoded = p.decode(p.Any, encoded);
698
+
699
+
assertEquals(decoded, { typeUrl: '', value: new Uint8Array(0) });
700
+
}
701
+
});
702
+
703
+
// #endregion
704
+
705
+
// #region Edge cases
706
+
707
+
Deno.test('large varint values', () => {
708
+
const Message = p.message({ value: p.int32() }, { value: 1 });
709
+
710
+
// Test values that require multiple bytes in varint encoding
711
+
const cases = [
712
+
127, // 1 byte
713
+
128, // 2 bytes
714
+
16383, // 2 bytes
715
+
16384, // 3 bytes
716
+
2097151, // 3 bytes
717
+
2097152, // 4 bytes
718
+
];
719
+
720
+
for (const value of cases) {
721
+
const encoded = p.encode(Message, { value });
722
+
const decoded = p.decode(Message, encoded);
723
+
724
+
assertEquals(decoded, { value });
725
+
}
726
+
});
727
+
728
+
Deno.test('very large varint handling', () => {
729
+
const schema = p.message({ value: p.int32() }, { value: 1 });
730
+
731
+
// Create a very large varint that would overflow 32-bit int
732
+
// This represents 0xFFFFFFFF (4294967295) which should become -1 when cast to int32
733
+
const largeVarintData = Uint8Array.from([
734
+
8,
735
+
0xFF,
736
+
0xFF,
737
+
0xFF,
738
+
0xFF,
739
+
0x0F, // field 1: max uint32 value
740
+
]);
741
+
742
+
const decoded = p.decode(schema, largeVarintData);
743
+
assertEquals(decoded, { value: -1 }); // Should wrap around to -1
744
+
});
745
+
746
+
Deno.test('unknown field handling', () => {
747
+
const Message = p.message({ known: p.string() }, { known: 1 });
748
+
749
+
const buffer = Uint8Array.from([
750
+
10,
751
+
5,
752
+
72,
753
+
101,
754
+
108,
755
+
108,
756
+
111, // field 1: "Hello"
757
+
18,
758
+
4,
759
+
116,
760
+
101,
761
+
115,
762
+
116, // field 2: "test" (unknown)
763
+
26,
764
+
3,
765
+
98,
766
+
121,
767
+
101, // field 3: "bye" (unknown)
768
+
]);
769
+
770
+
const decoded = p.decode(Message, buffer);
771
+
772
+
assertEquals(decoded, { known: 'Hello' });
773
+
});
774
+
775
+
Deno.test('duplicate field handling', () => {
776
+
const Message = p.message({ value: p.int32() }, { value: 1 });
777
+
778
+
const buffer = Uint8Array.from([
779
+
8,
780
+
42, // field 1: value 42
781
+
8,
782
+
24, // field 1: value 24 (duplicate)
783
+
]);
784
+
785
+
const decoded = p.decode(Message, buffer);
786
+
787
+
assertEquals(decoded, { value: 24 });
788
+
});
789
+
790
+
Deno.test('encoding produces correct wire format', () => {
791
+
// Test that our encoding matches expected protobuf wire format
792
+
const schema = p.message({
793
+
a: p.int32(),
794
+
b: p.string(),
795
+
}, {
796
+
a: 1,
797
+
b: 2,
798
+
});
799
+
800
+
const encoded = p.encode(schema, { a: 150, b: 'testing' });
801
+
802
+
// Manual verification of wire format:
803
+
// Field 1 (a=150): tag=1<<3|0=8, value=150 (varint) = [8, 150, 1]
804
+
// Field 2 (b="testing"): tag=2<<3|2=18, length=7, "testing" = [18, 7, 116, 101, 115, 116, 105, 110, 103]
805
+
806
+
const expected = Uint8Array.from([
807
+
8,
808
+
150,
809
+
1, // field 1: int32 value 150
810
+
18,
811
+
7,
812
+
116,
813
+
101,
814
+
115,
815
+
116,
816
+
105,
817
+
110,
818
+
103, // field 2: string "testing"
819
+
]);
820
+
821
+
assertEquals(encoded, expected);
822
+
});
823
+
824
+
// #endregion
825
+
826
+
// #region Input validation errors
827
+
828
+
Deno.test('type validation errors during encoding', () => {
829
+
const StringMessage = p.message({ text: p.string() }, { text: 1 });
830
+
{
831
+
// @ts-expect-error: purposeful type error
832
+
const result = p.tryEncode(StringMessage, 123);
833
+
834
+
assert(!result.ok);
835
+
assertEquals(result.message, `invalid_type at . (expected object)`);
836
+
}
837
+
838
+
{
839
+
// @ts-expect-error: purposeful type error
840
+
const result = p.tryEncode(StringMessage, { text: 123 });
841
+
842
+
assert(!result.ok);
843
+
assertEquals(result.message, `invalid_type at .text (expected string)`);
844
+
}
845
+
846
+
const Int64Message = p.message({ value: p.int64() }, { value: 1 });
847
+
{
848
+
// @ts-expect-error: purposeful type error
849
+
const result = p.tryEncode(Int64Message, { value: 123 });
850
+
851
+
assert(!result.ok);
852
+
assertEquals(result.message, `invalid_type at .value (expected bigint)`);
853
+
}
854
+
855
+
const Uint64Message = p.message({ value: p.uint64() }, { value: 1 });
856
+
{
857
+
// @ts-expect-error: purposeful type error
858
+
const result = p.tryEncode(Uint64Message, { value: 123 });
859
+
860
+
assert(!result.ok);
861
+
assertEquals(result.message, `invalid_type at .value (expected bigint)`);
862
+
}
863
+
});
864
+
865
+
Deno.test('range validation for unsigned types', () => {
866
+
const Uint32Message = p.message({ value: p.uint32() }, { value: 1 });
867
+
const Uint64Message = p.message({ value: p.uint64() }, { value: 1 });
868
+
const Fixed32Message = p.message({ value: p.fixed32() }, { value: 1 });
869
+
const Fixed64Message = p.message({ value: p.fixed64() }, { value: 1 });
870
+
871
+
// uint32 range errors
872
+
assertThrows(() => p.encode(Uint32Message, { value: -1 }), p.ProtobufError);
873
+
assertThrows(() => p.encode(Uint32Message, { value: 4294967296 }), p.ProtobufError);
874
+
875
+
// uint64 range errors
876
+
assertThrows(() => p.encode(Uint64Message, { value: -1n }), p.ProtobufError);
877
+
878
+
// fixed32 range errors
879
+
assertThrows(() => p.encode(Fixed32Message, { value: -1 }), p.ProtobufError);
880
+
assertThrows(() => p.encode(Fixed32Message, { value: 4294967296 }), p.ProtobufError);
881
+
882
+
// fixed64 range errors
883
+
assertThrows(() => p.encode(Fixed64Message, { value: -1n }), p.ProtobufError);
884
+
});
885
+
886
+
// #endregion
887
+
888
+
// #region Decoding validation errors
889
+
890
+
Deno.test('invalid wire type handling', () => {
891
+
const Message = p.message({ text: p.string() }, { text: 1 });
892
+
893
+
// Manually create invalid wire data: field 1 with wire type 0 (varint) instead of 2 (length-delimited)
894
+
const invalidData = Uint8Array.from([
895
+
8,
896
+
72, // field 1, wire type 0, value 72
897
+
]);
898
+
899
+
assertThrows(() => p.decode(Message, invalidData), p.ProtobufError);
900
+
901
+
const result = p.tryDecode(Message, invalidData);
902
+
assert(!result.ok);
903
+
904
+
assertEquals(result.issues.length, 1);
905
+
const issue = result.issues[0];
906
+
907
+
assertEquals(issue.code, 'invalid_wire');
908
+
assertEquals(issue.path, ['text']);
909
+
910
+
if (issue.code === 'invalid_wire') {
911
+
assertEquals(issue.expected, 2); // string expects wire type 2
912
+
}
913
+
914
+
assertStringIncludes(result.message, 'invalid_wire at');
915
+
assertStringIncludes(result.message, 'expected wire type');
916
+
assertStringIncludes(result.message, 'text');
917
+
});
918
+
919
+
Deno.test('multiple validation errors', () => {
920
+
const Message = p.message({
921
+
field1: p.string(),
922
+
field2: p.int32(),
923
+
field3: p.bytes(),
924
+
}, {
925
+
field1: 1,
926
+
field2: 2,
927
+
field3: 3,
928
+
});
929
+
930
+
// Create data with wrong wire types for multiple fields
931
+
const invalidData = Uint8Array.from([
932
+
8,
933
+
72, // field 1, wire type 0 instead of 2
934
+
18,
935
+
1,
936
+
65, // field 2, wire type 2 instead of 0
937
+
24,
938
+
100, // field 3, wire type 0 instead of 2
939
+
]);
940
+
941
+
const result = p.tryDecode(Message, invalidData);
942
+
assert(!result.ok);
943
+
944
+
assertEquals(result.issues.length, 3); // field1, field2, and field3 have wrong wire types
945
+
946
+
const codes = result.issues.map((issue) => issue.code);
947
+
assert(codes.every((code) => code === 'invalid_wire'));
948
+
949
+
const fields = result.issues.map((issue) => issue.path[0]);
950
+
assertArrayIncludes(fields, ['field1', 'field2', 'field3']);
951
+
952
+
assertStringIncludes(result.message, '+2 other issue(s)');
953
+
});
954
+
955
+
Deno.test('missing required fields during decoding', () => {
956
+
const Message = p.message({
957
+
required1: p.string(),
958
+
required2: p.int32(),
959
+
optional: p.optional(p.string()),
960
+
}, {
961
+
required1: 1,
962
+
required2: 2,
963
+
optional: 3,
964
+
});
965
+
966
+
// Create a message missing required fields
967
+
const partialSchema = p.message({
968
+
optional: p.optional(p.string()),
969
+
}, {
970
+
optional: 3,
971
+
});
972
+
973
+
const encoded = p.encode(partialSchema, { optional: 'hello' });
974
+
975
+
const result = p.tryDecode(Message, encoded);
976
+
assert(!result.ok);
977
+
978
+
const issues = result.issues;
979
+
assertEquals(issues.length, 2);
980
+
981
+
const missingCodes = issues.map((issue) => issue.code);
982
+
assertArrayIncludes(missingCodes, ['missing_value', 'missing_value']);
983
+
984
+
const missingKeys = issues.map((issue) => issue.path.join('.'));
985
+
assertArrayIncludes(missingKeys, ['required1', 'required2']);
986
+
});
987
+
988
+
Deno.test('empty buffer handling', () => {
989
+
const Message = p.message({
990
+
required: p.string(),
991
+
optional: p.optional(p.string()),
992
+
}, {
993
+
required: 1,
994
+
optional: 2,
995
+
});
996
+
997
+
const emptyBuffer = new Uint8Array(0);
998
+
999
+
const result = p.tryDecode(Message, emptyBuffer);
1000
+
assert(!result.ok);
1001
+
1002
+
assertEquals(result.issues.length, 1);
1003
+
assertEquals(result.issues[0].code, 'missing_value');
1004
+
assertEquals(result.issues[0].path, ['required']);
1005
+
});
1006
+
1007
+
// #endregion
1008
+
1009
+
// #region Buffer corruption and underrun
1010
+
1011
+
Deno.test('corrupted data handling', () => {
1012
+
const Message = p.message({ text: p.string() }, { text: 1 });
1013
+
1014
+
// Test with truncated data (incomplete varint)
1015
+
const truncatedData = Uint8Array.from([10, 5, 72, 101]); // says length 5 but only has 2 bytes
1016
+
1017
+
assertThrows(() => p.decode(Message, truncatedData));
1018
+
});
1019
+
1020
+
Deno.test('buffer underrun during decoding', () => {
1021
+
const Message = p.message({ text: p.string() }, { text: 1 });
1022
+
1023
+
// Create a truncated buffer that claims to have more data than it actually has
1024
+
// Wire format: tag (8=field 1, wire type 2) + length (5) + partial data (only 2 bytes instead of 5)
1025
+
const truncatedBuffer = Uint8Array.from([
1026
+
8 | 2, // tag: field 1, wire type 2 (length-delimited)
1027
+
5, // length: claims 5 bytes follow
1028
+
65,
1029
+
66, // only 2 bytes: "AB"
1030
+
// missing 3 bytes!
1031
+
]);
1032
+
1033
+
const result = p.tryDecode(Message, truncatedBuffer);
1034
+
1035
+
assert(!result.ok);
1036
+
assertEquals(result.message, `unexpected_eof at .text (unexpected end of input) (+1 other issue(s))`);
1037
+
});
1038
+
1039
+
Deno.test('buffer underrun during varint reading', () => {
1040
+
const Message = p.message({ value: p.int32() }, { value: 1 });
1041
+
1042
+
// Create a buffer with incomplete varint (continuation bit set but no more bytes)
1043
+
const incompleteVarint = Uint8Array.from([
1044
+
8, // tag: field 1, wire type 0 (varint)
1045
+
0x80, // varint with continuation bit set, but no more bytes
1046
+
]);
1047
+
1048
+
const result = p.tryDecode(Message, incompleteVarint);
1049
+
1050
+
assert(!result.ok);
1051
+
assertEquals(result.message, `unexpected_eof at .value (unexpected end of input) (+1 other issue(s))`);
1052
+
});
1053
+
1054
+
// #endregion
1055
+
1056
+
// #region Performance tests
1057
+
1058
+
Deno.test('round-trip consistency stress test', () => {
1059
+
const Message = p.message({
1060
+
id: p.int64(),
1061
+
name: p.string(),
1062
+
email: p.optional(p.string()),
1063
+
tags: p.repeated(p.string()),
1064
+
metadata: p.bytes(),
1065
+
score: p.double(),
1066
+
active: p.boolean(),
1067
+
}, {
1068
+
id: 1,
1069
+
name: 2,
1070
+
email: 3,
1071
+
tags: 4,
1072
+
metadata: 5,
1073
+
score: 6,
1074
+
active: 7,
1075
+
});
1076
+
1077
+
// Generate random test data
1078
+
for (let i = 0; i < 100; i++) {
1079
+
const data = {
1080
+
id: BigInt(Math.floor(Math.random() * 1000000)),
1081
+
name: `user_${i}`,
1082
+
email: Math.random() > 0.5 ? `user_${i}@example.com` : undefined,
1083
+
tags: Array.from({ length: Math.floor(Math.random() * 5) }, (_, j) => `tag_${j}`),
1084
+
metadata: new Uint8Array(
1085
+
Array.from({ length: Math.floor(Math.random() * 20) }, () => Math.floor(Math.random() * 255)),
1086
+
),
1087
+
score: Math.random() * 100,
1088
+
active: Math.random() > 0.5,
1089
+
};
1090
+
1091
+
const encoded = p.encode(Message, data);
1092
+
const decoded = p.decode(Message, encoded);
1093
+
1094
+
// `assertEquals` throws if properties aren't present entirely.
1095
+
decoded.email ??= undefined;
1096
+
1097
+
assertEquals(decoded, data, `Failed for iteration ${i}`);
1098
+
}
1099
+
});
1100
+
1101
+
Deno.test('very large arrays', () => {
1102
+
const Message = p.message({ strings: p.repeated(p.string()) }, { strings: 1 });
1103
+
1104
+
const strings = Array.from({ length: 10000 }, () => nanoid(8));
1105
+
1106
+
const encoded = p.encode(Message, { strings });
1107
+
const decoded = p.decode(Message, encoded);
1108
+
1109
+
assertEquals(decoded, { strings });
1110
+
});
1111
+
1112
+
Deno.test('large string handling', () => {
1113
+
const Message = p.message({ text: p.string() }, { text: 1 });
1114
+
1115
+
const text = nanoid(1048576);
1116
+
1117
+
const encoded = p.encode(Message, { text });
1118
+
const decoded = p.decode(Message, encoded);
1119
+
1120
+
assertEquals(decoded, { text });
1121
+
});
1122
+
1123
+
Deno.test('large byte array handling', () => {
1124
+
const Message = p.message({ data: p.bytes() }, { data: 1 });
1125
+
1126
+
const data = new Uint8Array(1024 * 1024);
1127
+
for (let i = 0; i < data.length; i++) {
1128
+
data[i] = Math.floor(Math.random() * 255);
1129
+
}
1130
+
1131
+
const encoded = p.encode(Message, { data });
1132
+
const decoded = p.decode(Message, encoded);
1133
+
1134
+
assertEquals(decoded, { data });
1135
+
});
1136
+
1137
+
// #endregion
+1916
lib/mod.ts
+1916
lib/mod.ts
···
1
+
// deno-lint-ignore-file no-explicit-any
2
+
3
+
import { type BitSet, getBit, setBit } from './bitset.ts';
4
+
import { decodeUtf8, encodeUtf8Into, lazy, lazyProperty } from './utils.ts';
5
+
6
+
const CHUNK_SIZE = 1024;
7
+
8
+
type Identity<T> = T;
9
+
type Flatten<T> = Identity<{ [K in keyof T]: T[K] }>;
10
+
11
+
type WireType = 0 | 1 | 2 | 5;
12
+
13
+
type Key = string | number;
14
+
15
+
type InputType =
16
+
| 'array'
17
+
| 'bigint'
18
+
| 'boolean'
19
+
| 'bytes'
20
+
| 'map'
21
+
| 'number'
22
+
| 'object'
23
+
| 'string';
24
+
25
+
type RangeType =
26
+
| 'float'
27
+
| 'int32'
28
+
| 'int64'
29
+
| 'uint32'
30
+
| 'uint64';
31
+
32
+
// #region Schema issue types
33
+
type IssueLeaf =
34
+
| { ok: false; code: 'unexpected_eof' }
35
+
| { ok: false; code: 'invalid_wire'; expected: WireType }
36
+
| { ok: false; code: 'missing_value' }
37
+
| { ok: false; code: 'invalid_type'; expected: InputType }
38
+
| { ok: false; code: 'invalid_range'; type: RangeType };
39
+
40
+
type IssueTree =
41
+
| IssueLeaf
42
+
| { ok: false; code: 'prepend'; key: Key; tree: IssueTree }
43
+
| { ok: false; code: 'join'; left: IssueTree; right: IssueTree };
44
+
45
+
export type Issue = Readonly<
46
+
| { code: 'unexpected_eof'; path: Key[] }
47
+
| { code: 'invalid_wire'; path: Key[]; expected: WireType }
48
+
| { code: 'missing_value'; path: Key[] }
49
+
| { code: 'invalid_type'; path: Key[]; expected: InputType }
50
+
| { code: 'invalid_range'; path: Key[]; type: RangeType }
51
+
>;
52
+
53
+
const EOF_ISSUE: IssueLeaf = { ok: false, code: 'unexpected_eof' };
54
+
55
+
const ARRAY_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'array' };
56
+
const BIGINT_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'bigint' };
57
+
const BOOLEAN_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'boolean' };
58
+
const BYTES_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'bytes' };
59
+
const MAP_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'map' };
60
+
const NUMBER_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'number' };
61
+
const OBJECT_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'object' };
62
+
const STRING_TYPE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_type', expected: 'string' };
63
+
64
+
const FLOAT_RANGE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_range', type: 'float' };
65
+
const INT32_RANGE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_range', type: 'int32' };
66
+
const INT64_RANGE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_range', type: 'int64' };
67
+
const UINT32_RANGE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_range', type: 'uint32' };
68
+
const UINT64_RANGE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_range', type: 'uint64' };
69
+
70
+
// #__NO_SIDE_EFFECTS__
71
+
const joinIssues = (left: IssueTree | undefined, right: IssueTree): IssueTree => {
72
+
return left ? { ok: false, code: 'join', left, right } : right;
73
+
};
74
+
75
+
// #__NO_SIDE_EFFECTS__
76
+
const prependPath = (key: Key, tree: IssueTree): IssueTree => {
77
+
return { ok: false, code: 'prepend', key, tree };
78
+
};
79
+
80
+
// #region Error formatting utilities
81
+
82
+
const cloneIssueWithPath = (issue: IssueLeaf, path: Key[]): Issue => {
83
+
const { ok: _ok, ...clone } = issue;
84
+
85
+
return { ...clone, path };
86
+
};
87
+
88
+
const collectIssues = (tree: IssueTree, path: Key[] = [], issues: Issue[] = []): Issue[] => {
89
+
for (;;) {
90
+
switch (tree.code) {
91
+
case 'join': {
92
+
collectIssues(tree.left, path.slice(), issues);
93
+
tree = tree.right;
94
+
continue;
95
+
}
96
+
case 'prepend': {
97
+
path.push(tree.key);
98
+
tree = tree.tree;
99
+
continue;
100
+
}
101
+
default: {
102
+
issues.push(cloneIssueWithPath(tree, path));
103
+
return issues;
104
+
}
105
+
}
106
+
}
107
+
};
108
+
109
+
const countIssues = (tree: IssueTree): number => {
110
+
let count = 0;
111
+
for (;;) {
112
+
switch (tree.code) {
113
+
case 'join': {
114
+
count += countIssues(tree.left);
115
+
tree = tree.right;
116
+
continue;
117
+
}
118
+
case 'prepend': {
119
+
tree = tree.tree;
120
+
continue;
121
+
}
122
+
default: {
123
+
return count + 1;
124
+
}
125
+
}
126
+
}
127
+
};
128
+
129
+
const formatIssueTree = (tree: IssueTree): string => {
130
+
let path = '';
131
+
let count = 0;
132
+
for (;;) {
133
+
switch (tree.code) {
134
+
case 'join': {
135
+
count += countIssues(tree.right);
136
+
tree = tree.left;
137
+
continue;
138
+
}
139
+
case 'prepend': {
140
+
path += `.${tree.key}`;
141
+
tree = tree.tree;
142
+
continue;
143
+
}
144
+
}
145
+
146
+
break;
147
+
}
148
+
149
+
let message: string;
150
+
switch (tree.code) {
151
+
case 'missing_value':
152
+
message = 'required field is missing';
153
+
break;
154
+
case 'invalid_wire':
155
+
message = `expected wire type ${tree.expected}`;
156
+
break;
157
+
case 'unexpected_eof':
158
+
message = `unexpected end of input`;
159
+
break;
160
+
case 'invalid_type':
161
+
message = `expected ${tree.expected}`;
162
+
break;
163
+
case 'invalid_range':
164
+
message = `value out of range for ${tree.type}`;
165
+
break;
166
+
default:
167
+
message = 'unknown error';
168
+
break;
169
+
}
170
+
171
+
let msg = `${tree.code} at ${path || '.'} (${message})`;
172
+
if (count > 0) {
173
+
msg += ` (+${count} other issue(s))`;
174
+
}
175
+
176
+
return msg;
177
+
};
178
+
179
+
// #endregion
180
+
181
+
// #region Result types
182
+
183
+
export type Ok<T> = {
184
+
ok: true;
185
+
value: T;
186
+
};
187
+
export type Err = {
188
+
ok: false;
189
+
readonly message: string;
190
+
readonly issues: readonly Issue[];
191
+
throw(): never;
192
+
};
193
+
194
+
export type Result<T> = Ok<T> | Err;
195
+
196
+
export class ProtobufError extends Error {
197
+
override readonly name = 'ProtobufError';
198
+
199
+
#issueTree: IssueTree;
200
+
201
+
constructor(issueTree: IssueTree) {
202
+
super();
203
+
204
+
this.#issueTree = issueTree;
205
+
}
206
+
207
+
override get message(): string {
208
+
return formatIssueTree(this.#issueTree);
209
+
}
210
+
211
+
get issues(): readonly Issue[] {
212
+
return collectIssues(this.#issueTree);
213
+
}
214
+
}
215
+
216
+
class ErrImpl implements Err {
217
+
readonly ok = false;
218
+
219
+
#issueTree: IssueTree;
220
+
221
+
constructor(issueTree: IssueTree) {
222
+
this.#issueTree = issueTree;
223
+
}
224
+
225
+
get message(): string {
226
+
return formatIssueTree(this.#issueTree);
227
+
}
228
+
229
+
get issues(): readonly Issue[] {
230
+
return collectIssues(this.#issueTree);
231
+
}
232
+
233
+
throw(): never {
234
+
throw new ProtobufError(this.#issueTree);
235
+
}
236
+
}
237
+
238
+
const createDecoderState = (buffer: Uint8Array): DecoderState => {
239
+
return {
240
+
b: buffer,
241
+
p: 0,
242
+
v: null,
243
+
};
244
+
};
245
+
246
+
const createEncoderState = (): EncoderState => {
247
+
return {
248
+
c: [],
249
+
b: new Uint8Array(CHUNK_SIZE),
250
+
v: null,
251
+
p: 0,
252
+
l: 0,
253
+
};
254
+
};
255
+
256
+
/**
257
+
* gracefully decode protobuf message
258
+
* @param schema message schema
259
+
* @param buffer byte array
260
+
* @returns decode result
261
+
*/
262
+
// #__NO_SIDE_EFFECTS__
263
+
export const tryDecode = <TSchema extends MessageSchema>(
264
+
schema: TSchema,
265
+
buffer: Uint8Array,
266
+
): Result<InferOutput<TSchema>> => {
267
+
const state = createDecoderState(buffer);
268
+
269
+
const result = schema['~~decode'](state, FLAG_EMPTY);
270
+
271
+
if (result.ok) {
272
+
return result as Ok<InferOutput<TSchema>>;
273
+
}
274
+
275
+
return new ErrImpl(result);
276
+
};
277
+
278
+
/**
279
+
* gracefully encode protobuf message
280
+
* @param schema message schema
281
+
* @param value JavaScript value
282
+
* @returns encode result
283
+
*/
284
+
// #__NO_SIDE_EFFECTS__
285
+
export const tryEncode = <TSchema extends MessageSchema>(
286
+
schema: TSchema,
287
+
value: InferInput<TSchema>,
288
+
): Result<Uint8Array> => {
289
+
const state = createEncoderState();
290
+
291
+
const result = schema['~~encode'](state, value);
292
+
if (result !== undefined) {
293
+
return new ErrImpl(result);
294
+
}
295
+
296
+
return { ok: true, value: finishEncode(state) };
297
+
};
298
+
299
+
/**
300
+
* decode protobuf message
301
+
* @param schema message schema
302
+
* @param buffer byte array
303
+
* @returns decoded JavaScript value
304
+
* @throws {ProtobufError} when decoding fails
305
+
*/
306
+
// #__NO_SIDE_EFFECTS__
307
+
export const decode = <TSchema extends MessageSchema>(
308
+
schema: TSchema,
309
+
buffer: Uint8Array,
310
+
): InferOutput<TSchema> => {
311
+
const state = createDecoderState(buffer);
312
+
313
+
const result = schema['~~decode'](state, FLAG_EMPTY);
314
+
315
+
if (result.ok) {
316
+
return result.value as InferOutput<TSchema>;
317
+
}
318
+
319
+
throw new ProtobufError(result);
320
+
};
321
+
322
+
/**
323
+
* encode protobuf message
324
+
* @param schema message schema
325
+
* @param value JavaScript value
326
+
* @returns encoded byte array
327
+
* @throws {ProtobufError} when encoding fails
328
+
*/
329
+
// #__NO_SIDE_EFFECTS__
330
+
export const encode = <TSchema extends MessageSchema>(
331
+
schema: TSchema,
332
+
value: InferInput<TSchema>,
333
+
): Uint8Array => {
334
+
const state = createEncoderState();
335
+
336
+
const result = schema['~~encode'](state, value);
337
+
338
+
if (result !== undefined) {
339
+
throw new ProtobufError(result);
340
+
}
341
+
342
+
return finishEncode(state);
343
+
};
344
+
345
+
// #region Raw decoders
346
+
347
+
interface DecoderState {
348
+
b: Uint8Array;
349
+
p: number;
350
+
v: DataView | null;
351
+
}
352
+
353
+
const readVarint = (state: DecoderState): RawResult<number> => {
354
+
const buf = state.b;
355
+
let pos = state.p;
356
+
357
+
let result = 0;
358
+
let shift = 0;
359
+
let byte;
360
+
361
+
do {
362
+
if (pos >= buf.length) {
363
+
return EOF_ISSUE;
364
+
}
365
+
byte = buf[pos++];
366
+
result |= (byte & 0x7F) << shift;
367
+
shift += 7;
368
+
} while (byte & 0x80);
369
+
370
+
state.p = pos;
371
+
return { ok: true, value: result };
372
+
};
373
+
374
+
const readBytes = (state: DecoderState, length: number): RawResult<Uint8Array> => {
375
+
const buf = state.b;
376
+
377
+
const start = state.p;
378
+
const end = start + length;
379
+
380
+
if (end > buf.length) {
381
+
return EOF_ISSUE;
382
+
}
383
+
384
+
state.p = end;
385
+
return { ok: true, value: buf.subarray(start, end) };
386
+
};
387
+
388
+
const skipField = (state: DecoderState, wire: WireType): RawResult<void> => {
389
+
switch (wire) {
390
+
case 0: {
391
+
const result = readVarint(state);
392
+
if (!result.ok) {
393
+
return result;
394
+
}
395
+
396
+
break;
397
+
}
398
+
case 1: {
399
+
if (state.p + 8 > state.b.length) {
400
+
return EOF_ISSUE;
401
+
}
402
+
403
+
state.p += 8;
404
+
break;
405
+
}
406
+
case 2: {
407
+
const length = readVarint(state);
408
+
if (!length.ok) {
409
+
return length;
410
+
}
411
+
412
+
if (state.p + length.value > state.b.length) {
413
+
return EOF_ISSUE;
414
+
}
415
+
416
+
state.p += length.value;
417
+
break;
418
+
}
419
+
case 5: {
420
+
if (state.p + 4 > state.b.length) {
421
+
return EOF_ISSUE;
422
+
}
423
+
424
+
state.p += 4;
425
+
break;
426
+
}
427
+
}
428
+
429
+
return { ok: true, value: undefined };
430
+
};
431
+
432
+
// #region Raw encoders
433
+
434
+
interface EncoderState {
435
+
c: Uint8Array[];
436
+
b: Uint8Array;
437
+
v: DataView | null;
438
+
p: number;
439
+
l: number;
440
+
}
441
+
442
+
const resizeIfNeeded = (state: EncoderState, needed: number): void => {
443
+
const buf = state.b;
444
+
const pos = state.p;
445
+
446
+
if (buf.byteLength < pos + needed) {
447
+
state.c.push(buf.subarray(0, pos));
448
+
state.l += pos;
449
+
450
+
state.b = new Uint8Array(Math.max(CHUNK_SIZE, needed));
451
+
state.v = null;
452
+
state.p = 0;
453
+
}
454
+
};
455
+
456
+
const writeVarint = (state: EncoderState, input: number | bigint): void => {
457
+
if (typeof input === 'bigint') {
458
+
resizeIfNeeded(state, 10);
459
+
460
+
// Handle negative BigInt values properly for two's complement
461
+
let n = input;
462
+
if (n < 0n) {
463
+
// Convert to unsigned representation for encoding
464
+
n = (1n << 64n) + n;
465
+
}
466
+
467
+
while (n >= 0x80n) {
468
+
state.b[state.p++] = Number(n & 0x7fn) | 0x80;
469
+
n >>= 7n;
470
+
}
471
+
472
+
state.b[state.p++] = Number(n);
473
+
} else {
474
+
resizeIfNeeded(state, 5);
475
+
476
+
let n = input >>> 0;
477
+
while (n >= 0x80) {
478
+
state.b[state.p++] = (n & 0x7f) | 0x80;
479
+
n >>>= 7;
480
+
}
481
+
482
+
state.b[state.p++] = n;
483
+
}
484
+
};
485
+
486
+
// Helper function to calculate varint length without encoding
487
+
const getVarintLength = (value: number): number => {
488
+
if (value < 0x80) return 1;
489
+
if (value < 0x4000) return 2;
490
+
if (value < 0x200000) return 3;
491
+
if (value < 0x10000000) return 4;
492
+
return 5;
493
+
};
494
+
495
+
const writeBytes = (state: EncoderState, bytes: Uint8Array): void => {
496
+
resizeIfNeeded(state, bytes.length);
497
+
498
+
state.b.set(bytes, state.p);
499
+
state.p += bytes.length;
500
+
};
501
+
502
+
const finishEncode = (state: EncoderState): Uint8Array => {
503
+
const chunks = state.c;
504
+
505
+
if (chunks.length === 0) {
506
+
return state.b.subarray(0, state.p);
507
+
}
508
+
509
+
const buffer = new Uint8Array(state.l + state.p);
510
+
511
+
let written = 0;
512
+
for (let idx = 0, len = chunks.length; idx < len; idx++) {
513
+
const chunk = chunks[idx];
514
+
515
+
buffer.set(chunk, written);
516
+
written += chunk.length;
517
+
}
518
+
519
+
buffer.set(state.b.subarray(0, state.p), written);
520
+
return buffer;
521
+
};
522
+
523
+
// #endregion
524
+
525
+
// #region Common utilities
526
+
527
+
const getDataView = (state: EncoderState | DecoderState): DataView => {
528
+
return state.v ??= new DataView(state.b.buffer, state.b.byteOffset, state.b.byteLength);
529
+
};
530
+
531
+
// #endregion
532
+
533
+
// #region Base schema
534
+
535
+
// Private symbols meant to hold types
536
+
declare const kType: unique symbol;
537
+
type kType = typeof kType;
538
+
539
+
// We need a special symbol to hold the types for objects due to their
540
+
// recursive nature.
541
+
declare const kObjectType: unique symbol;
542
+
type kObjectType = typeof kObjectType;
543
+
544
+
// None set
545
+
export const FLAG_EMPTY = 0;
546
+
// Don't continue validation if an error is encountered
547
+
export const FLAG_ABORT_EARLY = 1 << 0;
548
+
549
+
type RawResult<T = unknown> = Ok<T> | IssueTree;
550
+
551
+
type Decoder = (this: void, state: DecoderState, flags: number) => RawResult;
552
+
553
+
type Encoder = (this: void, state: EncoderState, input: unknown) => IssueTree | void;
554
+
555
+
export interface BaseSchema<TInput = unknown, TOutput = TInput> {
556
+
readonly kind: 'schema';
557
+
readonly type: string;
558
+
readonly wire: WireType;
559
+
readonly '~decode': Decoder;
560
+
readonly '~encode': Encoder;
561
+
562
+
readonly [kType]?: { in: TInput; out: TOutput };
563
+
}
564
+
565
+
export type InferInput<T extends BaseSchema> = T extends { [kObjectType]?: any }
566
+
? NonNullable<T[kObjectType]>['in']
567
+
: NonNullable<T[kType]>['in'];
568
+
569
+
export type InferOutput<T extends BaseSchema> = T extends { [kObjectType]?: any }
570
+
? NonNullable<T[kObjectType]>['out']
571
+
: NonNullable<T[kType]>['out'];
572
+
573
+
// #region String schema
574
+
export interface StringSchema extends BaseSchema<string> {
575
+
readonly type: 'string';
576
+
readonly wire: 2;
577
+
}
578
+
579
+
const STRING_SINGLETON: StringSchema = {
580
+
kind: 'schema',
581
+
type: 'string',
582
+
wire: 2,
583
+
'~decode'(state, _flags) {
584
+
const length = readVarint(state);
585
+
if (!length.ok) {
586
+
return length;
587
+
}
588
+
589
+
const bytes = readBytes(state, length.value);
590
+
if (!bytes.ok) {
591
+
return bytes;
592
+
}
593
+
594
+
return { ok: true, value: decodeUtf8(bytes.value) };
595
+
},
596
+
'~encode'(state, input) {
597
+
if (typeof input !== 'string') {
598
+
return STRING_TYPE_ISSUE;
599
+
}
600
+
601
+
// 1. Estimate the length of the header based on the UTF-16 size of the string
602
+
// 2. Directly write the string at the estimated location, retrieving the actual length
603
+
// 3. Write the header now that the length is available
604
+
// 4. If the estimation was wrong, correct the placement of the string
605
+
606
+
// JS strings are UTF-16, worst case UTF-8 length is length * 3
607
+
const strLength = input.length;
608
+
resizeIfNeeded(state, strLength * 3 + 5); // +5 for max varint length
609
+
610
+
const estimatedHeaderSize = getVarintLength(strLength);
611
+
const estimatedPosition = state.p + estimatedHeaderSize;
612
+
const actualLength = encodeUtf8Into(state.b, input, estimatedPosition);
613
+
614
+
const actualHeaderSize = getVarintLength(actualLength);
615
+
if (estimatedHeaderSize !== actualHeaderSize) {
616
+
// Estimation was incorrect, move the bytes to the real place
617
+
state.b.copyWithin(state.p + actualHeaderSize, estimatedPosition, estimatedPosition + actualLength);
618
+
}
619
+
620
+
writeVarint(state, actualLength);
621
+
state.p += actualLength;
622
+
},
623
+
};
624
+
625
+
/**
626
+
* creates a string schema
627
+
* strings are encoded as UTF-8 with length-prefixed wire format
628
+
* @returns string schema
629
+
*/
630
+
// #__NO_SIDE_EFFECTS__
631
+
export const string = (): StringSchema => {
632
+
return STRING_SINGLETON;
633
+
};
634
+
635
+
// #region Bytes schema
636
+
export interface BytesSchema extends BaseSchema<Uint8Array> {
637
+
readonly type: 'bytes';
638
+
readonly wire: 2;
639
+
}
640
+
641
+
const BYTES_SINGLETON: BytesSchema = {
642
+
kind: 'schema',
643
+
type: 'bytes',
644
+
wire: 2,
645
+
'~decode'(state, _flags) {
646
+
const length = readVarint(state);
647
+
if (!length.ok) {
648
+
return length;
649
+
}
650
+
651
+
return readBytes(state, length.value);
652
+
},
653
+
'~encode'(state, input) {
654
+
if (!(input instanceof Uint8Array)) {
655
+
return BYTES_TYPE_ISSUE;
656
+
}
657
+
658
+
resizeIfNeeded(state, 5 + input.length);
659
+
writeVarint(state, input.length);
660
+
writeBytes(state, input);
661
+
},
662
+
};
663
+
664
+
/**
665
+
* creates a bytes schema
666
+
* handles arbitrary binary data as Uint8Array with length-prefixed wire format
667
+
* @returns bytes schema
668
+
*/
669
+
// #__NO_SIDE_EFFECTS__
670
+
export const bytes = (): BytesSchema => {
671
+
return BYTES_SINGLETON;
672
+
};
673
+
674
+
// #region Boolean schema
675
+
export interface BooleanSchema extends BaseSchema<boolean> {
676
+
readonly type: 'boolean';
677
+
readonly wire: 0;
678
+
}
679
+
680
+
const BOOLEAN_SINGLETON: BooleanSchema = {
681
+
kind: 'schema',
682
+
type: 'boolean',
683
+
wire: 0,
684
+
'~decode'(state, _flags) {
685
+
const result = readVarint(state);
686
+
if (!result.ok) {
687
+
return result;
688
+
}
689
+
690
+
return { ok: true, value: result.value !== 0 };
691
+
},
692
+
'~encode'(state, input) {
693
+
if (typeof input !== 'boolean') {
694
+
return BOOLEAN_TYPE_ISSUE;
695
+
}
696
+
697
+
writeVarint(state, input ? 1 : 0);
698
+
},
699
+
};
700
+
701
+
/**
702
+
* creates a boolean schema
703
+
* booleans are encoded as varint (0 for false, 1 for true)
704
+
* @returns boolean schema
705
+
*/
706
+
// #__NO_SIDE_EFFECTS__
707
+
export const boolean = (): BooleanSchema => {
708
+
return BOOLEAN_SINGLETON;
709
+
};
710
+
711
+
// #region Double schema
712
+
export interface DoubleSchema extends BaseSchema<number> {
713
+
readonly type: 'double';
714
+
readonly wire: 1;
715
+
}
716
+
717
+
const DOUBLE_SINGLETON: DoubleSchema = {
718
+
kind: 'schema',
719
+
type: 'double',
720
+
wire: 1,
721
+
'~decode'(state, _flags) {
722
+
const view = getDataView(state);
723
+
const value = view.getFloat64(state.p, true);
724
+
725
+
state.p += 8;
726
+
return { ok: true, value };
727
+
},
728
+
'~encode'(state, input) {
729
+
if (typeof input !== 'number') {
730
+
return NUMBER_TYPE_ISSUE;
731
+
}
732
+
733
+
resizeIfNeeded(state, 8);
734
+
735
+
const view = getDataView(state);
736
+
view.setFloat64(state.p, input, true);
737
+
738
+
state.p += 8;
739
+
},
740
+
};
741
+
742
+
/**
743
+
* creates a double-precision floating point schema
744
+
* uses 64-bit IEEE 754 format, encoded as 8 bytes in little-endian
745
+
* @returns double schema
746
+
*/
747
+
// #__NO_SIDE_EFFECTS__
748
+
export const double = (): DoubleSchema => {
749
+
return DOUBLE_SINGLETON;
750
+
};
751
+
752
+
// #region Float schema
753
+
export interface FloatSchema extends BaseSchema<number> {
754
+
readonly type: 'float';
755
+
readonly wire: 5;
756
+
}
757
+
758
+
const FLOAT_SINGLETON: FloatSchema = {
759
+
kind: 'schema',
760
+
type: 'float',
761
+
wire: 5,
762
+
'~decode'(state, _flags) {
763
+
const view = getDataView(state);
764
+
const value = view.getFloat32(state.p, true);
765
+
766
+
state.p += 4;
767
+
return { ok: true, value };
768
+
},
769
+
'~encode'(state, input) {
770
+
if (typeof input !== 'number') {
771
+
return NUMBER_TYPE_ISSUE;
772
+
}
773
+
if (isFinite(input)) {
774
+
const abs = Math.abs(input);
775
+
776
+
if ((abs > 3.4028235e38 || (abs < 1.175494e-38 && input !== 0))) {
777
+
return FLOAT_RANGE_ISSUE;
778
+
}
779
+
}
780
+
781
+
resizeIfNeeded(state, 4);
782
+
783
+
const view = getDataView(state);
784
+
view.setFloat32(state.p, input, true);
785
+
786
+
state.p += 4;
787
+
},
788
+
};
789
+
790
+
/**
791
+
* creates a single-precision floating point schema
792
+
* uses 32-bit IEEE 754 format, encoded as 4 bytes in little-endian
793
+
* @returns float schema
794
+
*/
795
+
// #__NO_SIDE_EFFECTS__
796
+
export const float = (): FloatSchema => {
797
+
return FLOAT_SINGLETON;
798
+
};
799
+
800
+
// #region Int32 schema
801
+
export interface Int32Schema extends BaseSchema<number> {
802
+
readonly type: 'int32';
803
+
readonly wire: 0;
804
+
}
805
+
806
+
const INT32_SINGLETON: Int32Schema = {
807
+
kind: 'schema',
808
+
type: 'int32',
809
+
wire: 0,
810
+
'~decode'(state, _flags) {
811
+
const result = readVarint(state);
812
+
if (!result.ok) {
813
+
return result;
814
+
}
815
+
816
+
// Read as unsigned, then convert to signed 32-bit (handling sign extension)
817
+
const value = result.value | 0;
818
+
819
+
return { ok: true, value };
820
+
},
821
+
'~encode'(state, input) {
822
+
if (typeof input !== 'number') {
823
+
return NUMBER_TYPE_ISSUE;
824
+
}
825
+
if (input < -0x80000000 || input > 0x7fffffff) {
826
+
return INT32_RANGE_ISSUE;
827
+
}
828
+
829
+
const n = input | 0;
830
+
831
+
writeVarint(state, n);
832
+
},
833
+
};
834
+
835
+
/**
836
+
* creates a 32-bit signed integer schema
837
+
* uses varint encoding. values must be in range [-2^31, 2^31-1]
838
+
* @returns int32 schema
839
+
*/
840
+
// #__NO_SIDE_EFFECTS__
841
+
export const int32 = (): Int32Schema => {
842
+
return INT32_SINGLETON;
843
+
};
844
+
845
+
// #region Int64 schema
846
+
export interface Int64Schema extends BaseSchema<bigint> {
847
+
readonly type: 'int64';
848
+
readonly wire: 0;
849
+
}
850
+
851
+
const INT64_SINGLETON: Int64Schema = {
852
+
kind: 'schema',
853
+
type: 'int64',
854
+
wire: 0,
855
+
'~decode'(state, _flags) {
856
+
const buf = state.b;
857
+
let pos = state.p;
858
+
859
+
let result = 0n;
860
+
let shift = 0n;
861
+
let byte;
862
+
863
+
do {
864
+
byte = buf[pos++];
865
+
result |= BigInt(byte & 0x7F) << shift;
866
+
shift += 7n;
867
+
} while (byte & 0x80);
868
+
869
+
state.p = pos;
870
+
871
+
// Convert from unsigned to signed (two's complement)
872
+
if (result >= (1n << 63n)) {
873
+
result = result - (1n << 64n);
874
+
}
875
+
876
+
return { ok: true, value: result };
877
+
},
878
+
'~encode'(state, input) {
879
+
if (typeof input !== 'bigint') {
880
+
return BIGINT_TYPE_ISSUE;
881
+
}
882
+
if (input < -0x8000000000000000n || input > 0x7fffffffffffffffn) {
883
+
return INT64_RANGE_ISSUE;
884
+
}
885
+
886
+
// Convert signed to unsigned representation for wire format
887
+
let value = input;
888
+
if (input < 0n) {
889
+
value = input + 0x10000000000000000n;
890
+
}
891
+
892
+
writeVarint(state, value);
893
+
},
894
+
};
895
+
896
+
/**
897
+
* creates a 64-bit signed integer schema
898
+
* uses varint encoding. values must be in range [-2^63, 2^63-1]
899
+
* JavaScript values are represented as bigint
900
+
* @returns int64 schema
901
+
*/
902
+
// #__NO_SIDE_EFFECTS__
903
+
export const int64 = (): Int64Schema => {
904
+
return INT64_SINGLETON;
905
+
};
906
+
907
+
// #region Uint32 schema
908
+
export interface Uint32Schema extends BaseSchema<number> {
909
+
readonly type: 'uint32';
910
+
readonly wire: 0;
911
+
}
912
+
913
+
const UINT32_SINGLETON: Uint32Schema = {
914
+
kind: 'schema',
915
+
type: 'uint32',
916
+
wire: 0,
917
+
'~decode'(state, _flags) {
918
+
const result = readVarint(state);
919
+
if (!result.ok) {
920
+
return result;
921
+
}
922
+
923
+
// Limit to unsigned 32-bit
924
+
const value = result.value >>> 0;
925
+
926
+
return { ok: true, value };
927
+
},
928
+
'~encode'(state, input) {
929
+
if (typeof input !== 'number') {
930
+
return NUMBER_TYPE_ISSUE;
931
+
}
932
+
if (input < 0 || input > 0xffffffff) {
933
+
return UINT32_RANGE_ISSUE;
934
+
}
935
+
936
+
writeVarint(state, input >>> 0);
937
+
},
938
+
};
939
+
940
+
/**
941
+
* creates a 32-bit unsigned integer schema
942
+
* uses varint encoding. values must be in range [0, 2^32-1]
943
+
* @returns uint32 schema
944
+
*/
945
+
// #__NO_SIDE_EFFECTS__
946
+
export const uint32 = (): Uint32Schema => {
947
+
return UINT32_SINGLETON;
948
+
};
949
+
950
+
// #region Uint64 schema
951
+
export interface Uint64Schema extends BaseSchema<bigint> {
952
+
readonly type: 'uint64';
953
+
readonly wire: 0;
954
+
}
955
+
956
+
const UINT64_SINGLETON: Uint64Schema = {
957
+
kind: 'schema',
958
+
type: 'uint64',
959
+
wire: 0,
960
+
'~decode'(state, _flags) {
961
+
const buf = state.b;
962
+
let pos = state.p;
963
+
964
+
let result = 0n;
965
+
let shift = 0n;
966
+
let byte;
967
+
968
+
do {
969
+
byte = buf[pos++];
970
+
result |= BigInt(byte & 0x7F) << shift;
971
+
shift += 7n;
972
+
} while (byte & 0x80);
973
+
974
+
state.p = pos;
975
+
return { ok: true, value: result };
976
+
},
977
+
'~encode'(state, input) {
978
+
if (typeof input !== 'bigint') {
979
+
return BIGINT_TYPE_ISSUE;
980
+
}
981
+
if (input < 0n) {
982
+
return UINT64_RANGE_ISSUE;
983
+
}
984
+
985
+
writeVarint(state, input);
986
+
},
987
+
};
988
+
989
+
/**
990
+
* creates a 64-bit unsigned integer schema
991
+
* uses varint encoding. values must be in range [0, 2^64-1]
992
+
* JavaScript values are represented as bigint
993
+
* @returns uint64 schema
994
+
*/
995
+
// #__NO_SIDE_EFFECTS__
996
+
export const uint64 = (): Uint64Schema => {
997
+
return UINT64_SINGLETON;
998
+
};
999
+
1000
+
// #region Sint32 schema (zigzag encoding)
1001
+
export interface Sint32Schema extends BaseSchema<number> {
1002
+
readonly type: 'sint32';
1003
+
readonly wire: 0;
1004
+
}
1005
+
1006
+
const SINT32_SINGLETON: Sint32Schema = {
1007
+
kind: 'schema',
1008
+
type: 'sint32',
1009
+
wire: 0,
1010
+
'~decode'(state, _flags) {
1011
+
const result = readVarint(state);
1012
+
if (!result.ok) {
1013
+
return result;
1014
+
}
1015
+
1016
+
const n = result.value;
1017
+
return { ok: true, value: (n >>> 1) ^ (-(n & 1)) };
1018
+
},
1019
+
'~encode'(state, input) {
1020
+
if (typeof input !== 'number') {
1021
+
return NUMBER_TYPE_ISSUE;
1022
+
}
1023
+
if (input < -0x80000000 || input > 0x7fffffff) {
1024
+
return INT32_RANGE_ISSUE;
1025
+
}
1026
+
1027
+
const n = input | 0;
1028
+
1029
+
writeVarint(state, (n << 1) ^ (n >> 31));
1030
+
},
1031
+
};
1032
+
1033
+
/**
1034
+
* creates a 32-bit signed integer schema
1035
+
* uses zigzag encoding to efficiently encode negative numbers. values must be in range [-2^31, 2^31-1]
1036
+
* @returns sint32 schema
1037
+
*/
1038
+
// #__NO_SIDE_EFFECTS__
1039
+
export const sint32 = (): Sint32Schema => {
1040
+
return SINT32_SINGLETON;
1041
+
};
1042
+
1043
+
// #region Sint64 schema (zigzag encoding with BigInt)
1044
+
export interface Sint64Schema extends BaseSchema<bigint> {
1045
+
readonly type: 'sint64';
1046
+
readonly wire: 0;
1047
+
}
1048
+
1049
+
const SINT64_SINGLETON: Sint64Schema = {
1050
+
kind: 'schema',
1051
+
type: 'sint64',
1052
+
wire: 0,
1053
+
'~decode'(state, _flags) {
1054
+
const buf = state.b;
1055
+
let pos = state.p;
1056
+
1057
+
let result = 0n;
1058
+
let shift = 0n;
1059
+
let byte;
1060
+
1061
+
do {
1062
+
byte = buf[pos++];
1063
+
result |= BigInt(byte & 0x7F) << shift;
1064
+
shift += 7n;
1065
+
} while (byte & 0x80);
1066
+
1067
+
state.p = pos;
1068
+
1069
+
return { ok: true, value: (result >> 1n) ^ (-(result & 1n)) };
1070
+
},
1071
+
'~encode'(state, input) {
1072
+
if (typeof input !== 'bigint') {
1073
+
return BIGINT_TYPE_ISSUE;
1074
+
}
1075
+
if (input < -0x8000000000000000n || input > 0x7fffffffffffffffn) {
1076
+
return INT64_RANGE_ISSUE;
1077
+
}
1078
+
1079
+
writeVarint(state, (input << 1n) ^ (input >> 63n));
1080
+
},
1081
+
};
1082
+
1083
+
/**
1084
+
* creates a 64-bit signed integer schema
1085
+
* uses zigzag encoding to efficiently encode negative numbers. values must be in range [-2^63, 2^63-1]
1086
+
* JavaScript values are represented as bigint
1087
+
* @returns sint64 schema
1088
+
*/
1089
+
// #__NO_SIDE_EFFECTS__
1090
+
export const sint64 = (): Sint64Schema => {
1091
+
return SINT64_SINGLETON;
1092
+
};
1093
+
1094
+
// #region Fixed32 schema
1095
+
export interface Fixed32Schema extends BaseSchema<number> {
1096
+
readonly type: 'fixed32';
1097
+
readonly wire: 5;
1098
+
}
1099
+
1100
+
const FIXED32_SINGLETON: Fixed32Schema = {
1101
+
kind: 'schema',
1102
+
type: 'fixed32',
1103
+
wire: 5,
1104
+
'~decode'(state, _flags) {
1105
+
const view = getDataView(state);
1106
+
const value = view.getUint32(state.p, true);
1107
+
1108
+
state.p += 4;
1109
+
return { ok: true, value };
1110
+
},
1111
+
'~encode'(state, input) {
1112
+
if (typeof input !== 'number') {
1113
+
return NUMBER_TYPE_ISSUE;
1114
+
}
1115
+
if (input < 0 || input > 0xffffffff) {
1116
+
return UINT32_RANGE_ISSUE;
1117
+
}
1118
+
1119
+
resizeIfNeeded(state, 4);
1120
+
1121
+
const view = getDataView(state);
1122
+
view.setUint32(state.p, input, true);
1123
+
1124
+
state.p += 4;
1125
+
},
1126
+
};
1127
+
1128
+
/**
1129
+
* creates a 32-bit fixed-width unsigned integer schema.
1130
+
* always uses exactly 4 bytes in little-endian format. values must be in range [0, 2^32-1]
1131
+
* @returns fixed32 schema
1132
+
*/
1133
+
// #__NO_SIDE_EFFECTS__
1134
+
export const fixed32 = (): Fixed32Schema => {
1135
+
return FIXED32_SINGLETON;
1136
+
};
1137
+
1138
+
// #region Fixed64 schema
1139
+
export interface Fixed64Schema extends BaseSchema<bigint> {
1140
+
readonly type: 'fixed64';
1141
+
readonly wire: 1;
1142
+
}
1143
+
1144
+
const FIXED64_SINGLETON: Fixed64Schema = {
1145
+
kind: 'schema',
1146
+
type: 'fixed64',
1147
+
wire: 1,
1148
+
'~decode'(state, _flags) {
1149
+
const view = getDataView(state);
1150
+
1151
+
// Read as two 32-bit values and combine into a BigInt
1152
+
const lo = view.getUint32(state.p, true);
1153
+
const hi = view.getUint32(state.p + 4, true);
1154
+
1155
+
state.p += 8;
1156
+
return { ok: true, value: (BigInt(hi) << 32n) | BigInt(lo) };
1157
+
},
1158
+
'~encode'(state, input) {
1159
+
if (typeof input !== 'bigint') {
1160
+
return BIGINT_TYPE_ISSUE;
1161
+
}
1162
+
if (input < 0n) {
1163
+
return UINT64_RANGE_ISSUE;
1164
+
}
1165
+
1166
+
resizeIfNeeded(state, 8);
1167
+
1168
+
const view = getDataView(state);
1169
+
1170
+
view.setUint32(state.p, Number(input & 0xffffffffn), true);
1171
+
view.setUint32(state.p + 4, Number(input >> 32n), true);
1172
+
1173
+
state.p += 8;
1174
+
},
1175
+
};
1176
+
1177
+
/**
1178
+
* creates a 64-bit fixed-width unsigned integer schema
1179
+
* always uses exactly 8 bytes in little-endian format. values must be in range [0, 2^64-1]
1180
+
* JavaScript values are represented as bigint
1181
+
*
1182
+
* @returns fixed64 schema
1183
+
*/
1184
+
// #__NO_SIDE_EFFECTS__
1185
+
export const fixed64 = (): Fixed64Schema => {
1186
+
return FIXED64_SINGLETON;
1187
+
};
1188
+
1189
+
// #region Sfixed32 schema
1190
+
export interface Sfixed32Schema extends BaseSchema<number> {
1191
+
readonly type: 'sfixed32';
1192
+
readonly wire: 5;
1193
+
}
1194
+
1195
+
const SFIXED32_SINGLETON: Sfixed32Schema = {
1196
+
kind: 'schema',
1197
+
type: 'sfixed32',
1198
+
wire: 5,
1199
+
'~decode'(state, _flags) {
1200
+
const view = getDataView(state);
1201
+
const value = view.getInt32(state.p, true);
1202
+
1203
+
state.p += 4;
1204
+
return { ok: true, value };
1205
+
},
1206
+
'~encode'(state, input) {
1207
+
if (typeof input !== 'number') {
1208
+
return NUMBER_TYPE_ISSUE;
1209
+
}
1210
+
if (input < -0x80000000 || input > 0x7fffffff) {
1211
+
return INT32_RANGE_ISSUE;
1212
+
}
1213
+
1214
+
resizeIfNeeded(state, 4);
1215
+
1216
+
const view = getDataView(state);
1217
+
view.setInt32(state.p, input | 0, true);
1218
+
1219
+
state.p += 4;
1220
+
},
1221
+
};
1222
+
1223
+
/**
1224
+
* creates a 32-bit fixed-width signed integer schema
1225
+
* always uses exactly 4 bytes in little-endian format. values must be in range [-2^31, 2^31-1]
1226
+
* @returns sfixed32 schema
1227
+
*/
1228
+
// #__NO_SIDE_EFFECTS__
1229
+
export const sfixed32 = (): Sfixed32Schema => {
1230
+
return SFIXED32_SINGLETON;
1231
+
};
1232
+
1233
+
// #region Sfixed64 schema
1234
+
export interface Sfixed64Schema extends BaseSchema<bigint> {
1235
+
readonly type: 'sfixed64';
1236
+
readonly wire: 1;
1237
+
}
1238
+
1239
+
const SFIXED64_SINGLETON: Sfixed64Schema = {
1240
+
kind: 'schema',
1241
+
type: 'sfixed64',
1242
+
wire: 1,
1243
+
'~decode'(state, _flags) {
1244
+
const view = getDataView(state);
1245
+
1246
+
// Read as two 32-bit values and combine into a BigInt
1247
+
const lo = view.getUint32(state.p, true);
1248
+
const hi = view.getInt32(state.p + 4, true); // High bits should be signed
1249
+
1250
+
state.p += 8;
1251
+
1252
+
// Combine into a single signed 64-bit bigint
1253
+
return { ok: true, value: (BigInt(hi) << 32n) | BigInt(lo) };
1254
+
},
1255
+
'~encode'(state, input) {
1256
+
if (typeof input !== 'bigint') {
1257
+
return BIGINT_TYPE_ISSUE;
1258
+
}
1259
+
if (input < -0x8000000000000000n || input > 0x7fffffffffffffffn) {
1260
+
return INT64_RANGE_ISSUE;
1261
+
}
1262
+
1263
+
resizeIfNeeded(state, 8);
1264
+
1265
+
const view = getDataView(state);
1266
+
1267
+
view.setUint32(state.p, Number(input & 0xffffffffn), true);
1268
+
view.setInt32(state.p + 4, Number(input >> 32n), true);
1269
+
1270
+
state.p += 8;
1271
+
},
1272
+
};
1273
+
1274
+
/**
1275
+
* creates a 64-bit fixed-width signed integer schema
1276
+
* uses exactly 8 bytes in little-endian format. values must be in range [-2^63, 2^63-1]
1277
+
* JavaScript values are represented as bigint
1278
+
* @returns sfixed64 schema
1279
+
*/
1280
+
// #__NO_SIDE_EFFECTS__
1281
+
export const sfixed64 = (): Sfixed64Schema => {
1282
+
return SFIXED64_SINGLETON;
1283
+
};
1284
+
1285
+
// #region Repeated schema
1286
+
export interface RepeatedSchema<TItem extends BaseSchema> extends BaseSchema<unknown[]> {
1287
+
readonly type: 'repeated';
1288
+
readonly wire: 2;
1289
+
readonly item: TItem;
1290
+
1291
+
readonly [kObjectType]?: { in: InferInput<TItem>[]; out: InferOutput<TItem>[] };
1292
+
}
1293
+
1294
+
/**
1295
+
* creates a value array schema
1296
+
* @param item item schema to repeat
1297
+
* @returns repeated schema
1298
+
*/
1299
+
// #__NO_SIDE_EFFECTS__
1300
+
export const repeated = <TItem extends BaseSchema>(item: TItem | (() => TItem)): RepeatedSchema<TItem> => {
1301
+
const resolvedShape = lazy(() => {
1302
+
return typeof item === 'function' ? item() : item;
1303
+
});
1304
+
1305
+
return {
1306
+
kind: 'schema',
1307
+
type: 'repeated',
1308
+
wire: 2,
1309
+
get item() {
1310
+
return lazyProperty(this, 'item', resolvedShape.value);
1311
+
},
1312
+
get '~decode'() {
1313
+
const shape = resolvedShape.value;
1314
+
1315
+
const decoder: Decoder = (state, flags) => {
1316
+
const length = readVarint(state);
1317
+
if (!length.ok) {
1318
+
return length;
1319
+
}
1320
+
1321
+
const bytes = readBytes(state, length.value);
1322
+
if (!bytes.ok) {
1323
+
return bytes;
1324
+
}
1325
+
1326
+
const children: DecoderState = {
1327
+
b: bytes.value,
1328
+
p: 0,
1329
+
v: null,
1330
+
};
1331
+
1332
+
const array: any[] = [];
1333
+
1334
+
let idx = 0;
1335
+
let issues: IssueTree | undefined;
1336
+
1337
+
while (children.p < length.value) {
1338
+
const r = shape['~decode'](children, flags);
1339
+
1340
+
if (r.ok) {
1341
+
array.push(r.value);
1342
+
} else {
1343
+
issues = joinIssues(issues, prependPath(idx, r));
1344
+
1345
+
if (flags & FLAG_ABORT_EARLY) {
1346
+
return issues;
1347
+
}
1348
+
}
1349
+
1350
+
idx++;
1351
+
}
1352
+
1353
+
if (issues !== undefined) {
1354
+
return issues;
1355
+
}
1356
+
1357
+
return { ok: true, value: array };
1358
+
};
1359
+
1360
+
return lazyProperty(this, '~decode', decoder);
1361
+
},
1362
+
get '~encode'() {
1363
+
const shape = resolvedShape.value;
1364
+
1365
+
const encoder: Encoder = (state, input) => {
1366
+
if (!Array.isArray(input)) {
1367
+
return ARRAY_TYPE_ISSUE;
1368
+
}
1369
+
1370
+
const children: EncoderState = {
1371
+
c: [],
1372
+
b: new Uint8Array(CHUNK_SIZE),
1373
+
v: null,
1374
+
p: 0,
1375
+
l: 0,
1376
+
};
1377
+
1378
+
for (let idx = 0, len = input.length; idx < len; idx++) {
1379
+
const result = shape['~encode'](children, input[idx]);
1380
+
1381
+
if (result) {
1382
+
return prependPath(idx, result);
1383
+
}
1384
+
}
1385
+
1386
+
const packed = finishEncode(children);
1387
+
1388
+
writeVarint(state, packed.length);
1389
+
writeBytes(state, packed);
1390
+
};
1391
+
1392
+
return lazyProperty(this, '~encode', encoder);
1393
+
},
1394
+
};
1395
+
};
1396
+
1397
+
// #region Optional schema
1398
+
1399
+
type DefaultValue<TItem extends BaseSchema> =
1400
+
| InferOutput<TItem>
1401
+
| (() => InferOutput<TItem>)
1402
+
| undefined;
1403
+
1404
+
type InferOptionalOutput<
1405
+
TItem extends BaseSchema,
1406
+
TDefault extends DefaultValue<TItem>,
1407
+
> = undefined extends TDefault ? InferOutput<TItem> | undefined : InferOutput<TItem>;
1408
+
1409
+
export interface OptionalSchema<
1410
+
TItem extends BaseSchema = BaseSchema,
1411
+
TDefault extends DefaultValue<TItem> = DefaultValue<TItem>,
1412
+
> extends BaseSchema<InferInput<TItem> | undefined, InferOptionalOutput<TItem, TDefault>> {
1413
+
readonly type: 'optional';
1414
+
readonly wrapped: TItem;
1415
+
readonly default: TDefault;
1416
+
}
1417
+
1418
+
/**
1419
+
* creates an optional field schema
1420
+
* @param wrapped schema to make optional
1421
+
* @param defaultValue default value when the field is not present
1422
+
* @returns optional field schema
1423
+
*/
1424
+
// #__NO_SIDE_EFFECTS__
1425
+
export const optional: {
1426
+
<TItem extends BaseSchema>(wrapped: TItem): OptionalSchema<TItem, undefined>;
1427
+
<TItem extends BaseSchema, TDefault extends DefaultValue<TItem>>(
1428
+
wrapped: TItem,
1429
+
defaultValue: TDefault,
1430
+
): OptionalSchema<TItem, TDefault>;
1431
+
} = (wrapped: BaseSchema, defaultValue?: any): OptionalSchema<any, any> => {
1432
+
return {
1433
+
kind: 'schema',
1434
+
type: 'optional',
1435
+
wrapped: wrapped,
1436
+
default: defaultValue,
1437
+
wire: wrapped.wire,
1438
+
'~decode'(state, flags) {
1439
+
return wrapped['~decode'](state, flags);
1440
+
},
1441
+
'~encode'(state, input) {
1442
+
return wrapped['~encode'](state, input);
1443
+
},
1444
+
};
1445
+
};
1446
+
1447
+
const isOptionalSchema = (schema: BaseSchema): schema is OptionalSchema<any> => {
1448
+
return schema.type === 'optional';
1449
+
};
1450
+
1451
+
// #region Message schema
1452
+
1453
+
export type LooseMessageShape = Record<string, any>;
1454
+
export type MessageShape = Record<string, BaseSchema>;
1455
+
1456
+
export type OptionalObjectInputKeys<TShape extends MessageShape> = {
1457
+
[Key in keyof TShape]: TShape[Key] extends OptionalSchema<any, any> ? Key : never;
1458
+
}[keyof TShape];
1459
+
1460
+
export type OptionalObjectOutputKeys<TShape extends MessageShape> = {
1461
+
[Key in keyof TShape]: TShape[Key] extends OptionalSchema<any, infer Default>
1462
+
? undefined extends Default ? Key
1463
+
: never
1464
+
: never;
1465
+
}[keyof TShape];
1466
+
1467
+
type InferMessageInput<TShape extends MessageShape> = Flatten<
1468
+
& {
1469
+
-readonly [Key in Exclude<keyof TShape, OptionalObjectInputKeys<TShape>>]: InferInput<
1470
+
TShape[Key]
1471
+
>;
1472
+
}
1473
+
& {
1474
+
-readonly [Key in OptionalObjectInputKeys<TShape>]?: InferInput<TShape[Key]>;
1475
+
}
1476
+
>;
1477
+
1478
+
type InferMessageOutput<TShape extends MessageShape> = Flatten<
1479
+
& {
1480
+
-readonly [Key in Exclude<keyof TShape, OptionalObjectOutputKeys<TShape>>]: InferOutput<
1481
+
TShape[Key]
1482
+
>;
1483
+
}
1484
+
& {
1485
+
-readonly [Key in OptionalObjectOutputKeys<TShape>]?: InferOutput<TShape[Key]>;
1486
+
}
1487
+
>;
1488
+
1489
+
export interface MessageSchema<
1490
+
TShape extends LooseMessageShape = LooseMessageShape,
1491
+
TTags extends Record<keyof TShape, number> = Record<keyof TShape, number>,
1492
+
> extends BaseSchema<Record<string, unknown>> {
1493
+
readonly type: 'message';
1494
+
readonly wire: 2;
1495
+
readonly shape: Readonly<TShape>;
1496
+
readonly tags: Readonly<TTags>;
1497
+
1498
+
readonly '~~decode': Decoder;
1499
+
readonly '~~encode': Encoder;
1500
+
1501
+
readonly [kObjectType]?: { in: InferMessageInput<TShape>; out: InferMessageOutput<TShape> };
1502
+
}
1503
+
1504
+
interface MessageEntry {
1505
+
key: string;
1506
+
1507
+
schema: BaseSchema;
1508
+
tag: number;
1509
+
wire: WireType;
1510
+
1511
+
optional: boolean;
1512
+
1513
+
wireIssue: IssueTree;
1514
+
missingIssue: IssueTree;
1515
+
}
1516
+
1517
+
const ISSUE_MISSING: IssueLeaf = {
1518
+
ok: false,
1519
+
code: 'missing_value',
1520
+
};
1521
+
1522
+
const set = (obj: Record<string, unknown>, key: string, value: unknown): void => {
1523
+
if (key === '__proto__') {
1524
+
Object.defineProperty(obj, key, { value });
1525
+
} else {
1526
+
obj[key] = value;
1527
+
}
1528
+
};
1529
+
1530
+
/**
1531
+
* creates a structured message schema
1532
+
* @param shape message shape
1533
+
* @param tags fields mapped to tag numbers
1534
+
* @returns structured message schema
1535
+
*/
1536
+
// #__NO_SIDE_EFFECTS__
1537
+
export const message = <TShape extends LooseMessageShape, TTags extends Record<keyof TShape, number>>(
1538
+
shape: TShape,
1539
+
tags: TTags,
1540
+
): MessageSchema<TShape, TTags> => {
1541
+
const resolvedEntries = lazy((): Record<number, MessageEntry> => {
1542
+
const resolved: Record<number, MessageEntry> = {};
1543
+
const obj = shape as MessageShape;
1544
+
1545
+
for (const key in obj) {
1546
+
const schema = obj[key];
1547
+
const tag = tags[key];
1548
+
1549
+
resolved[tag] = {
1550
+
key: key,
1551
+
schema: schema,
1552
+
tag: tag,
1553
+
wire: schema.wire,
1554
+
optional: isOptionalSchema(schema),
1555
+
wireIssue: prependPath(key, { ok: false, code: 'invalid_wire', expected: schema.wire }),
1556
+
missingIssue: prependPath(key, ISSUE_MISSING),
1557
+
};
1558
+
}
1559
+
1560
+
return resolved;
1561
+
});
1562
+
1563
+
return {
1564
+
kind: 'schema',
1565
+
type: 'message',
1566
+
wire: 2,
1567
+
tags: tags,
1568
+
get shape() {
1569
+
// if we just return the shape as is then it wouldn't be the same exact
1570
+
// shape when getters are present.
1571
+
const resolved = resolvedEntries.value;
1572
+
const obj: any = {};
1573
+
1574
+
for (const index in resolved) {
1575
+
const entry = resolved[index];
1576
+
obj[entry.key] = entry.schema;
1577
+
}
1578
+
1579
+
return lazyProperty(this, 'shape', obj as TShape);
1580
+
},
1581
+
1582
+
get '~~decode'() {
1583
+
const shape = resolvedEntries.value;
1584
+
const len = Object.keys(shape).length;
1585
+
1586
+
const decoder: Decoder = (state, flags) => {
1587
+
let seenBits: BitSet = 0;
1588
+
let seenCount = 0;
1589
+
1590
+
const obj: Record<string, unknown> = {};
1591
+
let issues: IssueTree | undefined;
1592
+
1593
+
const end = state.b.length;
1594
+
while (state.p < end) {
1595
+
const prelude = readVarint(state);
1596
+
if (!prelude.ok) {
1597
+
issues = joinIssues(issues, prelude);
1598
+
if (flags & FLAG_ABORT_EARLY) {
1599
+
return issues;
1600
+
}
1601
+
1602
+
break;
1603
+
}
1604
+
1605
+
const magic = prelude.value;
1606
+
const tag = magic >> 3;
1607
+
const wire = (magic & 0x7) as WireType;
1608
+
1609
+
const entry = shape[tag];
1610
+
1611
+
// We don't know what this tag is, skip
1612
+
if (!entry) {
1613
+
const result = skipField(state, wire);
1614
+
if (!result.ok) {
1615
+
issues = joinIssues(issues, result);
1616
+
if (flags & FLAG_ABORT_EARLY) {
1617
+
return issues;
1618
+
}
1619
+
1620
+
break;
1621
+
}
1622
+
continue;
1623
+
}
1624
+
1625
+
// We've not seen this tag before
1626
+
if (!getBit(seenBits, tag)) {
1627
+
seenBits = setBit(seenBits, tag);
1628
+
seenCount++;
1629
+
}
1630
+
1631
+
// It doesn't match with our wire, file an issue
1632
+
if (entry.wire !== wire) {
1633
+
issues = joinIssues(issues, entry.wireIssue);
1634
+
if (flags & FLAG_ABORT_EARLY) {
1635
+
return issues;
1636
+
}
1637
+
1638
+
const skip = skipField(state, wire);
1639
+
if (!skip.ok) {
1640
+
issues = joinIssues(issues, prependPath(entry.key, skip));
1641
+
if (flags & FLAG_ABORT_EARLY) {
1642
+
return issues;
1643
+
}
1644
+
1645
+
break;
1646
+
}
1647
+
1648
+
continue;
1649
+
}
1650
+
1651
+
// Decode the value
1652
+
const result = entry.schema['~decode'](state, flags);
1653
+
1654
+
// Failed to decode, file an issue
1655
+
if (!result.ok) {
1656
+
issues = joinIssues(issues, prependPath(entry.key, result));
1657
+
if (flags & FLAG_ABORT_EARLY) {
1658
+
return issues;
1659
+
}
1660
+
1661
+
continue;
1662
+
}
1663
+
1664
+
/*#__INLINE__*/ set(obj, entry.key, result.value);
1665
+
}
1666
+
1667
+
if (seenCount < len) {
1668
+
for (const strtag in shape) {
1669
+
const entry = shape[strtag];
1670
+
1671
+
if (!getBit(seenBits, entry.tag)) {
1672
+
if (entry.optional) {
1673
+
const schema = entry.schema as OptionalSchema;
1674
+
1675
+
let defaultValue = schema.default;
1676
+
if (defaultValue !== undefined) {
1677
+
if (typeof defaultValue === 'function') {
1678
+
defaultValue = defaultValue();
1679
+
}
1680
+
1681
+
/*#__INLINE__*/ set(obj, entry.key, defaultValue);
1682
+
}
1683
+
} else {
1684
+
issues = joinIssues(issues, entry.missingIssue);
1685
+
1686
+
if (flags & FLAG_ABORT_EARLY) {
1687
+
return issues;
1688
+
}
1689
+
}
1690
+
}
1691
+
}
1692
+
}
1693
+
1694
+
if (issues !== undefined) {
1695
+
return issues;
1696
+
}
1697
+
1698
+
return { ok: true, value: obj };
1699
+
};
1700
+
1701
+
return lazyProperty(this, '~~decode', decoder);
1702
+
},
1703
+
get '~decode'() {
1704
+
const raw = this['~~decode'];
1705
+
1706
+
const decoder: Decoder = (state, flags) => {
1707
+
const length = readVarint(state);
1708
+
if (!length.ok) {
1709
+
return length;
1710
+
}
1711
+
1712
+
const bytes = readBytes(state, length.value);
1713
+
if (!bytes.ok) {
1714
+
return bytes;
1715
+
}
1716
+
1717
+
const child: DecoderState = {
1718
+
b: bytes.value,
1719
+
p: 0,
1720
+
v: null,
1721
+
};
1722
+
1723
+
return raw(child, flags);
1724
+
};
1725
+
1726
+
return lazyProperty(this, '~decode', decoder);
1727
+
},
1728
+
get '~~encode'() {
1729
+
const shape = resolvedEntries.value;
1730
+
1731
+
const encoder: Encoder = (state, input) => {
1732
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
1733
+
return OBJECT_TYPE_ISSUE;
1734
+
}
1735
+
1736
+
const obj = input as Record<string, unknown>;
1737
+
1738
+
for (const tag in shape) {
1739
+
const entry = shape[tag];
1740
+
const fieldValue = obj[entry.key];
1741
+
1742
+
if (entry.optional && fieldValue === undefined) {
1743
+
continue;
1744
+
}
1745
+
1746
+
writeVarint(state, (entry.tag << 3) | entry.wire);
1747
+
1748
+
const result = entry.schema['~encode'](state, fieldValue);
1749
+
1750
+
if (result) {
1751
+
return prependPath(entry.key, result);
1752
+
}
1753
+
}
1754
+
};
1755
+
1756
+
return lazyProperty(this, '~~encode', encoder);
1757
+
},
1758
+
get '~encode'() {
1759
+
const raw = this['~~encode'];
1760
+
1761
+
const encoder: Encoder = (state, input) => {
1762
+
const children: EncoderState = {
1763
+
c: [],
1764
+
b: new Uint8Array(CHUNK_SIZE),
1765
+
v: null,
1766
+
p: 0,
1767
+
l: 0,
1768
+
};
1769
+
1770
+
const result = raw(children, input);
1771
+
if (result) {
1772
+
return result;
1773
+
}
1774
+
1775
+
const chunk = finishEncode(children);
1776
+
1777
+
writeVarint(state, chunk.length);
1778
+
writeBytes(state, chunk);
1779
+
};
1780
+
1781
+
return lazyProperty(this, '~encode', encoder);
1782
+
},
1783
+
};
1784
+
};
1785
+
1786
+
// #endregion
1787
+
1788
+
// #region Map schema
1789
+
export type MapKeySchema =
1790
+
| BooleanSchema
1791
+
| Fixed32Schema
1792
+
| Fixed64Schema
1793
+
| Int32Schema
1794
+
| Int64Schema
1795
+
| Sint32Schema
1796
+
| Sint64Schema
1797
+
| StringSchema
1798
+
| Uint32Schema
1799
+
| Uint64Schema;
1800
+
1801
+
export type MapValueSchema = BaseSchema;
1802
+
1803
+
export interface MapSchema<TKey extends MapKeySchema, TValue extends MapValueSchema>
1804
+
extends BaseSchema<unknown[]> {
1805
+
readonly type: 'map';
1806
+
readonly wire: 2;
1807
+
readonly key: TKey;
1808
+
readonly value: TValue;
1809
+
1810
+
readonly [kObjectType]?: {
1811
+
in: Map<InferInput<TKey>, InferInput<TValue>>;
1812
+
out: Map<InferOutput<TKey>, InferOutput<TValue>>;
1813
+
};
1814
+
}
1815
+
1816
+
/**
1817
+
* creates a key-value map schema
1818
+
* @param key schema for map keys
1819
+
* @param value schema for map values
1820
+
* @returns map schema
1821
+
*/
1822
+
export const map = <TKey extends MapKeySchema, TValue extends MapValueSchema>(
1823
+
key: TKey,
1824
+
value: TValue,
1825
+
): MapSchema<TKey, TValue> => {
1826
+
const Schema = repeated(message({ key, value }, { key: 1, value: 2 }));
1827
+
1828
+
type Entry = { key: TKey; value: TValue };
1829
+
1830
+
return {
1831
+
kind: 'schema',
1832
+
type: 'map',
1833
+
wire: 2,
1834
+
key,
1835
+
value,
1836
+
get '~decode'() {
1837
+
const decoder: Decoder = (state, flags) => {
1838
+
const result = Schema['~decode'](state, flags);
1839
+
if (!result.ok) {
1840
+
return result;
1841
+
}
1842
+
1843
+
const map = new Map();
1844
+
1845
+
const entries = result.value as Entry[];
1846
+
for (let idx = 0, len = entries.length; idx < len; idx++) {
1847
+
const entry = entries[idx];
1848
+
map.set(entry.key, entry.value);
1849
+
}
1850
+
1851
+
return { ok: true, value: map };
1852
+
};
1853
+
1854
+
return lazyProperty(this, '~decode', decoder);
1855
+
},
1856
+
get '~encode'() {
1857
+
const encoder: Encoder = (state, input) => {
1858
+
if (!(input instanceof Map)) {
1859
+
return MAP_TYPE_ISSUE;
1860
+
}
1861
+
1862
+
const entries: Entry[] = [];
1863
+
for (const [key, value] of input) {
1864
+
entries.push({ key, value });
1865
+
}
1866
+
1867
+
return Schema['~encode'](state, entries);
1868
+
};
1869
+
1870
+
return lazyProperty(this, '~encode', encoder);
1871
+
},
1872
+
};
1873
+
};
1874
+
1875
+
// #endregion
1876
+
1877
+
// #region Well-known types
1878
+
1879
+
/**
1880
+
* represents a point in time independent of any time zone or local calendar,
1881
+
* encoded as a count of seconds and fractions of seconds at nanosecond
1882
+
* resolution.
1883
+
*/
1884
+
export const Timestamp = /*#__PURE__*/ message({
1885
+
seconds: /*#__PURE__*/ optional(/*#__PURE__*/ int64(), 0n),
1886
+
nanos: /*#__PURE__*/ optional(/*#__PURE__*/ int32(), 0),
1887
+
}, {
1888
+
seconds: 1,
1889
+
nanos: 2,
1890
+
});
1891
+
1892
+
/**
1893
+
* represents a signed, fixed-length span of time represented as a count of
1894
+
* seconds and fractions of seconds at nanosecond resolution.
1895
+
*/
1896
+
export const Duration = /*#__PURE__*/ message({
1897
+
seconds: /*#__PURE__*/ optional(/*#__PURE__*/ int64(), 0n),
1898
+
nanos: /*#__PURE__*/ optional(/*#__PURE__*/ int32(), 0),
1899
+
}, {
1900
+
seconds: 1,
1901
+
nanos: 2,
1902
+
});
1903
+
1904
+
/**
1905
+
* contains an arbitrary serialized protocol buffer message along with a URL
1906
+
* that describes the type of the serialized message.
1907
+
*/
1908
+
export const Any = /*#__PURE__*/ message({
1909
+
typeUrl: /*#__PURE__*/ optional(/*#__PURE__*/ string(), ''),
1910
+
value: /*#__PURE__*/ optional(/*#__PURE__*/ bytes(), /*#__PURE__*/ new Uint8Array(0)),
1911
+
}, {
1912
+
typeUrl: 1,
1913
+
value: 2,
1914
+
});
1915
+
1916
+
// #endregion
+82
lib/utils.ts
+82
lib/utils.ts
···
1
+
// #__NO_SIDE_EFFECTS__
2
+
export const lazyProperty = <T>(obj: object, prop: string | number | symbol, value: T): T => {
3
+
Object.defineProperty(obj, prop, { value });
4
+
return value;
5
+
};
6
+
7
+
// #__NO_SIDE_EFFECTS__
8
+
export const lazy = <T>(getter: () => T): { readonly value: T } => {
9
+
return {
10
+
get value() {
11
+
const value = getter();
12
+
return lazyProperty(this, 'value', value);
13
+
},
14
+
};
15
+
};
16
+
17
+
const textDecoder = new TextDecoder();
18
+
const textEncoder = new TextEncoder();
19
+
const fromCharCode = String.fromCharCode;
20
+
21
+
export const decodeUtf8 = (from: Uint8Array, offset?: number, length?: number): string => {
22
+
let buffer: Uint8Array;
23
+
24
+
if (offset === undefined) {
25
+
buffer = from;
26
+
} else if (length === undefined) {
27
+
buffer = from.subarray(offset);
28
+
} else {
29
+
buffer = from.subarray(offset, offset + length);
30
+
}
31
+
32
+
const end = buffer.length;
33
+
if (end > 24) {
34
+
return textDecoder.decode(buffer);
35
+
}
36
+
37
+
{
38
+
let str = '';
39
+
let idx = 0;
40
+
41
+
for (; idx + 3 < end; idx += 4) {
42
+
const a = buffer[idx];
43
+
const b = buffer[idx + 1];
44
+
const c = buffer[idx + 2];
45
+
const d = buffer[idx + 3];
46
+
47
+
if ((a | b | c | d) & 0x80) {
48
+
return str + textDecoder.decode(buffer.subarray(idx));
49
+
}
50
+
51
+
str += fromCharCode(a, b, c, d);
52
+
}
53
+
54
+
for (; idx < end; idx++) {
55
+
const x = buffer[idx];
56
+
57
+
if (x & 0x80) {
58
+
return str + textDecoder.decode(buffer.subarray(idx));
59
+
}
60
+
61
+
str += fromCharCode(x);
62
+
}
63
+
64
+
return str;
65
+
}
66
+
};
67
+
68
+
export const encodeUtf8Into = (to: Uint8Array, str: string, offset?: number, length?: number): number => {
69
+
let buffer: Uint8Array;
70
+
71
+
if (offset === undefined) {
72
+
buffer = to;
73
+
} else if (length === undefined) {
74
+
buffer = to.subarray(offset);
75
+
} else {
76
+
buffer = to.subarray(offset, offset + length);
77
+
}
78
+
79
+
const result = textEncoder.encodeInto(str, buffer);
80
+
81
+
return result.written || 0;
82
+
};