diff --git a/src/async/deserializeAsync.ts b/src/async/deserializeAsync.ts index 4b5198c1..7d4b138f 100644 --- a/src/async/deserializeAsync.ts +++ b/src/async/deserializeAsync.ts @@ -76,6 +76,12 @@ export function createTsonParseAsync(opts: TsonAsyncOptions): TsonParseAsync { deferreds.set(idx, deferred); + if (typeof window === "undefined") { + deferred.promise.catch(() => { + // prevent unhandled promise rejection crashes 🤷‍♂️ + }); + } + return deferred.promise; }, ); @@ -94,16 +100,14 @@ export function createTsonParseAsync(opts: TsonAsyncOptions): TsonParseAsync { ) { function readLine(str: string) { str = str.trimStart(); - if (!str) { - return; - } if (str.startsWith(",")) { // ignore leading comma str = str.slice(1); } - if (!str.startsWith("[")) { + if (str.length < 2) { + // minimum length is 2: '[]' return; } @@ -111,10 +115,16 @@ export function createTsonParseAsync(opts: TsonAsyncOptions): TsonParseAsync { const [index, status, result] = JSON.parse(str) as TsonAsyncValueTuple; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const deferred = deferreds.get(index)!; - + const deferred = deferreds.get(index); + // console.log("got value", index, status, result, deferred); const walkedResult = walk(result); + + if (!deferred) { + throw new TsonError( + `No deferred found for index ${index} (status: ${status})`, + ); + } + status === PROMISE_RESOLVED ? deferred.resolve(walkedResult) : deferred.reject( @@ -156,7 +166,7 @@ export function createTsonParseAsync(opts: TsonAsyncOptions): TsonParseAsync { lines.push(...(lastResult.value as string).split("\n").filter(Boolean)); // console.log("got line", lines); - } while (lines.length < 4); + } while (lines.length < 2); const [ /** @@ -166,31 +176,22 @@ export function createTsonParseAsync(opts: TsonAsyncOptions): TsonParseAsync { /** * Second line is the shape of the JSON */ - secondLine, - /** - * Third line is a `,` - */ - _thirdLine, - /** - * Fourth line is the start of the values array - */ - _fourthLine, - /** - * Buffer is the rest of the iterator that came in the chunks while we were waiting for the first 4 lines - */ + headLine, + // .. third line is a `,` + // .. fourth line is the start of the values array ...buffer ] = lines; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const secondValueParsed = JSON.parse(secondLine!) as TsonSerialized; + const head = JSON.parse(headLine!) as TsonSerialized; - const walk = walker(secondValueParsed.nonce); + const walk = walker(head.nonce); void getStreamedValues(buffer, !!lastResult.done, walk).catch((cause) => { // Something went wrong while getting the streamed values const err = new TsonError( - "Stream interrupted: failed to get streamed values", + `Stream interrupted: ${(cause as Error).message}`, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment { cause }, ); @@ -203,7 +204,7 @@ export function createTsonParseAsync(opts: TsonAsyncOptions): TsonParseAsync { deferreds.clear(); }); - return walk(secondValueParsed.json); + return walk(head.json); } const result = await init().catch((cause: unknown) => { diff --git a/src/handlers/tsonPromise.test.ts b/src/handlers/tsonPromise.test.ts index ec2c16cd..6f78340a 100644 --- a/src/handlers/tsonPromise.test.ts +++ b/src/handlers/tsonPromise.test.ts @@ -205,7 +205,6 @@ test("stringifier - no promises", async () => { buffer.push(value.trimEnd()); } - // expect(buffer).toHaveLength(5); expect(buffer).toMatchInlineSnapshot(` [ "[", @@ -388,6 +387,7 @@ test("stringify and parse promise with a promise over a network connection", asy }>; } + // ----- server -------- const server = await new Promise((resolve) => { const server = http.createServer((_req, res) => { async function handle() { @@ -404,7 +404,7 @@ test("stringify and parse promise with a promise over a network connection", asy }, 8), rejectedPromise: createPromise(() => { throw new Error("foo"); - }, 10), + }, 1), }; }, 3), }; @@ -428,6 +428,12 @@ test("stringify and parse promise with a promise over a network connection", asy // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any const port = (server.address() as any).port as number; + // ----- client -------- + const tson = createTsonAsync({ + nonce: () => "__tson", + types: [tsonPromise], + }); + // do a streamed fetch request const response = await fetch(`http://localhost:${port}`); @@ -440,11 +446,6 @@ test("stringify and parse promise with a promise over a network connection", asy readableStreamToAsyncIterable(response.body), (v) => textDecoder.decode(v), ); - const tson = createTsonAsync({ - nonce: () => "__tson", - types: [tsonPromise], - }); - const value = await tson.parse(stringIterator); const asObj = value as Obj; @@ -452,6 +453,10 @@ test("stringify and parse promise with a promise over a network connection", asy expect(firstPromise).toHaveProperty("anotherPromise"); + const secondPromise = await firstPromise.anotherPromise; + + expect(secondPromise).toBe(42); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const err = await firstPromise.rejectedPromise.catch((err) => err); assert.instanceOf(err, Error); @@ -463,10 +468,6 @@ test("stringify and parse promise with a promise over a network connection", asy } `); - const secondPromise = await firstPromise.anotherPromise; - - expect(secondPromise).toBe(42); - expect(err).toMatchInlineSnapshot("[TsonError: Promise rejected on server]"); server.close();