UNPKG

seroval

Version:
933 lines (876 loc) 22.4 kB
import { createAggregateErrorNode, createArrayNode, createAsyncIteratorFactoryInstanceNode, createBigIntNode, createBigIntTypedArrayNode, createBoxedNode, createDataViewNode, createDateNode, createErrorNode, createIteratorFactoryInstanceNode, createNumberNode, createPluginNode, createRegExpNode, createSequenceNode, createSetNode, createStreamConstructorNode, createStreamNextNode, createStreamReturnNode, createStreamThrowNode, createStringNode, createTypedArrayNode, } from '../base-primitives'; import { Feature } from '../compat'; import { NIL, SerovalNodeType } from '../constants'; import { SerovalDepthLimitError, SerovalParserError, SerovalUnsupportedTypeError, } from '../errors'; import { FALSE_NODE, NULL_NODE, TRUE_NODE, UNDEFINED_NODE } from '../literals'; import { createSerovalNode } from '../node'; import { OpaqueReference } from '../opaque-reference'; import { type Plugin, SerovalMode } from '../plugin'; import { createSequenceFromIterable, isSequence, type Sequence, } from '../sequence'; import { SpecialReference } from '../special-reference'; import type { Stream } from '../stream'; import { createStream, createStreamFromAsyncIterable, isStream, } from '../stream'; import { serializeString } from '../string'; import { SYM_ASYNC_ITERATOR, SYM_IS_CONCAT_SPREADABLE, SYM_ITERATOR, SYM_TO_STRING_TAG, } from '../symbols'; import type { SerovalAggregateErrorNode, SerovalArrayNode, SerovalBigIntTypedArrayNode, SerovalBoxedNode, SerovalDataViewNode, SerovalErrorNode, SerovalMapNode, SerovalNode, SerovalNodeWithID, SerovalNullConstructorNode, SerovalObjectNode, SerovalObjectRecordKey, SerovalObjectRecordNode, SerovalPluginNode, SerovalPromiseConstructorNode, SerovalSequenceNode, SerovalSetNode, SerovalTypedArrayNode, } from '../types'; import { getErrorOptions } from '../utils/error'; import type { BigIntTypedArrayValue, TypedArrayValue, } from '../utils/typed-array'; import type { BaseParserContext, BaseParserContextOptions } from './parser'; import { createArrayBufferNode, createBaseParserContext, createIndexForValue, createMapNode, createObjectNode, createPromiseConstructorNode, getReferenceNode, parseAsyncIteratorFactory, parseIteratorFactory, ParserNodeType, parseSpecialReference, parseWellKnownSymbol, } from './parser'; type ObjectLikeNode = SerovalObjectNode | SerovalNullConstructorNode; export type SyncParserContextOptions = BaseParserContextOptions; const enum ParserMode { Sync = 1, Stream = 2, } export interface SyncParserContext { type: ParserMode.Sync; base: BaseParserContext; child: SyncParsePluginContext | undefined; } export function createSyncParserContext( mode: SerovalMode, options: SyncParserContextOptions, ): SyncParserContext { return { type: ParserMode.Sync, base: createBaseParserContext(mode, options), child: NIL, }; } export class SyncParsePluginContext { constructor( private _p: SyncParserContext, private depth: number, ) {} parse<T>(current: T): SerovalNode { return parseSOS(this._p, this.depth, current); } } export interface StreamParserContextOptions extends SyncParserContextOptions { onParse: (node: SerovalNode, initial: boolean) => void; onError?: (error: unknown) => void; onDone?: () => void; } export interface StreamParserContext { type: ParserMode.Stream; base: BaseParserContext; state: StreamParserState; } export class StreamParsePluginContext { constructor( private _p: StreamParserContext, private depth: number, ) {} parse<T>(current: T): SerovalNode { return parseSOS(this._p, this.depth, current); } parseWithError<T>(current: T): SerovalNode | undefined { return parseWithError(this._p, this.depth, current); } isAlive(): boolean { return this._p.state.alive; } pushPendingState(): void { pushPendingState(this._p); } popPendingState(): void { popPendingState(this._p); } onParse(node: SerovalNode): void { onParse(this._p, node); } onError(error: unknown): void { onError(this._p, error); } } interface StreamParserState { // Life cycle alive: boolean; // Number of pending things pending: number; // initial: boolean; // buffer: SerovalNode[]; // Callbacks onParse: (node: SerovalNode, initial: boolean) => void; onError?: (error: unknown) => void; onDone?: () => void; } function createStreamParserState( options: StreamParserContextOptions, ): StreamParserState { return { alive: true, pending: 0, initial: true, buffer: [], onParse: options.onParse, onError: options.onError, onDone: options.onDone, }; } export function createStreamParserContext( options: StreamParserContextOptions, ): StreamParserContext { return { type: ParserMode.Stream, base: createBaseParserContext(SerovalMode.Cross, options), state: createStreamParserState(options), }; } type SOSParserContext = SyncParserContext | StreamParserContext; function parseItems( ctx: SOSParserContext, depth: number, current: unknown[], ): (SerovalNode | 0)[] { const nodes: (SerovalNode | 0)[] = []; for (let i = 0, len = current.length; i < len; i++) { if (i in current) { nodes[i] = parseSOS(ctx, depth, current[i]); } else { nodes[i] = 0; } } return nodes; } function parseArray( ctx: SOSParserContext, depth: number, id: number, current: unknown[], ): SerovalArrayNode { return createArrayNode(id, current, parseItems(ctx, depth, current)); } function parseProperties( ctx: SOSParserContext, depth: number, properties: Record<string | symbol, unknown>, ): SerovalObjectRecordNode { const entries = Object.entries(properties); const keyNodes: SerovalObjectRecordKey[] = []; const valueNodes: SerovalNode[] = []; for (let i = 0, len = entries.length; i < len; i++) { keyNodes.push(serializeString(entries[i][0])); valueNodes.push(parseSOS(ctx, depth, entries[i][1])); } // Check special properties, symbols in this case if (SYM_ITERATOR in properties) { keyNodes.push(parseWellKnownSymbol(ctx.base, SYM_ITERATOR)); valueNodes.push( createIteratorFactoryInstanceNode( parseIteratorFactory(ctx.base), parseSOS( ctx, depth, createSequenceFromIterable( properties as unknown as Iterable<unknown>, ), ) as SerovalNodeWithID, ), ); } if (SYM_ASYNC_ITERATOR in properties) { keyNodes.push(parseWellKnownSymbol(ctx.base, SYM_ASYNC_ITERATOR)); valueNodes.push( createAsyncIteratorFactoryInstanceNode( parseAsyncIteratorFactory(ctx.base), parseSOS( ctx, depth, ctx.type === ParserMode.Sync ? createStream() : createStreamFromAsyncIterable( properties as unknown as AsyncIterable<unknown>, ), ) as SerovalNodeWithID, ), ); } if (SYM_TO_STRING_TAG in properties) { keyNodes.push(parseWellKnownSymbol(ctx.base, SYM_TO_STRING_TAG)); valueNodes.push(createStringNode(properties[SYM_TO_STRING_TAG] as string)); } if (SYM_IS_CONCAT_SPREADABLE in properties) { keyNodes.push(parseWellKnownSymbol(ctx.base, SYM_IS_CONCAT_SPREADABLE)); valueNodes.push( properties[SYM_IS_CONCAT_SPREADABLE] ? TRUE_NODE : FALSE_NODE, ); } return { k: keyNodes, v: valueNodes, }; } function parsePlainObject( ctx: SOSParserContext, depth: number, id: number, current: Record<string, unknown>, empty: boolean, ): ObjectLikeNode { return createObjectNode( id, current, empty, parseProperties(ctx, depth, current), ); } function parseBoxed( ctx: SOSParserContext, depth: number, id: number, current: object, ): SerovalBoxedNode { return createBoxedNode(id, parseSOS(ctx, depth, current.valueOf())); } function parseTypedArray( ctx: SOSParserContext, depth: number, id: number, current: TypedArrayValue, ): SerovalTypedArrayNode { return createTypedArrayNode( id, current, parseSOS(ctx, depth, current.buffer), ); } function parseBigIntTypedArray( ctx: SOSParserContext, depth: number, id: number, current: BigIntTypedArrayValue, ): SerovalBigIntTypedArrayNode { return createBigIntTypedArrayNode( id, current, parseSOS(ctx, depth, current.buffer), ); } function parseDataView( ctx: SOSParserContext, depth: number, id: number, current: DataView, ): SerovalDataViewNode { return createDataViewNode(id, current, parseSOS(ctx, depth, current.buffer)); } function parseError( ctx: SOSParserContext, depth: number, id: number, current: Error, ): SerovalErrorNode { const options = getErrorOptions(current, ctx.base.features); return createErrorNode( id, current, options ? parseProperties(ctx, depth, options) : NIL, ); } function parseAggregateError( ctx: SOSParserContext, depth: number, id: number, current: AggregateError, ): SerovalAggregateErrorNode { const options = getErrorOptions(current, ctx.base.features); return createAggregateErrorNode( id, current, options ? parseProperties(ctx, depth, options) : NIL, ); } function parseMap( ctx: SOSParserContext, depth: number, id: number, current: Map<unknown, unknown>, ): SerovalMapNode { const keyNodes: SerovalNode[] = []; const valueNodes: SerovalNode[] = []; for (const [key, value] of current.entries()) { keyNodes.push(parseSOS(ctx, depth, key)); valueNodes.push(parseSOS(ctx, depth, value)); } return createMapNode(ctx.base, id, keyNodes, valueNodes); } function parseSet( ctx: SOSParserContext, depth: number, id: number, current: Set<unknown>, ): SerovalSetNode { const items: SerovalNode[] = []; for (const item of current.keys()) { items.push(parseSOS(ctx, depth, item)); } return createSetNode(id, items); } function parseStream( ctx: SOSParserContext, depth: number, id: number, current: Stream<unknown>, ): SerovalNode { const result = createStreamConstructorNode( id, parseSpecialReference(ctx.base, SpecialReference.StreamConstructor), [], ); if (ctx.type === ParserMode.Sync) { return result; } pushPendingState(ctx); current.on({ next: value => { if (ctx.state.alive) { const parsed = parseWithError(ctx, depth, value); if (parsed) { onParse(ctx, createStreamNextNode(id, parsed)); } } }, throw: value => { if (ctx.state.alive) { const parsed = parseWithError(ctx, depth, value); if (parsed) { onParse(ctx, createStreamThrowNode(id, parsed)); } } popPendingState(ctx); }, return: value => { if (ctx.state.alive) { const parsed = parseWithError(ctx, depth, value); if (parsed) { onParse(ctx, createStreamReturnNode(id, parsed)); } } popPendingState(ctx); }, }); return result; } function handlePromiseSuccess( this: StreamParserContext, id: number, depth: number, data: unknown, ): void { if (this.state.alive) { const parsed = parseWithError(this, depth, data); if (parsed) { onParse( this, createSerovalNode( SerovalNodeType.PromiseSuccess, id, NIL, NIL, NIL, NIL, NIL, [ parseSpecialReference(this.base, SpecialReference.PromiseSuccess), parsed, ], NIL, NIL, NIL, NIL, ), ); } popPendingState(this); } } function handlePromiseFailure( this: StreamParserContext, id: number, depth: number, data: unknown, ): void { if (this.state.alive) { const parsed = parseWithError(this, depth, data); if (parsed) { onParse( this, createSerovalNode( SerovalNodeType.PromiseFailure, id, NIL, NIL, NIL, NIL, NIL, [ parseSpecialReference(this.base, SpecialReference.PromiseFailure), parsed, ], NIL, NIL, NIL, NIL, ), ); } } popPendingState(this); } function parsePromise( ctx: SOSParserContext, depth: number, id: number, current: Promise<unknown>, ): SerovalPromiseConstructorNode { // Creates a unique reference for the promise resolver const resolver = createIndexForValue(ctx.base, {}); if (ctx.type === ParserMode.Stream) { pushPendingState(ctx); current.then( handlePromiseSuccess.bind(ctx, resolver, depth), handlePromiseFailure.bind(ctx, resolver, depth), ); } return createPromiseConstructorNode(ctx.base, id, resolver); } function parsePluginSync( ctx: SyncParserContext, depth: number, id: number, current: unknown, currentPlugins: Plugin<any, any>[], ): SerovalPluginNode | undefined { for (let i = 0, len = currentPlugins.length; i < len; i++) { const plugin = currentPlugins[i]; if (plugin.parse.sync && plugin.test(current)) { return createPluginNode( id, plugin.tag, plugin.parse.sync(current, new SyncParsePluginContext(ctx, depth), { id, }), ); } } return NIL; } function parsePluginStream( ctx: StreamParserContext, depth: number, id: number, current: unknown, currentPlugins: Plugin<any, any>[], ): SerovalPluginNode | undefined { for (let i = 0, len = currentPlugins.length; i < len; i++) { const plugin = currentPlugins[i]; if (plugin.parse.stream && plugin.test(current)) { return createPluginNode( id, plugin.tag, plugin.parse.stream(current, new StreamParsePluginContext(ctx, depth), { id, }), ); } } return NIL; } function parsePlugin( ctx: SOSParserContext, depth: number, id: number, current: unknown, ): SerovalPluginNode | undefined { const currentPlugins = ctx.base.plugins; if (currentPlugins) { return ctx.type === ParserMode.Sync ? parsePluginSync(ctx, depth, id, current, currentPlugins) : parsePluginStream(ctx, depth, id, current, currentPlugins); } return NIL; } function parseSequence( ctx: SOSParserContext, depth: number, id: number, current: Sequence, ): SerovalSequenceNode { const nodes: SerovalNode[] = []; for (let i = 0, len = current.v.length; i < len; i++) { nodes[i] = parseSOS(ctx, depth, current.v[i]); } return createSequenceNode(id, nodes, current.t, current.d); } function parseObjectPhase2( ctx: SOSParserContext, depth: number, id: number, current: object, currentClass: unknown, ): SerovalNode { switch (currentClass) { case Object: return parsePlainObject( ctx, depth, id, current as Record<string, unknown>, false, ); case NIL: return parsePlainObject( ctx, depth, id, current as Record<string, unknown>, true, ); case Date: return createDateNode(id, current as unknown as Date); case Error: case EvalError: case RangeError: case ReferenceError: case SyntaxError: case TypeError: case URIError: return parseError(ctx, depth, id, current as unknown as Error); case Number: case Boolean: case String: case BigInt: return parseBoxed(ctx, depth, id, current); case ArrayBuffer: return createArrayBufferNode( ctx.base, id, current as unknown as ArrayBuffer, ); case Int8Array: case Int16Array: case Int32Array: case Uint8Array: case Uint16Array: case Uint32Array: case Uint8ClampedArray: case Float32Array: case Float64Array: return parseTypedArray( ctx, depth, id, current as unknown as TypedArrayValue, ); case DataView: return parseDataView(ctx, depth, id, current as unknown as DataView); case Map: return parseMap( ctx, depth, id, current as unknown as Map<unknown, unknown>, ); case Set: return parseSet(ctx, depth, id, current as unknown as Set<unknown>); default: break; } // Promises if (currentClass === Promise || current instanceof Promise) { return parsePromise(ctx, depth, id, current as unknown as Promise<unknown>); } const currentFeatures = ctx.base.features; if (currentFeatures & Feature.RegExp && currentClass === RegExp) { return createRegExpNode(id, current as unknown as RegExp); } // BigInt Typed Arrays if (currentFeatures & Feature.BigIntTypedArray) { switch (currentClass) { case BigInt64Array: case BigUint64Array: return parseBigIntTypedArray( ctx, depth, id, current as unknown as BigIntTypedArrayValue, ); default: break; } } if ( currentFeatures & Feature.AggregateError && typeof AggregateError !== 'undefined' && (currentClass === AggregateError || current instanceof AggregateError) ) { return parseAggregateError( ctx, depth, id, current as unknown as AggregateError, ); } // Slow path. We only need to handle Errors and Iterators // since they have very broad implementations. if (current instanceof Error) { return parseError(ctx, depth, id, current); } // Generator functions don't have a global constructor // despite existing if (SYM_ITERATOR in current || SYM_ASYNC_ITERATOR in current) { return parsePlainObject(ctx, depth, id, current, !!currentClass); } throw new SerovalUnsupportedTypeError(current); } function parseObject( ctx: SOSParserContext, depth: number, id: number, current: object, ): SerovalNode { if (Array.isArray(current)) { return parseArray(ctx, depth, id, current); } if (isStream(current)) { return parseStream(ctx, depth, id, current); } if (isSequence(current)) { return parseSequence(ctx, depth, id, current); } const currentClass = current.constructor; if (currentClass === OpaqueReference) { return parseSOS( ctx, depth, (current as OpaqueReference<unknown, unknown>).replacement, ); } const parsed = parsePlugin(ctx, depth, id, current); if (parsed) { return parsed; } return parseObjectPhase2(ctx, depth, id, current, currentClass); } function parseFunction( ctx: SOSParserContext, depth: number, current: unknown, ): SerovalNode { const ref = getReferenceNode(ctx.base, current); if (ref.type !== ParserNodeType.Fresh) { return ref.value; } const plugin = parsePlugin(ctx, depth, ref.value, current); if (plugin) { return plugin; } throw new SerovalUnsupportedTypeError(current); } export function parseSOS<T>( ctx: SOSParserContext, depth: number, current: T, ): SerovalNode { if (depth >= ctx.base.depthLimit) { throw new SerovalDepthLimitError(ctx.base.depthLimit); } switch (typeof current) { case 'boolean': return current ? TRUE_NODE : FALSE_NODE; case 'undefined': return UNDEFINED_NODE; case 'string': return createStringNode(current as string); case 'number': return createNumberNode(current as number); case 'bigint': return createBigIntNode(current as bigint); case 'object': { if (current) { const ref = getReferenceNode(ctx.base, current); return ref.type === ParserNodeType.Fresh ? parseObject(ctx, depth + 1, ref.value, current as object) : ref.value; } return NULL_NODE; } case 'symbol': return parseWellKnownSymbol(ctx.base, current); case 'function': { return parseFunction(ctx, depth, current); } default: throw new SerovalUnsupportedTypeError(current); } } export function parseTop<T>(ctx: SyncParserContext, current: T): SerovalNode { try { return parseSOS(ctx, 0, current); } catch (error) { throw error instanceof SerovalParserError ? error : new SerovalParserError(error); } } function onParse(ctx: StreamParserContext, node: SerovalNode): void { // If the value emitted happens to be during parsing, we push to the // buffer and emit after the initial parsing is done. if (ctx.state.initial) { ctx.state.buffer.push(node); } else { onParseInternal(ctx, node, false); } } function onError(ctx: StreamParserContext, error: unknown): void { if (ctx.state.onError) { ctx.state.onError(error); } else { throw error instanceof SerovalParserError ? error : new SerovalParserError(error); } } function onDone(ctx: StreamParserContext): void { if (ctx.state.onDone) { ctx.state.onDone(); } } function onParseInternal( ctx: StreamParserContext, node: SerovalNode, initial: boolean, ): void { try { ctx.state.onParse(node, initial); } catch (error) { onError(ctx, error); } } function pushPendingState(ctx: StreamParserContext): void { ctx.state.pending++; } function popPendingState(ctx: StreamParserContext): void { if (--ctx.state.pending <= 0) { onDone(ctx); } } function parseWithError<T>( ctx: StreamParserContext, depth: number, current: T, ): SerovalNode | undefined { try { return parseSOS(ctx, depth, current); } catch (err) { onError(ctx, err); return NIL; } } export function startStreamParse<T>( ctx: StreamParserContext, current: T, ): void { const parsed = parseWithError(ctx, 0, current); if (parsed) { onParseInternal(ctx, parsed, true); ctx.state.initial = false; flushStreamParse(ctx, ctx.state); // Check if there's any pending pushes if (ctx.state.pending <= 0) { destroyStreamParse(ctx); } } } function flushStreamParse( ctx: StreamParserContext, state: StreamParserState, ): void { for (let i = 0, len = state.buffer.length; i < len; i++) { onParseInternal(ctx, state.buffer[i], false); } } export function destroyStreamParse(ctx: StreamParserContext): void { if (ctx.state.alive) { onDone(ctx); ctx.state.alive = false; } }