UNPKG

tupleson

Version:

A hackable JSON serializer/deserializer

352 lines (287 loc) 8.38 kB
import { TsonCircularReferenceError } from "../errors.js"; import { assert } from "../internals/assert.js"; import { GetNonce, getDefaultNonce } from "../internals/getNonce.js"; import { mapOrReturn } from "../internals/mapOrReturn.js"; import { TsonAllTypes, TsonNonce, TsonSerialized, TsonSerializedValue, TsonTuple, TsonTypeHandlerKey, TsonTypeTesterCustom, TsonTypeTesterPrimitive, } from "../sync/syncTypes.js"; import { TsonStreamInterruptedError } from "./asyncErrors.js"; import { BrandSerialized, TsonAsyncIndex, TsonAsyncOptions, TsonAsyncStringifier, } from "./asyncTypes.js"; import { createReadableStream, createServerEvent } from "./iterableUtils.js"; type WalkFn = (value: unknown) => unknown; export type TsonAsyncValueTuple = [TsonAsyncIndex, unknown]; function walkerFactory(nonce: TsonNonce, types: TsonAsyncOptions["types"]) { // instance variables let asyncIndex = 0; const seen = new WeakSet(); const cache = new WeakMap<object, unknown>(); const iterators = new Map<TsonAsyncIndex, AsyncIterator<unknown>>(); const iterator = { async *[Symbol.asyncIterator]() { // race all active iterators and yield next value as they come // when one iterator is done, remove it from the list // when all iterators are done, we're done const nextAsyncIteratorValue = new Map< TsonAsyncIndex, Promise<[TsonAsyncIndex, IteratorResult<unknown>]> >(); // let _tmp = 0; while (iterators.size > 0) { // if (_tmp++ > 10) { // throw new Error("too many iterations"); // } // set next cursor for (const [idx, iterator] of iterators) { if (!nextAsyncIteratorValue.has(idx)) { nextAsyncIteratorValue.set( idx, iterator.next().then((result) => [idx, result]), ); } } const nextValues = Array.from(nextAsyncIteratorValue.values()); const [idx, result] = await Promise.race(nextValues); if (result.done) { nextAsyncIteratorValue.delete(idx); iterators.delete(idx); continue; } else { const iterator = iterators.get(idx); assert(iterator, `iterator ${idx} not found`); nextAsyncIteratorValue.set( idx, iterator.next().then((result) => [idx, result]), ); } const valueTuple: TsonAsyncValueTuple = [idx, walk(result.value)]; yield valueTuple; } }, }; const handlers = (() => { const all = types.map((handler) => { type Serializer = ( value: unknown, nonce: TsonNonce, walk: WalkFn, ) => TsonSerializedValue; const $serialize: Serializer = handler.serializeIterator ? (value): TsonTuple => { const idx = asyncIndex++ as TsonAsyncIndex; const iterator = handler.serializeIterator({ value, }); iterators.set(idx, iterator[Symbol.asyncIterator]()); return [handler.key as TsonTypeHandlerKey, idx, nonce]; } : handler.serialize ? (value, nonce, walk): TsonTuple => [ handler.key as TsonTypeHandlerKey, walk(handler.serialize(value)), nonce, ] : (value, _nonce, walk) => walk(value); return { ...handler, $serialize, }; }); type Handler = (typeof all)[number]; const byPrimitive: Partial< Record<TsonAllTypes, Extract<Handler, TsonTypeTesterPrimitive>> > = {}; const nonPrimitive: Extract<Handler, TsonTypeTesterCustom>[] = []; for (const handler of all) { if (handler.primitive) { if (byPrimitive[handler.primitive]) { throw new Error( `Multiple handlers for primitive ${handler.primitive} found`, ); } byPrimitive[handler.primitive] = handler; } else { nonPrimitive.push(handler); } } return [nonPrimitive, byPrimitive] as const; })(); const [nonPrimitive, byPrimitive] = handlers; const walk: WalkFn = (value) => { const type = typeof value; const isComplex = !!value && type === "object"; if (isComplex) { if (seen.has(value)) { const cached = cache.get(value); if (!cached) { throw new TsonCircularReferenceError(value); } return cached; } seen.add(value); } const cacheAndReturn = (result: unknown) => { if (isComplex) { cache.set(value, result); } return result; }; const primitiveHandler = byPrimitive[type]; if ( primitiveHandler && (!primitiveHandler.test || primitiveHandler.test(value)) ) { return cacheAndReturn(primitiveHandler.$serialize(value, nonce, walk)); } for (const handler of nonPrimitive) { if (handler.test(value)) { return cacheAndReturn(handler.$serialize(value, nonce, walk)); } } return cacheAndReturn(mapOrReturn(value, walk)); }; return [walk, iterator] as const; } type TsonAsyncSerializer = <T>( value: T, ) => [TsonSerialized<T>, AsyncIterable<TsonAsyncValueTuple>]; export function createAsyncTsonSerialize( opts: TsonAsyncOptions, ): TsonAsyncSerializer { const getNonce: GetNonce = (opts.nonce ?? getDefaultNonce) as GetNonce; return (value) => { const nonce = getNonce(); const [walk, iterator] = walkerFactory(nonce, opts.types); return [ { json: walk(value), nonce, } as TsonSerialized<typeof value>, iterator, ]; }; } /** * JSON stream */ export function createTsonStreamAsync( opts: TsonAsyncOptions, ): TsonAsyncStringifier { const indent = (length: number) => " ".repeat(length); const stringifier: (value: unknown, space?: number) => AsyncIterable<string> = async function* stringify(value, space = 0) { // head looks like // [ // { json: {}, nonce: "..." } // ,[ const [head, iterator] = createAsyncTsonSerialize(opts)(value); // first line of the json: init the array, ignored when parsing> yield "[" + "\n"; // second line: the shape of the json - used when parsing> yield indent(space * 1) + JSON.stringify(head) + "\n"; // third line: comma before values, ignored when parsing yield indent(space * 1) + "," + "\n"; // fourth line: the values array, ignored when parsing yield indent(space * 1) + "[" + "\n"; let isFirstStreamedValue = true; for await (const value of iterator) { const prefix = indent(space * 2) + (isFirstStreamedValue ? "" : ","); yield prefix + JSON.stringify(value) + "\n"; isFirstStreamedValue = false; } yield "]]" + "\n"; // end response and value array }; return stringifier as TsonAsyncStringifier; } export function createTsonSSEResponse(opts: TsonAsyncOptions) { const serialize = createAsyncTsonSerialize(opts); return <TValue>(value: TValue) => { const [readable, controller] = createReadableStream(); async function iterate() { const [head, iterable] = serialize(value); controller.enqueue( createServerEvent({ data: head, //event: "head", // id: "0", // retry: 0, }), ); for await (const chunk of iterable) { controller.enqueue( createServerEvent({ data: chunk, // event: "tson", // id: "0", // retry: 0, }), ); } // indicate the end of the stream controller.enqueue( createServerEvent({ data: null, event: "close", // id: "0", // retry: 0, }), ); controller.close(); controller.error( new TsonStreamInterruptedError(new Error("SSE stream ended")), ); } iterate().catch((err) => { controller.error(err); }); const res = new Response(readable, { headers: { "Cache-Control": "no-cache", Connection: "keep-alive", "Content-Type": "text/event-stream", // prevent buffering by nginx "X-Accel-Buffering": "no", }, status: 200, }); return res as BrandSerialized<typeof res, TValue>; }; } /** * JSON stream Response */ export function createTsonSerializeJsonStreamResponse(opts: TsonAsyncOptions) { const serialize = createTsonStreamAsync(opts); return <TValue>(value: TValue) => { const [readable, controller] = createReadableStream<string>(); async function iterate() { for await (const chunk of serialize(value)) { controller.enqueue(chunk); } controller.close(); } iterate().catch((err) => { controller.error(err); }); const res = new Response(readable, { headers: { "Cache-Control": "no-cache", Connection: "keep-alive", "Content-Type": "application/json", }, status: 200, }); return res as BrandSerialized<typeof res, TValue>; }; }