protobuf codec with static type inference jsr.io/@mary/protobuf
typescript jsr

initial commit

mary.my.id 7a0c3427

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