diff --git a/src/async/asyncTypes2.ts b/src/async/asyncTypes2.ts index 421485e..032e30c 100644 --- a/src/async/asyncTypes2.ts +++ b/src/async/asyncTypes2.ts @@ -68,7 +68,7 @@ export const ChunkTypes = { ERROR: "ERROR", HEAD: "HEAD", LEAF: "LEAF", - REFERENCE: "REFERENCE", + REF: "REF", TAIL: "TAIL", } as const; @@ -114,7 +114,7 @@ export type TsonAsyncHeadTuple = [ ]; export type TsonAsyncReferenceTuple = [ - ChunkType: ChunkTypes["REFERENCE"], + ChunkType: ChunkTypes["REF"], Header: TsonAsyncTupleHeader, OriginalNodeId: `${TsonNonce}${number}`, ]; diff --git a/src/async/handlers/tsonPromise2.test.ts b/src/async/handlers/tsonPromise2.test.ts index 731b299..bf59f09 100644 --- a/src/async/handlers/tsonPromise2.test.ts +++ b/src/async/handlers/tsonPromise2.test.ts @@ -1,11 +1,21 @@ import { expect, test } from "vitest"; import { TsonType } from "../../index.js"; -import { createPromise } from "../../internals/testUtils.js"; -import { ChunkTypes, TsonStatus } from "../asyncTypes2.js"; +import { createPromise, expectSequence } from "../../internals/testUtils.js"; +import { ChunkTypes, TsonAsyncTuple, TsonStatus } from "../asyncTypes2.js"; import { createTsonSerializeAsync } from "../serializeAsync2.js"; import { tsonPromise } from "./tsonPromise2.js"; +const nonce = "__tson"; +const anyId = expect.stringMatching(`^${nonce}[0-9]+$`); +const idOf = (id: TsonAsyncTuple | number | string) => { + if (Array.isArray(id)) { + return id[1][0]; + } + + return `${nonce}${id}`; +}; + const tsonError: TsonType = { deserialize: (v) => { const err = new Error(v.message); @@ -19,8 +29,6 @@ const tsonError: TsonType = { }; test("serialize promise", async () => { - const nonce = "__tson"; - const serialize = createTsonSerializeAsync({ nonce: () => nonce, types: [tsonPromise], @@ -29,73 +37,102 @@ test("serialize promise", async () => { const promise = Promise.resolve(42); const iterator = serialize(promise); - const values = []; + const chunks: TsonAsyncTuple[] = []; for await (const value of iterator) { - values.push(value); + chunks.push(value); } - const promiseId = `${nonce}0`; - const arrayId = `${nonce}1`; + const heads = chunks.filter((chunk) => chunk[0] === ChunkTypes.HEAD); + const tails = chunks.filter((chunk) => chunk[0] === ChunkTypes.TAIL); + const leaves = chunks.filter((chunk) => chunk[0] === ChunkTypes.LEAF); - expect(values).toEqual([ - [ChunkTypes.HEAD, [promiseId, nonce, null], tsonPromise.key], - [ChunkTypes.HEAD, [arrayId, promiseId, null]], - [ChunkTypes.LEAF, [`${nonce}2`, arrayId, 0], 0], - [ChunkTypes.TAIL, [`${nonce}3`, promiseId, null], TsonStatus.OK], - [ChunkTypes.LEAF, [`${nonce}4`, arrayId, 1], 42], - [ChunkTypes.TAIL, [`${nonce}5`, arrayId, null], TsonStatus.OK], - ]); + expect(chunks.length).toBe(6); + expect(heads).toHaveLength(2); + expect(tails).toHaveLength(2); + expect(leaves).toHaveLength(2); + + expectSequence(chunks) + .toHaveAll(heads) + .beforeAll([...leaves, ...tails]); + + heads.forEach((_, i) => { + expectSequence(chunks).toHave(heads[i]!).beforeAll([tails[i]!, leaves[i]!]); + expectSequence(chunks).toHave(tails[i]!).afterAll([heads[i]!, leaves[i]!]); + }); }); test("serialize promise that returns a promise", async () => { - const nonce = "__tson"; const serialize = createTsonSerializeAsync({ nonce: () => nonce, types: [tsonPromise], }); + const expected = 42; + const obj = { promise: createPromise(() => { return { anotherPromise: createPromise(() => { - return 42; + return expected; }), }; }), }; const iterator = serialize(obj); - const values = []; + const chunks: TsonAsyncTuple[] = []; for await (const value of iterator) { - values.push(value); + chunks.push(value); } - expect(values).toEqual([ - /* - TODO: The parent IDs are wrong here. They're not correct in the implementation, - TODO: either, and I don't know what they should be yet. - */ - [ChunkTypes.HEAD, [`${nonce}0`, `${nonce}`, null]], - [ChunkTypes.HEAD, [`${nonce}1`, `${nonce}0`, "promise"], "Promise"], - [ChunkTypes.TAIL, [`${nonce}2`, `${nonce}0`, null], 200], - [ChunkTypes.HEAD, [`${nonce}3`, `${nonce}1`, null]], - [ChunkTypes.TAIL, [`${nonce}4`, `${nonce}1`, null], 200], - [ChunkTypes.LEAF, [`${nonce}5`, `${nonce}3`, 0], 0], - [ChunkTypes.HEAD, [`${nonce}6`, `${nonce}3`, 1]], - [ChunkTypes.HEAD, [`${nonce}7`, `${nonce}6`, "anotherPromise"], "Promise"], - [ChunkTypes.TAIL, [`${nonce}8`, `${nonce}6`, null], 200], - [ChunkTypes.TAIL, [`${nonce}9`, `${nonce}7`, null], 200], - [ChunkTypes.HEAD, [`${nonce}10`, `${nonce}6`, null]], - [ChunkTypes.TAIL, [`${nonce}11`, `${nonce}9`, null], 200], - [ChunkTypes.LEAF, [`${nonce}12`, `${nonce}12`, 0], 0], - [ChunkTypes.LEAF, [`${nonce}13`, `${nonce}11`, 1], 42], - [ChunkTypes.TAIL, [`${nonce}14`, `${nonce}11`, null], 200], + const heads = chunks.filter((chunk) => chunk[0] === ChunkTypes.HEAD); + const tails = chunks.filter((chunk) => chunk[0] === ChunkTypes.TAIL); + const leaves = chunks.filter((chunk) => chunk[0] === ChunkTypes.LEAF); + + expect(chunks).toHaveLength(15); + expect(heads).toHaveLength(6); + expect(leaves).toHaveLength(3); + expect(tails).toHaveLength(6); + + heads.forEach((_, i) => { + expect(tails.filter((v) => v[1][1] === heads[i]![1][0])).toHaveLength(1); + expectSequence(chunks) + .toHave(heads[i]!) + .beforeAll( + [...tails, ...leaves].filter((v) => v[1][1] === heads[i]![1][0]), + ); + expectSequence(chunks) + .toHave(tails[i]!) + .afterAll( + [...heads, ...leaves].filter((v) => v[1][1] === tails[i]![1][0]), + ); + }); + + expect(heads[0]![1][0]).toBe(idOf(0)); + + expect(heads).toHaveLength(6); + expect(leaves).toHaveLength(3); + expect(tails).toHaveLength(6); + + expect(heads[0]).toStrictEqual([ChunkTypes.HEAD, [idOf(0), nonce, null]]); + expect(heads[1]).toStrictEqual([ + ChunkTypes.HEAD, + [anyId, idOf(0), "promise"], + tsonPromise.key, + ]); + + expect(heads[2]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, null]]); + expect(heads[3]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, 1]]); + expect(heads[4]).toStrictEqual([ + ChunkTypes.HEAD, + [anyId, anyId, "anotherPromise"], + tsonPromise.key, ]); + expect(heads[5]).toStrictEqual([ChunkTypes.HEAD, [anyId, anyId, null]]); }); test("promise that rejects", async () => { - const nonce = "__tson"; const serialize = createTsonSerializeAsync({ nonce: () => nonce, types: [tsonPromise, tsonError], @@ -104,19 +141,104 @@ test("promise that rejects", async () => { const promise = Promise.reject(new Error("foo")); const iterator = serialize(promise); - const values = []; + const chunks: TsonAsyncTuple[] = []; const expected = { message: "foo" }; for await (const value of iterator) { - values.push(value); + chunks.push(value); } - expect(values).toEqual([ - [ChunkTypes.HEAD, [`${nonce}0`, `${nonce}`, null], "Promise"], - [ChunkTypes.HEAD, [`${nonce}1`, `${nonce}0`, null]], - [ChunkTypes.LEAF, [`${nonce}2`, `${nonce}1`, 0], 1], - [ChunkTypes.TAIL, [`${nonce}3`, `${nonce}0`, null], 200], - [ChunkTypes.LEAF, [`${nonce}5`, `${nonce}1`, 1], expected, "Error"], - [ChunkTypes.TAIL, [`${nonce}6`, `${nonce}1`, null], 200], + expect(chunks.length).toBe(6); + + const heads = chunks.filter((chunk) => chunk[0] === ChunkTypes.HEAD); + const tails = chunks.filter((chunk) => chunk[0] === ChunkTypes.TAIL); + const leaves = chunks.filter((chunk) => chunk[0] === ChunkTypes.LEAF); + + expectSequence(chunks) + .toHaveAll(heads) + .beforeAll([...leaves, ...tails]); + + heads.forEach((_, i) => { + expect(tails.filter((v) => v[1][1] === heads[i]![1][0])).toHaveLength(1); + expectSequence(chunks) + .toHave(heads[i]!) + .beforeAll( + [...tails, ...leaves].filter((v) => v[1][1] === heads[i]![1][0]), + ); + expectSequence(chunks) + .toHave(tails[i]!) + .afterAll( + [...heads, ...leaves].filter((v) => v[1][1] === tails[i]![1][0]), + ); + }); + + expect(heads[0]![1][0]).toBe(idOf(0)); + + expect(heads).toHaveLength(2); + expect(tails).toHaveLength(2); + expect(leaves).toHaveLength(2); + + expect(heads[0]).toStrictEqual([ + ChunkTypes.HEAD, + [idOf(0), nonce, null], + tsonPromise.key, ]); + + expect(heads[1]).toEqual([ChunkTypes.HEAD, [anyId, idOf(0), null]]); + expect(leaves[0]).toEqual([ChunkTypes.LEAF, [anyId, anyId, 0], 1]); + expect(leaves[1]).toEqual([ + ChunkTypes.LEAF, + [anyId, anyId, 1], + expected, + tsonError.key, + ]); +}); + +test("racing promises", async () => { + const serialize = createTsonSerializeAsync({ + nonce: () => nonce, + types: [tsonPromise], + }); + + const iterator = serialize({ + promise: createPromise(() => { + return { + promise1: createPromise(() => { + return 42; + }, Math.random() * 100), + promise2: createPromise(() => { + return 43; + }, Math.random() * 100), + }; + }), + }); + + const chunks: TsonAsyncTuple[] = []; + + for await (const value of iterator) { + chunks.push(value); + } + + const heads = chunks.filter((chunk) => chunk[0] === ChunkTypes.HEAD); + const leaves = chunks.filter((chunk) => chunk[0] === ChunkTypes.LEAF); + const tails = chunks.filter((chunk) => chunk[0] === ChunkTypes.TAIL); + + expect(chunks).toHaveLength(21); + expect(heads).toHaveLength(8); + expect(leaves).toHaveLength(5); + expect(tails).toHaveLength(8); + + heads.forEach((_, i) => { + expect(tails.filter((v) => v[1][1] === heads[i]![1][0])).toHaveLength(1); + expectSequence(chunks) + .toHave(heads[i]!) + .beforeAll( + [...tails, ...leaves].filter((v) => v[1][1] === heads[i]![1][0]), + ); + expectSequence(chunks) + .toHave(tails[i]!) + .afterAll( + [...heads, ...leaves].filter((v) => v[1][1] === tails[i]![1][0]), + ); + }); }); diff --git a/src/async/serializeAsync2.test.ts b/src/async/serializeAsync2.test.ts index d3f13b5..f57d3de 100644 --- a/src/async/serializeAsync2.test.ts +++ b/src/async/serializeAsync2.test.ts @@ -1,15 +1,20 @@ -import { assertType, describe, test } from "vitest"; +import { assertType, describe, expect, test } from "vitest"; import { tsonBigint } from "../index.js"; +import { expectSequence } from "../internals/testUtils.js"; import { ChunkTypes, TsonAsyncTuple, TsonStatus } from "./asyncTypes2.js"; import { tsonPromise } from "./handlers/tsonPromise2.js"; import { createTsonSerializeAsync } from "./serializeAsync2.js"; +const nonce = "__tson"; +const anyId = expect.stringMatching(`^${nonce}[0-9]+$`); +const idOf = (id: number | string) => `${nonce}${id}`; + describe("serialize", (it) => { it("should handle primitives correctly", async ({ expect }) => { const options = { guards: [], - nonce: () => "__tsonNonce", + nonce: () => nonce, types: [ // Primitive handler mock { @@ -30,48 +35,136 @@ describe("serialize", (it) => { } expect(chunks.length).toBe(1); - expect(chunks[0]).toEqual([ - ChunkTypes.LEAF, - ["__tsonNonce0", "__tsonNonce", null], - "HELLO", - "string", + expect(chunks).toEqual([ + [ChunkTypes.LEAF, [idOf(0), nonce, null], "HELLO", "string"], ]); }); it("should handle circular references", async ({ expect }) => { - const options = { guards: [], nonce: () => "__tsonNonce", types: [] }; + const options = { guards: [], nonce: () => nonce, types: [] }; const serialize = createTsonSerializeAsync(options); const object: any = {}; - object.self = object; // Create a circular reference const chunks = []; + const rootId = idOf(0); + + // Create a circular reference + object.self = object; for await (const chunk of serialize(object)) { chunks.push(chunk); } - //console.log(chunks); - expect(chunks.length).toBe(3); - expect(chunks[0]).toEqual([ - ChunkTypes.HEAD, - ["__tsonNonce0", "__tsonNonce", null], + expect(chunks).toEqual([ + [ChunkTypes.HEAD, [rootId, nonce, null]], + [ChunkTypes.REF, [anyId, rootId, "self"], rootId], + [ChunkTypes.TAIL, [anyId, rootId, null], TsonStatus.OK], ]); + }); - expect - .soft(chunks[1]) - .toEqual([ - ChunkTypes.REFERENCE, - ["__tsonNonce1", "__tsonNonce0", "self"], - "__tsonNonce0", - ]); + test.each([ + ["number", 0], + ["string", "hello"], + ["boolean", true], + ["null", null], + ])( + `should serialize %s primitives without a handler`, + async (type, value) => { + const options = { guards: [], nonce: () => nonce, types: [] }; + const serialize = createTsonSerializeAsync(options); + const chunks: TsonAsyncTuple[] = []; + for await (const chunk of serialize(value)) { + chunks.push(chunk); + } - expect - .soft(chunks[2]) - .toEqual([ - ChunkTypes.TAIL, - ["__tsonNonce2", "__tsonNonce0", null], - TsonStatus.OK, + expect(chunks.length).toBe(1); + expect(chunks).toEqual([ + [ChunkTypes.LEAF, [idOf(0), nonce, null], value], ]); + }, + ); + + it("should serialize values with a sync handler", async ({ expect }) => { + const options = { + guards: [], + nonce: () => nonce, + types: [tsonBigint], + }; + + const serialize = createTsonSerializeAsync(options); + const source = 0n; + const chunks = []; + + for await (const chunk of serialize(source)) { + chunks.push(chunk); + } + + assertType(chunks); + expect(chunks.length).toBe(1); + expect(chunks).toEqual([ + [ChunkTypes.LEAF, [idOf(0), nonce, null], "0", "bigint"], + ]); + }); +}); + +describe("serializeAsync", (it) => { + it("should serialize values with an async handler", async ({ expect }) => { + const options = { + guards: [], + nonce: () => nonce, + types: [tsonPromise], + }; + + const serialize = createTsonSerializeAsync(options); + const source = Promise.resolve("hello"); + const chunks: TsonAsyncTuple[] = []; + + for await (const chunk of serialize(source)) { + chunks.push(chunk); + } + + const heads = chunks.filter((chunk) => chunk[0] === ChunkTypes.HEAD); + const tails = chunks.filter((chunk) => chunk[0] === ChunkTypes.TAIL); + const leaves = chunks.filter((chunk) => chunk[0] === ChunkTypes.LEAF); + + const head_1_id = heads[0]![1][0]; + const head_2_id = heads[1]![1][0]; + + expect(chunks.length).toBe(6); + + expectSequence(chunks) + .toHaveAll(heads) + .beforeAll([...leaves, ...tails]); + + heads.forEach((_, i) => { + expectSequence(chunks) + .toHave(heads[i]!) + .beforeAll([tails[i]!, leaves[i]!]); + expectSequence(chunks) + .toHave(tails[i]!) + .afterAll([heads[i]!, leaves[i]!]); + }); + + expect(head_1_id).toBe(idOf(0)); + + expect(heads).toHaveLength(2); + expect(tails).toHaveLength(2); + expect(leaves).toHaveLength(2); + + expect(heads).toStrictEqual([ + [ChunkTypes.HEAD, [head_1_id, nonce, null], tsonPromise.key], + [ChunkTypes.HEAD, [head_2_id, head_1_id, null]], + ]); + + expect(leaves).toStrictEqual([ + [ChunkTypes.LEAF, [anyId, head_2_id, 0], 0], + [ChunkTypes.LEAF, [anyId, head_2_id, 1], "hello"], + ]); + + expect(tails).toStrictEqual([ + [ChunkTypes.TAIL, [anyId, head_1_id, null], TsonStatus.OK], + [ChunkTypes.TAIL, [anyId, head_2_id, null], TsonStatus.OK], + ]); }); it("should apply guards and throw if they fail", async ({ expect }) => { @@ -224,110 +317,4 @@ describe("serialize", (it) => { expect(error).toBeInstanceOf(Error); expect(error).toHaveProperty("message", "testGuard error"); }); - - it("should serialize JSON-serializable values without a handler", async ({ - expect, - }) => { - const options = { guards: [], nonce: () => "__tsonNonce", types: [] }; - const serialize = createTsonSerializeAsync(options); - - const source = 1; - const chunks: TsonAsyncTuple[] = []; - - for await (const chunk of serialize(source)) { - chunks.push(chunk); - } - - const source2 = "hello"; - const chunks2: TsonAsyncTuple[] = []; - - for await (const chunk of serialize(source2)) { - chunks2.push(chunk); - } - - test.each([ - [source, chunks], - [source2, chunks2], - ])(`chunks`, (original, result) => { - expect(result.length).toBe(1); - expect(result[0]).toEqual([ - ChunkTypes.LEAF, - ["__tsonNonce1", "__tsonNonce", null], - JSON.stringify(original), - ]); - }); - }); - - it("should serialize values with a sync handler", async ({ expect }) => { - const options = { - guards: [], - nonce: () => "__tsonNonce", - types: [tsonBigint], - }; - - const serialize = createTsonSerializeAsync(options); - const source = 0n; - const chunks = []; - - for await (const chunk of serialize(source)) { - chunks.push(chunk); - } - - assertType(chunks); - expect(chunks.length).toBe(1); - expect(chunks[0]).toEqual([ - ChunkTypes.LEAF, - ["__tsonNonce0", "__tsonNonce", null], - "0", - "bigint", - ]); - }); - - it("should serialize values with an async handler", async ({ expect }) => { - const options = { - guards: [], - nonce: () => "__tsonNonce", - types: [tsonPromise], - }; - const serialize = createTsonSerializeAsync(options); - const source = Promise.resolve("hello"); - const chunks = []; - - for await (const chunk of serialize(source)) { - chunks.push(chunk); - } - - //console.log(chunks); - expect(chunks.length).toBe(6); - expect - .soft(chunks[0]) - .toEqual([ - ChunkTypes.HEAD, - ["__tsonNonce0", "__tsonNonce", null], - "Promise", - ]); - expect - .soft(chunks[1]) - .toEqual([ChunkTypes.HEAD, ["__tsonNonce1", "__tsonNonce0", null]]); - expect - .soft(chunks[2]) - .toEqual([ChunkTypes.LEAF, ["__tsonNonce2", "__tsonNonce1", 0], "0"]); - expect - .soft(chunks[3]) - .toEqual([ - ChunkTypes.TAIL, - ["__tsonNonce3", "__tsonNonce0", null], - TsonStatus.OK, - ]); - expect - .soft(chunks[4]) - .toEqual([ChunkTypes.LEAF, ["__tsonNonce4", "__tsonNonce1", 1], "hello"]); - expect - .soft(chunks[5]) - .toEqual([ - ChunkTypes.TAIL, - ["__tsonNonce5", "__tsonNonce1", null], - TsonStatus.OK, - ]); - }); }); diff --git a/src/async/serializeAsync2.ts b/src/async/serializeAsync2.ts index f6e3225..b3c4498 100644 --- a/src/async/serializeAsync2.ts +++ b/src/async/serializeAsync2.ts @@ -91,7 +91,7 @@ export function createTsonSerializeAsync(opts: TsonAsyncOptions) { return undefined; } - return [ChunkTypes.REFERENCE, [id, parentId, key], originalNodeId]; + return [ChunkTypes.REF, [id, parentId, key], originalNodeId]; }; const initializeIterable = ( @@ -117,7 +117,7 @@ export function createTsonSerializeAsync(opts: TsonAsyncOptions) { ] as TsonAsyncTailTuple); } - addToQueue(result.value.key ?? null, result.value.chunk, newId); + addToQueue(result.value.key ?? null, result.value.chunk, head[1][0]); return Promise.resolve([ ChunkTypes.BODY, [newId, head[1][0], null], @@ -169,11 +169,7 @@ export function createTsonSerializeAsync(opts: TsonAsyncOptions) { queue.set( thisId, - Promise.resolve([ - ChunkTypes.LEAF, - [thisId, parentId, key], - JSON.stringify(value), - ]), + Promise.resolve([ChunkTypes.LEAF, [thisId, parentId, key], value]), ); return; diff --git a/src/internals/testUtils.ts b/src/internals/testUtils.ts index f8e0736..9a4bf72 100644 --- a/src/internals/testUtils.ts +++ b/src/internals/testUtils.ts @@ -1,6 +1,8 @@ import http from "node:http"; import { expect } from "vitest"; +import { assert } from "./assert.js"; + export const expectError = (fn: () => unknown) => { let err: unknown; try { @@ -101,3 +103,89 @@ export const createPromise = (result: () => T, wait = 1) => { }, wait); }); }; + +export const expectSequence = (sequence: T[]) => ({ + toHave(value: T) { + expect(sequence).toContain(value); + assert(value); + + return { + after(preceding: T) { + expect(preceding).toBeDefined(); + assert(preceding); + + const index = sequence.indexOf(value); + const precedingIndex = sequence.indexOf(preceding); + expect(index).toBeGreaterThanOrEqual(0); + expect(precedingIndex).toBeGreaterThanOrEqual(0); + expect( + index, + `Expected ${JSON.stringify( + value, + null, + 2, + )} to come after ${JSON.stringify(preceding, null, 2)}`, + ).toBeGreaterThan(precedingIndex); + }, + afterAll(following: T[]) { + expect(following).toBeDefined(); + assert(following); + for (const followingValue of following) { + this.after(followingValue); + } + }, + before(following: T) { + expect(following, "following").toBeDefined(); + assert(following); + + const index = sequence.indexOf(value); + const followingIndex = sequence.indexOf(following); + expect(index).toBeGreaterThanOrEqual(0); + expect(followingIndex).toBeGreaterThanOrEqual(0); + expect( + index, + `Expected ${JSON.stringify( + value, + null, + 2, + )} to come before ${JSON.stringify(following, null, 2)}`, + ).toBeLessThan(followingIndex); + }, + beforeAll(following: T[]) { + for (const followingValue of following) { + this.before(followingValue); + } + }, + }; + }, + toHaveAll(values: T[]) { + const thisHas = this.toHave.bind(this); + + for (const value of values) { + thisHas(value); + } + + return { + after(preceding: T) { + for (const value of values) { + thisHas(value).after(preceding); + } + }, + afterAll(following: T[]) { + for (const value of values) { + thisHas(value).afterAll(following); + } + }, + before(following: T) { + for (const value of values) { + thisHas(value).before(following); + } + }, + beforeAll(following: T[]) { + for (const value of values) { + thisHas(value).beforeAll(following); + } + }, + }; + }, +});