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