index.ts
1 /** biome-ignore-all lint/suspicious/noExplicitAny: Serialization/Deserialization involves a lot of `any` */ 2 import * as duperFfi from "./generated/duper"; 3 4 duperFfi.default.initialize(); 5 6 /** 7 * Duper-specific errors. 8 */ 9 export type DuperError = duperFfi.DuperError; 10 11 const duperSymbol: unique symbol = Symbol(); 12 13 /** 14 * A valid Duper value. 15 */ 16 export type DuperValue = 17 | { 18 [duperSymbol]: "$__duper"; 19 type: "Object"; 20 value: Record<string, DuperValue | NonDuper>; 21 identifier?: string; 22 toJSON(): any; 23 } 24 | { 25 [duperSymbol]: "$__duper"; 26 type: "Array"; 27 value: (DuperValue | NonDuper)[]; 28 identifier?: string; 29 toJSON(): any; 30 } 31 | { 32 [duperSymbol]: "$__duper"; 33 type: "Tuple"; 34 value: (DuperValue | NonDuper)[]; 35 identifier?: string; 36 toJSON(): any; 37 } 38 | { 39 [duperSymbol]: "$__duper"; 40 type: "String"; 41 value: string; 42 identifier?: string; 43 toJSON(): any; 44 } 45 | { 46 [duperSymbol]: "$__duper"; 47 type: "Bytes"; 48 value: Uint8Array; 49 identifier?: string; 50 toJSON(): any; 51 } 52 | { 53 [duperSymbol]: "$__duper"; 54 type: "Temporal"; 55 value: any; 56 identifier?: string; 57 toJSON(): any; 58 } 59 | { 60 [duperSymbol]: "$__duper"; 61 type: "Integer"; 62 value: bigint; 63 identifier?: string; 64 toJSON(): any; 65 } 66 | { 67 [duperSymbol]: "$__duper"; 68 type: "Float"; 69 value: number; 70 identifier?: string; 71 toJSON(): any; 72 } 73 | { 74 [duperSymbol]: "$__duper"; 75 type: "Boolean"; 76 value: boolean; 77 identifier?: string; 78 toJSON(): any; 79 } 80 | { 81 [duperSymbol]: "$__duper"; 82 type: "Null"; 83 value: null; 84 identifier?: string; 85 toJSON(): any; 86 }; 87 88 type NonDuper = 89 | null 90 | undefined 91 | string 92 | boolean 93 | number 94 | bigint 95 | symbol 96 | { [duperSymbol]?: never; [key: string]: unknown }; 97 98 /** 99 * The possible types that a Duper value may have. 100 */ 101 export type DuperType = DuperValue extends { type: infer T } ? T : never; 102 103 export const DuperValue = { 104 /** 105 * Creates a Duper object. 106 * 107 * @param value The key/value mapping. 108 * @param identifier The identifier. 109 */ 110 Object: ( 111 value: Record<string, DuperValue | NonDuper>, 112 identifier?: string, 113 ) => { 114 if (typeof value !== "object") { 115 throw new Error(`Cannot cast value to object: ${value}`); 116 } 117 return { 118 [duperSymbol]: "$__duper" as const, 119 type: "Object" as const, 120 value, 121 identifier, 122 toJSON: () => 123 Object.fromEntries( 124 Object.entries(value).map(([key, val]) => [ 125 key, 126 (val as any).toJSON(), 127 ]), 128 ), 129 }; 130 }, 131 /** 132 * Creates a Duper array. 133 * 134 * @param value The value list. 135 * @param identifier The identifier. 136 */ 137 Array: (value: (DuperValue | NonDuper)[], identifier?: string) => { 138 if (!Array.isArray(value)) { 139 throw new Error(`Cannot cast value to array: ${value}`); 140 } 141 return { 142 [duperSymbol]: "$__duper" as const, 143 type: "Array" as const, 144 value, 145 identifier, 146 toJSON: () => value.map((val) => (val as any).toJSON()), 147 }; 148 }, 149 /** 150 * Creates a Duper tuple. 151 * 152 * @param value The value list. 153 * @param identifier The identifier. 154 */ 155 Tuple: (value: (DuperValue | NonDuper)[], identifier?: string) => { 156 if (!Array.isArray(value)) { 157 throw new Error(`Cannot cast value to tuple: ${value}`); 158 } 159 return { 160 [duperSymbol]: "$__duper" as const, 161 type: "Tuple" as const, 162 value, 163 identifier, 164 toJSON: () => value.map((val) => (val as any).toJSON()), 165 }; 166 }, 167 /** 168 * Creates a Duper string. 169 * 170 * @param value The value. 171 * @param identifier The identifier. 172 */ 173 String: (value: string, identifier?: string) => { 174 if (typeof value !== "string") { 175 throw new Error(`Cannot cast value to string: ${value}`); 176 } 177 return { 178 [duperSymbol]: "$__duper" as const, 179 type: "String" as const, 180 value, 181 identifier, 182 toJSON: () => value, 183 }; 184 }, 185 /** 186 * Creates a Duper byte string. 187 * 188 * @param value The value. 189 * @param identifier The identifier. 190 */ 191 Bytes: (value: Uint8Array | string | number[], identifier?: string) => { 192 if (typeof value === "string") { 193 const utf8 = new Uint8Array(value.length); 194 new TextEncoder().encodeInto(value, utf8); 195 return { 196 [duperSymbol]: "$__duper" as const, 197 type: "Bytes" as const, 198 value: utf8, 199 identifier, 200 toJSON: () => { 201 const array = new Array<number>(utf8.byteLength); 202 utf8.forEach((val, i) => { 203 array[i] = val; 204 }); 205 return array; 206 }, 207 }; 208 } else if (value instanceof Uint8Array) { 209 return { 210 [duperSymbol]: "$__duper" as const, 211 type: "Bytes" as const, 212 value, 213 identifier, 214 toJSON: () => { 215 const array = new Array<number>(value.byteLength); 216 value.forEach((val, i) => { 217 array[i] = val; 218 }); 219 return array; 220 }, 221 }; 222 } else if (Array.isArray(value)) { 223 const array = new Uint8Array(value.length); 224 array.set(value); 225 return { 226 [duperSymbol]: "$__duper" as const, 227 type: "Bytes" as const, 228 value: array, 229 identifier, 230 toJSON: () => value, 231 }; 232 } else { 233 throw new Error(`Cannot cast value to bytes: ${value}`); 234 } 235 }, 236 /** 237 * Creates a Duper Temporal value. 238 * 239 * @param value The value. 240 * @param identifier The identifier. 241 */ 242 Temporal: (value: any, identifier?: string) => { 243 if ("Temporal" in globalThis) { 244 const Temporal = (globalThis as any).Temporal; 245 switch (identifier) { 246 case "Instant": { 247 const v = 248 value instanceof Temporal.Instant 249 ? value 250 : typeof value === "string" 251 ? Temporal.Instant.from(value) 252 : value.toInstant(); 253 return { 254 [duperSymbol]: "$__duper" as const, 255 type: "Temporal" as const, 256 value: v, 257 identifier, 258 toJSON: () => v.toJSON(), 259 }; 260 } 261 case "ZonedDateTime": { 262 const v = 263 value instanceof Temporal.ZonedDateTime 264 ? value 265 : typeof value === "string" 266 ? Temporal.ZonedDateTime.from(value) 267 : value.toZonedDateTimeISO(); 268 return { 269 [duperSymbol]: "$__duper" as const, 270 type: "Temporal" as const, 271 value: v, 272 identifier, 273 toJSON: () => v.toJSON(), 274 }; 275 } 276 case "PlainDate": { 277 const v = 278 value instanceof Temporal.PlainDate 279 ? value 280 : typeof value === "string" 281 ? Temporal.PlainDate.from(value) 282 : value.toPlainDate(); 283 return { 284 [duperSymbol]: "$__duper" as const, 285 type: "Temporal" as const, 286 value: v, 287 identifier, 288 toJSON: () => v.toJSON(), 289 }; 290 } 291 case "PlainTime": { 292 const v = 293 value instanceof Temporal.PlainTime 294 ? value 295 : typeof value === "string" 296 ? Temporal.PlainTime.from(value) 297 : value.toPlainTime(); 298 return { 299 [duperSymbol]: "$__duper" as const, 300 type: "Temporal" as const, 301 value: v, 302 identifier, 303 toJSON: () => v.toJSON(), 304 }; 305 } 306 case "PlainDateTime": { 307 const v = 308 value instanceof Temporal.PlainDateTime 309 ? value 310 : typeof value === "string" 311 ? Temporal.PlainDateTime.from(value) 312 : value.toPlainDateTime(); 313 return { 314 [duperSymbol]: "$__duper" as const, 315 type: "Temporal" as const, 316 value: v, 317 identifier, 318 toJSON: () => v.toJSON(), 319 }; 320 } 321 case "PlainYearMonth": { 322 const v = 323 value instanceof Temporal.PlainYearMonth 324 ? value 325 : typeof value === "string" 326 ? Temporal.PlainYearMonth.from(value) 327 : value.toPlainYearMonth(); 328 return { 329 [duperSymbol]: "$__duper" as const, 330 type: "Temporal" as const, 331 value: v, 332 identifier, 333 toJSON: () => v.toJSON(), 334 }; 335 } 336 case "PlainMonthDay": { 337 const v = 338 value instanceof Temporal.PlainMonthDay 339 ? value 340 : typeof value === "string" 341 ? Temporal.PlainMonthDay.from(value) 342 : value.toPlainMonthDay(); 343 return { 344 [duperSymbol]: "$__duper" as const, 345 type: "Temporal" as const, 346 value: v, 347 identifier, 348 toJSON: () => v.toJSON(), 349 }; 350 } 351 case "Duration": { 352 const v = 353 value instanceof Temporal.Duration 354 ? value 355 : typeof value === "string" 356 ? Temporal.Duration.from(value) 357 : null; 358 if (v === null) { 359 throw new Error(`Cannot cast value to Temporal duration: ${value}`); 360 } 361 return { 362 [duperSymbol]: "$__duper" as const, 363 type: "Temporal" as const, 364 value: v, 365 identifier, 366 toJSON: () => v.toJSON(), 367 }; 368 } 369 } 370 } 371 if (typeof value === "string") { 372 return { 373 [duperSymbol]: "$__duper" as const, 374 type: "Temporal" as const, 375 value, 376 identifier, 377 toJSON: () => value, 378 }; 379 } 380 throw new Error(`Cannot cast value to Temporal value: ${value}`); 381 }, 382 /** 383 * Creates a Duper integer. 384 * 385 * @param value The value. 386 * @param identifier The identifier. 387 */ 388 Integer: (value: bigint | number | string, identifier?: string) => { 389 const bigintValue = BigInt(value); 390 return { 391 [duperSymbol]: "$__duper" as const, 392 type: "Integer" as const, 393 value: bigintValue, 394 identifier, 395 toJSON: () => { 396 const float = Number(bigintValue); 397 return BigInt(float) === bigintValue ? float : value.toString(); 398 }, 399 }; 400 }, 401 /** 402 * Creates a Duper float. 403 * 404 * @param value The value. 405 * @param identifier The identifier. 406 */ 407 Float: (value: bigint | number, identifier?: string) => { 408 const float = Number(value); 409 if (!Number.isFinite(float)) { 410 throw new Error(`Cannot cast value to finite float: ${value}`); 411 } 412 return { 413 [duperSymbol]: "$__duper" as const, 414 type: "Float" as const, 415 value: float, 416 identifier, 417 toJSON: () => value, 418 }; 419 }, 420 /** 421 * Creates a Duper boolean. 422 * 423 * @param value The value. 424 * @param identifier The identifier. 425 */ 426 Boolean: (value: boolean, identifier?: string) => { 427 if (typeof value !== "boolean") { 428 throw new Error(`Cannot cast value to boolean: ${value}`); 429 } 430 return { 431 [duperSymbol]: "$__duper" as const, 432 type: "Boolean" as const, 433 value, 434 identifier, 435 toJSON: () => value, 436 }; 437 }, 438 /** 439 * Creates a Duper null value. 440 * 441 * @param _value The value. 442 * @param identifier The identifier. 443 */ 444 Null: (_value?: null, identifier?: string) => ({ 445 [duperSymbol]: "$__duper" as const, 446 type: "Null" as const, 447 value: null, 448 identifier, 449 toJSON: () => null, 450 }), 451 }; 452 453 function toFfi( 454 value: DuperValue | NonDuper, 455 convertingToJSON?: boolean, 456 ): duperFfi.DuperValue { 457 if ( 458 !convertingToJSON && 459 value && 460 typeof value === "object" && 461 value[duperSymbol] === "$__duper" 462 ) { 463 switch (value.type) { 464 case "Object": { 465 const array = Object.entries(value.value).map(([key, val]) => 466 duperFfi.DuperObjectEntry.new({ key, value: toFfi(val) }), 467 ); 468 return duperFfi.DuperValue.Object.new({ 469 identifier: value.identifier || undefined, 470 value: array, 471 }); 472 } 473 case "Array": { 474 const array = value.value.map((val) => toFfi(val)); 475 return duperFfi.DuperValue.Array.new({ 476 identifier: value.identifier || undefined, 477 value: array, 478 }); 479 } 480 case "Tuple": { 481 const array = value.value.map((val) => toFfi(val)); 482 return duperFfi.DuperValue.Tuple.new({ 483 identifier: value.identifier || undefined, 484 value: array, 485 }); 486 } 487 case "String": { 488 return duperFfi.DuperValue.String.new({ 489 identifier: value.identifier || undefined, 490 value: value.value, 491 }); 492 } 493 case "Bytes": { 494 return duperFfi.DuperValue.Bytes.new({ 495 identifier: value.identifier || undefined, 496 value: value.value.buffer as ArrayBuffer, 497 }); 498 } 499 case "Temporal": { 500 return duperFfi.DuperValue.Temporal.new({ 501 identifier: value.identifier || undefined, 502 value: value.value.toString(), 503 }); 504 } 505 case "Integer": { 506 return duperFfi.DuperValue.Integer.new({ 507 identifier: value.identifier || undefined, 508 value: value.value, 509 }); 510 } 511 case "Float": { 512 return duperFfi.DuperValue.Float.new({ 513 identifier: value.identifier || undefined, 514 value: value.value, 515 }); 516 } 517 case "Boolean": { 518 return duperFfi.DuperValue.Boolean.new({ 519 identifier: value.identifier || undefined, 520 value: value.value, 521 }); 522 } 523 case "Null": { 524 return duperFfi.DuperValue.Null.new({ 525 identifier: value.identifier || undefined, 526 }); 527 } 528 default: { 529 const _: never = value; 530 throw new Error(`Unknown Duper value ${value}`); 531 } 532 } 533 } else if (value === null || value === undefined) { 534 return duperFfi.DuperValue.Null.new({ identifier: undefined }); 535 } else if (typeof value === "boolean") { 536 return duperFfi.DuperValue.Boolean.new({ identifier: undefined, value }); 537 } else if (typeof value === "bigint") { 538 return duperFfi.DuperValue.Integer.new({ 539 identifier: undefined, 540 value: BigInt(value), 541 }); 542 } else if (typeof value === "number") { 543 return duperFfi.DuperValue.Float.new({ 544 identifier: undefined, 545 value, 546 }); 547 } else if (value instanceof Uint8Array) { 548 return duperFfi.DuperValue.Bytes.new({ 549 identifier: undefined, 550 value: value.buffer as ArrayBuffer, 551 }); 552 } else if (typeof value === "string") { 553 return duperFfi.DuperValue.String.new({ 554 identifier: undefined, 555 value, 556 }); 557 } else if ( 558 "Temporal" in globalThis && 559 value instanceof (globalThis as any).Temporal.Instant 560 ) { 561 return duperFfi.DuperValue.Temporal.new({ 562 identifier: "Instant", 563 value: value.toString(), 564 }); 565 } else if ( 566 "Temporal" in globalThis && 567 value instanceof (globalThis as any).Temporal.ZonedDateTime 568 ) { 569 return duperFfi.DuperValue.Temporal.new({ 570 identifier: "ZonedDateTime", 571 value: value.toString(), 572 }); 573 } else if ( 574 "Temporal" in globalThis && 575 value instanceof (globalThis as any).Temporal.PlainDate 576 ) { 577 return duperFfi.DuperValue.Temporal.new({ 578 identifier: "PlainDate", 579 value: value.toString(), 580 }); 581 } else if ( 582 "Temporal" in globalThis && 583 value instanceof (globalThis as any).Temporal.PlainTime 584 ) { 585 return duperFfi.DuperValue.Temporal.new({ 586 identifier: "PlainTime", 587 value: value.toString(), 588 }); 589 } else if ( 590 "Temporal" in globalThis && 591 value instanceof (globalThis as any).Temporal.PlainDateTime 592 ) { 593 return duperFfi.DuperValue.Temporal.new({ 594 identifier: "PlainDateTime", 595 value: value.toString(), 596 }); 597 } else if ( 598 "Temporal" in globalThis && 599 value instanceof (globalThis as any).Temporal.PlainYearMonth 600 ) { 601 return duperFfi.DuperValue.Temporal.new({ 602 identifier: "PlainYearMonth", 603 value: value.toString(), 604 }); 605 } else if ( 606 "Temporal" in globalThis && 607 value instanceof (globalThis as any).Temporal.PlainMonthDay 608 ) { 609 return duperFfi.DuperValue.Temporal.new({ 610 identifier: "PlainMonthDay", 611 value: value.toString(), 612 }); 613 } else if ( 614 "Temporal" in globalThis && 615 value instanceof (globalThis as any).Temporal.Duration 616 ) { 617 return duperFfi.DuperValue.Temporal.new({ 618 identifier: "Duration", 619 value: value.toString(), 620 }); 621 } else if (value instanceof Date) { 622 throw new Error( 623 `Invalid Date value; convert it into a Temporal value first`, 624 ); 625 } else if (Array.isArray(value)) { 626 return duperFfi.DuperValue.Array.new({ 627 identifier: undefined, 628 value: value.map((val) => toFfi(val)), 629 }); 630 } else if ( 631 !convertingToJSON && 632 value && 633 typeof value === "object" && 634 "toJSON" in value && 635 typeof value.toJSON === "function" 636 ) { 637 return toFfi(value.toJSON(), true); 638 } else if (typeof value !== "function" && typeof value !== "symbol") { 639 const array = Object.entries(value).map(([key, val]) => 640 duperFfi.DuperObjectEntry.new({ key, value: toFfi(val as any) }), 641 ); 642 return duperFfi.DuperValue.Object.new({ 643 identifier: undefined, 644 value: array, 645 }); 646 } 647 throw new Error(`Unknown value ${String(value)}`); 648 } 649 650 function fromFfi(value: duperFfi.DuperValue): DuperValue { 651 switch (value.tag) { 652 case duperFfi.DuperValue_Tags.Object: { 653 const obj = Object.fromEntries( 654 value.inner.value.map((entry) => [entry.key, fromFfi(entry.value)]), 655 ); 656 return DuperValue.Object(obj, value.inner.identifier); 657 } 658 case duperFfi.DuperValue_Tags.Array: { 659 return DuperValue.Array( 660 value.inner.value.map((val) => fromFfi(val)), 661 value.inner.identifier, 662 ); 663 } 664 case duperFfi.DuperValue_Tags.Tuple: { 665 return DuperValue.Tuple( 666 value.inner.value.map((val) => fromFfi(val)), 667 value.inner.identifier, 668 ); 669 } 670 case duperFfi.DuperValue_Tags.String: { 671 return DuperValue.String(value.inner.value, value.inner.identifier); 672 } 673 case duperFfi.DuperValue_Tags.Bytes: { 674 return DuperValue.Bytes( 675 new Uint8Array(value.inner.value), 676 value.inner.identifier, 677 ); 678 } 679 case duperFfi.DuperValue_Tags.Temporal: { 680 return DuperValue.Temporal(value.inner.value, value.inner.identifier); 681 } 682 case duperFfi.DuperValue_Tags.Integer: { 683 return DuperValue.Integer(value.inner.value, value.inner.identifier); 684 } 685 case duperFfi.DuperValue_Tags.Float: { 686 return DuperValue.Float(value.inner.value, value.inner.identifier); 687 } 688 case duperFfi.DuperValue_Tags.Boolean: { 689 return DuperValue.Boolean(value.inner.value, value.inner.identifier); 690 } 691 case duperFfi.DuperValue_Tags.Null: { 692 return DuperValue.Null(null, value.inner.identifier); 693 } 694 default: { 695 const _: never = value; 696 throw new Error(`Unknown Duper value ${value}`); 697 } 698 } 699 } 700 701 /** 702 * Options available to the `stringify` function. 703 * 704 * @property {string | number} [indent] - Optional whitespace string to use as 705 * indentation, or the number of spaces to use as indentation. 706 * @property {boolean} [stripIdentifiers] - Whether Duper identifiers should be 707 * removed from the stringified value. 708 * @property {boolean} [minify] - Whether stringify should minify the value. Not 709 * compatible with `indent`. 710 */ 711 type StringifyOptions = 712 | { 713 indent?: string | number; 714 stripIdentifiers?: boolean; 715 minify?: false; 716 } 717 | { 718 indent?: never; 719 stripIdentifiers?: boolean; 720 minify: true; 721 }; 722 723 /** 724 * Converts the provided value into a Duper string. 725 * 726 * @param value The value to stringify. 727 * @param options Options for stringification. 728 * @returns The Duper string. 729 */ 730 export function stringify(value: any, options?: StringifyOptions): string { 731 return duperFfi.serialize( 732 toFfi(value), 733 options && { 734 indent: 735 typeof options.indent === "number" 736 ? " ".repeat(options.indent) 737 : options.indent, 738 stripIdentifiers: options.stripIdentifiers ?? false, 739 minify: options.minify ?? false, 740 }, 741 ); 742 } 743 744 /** 745 * Parses the provided Duper string into a Duper value, or a JSON-safe alternative if specified. 746 * 747 * @param value The Duper string to parse. 748 * @param jsonSafe Whether to emit a JSON-safe alternative instead of a `DuperValue`. 749 * @returns The parsed value. 750 */ 751 export function parse(value: string, jsonSafe?: false): DuperValue; 752 export function parse(value: string, jsonSafe: true): any; 753 export function parse(value: string, jsonSafe?: boolean): DuperValue | any { 754 const parsed = duperFfi.parse(value, true); 755 const transformed = fromFfi(parsed); 756 if (jsonSafe) { 757 return transformed.toJSON(); 758 } 759 return transformed; 760 }