diff --git a/src/index.test.ts b/src/index.test.ts index 1d9c5fe2..9f73a4fe 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -7,6 +7,7 @@ import { createTsonAsync, tsonDate, tsonMap, + tsonSet, } from "./index.js"; import { waitError } from "./internals/testUtils.js"; @@ -234,6 +235,115 @@ test("back-reference: self-referencing Map", () => { const res = t.parse(str); expect(res).toEqual(expected); + expect(res.get("a")).toBe(res); +}); + +test("back-reference: self-referencing Map deep", () => { + const t = createTson({ + nonce: () => "__tson__", + types: [tsonMap], + }); + + const expected = new Map(); + expected.set("a", { + foo: expected, + }); + + const str = t.stringify(expected, 2); + + expect(str).toMatchInlineSnapshot(` + "{ + \\"json\\": [ + \\"Map\\", + [ + [ + \\"a\\", + { + \\"foo\\": [ + \\"CIRCULAR\\", + \\"\\", + \\"__tson__\\" + ] + } + ] + ], + \\"__tson__\\" + ], + \\"nonce\\": \\"__tson__\\" + }" + `); + const res = t.parse(str); + + expect(res).toEqual(expected); + expect(res.get("a").foo).toBe(res); +}); + +test("back-reference: self-referencing Set", () => { + const t = createTson({ + nonce: () => "__tson__", + types: [tsonSet], + }); + + const expected = new Set(); + expected.add(expected); + + const str = t.stringify(expected, 2); + + expect(str).toMatchInlineSnapshot(` + "{ + \\"json\\": [ + \\"Set\\", + [ + [ + \\"CIRCULAR\\", + \\"\\", + \\"__tson__\\" + ] + ], + \\"__tson__\\" + ], + \\"nonce\\": \\"__tson__\\" + }" + `); + const res = t.parse(str); + + expect(res).toEqual(expected); + expect(res.has(res)).toBe(true); +}); + +test("back-reference: self-referencing Set deep", () => { + const t = createTson({ + nonce: () => "__tson__", + types: [tsonSet], + }); + + const expected = new Set(); + expected.add({ foo: expected }); + + const str = t.stringify(expected, 2); + + expect(str).toMatchInlineSnapshot(` + "{ + \\"json\\": [ + \\"Set\\", + [ + { + \\"foo\\": [ + \\"CIRCULAR\\", + \\"\\", + \\"__tson__\\" + ] + } + ], + \\"__tson__\\" + ], + \\"nonce\\": \\"__tson__\\" + }" + `); + const res = t.parse(str); + + expect(res).toEqual(expected); + expect(res.values().next().value.foo).toBe(res); }); /** * WILL NOT WORK: the async serialize/deserialize functions haven't diff --git a/src/sync/deserialize.ts b/src/sync/deserialize.ts index 1c89ed35..d96d85b2 100644 --- a/src/sync/deserialize.ts +++ b/src/sync/deserialize.ts @@ -39,7 +39,7 @@ export function createTsonDeserialize(opts: TsonOptions): TsonDeserializeFn { const [type, serializedValue] = value; if (type === "CIRCULAR") { references.push([key, serializedValue as string]); - return; + return nonce; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -75,15 +75,52 @@ export function createTsonDeserialize(opts: TsonOptions): TsonDeserializeFn { let insertAt = res; try { while (path.length > 1) { - //@ts-expect-error -- insertAt is unknown and not checked, but if it passed serialization, it should be an object - insertAt = insertAt[path.shift()]; + if (insertAt instanceof Map) { + if (path.length <= 2) { + break; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion -- if it passed serialization, path will have an index at this point + const key = Array.from(insertAt.keys())[Number(path.shift()!)]; + insertAt = insertAt.get(key); + path.shift(); + } else if (insertAt instanceof Set) { + //@ts-expect-error -- if it passed serialization, path will have an index at this point + insertAt = Array.from(insertAt)[path.shift()]; + } else { + //@ts-expect-error -- insertAt is unknown and not checked, but if it passed serialization, it should be an object + insertAt = insertAt[path.shift()]; + } } - //@ts-expect-error -- see above, + if it passed serialization, path should be length 1 at this point - insertAt[path[0]] = prev; + if (insertAt instanceof Map) { + if (path.length !== 2) { + throw new Error( + `Invalid path to Map insertion ${copyKey + .split(nonce) + .join(".")}`, + ); + } + + const mapKeys = Array.from(insertAt.keys()); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion -- if it passed serialization, path will have an index at this point + const key = mapKeys[Number(path[0]!)]; + insertAt.set(key, prev); + } else if (insertAt instanceof Set) { + /** + * WARNING: this doesn't preserve order in the Set + */ + insertAt.delete(nonce); + insertAt.add(prev); + } else { + //@ts-expect-error -- see above, + if it passed serialization, path should be length 1 at this point + insertAt[path[0]] = prev; + } } catch (cause) { throw new Error( - `Invalid path to reference insertion ${copyKey.split(nonce).join(".")}`, + `Invalid path to reference insertion ${copyKey + .split(nonce) + .join(".")}`, { cause }, ); }