seroval
Version:
Stringify JS values
529 lines (506 loc) • 15 kB
text/typescript
import { abortSignalToPromise } from '../../abort-signal';
import {
createAggregateErrorNode,
createArrayBufferNode,
createArrayNode,
createAsyncIteratorFactoryInstanceNode,
createBigIntNode,
createBigIntTypedArrayNode,
createBoxedNode,
createDataViewNode,
createDateNode,
createErrorNode,
createIteratorFactoryInstanceNode,
createNumberNode,
createPluginNode,
createRegExpNode,
createSetNode,
createStreamConstructorNode,
createStreamNextNode,
createStreamReturnNode,
createStreamThrowNode,
createStringNode,
createTypedArrayNode,
} from '../../base-primitives';
import { Feature } from '../../compat';
import { NIL, SerovalNodeType } from '../../constants';
import { 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 { SpecialReference } from '../../special-reference';
import type { Stream } from '../../stream';
import { createStreamFromAsyncIterable, isStream } from '../../stream';
import { serializeString } from '../../string';
import type {
SerovalAbortSignalSyncNode,
SerovalAggregateErrorNode,
SerovalArrayNode,
SerovalBigIntTypedArrayNode,
SerovalBoxedNode,
SerovalDataViewNode,
SerovalErrorNode,
SerovalMapNode,
SerovalNode,
SerovalNullConstructorNode,
SerovalObjectNode,
SerovalObjectRecordKey,
SerovalObjectRecordNode,
SerovalPluginNode,
SerovalPromiseNode,
SerovalSetNode,
SerovalStreamConstructorNode,
SerovalTypedArrayNode,
} from '../../types';
import { getErrorOptions } from '../../utils/error';
import { iteratorToSequence } from '../../utils/iterator-to-sequence';
import promiseToResult from '../../utils/promise-to-result';
import type {
BigIntTypedArrayValue,
TypedArrayValue,
} from '../../utils/typed-array';
import { BaseParserContext, ParserNodeType } from '../parser';
type ObjectLikeNode =
| SerovalObjectNode
| SerovalNullConstructorNode
| SerovalPromiseNode;
export default abstract class BaseAsyncParserContext extends BaseParserContext {
private async parseItems(current: unknown[]): Promise<SerovalNode[]> {
const nodes = [];
for (let i = 0, len = current.length; i < len; i++) {
// For consistency in holes
if (i in current) {
nodes[i] = await this.parse(current[i]);
}
}
return nodes;
}
private async parseArray(
id: number,
current: unknown[],
): Promise<SerovalArrayNode> {
return createArrayNode(id, current, await this.parseItems(current));
}
private async parseProperties(
properties: Record<string | symbol, unknown>,
): Promise<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(await this.parse(entries[i][1]));
}
// Check special properties
let symbol = Symbol.iterator;
if (symbol in properties) {
keyNodes.push(this.parseWellKnownSymbol(symbol));
valueNodes.push(
createIteratorFactoryInstanceNode(
this.parseIteratorFactory(),
await this.parse(
iteratorToSequence(properties as unknown as Iterable<unknown>),
),
),
);
}
symbol = Symbol.asyncIterator;
if (symbol in properties) {
keyNodes.push(this.parseWellKnownSymbol(symbol));
valueNodes.push(
createAsyncIteratorFactoryInstanceNode(
this.parseAsyncIteratorFactory(),
await this.parse(
createStreamFromAsyncIterable(
properties as unknown as AsyncIterable<unknown>,
),
),
),
);
}
symbol = Symbol.toStringTag;
if (symbol in properties) {
keyNodes.push(this.parseWellKnownSymbol(symbol));
valueNodes.push(createStringNode(properties[symbol] as string));
}
symbol = Symbol.isConcatSpreadable;
if (symbol in properties) {
keyNodes.push(this.parseWellKnownSymbol(symbol));
valueNodes.push(properties[symbol] ? TRUE_NODE : FALSE_NODE);
}
return {
k: keyNodes,
v: valueNodes,
s: keyNodes.length,
};
}
private async parsePlainObject(
id: number,
current: Record<string, unknown>,
empty: boolean,
): Promise<ObjectLikeNode> {
return this.createObjectNode(
id,
current,
empty,
await this.parseProperties(current),
);
}
private async parseBoxed(
id: number,
current: object,
): Promise<SerovalBoxedNode> {
return createBoxedNode(id, await this.parse(current.valueOf()));
}
private async parseTypedArray(
id: number,
current: TypedArrayValue,
): Promise<SerovalTypedArrayNode> {
return createTypedArrayNode(id, current, await this.parse(current.buffer));
}
private async parseBigIntTypedArray(
id: number,
current: BigIntTypedArrayValue,
): Promise<SerovalBigIntTypedArrayNode> {
return createBigIntTypedArrayNode(
id,
current,
await this.parse(current.buffer),
);
}
private async parseDataView(
id: number,
current: DataView,
): Promise<SerovalDataViewNode> {
return createDataViewNode(id, current, await this.parse(current.buffer));
}
private async parseError(
id: number,
current: Error,
): Promise<SerovalErrorNode> {
const options = getErrorOptions(current, this.features);
return createErrorNode(
id,
current,
options ? await this.parseProperties(options) : NIL,
);
}
private async parseAggregateError(
id: number,
current: AggregateError,
): Promise<SerovalAggregateErrorNode> {
const options = getErrorOptions(current, this.features);
return createAggregateErrorNode(
id,
current,
options ? await this.parseProperties(options) : NIL,
);
}
private async parseMap(
id: number,
current: Map<unknown, unknown>,
): Promise<SerovalMapNode> {
const keyNodes: SerovalNode[] = [];
const valueNodes: SerovalNode[] = [];
for (const [key, value] of current.entries()) {
keyNodes.push(await this.parse(key));
valueNodes.push(await this.parse(value));
}
return this.createMapNode(id, keyNodes, valueNodes, current.size);
}
private async parseSet(
id: number,
current: Set<unknown>,
): Promise<SerovalSetNode> {
const items: SerovalNode[] = [];
for (const item of current.keys()) {
items.push(await this.parse(item));
}
return createSetNode(id, current.size, items);
}
private async parsePromise(
id: number,
current: Promise<unknown>,
): Promise<SerovalPromiseNode> {
const [status, result] = await promiseToResult(current);
return createSerovalNode(
SerovalNodeType.Promise,
id,
status,
NIL,
NIL,
NIL,
NIL,
NIL,
NIL,
await this.parse(result),
NIL,
NIL,
);
}
private async parsePlugin(
id: number,
current: unknown,
): Promise<SerovalPluginNode | undefined> {
const currentPlugins = this.plugins;
if (currentPlugins) {
for (let i = 0, len = currentPlugins.length; i < len; i++) {
const plugin = currentPlugins[i];
if (plugin.parse.async && plugin.test(current)) {
return createPluginNode(
id,
plugin.tag,
await plugin.parse.async(current, this, {
id,
}),
);
}
}
}
return NIL;
}
private async parseStream(
id: number,
current: Stream<unknown>,
): Promise<SerovalStreamConstructorNode> {
return createStreamConstructorNode(
id,
this.parseSpecialReference(SpecialReference.StreamConstructor),
await new Promise<SerovalNode[]>((resolve, reject) => {
const sequence: SerovalNode[] = [];
const cleanup = current.on({
next: value => {
this.markRef(id);
this.parse(value).then(
data => {
sequence.push(createStreamNextNode(id, data));
},
data => {
reject(data);
cleanup();
},
);
},
throw: value => {
this.markRef(id);
this.parse(value).then(
data => {
sequence.push(createStreamThrowNode(id, data));
resolve(sequence);
cleanup();
},
data => {
reject(data);
cleanup();
},
);
},
return: value => {
this.markRef(id);
this.parse(value).then(
data => {
sequence.push(createStreamReturnNode(id, data));
resolve(sequence);
cleanup();
},
data => {
reject(data);
cleanup();
},
);
},
});
}),
);
}
private async parseAbortSignalSync(
id: number,
current: AbortSignal,
): Promise<SerovalAbortSignalSyncNode> {
return createSerovalNode(
SerovalNodeType.AbortSignalSync,
id,
NIL,
NIL,
NIL,
NIL,
NIL,
NIL,
NIL,
await this.parse(current.reason),
NIL,
NIL,
);
}
private async parseAbortSignal(
id: number,
current: AbortSignal,
): Promise<SerovalAbortSignalSyncNode> {
if (!current.aborted) {
await abortSignalToPromise(current);
}
return this.parseAbortSignalSync(id, current);
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ehh
private async parseObject(id: number, current: object): Promise<SerovalNode> {
if (Array.isArray(current)) {
return this.parseArray(id, current);
}
if (isStream(current)) {
return this.parseStream(id, current);
}
const currentClass = current.constructor;
if (currentClass === OpaqueReference) {
return this.parse(
(current as OpaqueReference<unknown, unknown>).replacement,
);
}
const parsed = await this.parsePlugin(id, current);
if (parsed) {
return parsed;
}
switch (currentClass) {
case Object:
return this.parsePlainObject(
id,
current as Record<string, unknown>,
false,
);
case NIL:
return this.parsePlainObject(
id,
current as Record<string, unknown>,
true,
);
case Date:
return createDateNode(id, current as unknown as Date);
case RegExp:
return createRegExpNode(id, current as unknown as RegExp);
case Error:
case EvalError:
case RangeError:
case ReferenceError:
case SyntaxError:
case TypeError:
case URIError:
return this.parseError(id, current as unknown as Error);
case Number:
case Boolean:
case String:
case BigInt:
return this.parseBoxed(id, current);
case ArrayBuffer:
return createArrayBufferNode(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 this.parseTypedArray(id, current as unknown as TypedArrayValue);
case DataView:
return this.parseDataView(id, current as unknown as DataView);
case Map:
return this.parseMap(id, current as unknown as Map<unknown, unknown>);
case Set:
return this.parseSet(id, current as unknown as Set<unknown>);
default:
break;
}
// Promises
if (currentClass === Promise || current instanceof Promise) {
return this.parsePromise(id, current as unknown as Promise<unknown>);
}
const currentFeatures = this.features;
if (
currentFeatures & Feature.AbortSignal &&
typeof AbortSignal !== 'undefined' &&
currentClass === AbortSignal
) {
return this.parseAbortSignal(id, current as unknown as AbortSignal);
}
// BigInt Typed Arrays
if (currentFeatures & Feature.BigIntTypedArray) {
switch (currentClass) {
case BigInt64Array:
case BigUint64Array:
return this.parseBigIntTypedArray(
id,
current as unknown as BigIntTypedArrayValue,
);
default:
break;
}
}
if (
currentFeatures & Feature.AggregateError &&
typeof AggregateError !== 'undefined' &&
(currentClass === AggregateError || current instanceof AggregateError)
) {
return this.parseAggregateError(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 this.parseError(id, current);
}
// Generator functions don't have a global constructor
// despite existing
if (Symbol.iterator in current || Symbol.asyncIterator in current) {
return this.parsePlainObject(id, current, !!currentClass);
}
throw new SerovalUnsupportedTypeError(current);
}
protected async parseFunction(current: unknown): Promise<SerovalNode> {
const ref = this.getReference(current);
if (ref.type !== ParserNodeType.Fresh) {
return ref.value;
}
const plugin = await this.parsePlugin(ref.value, current);
if (plugin) {
return plugin;
}
throw new SerovalUnsupportedTypeError(current);
}
async parse<T>(current: T): Promise<SerovalNode> {
try {
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 = this.getReference(current);
return ref.type === 0
? await this.parseObject(ref.value, current as object)
: ref.value;
}
return NULL_NODE;
}
case 'symbol':
return this.parseWellKnownSymbol(current);
case 'function':
return this.parseFunction(current);
default:
throw new SerovalUnsupportedTypeError(current);
}
} catch (error) {
throw new SerovalParserError(error);
}
}
}