/ duper-js-wasm / src / index.ts
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  }