import { describe, expect, it } from "vitest"; import canonicalize from "canonicalize"; import { bufferedJsonParts, canonicalJsonParts, jsonParts, } from "./jsonSerialization.js"; /** Compact and pretty streamed JSON must equal native JSON. */ const expectNativeJson = (value: unknown): void => { expect([...bufferedJsonParts(jsonParts(value))].join("")).toBe( JSON.stringify(value), ); expect([...jsonParts(value, true)].join("")).toBe( JSON.stringify(value, null, 2), ); }; describe("streamed JSON representation", () => { it.each([ null, true, -0, [1, false, null, 'quotes: " / slash: \\ / newline: \n'], { nested: { value: "δΈ­ζ–‡ πŸ˜€ \ud800" }, items: [1, 2] }, JSON.parse('{"__proto__":{"value":1},"constructor":2,"10":3,"2":4}'), ])("matches native JSON for ordinary values: %j", expectNativeJson); it("preserves surrogate pairs across string and buffer boundaries", () => { const value = { text: `${"x".repeat(8190)}πŸ˜€${'δΈ­ζ–‡\n"'.repeat(20000)}` }; const parts = [...bufferedJsonParts(jsonParts(value))]; expect( Buffer.concat(parts.map((part) => Buffer.from(part))).toString(), ).toBe(JSON.stringify(value)); expect(JSON.parse(parts.join(""))).toEqual(value); }); it("matches existing canonical bytes for numeric keys, Unicode, and shared values", () => { const shared = { z: -0, a: [1e-7, 1e30, "ι›ͺπŸ˜€\ud800"] }; const ownKeys: unknown = JSON.parse( '{"__proto__":1,"constructor":2,"10":3,"2":4}', ); const value = { z: shared, a: shared, keys: ownKeys, long: 'ι›ͺπŸ˜€"'.repeat(20000), }; expect([...bufferedJsonParts(canonicalJsonParts(value))].join("")).toBe( canonicalize(value), ); }); }); describe("iterative JSON encoding", () => { it("matches native JSON for long keys, omitted members, and deep nesting", () => { let deep: unknown = { leaf: [1, "two", null] }; for (let level = 0; level < 500; level += 1) deep = level % 2 === 0 ? [deep, level] : { [`k${String(level)}`]: deep }; const value = { [`${"k".repeat(9000)}πŸ˜€`]: { nested: ["x".repeat(20000), {}, []] }, omitted: undefined, method: () => 1, symbol: Symbol("ignored"), deep, }; expectNativeJson(value); }); it("rejects values native JSON would not encode as ordinary JSON", () => { expect(() => [...jsonParts([1, undefined])]).toThrow( "ordinary JSON values", ); expect(() => [...jsonParts({ value: Number.NaN })]).toThrow( "ordinary JSON values", ); expect(() => [...jsonParts({ value: new Date(0) })]).toThrow( "ordinary JSON objects", ); }); it("yields bounded parts before reading later members", () => { const value: Record = { first: "x".repeat(100_000) }; Object.defineProperty(value, "later", { enumerable: true, get() { throw new Error("later members must not be read yet"); }, }); const parts = jsonParts(value); const first = parts.next(); expect(first.done).toBe(false); expect(first.value?.length).toBeLessThanOrEqual(64 * 1024); // Many small members are grouped into parts instead of one per token. const small = Object.fromEntries( Array.from({ length: 10_000 }, (_, index) => [ `k${String(index)}`, index, ]), ); const smallParts = [...jsonParts(small)]; expect(smallParts.join("")).toBe(JSON.stringify(small)); expect(smallParts.length).toBeLessThan(20); }); });