diff --git a/README.md b/README.md index be64a194..f0da24d6 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,12 @@ import { } from "tupleson"; const tson = createTson({ - // This nonce function is used to generate a nonce for the serialized value - // This is used to identify the value as a serialized value - nonce: () => "__tson", + /** + * The nonce function every time we start serializing a new object + * Should return a unique value every time it's called + * @default `${crypto.randomUUID} if available, otherwise a random string generated by Math.random` + */ + // nonce: () => "__tson", types: [ // Pick which types you want to support tsonSet, diff --git a/src/extend/decimal.test.ts b/src/extend/decimal.test.ts index c0c34eb2..52cb96bd 100644 --- a/src/extend/decimal.test.ts +++ b/src/extend/decimal.test.ts @@ -11,6 +11,7 @@ const decimalJs: TsonType = { }; const tson = createTson({ + nonce: () => "__tson", types: [decimalJs], }); diff --git a/src/handlers/tsonRegExp.test.ts b/src/handlers/tsonRegExp.test.ts index 00d5190a..8f2e55b2 100644 --- a/src/handlers/tsonRegExp.test.ts +++ b/src/handlers/tsonRegExp.test.ts @@ -5,6 +5,7 @@ import { tsonRegExp } from "./index.js"; test("regex", () => { const t = createTson({ + nonce: () => "__tson", types: [tsonRegExp], }); diff --git a/src/handlers/tsonURL.test.ts b/src/handlers/tsonURL.test.ts index b50a54b3..1ca3b08b 100644 --- a/src/handlers/tsonURL.test.ts +++ b/src/handlers/tsonURL.test.ts @@ -5,6 +5,7 @@ import { tsonURL } from "./index.js"; test("URL", () => { const ctx = createTson({ + nonce: () => "__tson", types: [tsonURL], }); diff --git a/src/internals/getNonce.crypto.test.ts b/src/internals/getNonce.crypto.test.ts new file mode 100644 index 00000000..d003a465 --- /dev/null +++ b/src/internals/getNonce.crypto.test.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect, test } from "vitest"; + +test("with crypto", async () => { + const before = global.crypto; + + global.crypto = { + randomUUID: () => "test", + } as any; + const { getNonce } = await import("./getNonce.js"); + + expect(getNonce()).toBe("test"); + + global.crypto = before; +}); diff --git a/src/internals/getNonce.nocrypto.test.ts b/src/internals/getNonce.nocrypto.test.ts new file mode 100644 index 00000000..6a2c68ef --- /dev/null +++ b/src/internals/getNonce.nocrypto.test.ts @@ -0,0 +1,15 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect, test } from "vitest"; + +test("without crypto", async () => { + const before = global.crypto; + + global.crypto = undefined as any; + + const { getNonce } = await import("./getNonce.js"); + + expect(getNonce().length).toBeGreaterThan(20); + + global.crypto = before; +}); diff --git a/src/internals/getNonce.ts b/src/internals/getNonce.ts new file mode 100644 index 00000000..abe97f60 --- /dev/null +++ b/src/internals/getNonce.ts @@ -0,0 +1,11 @@ +import { TsonNonce } from "../types.js"; + +const randomString = () => Math.random().toString(36).slice(2); + +export type GetNonce = () => TsonNonce; + +export const getNonce: GetNonce = + typeof crypto === "object" && typeof crypto.randomUUID === "function" + ? () => crypto.randomUUID() as TsonNonce + : () => + [randomString(), randomString(), randomString()].join("") as TsonNonce; diff --git a/src/serialize.ts b/src/serialize.ts index 3684fe85..81cdc913 100644 --- a/src/serialize.ts +++ b/src/serialize.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any, eslint-comments/disable-enable-pair */ import { CircularReferenceError } from "./errors.js"; +import { GetNonce, getNonce } from "./internals/getNonce.js"; import { mapOrReturn } from "./internals/mapOrReturn.js"; import { TsonAllTypes, @@ -69,7 +70,6 @@ export function createTsonSerialize(opts: TsonOptions): TsonSerializeFn { return [nonPrimitive, byPrimitive] as const; })(); - const maybeNonce = opts.nonce; const [nonPrimitive, byPrimitive] = handlers; @@ -122,11 +122,10 @@ export function createTsonSerialize(opts: TsonOptions): TsonSerializeFn { return walk; }; + const nonceFn: GetNonce = opts.nonce ? (opts.nonce as GetNonce) : getNonce; + return ((obj): TsonSerialized => { - const nonce: TsonNonce = - typeof maybeNonce === "function" - ? (maybeNonce() as TsonNonce) - : ("__tson" as TsonNonce); + const nonce = nonceFn(); const json = walker(nonce)(obj); diff --git a/src/stringify.test.ts b/src/stringify.test.ts index 438ca0c3..3735ac27 100644 --- a/src/stringify.test.ts +++ b/src/stringify.test.ts @@ -8,6 +8,7 @@ import { tsonUndefined } from "./handlers/tsonUndefined.js"; test("lets have a look at the stringified output", () => { const t = createTson({ + nonce: () => "__tson", types: [tsonMap, tsonSet, tsonBigint, tsonUndefined], }); diff --git a/src/types.ts b/src/types.ts index 99584f45..a6d04749 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,7 +95,15 @@ export type TsonType< > = TsonTypeTester & TsonTransformer; export interface TsonOptions { + /** + * The nonce function every time we start serializing a new object + * Should return a unique value every time it's called + * @default `${crypto.randomUUID} if available, otherwise a random string generated by Math.random` + */ nonce?: () => number | string; + /** + * The list of types to use + */ types: (TsonType | TsonType)[]; }