protobuf codec with static type inference jsr.io/@mary/protobuf
typescript jsr
at trunk 43 kB view raw
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