diff --git a/packages/seroval/src/core/base/async.ts b/packages/seroval/src/core/base/async.ts index 7235e5bd..ac10054d 100644 --- a/packages/seroval/src/core/base/async.ts +++ b/packages/seroval/src/core/base/async.ts @@ -60,8 +60,6 @@ import type { SerovalResponseNode, SerovalSetNode, SerovalDataViewNode, - SerovalIndexedValueNode, - SerovalReferenceNode, } from '../types'; import { createURLNode, createURLSearchParamsNode, createDOMExceptionNode } from '../web-api'; @@ -71,14 +69,6 @@ type ObjectLikeNode = | SerovalPromiseNode; export default abstract class BaseAsyncParserContext extends BaseParserContext { - protected abstract getReference( - current: T, - ): number | SerovalIndexedValueNode | SerovalReferenceNode; - - protected abstract getStrictReference( - current: T, - ): SerovalIndexedValueNode | SerovalReferenceNode; - private async parseItems( current: unknown[], ): Promise { diff --git a/packages/seroval/src/core/base/sync.ts b/packages/seroval/src/core/base/sync.ts index fa3db5ba..7435f12c 100644 --- a/packages/seroval/src/core/base/sync.ts +++ b/packages/seroval/src/core/base/sync.ts @@ -51,8 +51,6 @@ import type { SerovalTypedArrayNode, SerovalBigIntTypedArrayNode, SerovalDataViewNode, - SerovalIndexedValueNode, - SerovalReferenceNode, } from '../types'; import { createDOMExceptionNode, createURLNode, createURLSearchParamsNode } from '../web-api'; @@ -65,14 +63,6 @@ export interface BaseSyncParserContextOptions extends BaseParserContextOptions { } export default abstract class BaseSyncParserContext extends BaseParserContext { - protected abstract getReference( - current: T, - ): number | SerovalIndexedValueNode | SerovalReferenceNode; - - protected abstract getStrictReference( - current: T, - ): SerovalIndexedValueNode | SerovalReferenceNode; - protected parseItems( current: unknown[], ): SerovalNode[] { diff --git a/packages/seroval/src/core/cross/async.ts b/packages/seroval/src/core/cross/async.ts index feb22f5f..89c9c88a 100644 --- a/packages/seroval/src/core/cross/async.ts +++ b/packages/seroval/src/core/cross/async.ts @@ -1,26 +1,9 @@ import BaseAsyncParserContext from '../base/async'; -import type { SerovalIndexedValueNode, SerovalReferenceNode } from '../types'; import type { CrossParserContextOptions } from './cross-parser'; -import { getCrossReference, getStrictCrossReference } from './cross-parser'; import type { SerovalMode } from '../plugin'; export type CrossAsyncParserContextOptions = CrossParserContextOptions export default class CrossAsyncParserContext extends BaseAsyncParserContext { readonly mode: SerovalMode = 'cross'; - - refs: Map; - - constructor(options: CrossAsyncParserContextOptions) { - super(options); - this.refs = options.refs || new Map(); - } - - protected getReference(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode { - return getCrossReference(this.refs, current); - } - - protected getStrictReference(current: T): SerovalIndexedValueNode | SerovalReferenceNode { - return getStrictCrossReference(this.refs, current); - } } diff --git a/packages/seroval/src/core/cross/cross-parser.ts b/packages/seroval/src/core/cross/cross-parser.ts index 64a857ac..99932aa7 100644 --- a/packages/seroval/src/core/cross/cross-parser.ts +++ b/packages/seroval/src/core/cross/cross-parser.ts @@ -1,7 +1,4 @@ -import { createIndexedValueNode, createReferenceNode } from '../base-primitives'; import type { BaseParserContextOptions } from '../parser-context'; -import { hasReferenceID } from '../reference'; -import type { SerovalIndexedValueNode, SerovalReferenceNode } from '../types'; export interface CrossParserContextOptions extends BaseParserContextOptions { refs?: Map; @@ -10,32 +7,3 @@ export interface CrossParserContextOptions extends BaseParserContextOptions { export interface CrossContextOptions { scopeId?: string; } - -export function getCrossReference( - refs: Map, - value: T, -): number | SerovalIndexedValueNode | SerovalReferenceNode { - const registeredID = refs.get(value); - if (registeredID != null) { - return createIndexedValueNode(registeredID); - } - const id = refs.size; - refs.set(value, id); - if (hasReferenceID(value)) { - return createReferenceNode(id, value); - } - return id; -} - -export function getStrictCrossReference( - refs: Map, - value: T, -): SerovalIndexedValueNode | SerovalReferenceNode { - const id = refs.get(value); - if (id != null) { - return createIndexedValueNode(id); - } - const newID = refs.size; - refs.set(value, newID); - return createReferenceNode(newID, value); -} diff --git a/packages/seroval/src/core/cross/index.ts b/packages/seroval/src/core/cross/index.ts index d18b1109..9c20f5c6 100644 --- a/packages/seroval/src/core/cross/index.ts +++ b/packages/seroval/src/core/cross/index.ts @@ -15,12 +15,17 @@ export function crossSerialize( source: T, options: CrossSerializeOptions = {}, ): string { - const ctx = new SyncCrossParserContext(options); + const ctx = new SyncCrossParserContext({ + plugins: options.plugins, + disabledFeatures: options.disabledFeatures, + refs: options.refs, + }); const tree = ctx.parse(source); const serial = new CrossSerializerContext({ plugins: options.plugins, features: ctx.features, scopeId: options.scopeId, + markedRefs: ctx.marked, }); return serial.serializeTop(tree); } @@ -33,12 +38,17 @@ export async function crossSerializeAsync( source: T, options: CrossSerializeAsyncOptions = {}, ): Promise { - const ctx = new AsyncCrossParserContext(options); + const ctx = new AsyncCrossParserContext({ + plugins: options.plugins, + disabledFeatures: options.disabledFeatures, + refs: options.refs, + }); const tree = await ctx.parse(source); const serial = new CrossSerializerContext({ plugins: options.plugins, features: ctx.features, scopeId: options.scopeId, + markedRefs: ctx.marked, }); return serial.serializeTop(tree); } @@ -106,6 +116,7 @@ export function crossSerializeStream( plugins: options.plugins, features: ctx.features, scopeId: options.scopeId, + markedRefs: ctx.marked, }); options.onSerialize( diff --git a/packages/seroval/src/core/cross/serialize.ts b/packages/seroval/src/core/cross/serialize.ts index a1e07a4a..d3afca0c 100644 --- a/packages/seroval/src/core/cross/serialize.ts +++ b/packages/seroval/src/core/cross/serialize.ts @@ -38,10 +38,6 @@ export default class CrossSerializerContext extends BaseSerializerContext { this.scopeId = options.scopeId; } - markRef(): void { - // no-op - } - getRefParam(id: number): string { return GLOBAL_CONTEXT_REFERENCES + '[' + id + ']'; } diff --git a/packages/seroval/src/core/cross/stream.ts b/packages/seroval/src/core/cross/stream.ts index efb40792..018f4d59 100644 --- a/packages/seroval/src/core/cross/stream.ts +++ b/packages/seroval/src/core/cross/stream.ts @@ -1,29 +1,9 @@ -import type { - SerovalIndexedValueNode, - SerovalReferenceNode, -} from '../types'; import type { BaseStreamParserContextOptions } from '../base/stream'; import BaseStreamParserContext from '../base/stream'; import type { SerovalMode } from '../plugin'; -import { getCrossReference, getStrictCrossReference } from './cross-parser'; export type CrossStreamParserContextOptions = BaseStreamParserContextOptions export default class CrossStreamParserContext extends BaseStreamParserContext { readonly mode: SerovalMode = 'cross'; - - refs: Map; - - constructor(options: CrossStreamParserContextOptions) { - super(options); - this.refs = options.refs || new Map(); - } - - protected getReference(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode { - return getCrossReference(this.refs, current); - } - - protected getStrictReference(current: T): SerovalIndexedValueNode | SerovalReferenceNode { - return getStrictCrossReference(this.refs, current); - } } diff --git a/packages/seroval/src/core/cross/sync.ts b/packages/seroval/src/core/cross/sync.ts index e47ffb46..4698c90d 100644 --- a/packages/seroval/src/core/cross/sync.ts +++ b/packages/seroval/src/core/cross/sync.ts @@ -1,25 +1,9 @@ import BaseSyncParserContext from '../base/sync'; import type { SerovalMode } from '../plugin'; -import type { SerovalIndexedValueNode, SerovalReferenceNode } from '../types'; -import { getStrictCrossReference, type CrossParserContextOptions, getCrossReference } from './cross-parser'; +import type { CrossParserContextOptions } from './cross-parser'; export type CrossSyncParserContextOptions = CrossParserContextOptions export default class CrossSyncParserContext extends BaseSyncParserContext { readonly mode: SerovalMode = 'cross'; - - refs: Map; - - constructor(options: CrossSyncParserContextOptions) { - super(options); - this.refs = options.refs || new Map(); - } - - protected getReference(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode { - return getCrossReference(this.refs, current); - } - - protected getStrictReference(current: T): SerovalIndexedValueNode | SerovalReferenceNode { - return getStrictCrossReference(this.refs, current); - } } diff --git a/packages/seroval/src/core/parser-context.ts b/packages/seroval/src/core/parser-context.ts index 565fe266..5ee144c1 100644 --- a/packages/seroval/src/core/parser-context.ts +++ b/packages/seroval/src/core/parser-context.ts @@ -1,15 +1,14 @@ +import { createIndexedValueNode, createReferenceNode } from './base-primitives'; import { ALL_ENABLED, BIGINT_FLAG, Feature } from './compat'; import { ERROR_CONSTRUCTOR_STRING } from './constants'; import type { Plugin, PluginAccessOptions, SerovalMode } from './plugin'; +import { hasReferenceID } from './reference'; import { getErrorConstructor } from './shared'; - -export interface ParserReference { - ids: Map; - marked: Set; -} +import type { SerovalIndexedValueNode, SerovalReferenceNode } from './types'; export interface BaseParserContextOptions extends PluginAccessOptions { disabledFeatures?: number; + refs?: Map; } export abstract class BaseParserContext implements PluginAccessOptions { @@ -17,11 +16,49 @@ export abstract class BaseParserContext implements PluginAccessOptions { features: number; + marked = new Set(); + + refs: Map; + plugins?: Plugin[] | undefined; constructor(options: BaseParserContextOptions) { this.plugins = options.plugins; this.features = ALL_ENABLED ^ (options.disabledFeatures || 0); + this.refs = options.refs || new Map(); + } + + protected markRef(id: number): void { + this.marked.add(id); + } + + protected isMarked(id: number): boolean { + return this.marked.has(id); + } + + protected getReference(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode { + const registeredID = this.refs.get(current); + if (registeredID != null) { + this.markRef(registeredID); + return createIndexedValueNode(registeredID); + } + const id = this.refs.size; + this.refs.set(current, id); + if (hasReferenceID(current)) { + return createReferenceNode(id, current); + } + return id; + } + + protected getStrictReference(current: T): SerovalIndexedValueNode | SerovalReferenceNode { + const registeredID = this.refs.get(current); + if (registeredID != null) { + this.markRef(registeredID); + return createIndexedValueNode(registeredID); + } + const id = this.refs.size; + this.refs.set(current, id); + return createReferenceNode(id, current); } /** diff --git a/packages/seroval/src/core/serializer-context.ts b/packages/seroval/src/core/serializer-context.ts index c93353f7..f26f2558 100644 --- a/packages/seroval/src/core/serializer-context.ts +++ b/packages/seroval/src/core/serializer-context.ts @@ -66,6 +66,7 @@ const PROMISE_REJECT = 'Promise.reject'; export interface BaseSerializerContextOptions extends PluginAccessOptions { features: number; + markedRefs: number[] | Set; } export default abstract class BaseSerializerContext implements PluginAccessOptions { @@ -94,9 +95,16 @@ export default abstract class BaseSerializerContext implements PluginAccessOptio plugins?: Plugin[] | undefined; + /** + * Refs that are...referenced + * @private + */ + marked: Set; + constructor(options: BaseSerializerContextOptions) { this.plugins = options.plugins; this.features = options.features; + this.marked = new Set(options.markedRefs); } abstract readonly mode: SerovalMode; @@ -107,7 +115,13 @@ export default abstract class BaseSerializerContext implements PluginAccessOptio * deciding whether or not we should generate * an identifier for the object */ - abstract markRef(id: number): void; + protected markRef(id: number): void { + this.marked.add(id); + } + + protected isMarked(id: number): boolean { + return this.marked.has(id); + } /** * Converts the ID of a reference into a identifier string @@ -197,6 +211,29 @@ export default abstract class BaseSerializerContext implements PluginAccessOptio this.createAssignment(this.getRefParam(ref) + '.' + key, value); } + /** + * Seroval dedupes references by keeping only one of the reference, + * and the rest reusing the id of the original. + * + * Normally, since serialization is depth-first process, + * it should always be able to serialize the original before + * the other references. + * + * However, Seroval also has another mechanism called "assignments" which + * defers serialization when a reference is made inside a temporal + * dead zone or when a recursion is detected. Most of the time, assignments + * will only use the reference not the original, the only exception + * here is Map which has a key-value pair, so there's a case where + * only one of the two has the original, the other being a reference + * that can cause an assignment to occur. + * + * `defer` will help us here by serializing the item without assigning + * it the position it's supposed to be assigned. The next reference + * will be replaced with the original. + * + * Take note that this only matters if the object in question has more than + * one deduped reference in the entire tree. + */ deferred = new Map(); defer(id: number, value: string): void { @@ -526,7 +563,7 @@ export default abstract class BaseSerializerContext implements PluginAccessOptio // assignment const parent = this.stack; this.stack = []; - if (val.t !== SerovalNodeType.IndexedValue && val.i != null) { + if (val.t !== SerovalNodeType.IndexedValue && val.i != null && this.isMarked(val.i)) { this.defer(val.i, this.serialize(val)); this.createSetAssignment(id, keyRef, this.getRefParam(val.i)); } else { @@ -542,7 +579,7 @@ export default abstract class BaseSerializerContext implements PluginAccessOptio // Reset stack for the key serialization const parent = this.stack; this.stack = []; - if (val.t !== SerovalNodeType.IndexedValue && val.i != null) { + if (val.t !== SerovalNodeType.IndexedValue && val.i != null && this.isMarked(val.i)) { this.defer(val.i, this.serialize(val)); this.createSetAssignment(id, this.getRefParam(val.i), valueRef); } else { diff --git a/packages/seroval/src/core/tree/async.ts b/packages/seroval/src/core/tree/async.ts index 75c95294..52ce872f 100644 --- a/packages/seroval/src/core/tree/async.ts +++ b/packages/seroval/src/core/tree/async.ts @@ -1,29 +1,9 @@ import BaseAsyncParserContext from '../base/async'; import type { BaseParserContextOptions } from '../parser-context'; -import type { SerovalIndexedValueNode, SerovalReferenceNode } from '../types'; import type { SerovalMode } from '../plugin'; -import { getStrictVanillaReference, getVanillaReference } from './parser'; -export type AsyncParserContextOptions = BaseParserContextOptions +export type AsyncParserContextOptions = Omit export default class AsyncParserContext extends BaseAsyncParserContext { readonly mode: SerovalMode = 'vanilla'; - - /** - * @private - */ - ids: Map = new Map(); - - /** - * @private - */ - marked: Set = new Set(); - - protected getReference(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode { - return getVanillaReference(this.ids, this.marked, current); - } - - protected getStrictReference(current: T): SerovalIndexedValueNode | SerovalReferenceNode { - return getStrictVanillaReference(this.ids, this.marked, current); - } } diff --git a/packages/seroval/src/core/tree/index.ts b/packages/seroval/src/core/tree/index.ts index 9b332a38..22ba6e7e 100644 --- a/packages/seroval/src/core/tree/index.ts +++ b/packages/seroval/src/core/tree/index.ts @@ -11,7 +11,10 @@ export function serialize( source: T, options: SyncParserContextOptions = {}, ): string { - const ctx = new SyncParserContext(options); + const ctx = new SyncParserContext({ + plugins: options.plugins, + disabledFeatures: options.disabledFeatures, + }); const tree = ctx.parse(source); const serial = new VanillaSerializerContext({ plugins: options.plugins, @@ -25,7 +28,10 @@ export async function serializeAsync( source: T, options: AsyncParserContextOptions = {}, ): Promise { - const ctx = new AsyncParserContext(options); + const ctx = new AsyncParserContext({ + plugins: options.plugins, + disabledFeatures: options.disabledFeatures, + }); const tree = await ctx.parse(source); const serial = new VanillaSerializerContext({ plugins: options.plugins, @@ -50,7 +56,10 @@ export function toJSON( source: T, options: SyncParserContextOptions = {}, ): SerovalJSON { - const ctx = new SyncParserContext(options); + const ctx = new SyncParserContext({ + plugins: options.plugins, + disabledFeatures: options.disabledFeatures, + }); return { t: ctx.parse(source), f: ctx.features, @@ -62,7 +71,10 @@ export async function toJSONAsync( source: T, options: AsyncParserContextOptions = {}, ): Promise { - const ctx = new AsyncParserContext(options); + const ctx = new AsyncParserContext({ + plugins: options.plugins, + disabledFeatures: options.disabledFeatures, + }); return { t: await ctx.parse(source), f: ctx.features, diff --git a/packages/seroval/src/core/tree/serialize.ts b/packages/seroval/src/core/tree/serialize.ts index 4fa6671f..bf5d7b89 100644 --- a/packages/seroval/src/core/tree/serialize.ts +++ b/packages/seroval/src/core/tree/serialize.ts @@ -15,9 +15,7 @@ import type { SerovalMode } from '../plugin'; import { Feature } from '../compat'; import { SerovalNodeType } from '../constants'; -export interface VanillaSerializerContextOptions extends BaseSerializerContextOptions { - markedRefs: number[] | Set; -} +export type VanillaSerializerContextOptions = BaseSerializerContextOptions export default class VanillaSerializerContext extends BaseSerializerContext { readonly mode: SerovalMode = 'cross'; @@ -34,23 +32,12 @@ export default class VanillaSerializerContext extends BaseSerializerContext { */ valid: (number | undefined)[] = []; - /** - * Refs that are...referenced - * @private - */ - marked: Set; - /** * Variables * @private */ vars: (string | undefined)[] = []; - constructor(options: VanillaSerializerContextOptions) { - super(options); - this.marked = new Set(options.markedRefs); - } - /** * Increments the number of references the referenced value has */ @@ -88,7 +75,7 @@ export default class VanillaSerializerContext extends BaseSerializerContext { index: number, value: string, ): string { - if (this.marked.has(index)) { + if (this.isMarked(index)) { return this.getRefParam(index) + '=' + value; } return value; diff --git a/packages/seroval/src/core/tree/sync.ts b/packages/seroval/src/core/tree/sync.ts index adb4947a..8e730bfd 100644 --- a/packages/seroval/src/core/tree/sync.ts +++ b/packages/seroval/src/core/tree/sync.ts @@ -1,29 +1,9 @@ import BaseSyncParserContext from '../base/sync'; import type { BaseParserContextOptions } from '../parser-context'; import type { SerovalMode } from '../plugin'; -import type { SerovalIndexedValueNode, SerovalReferenceNode } from '../types'; -import { getStrictVanillaReference, getVanillaReference } from './parser'; -export type SyncParserContextOptions = BaseParserContextOptions +export type SyncParserContextOptions = Omit; export default class SyncParserContext extends BaseSyncParserContext { readonly mode: SerovalMode = 'vanilla'; - - /** - * @private - */ - ids: Map = new Map(); - - /** - * @private - */ - marked: Set = new Set(); - - protected getReference(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode { - return getVanillaReference(this.ids, this.marked, current); - } - - protected getStrictReference(current: T): SerovalIndexedValueNode | SerovalReferenceNode { - return getStrictVanillaReference(this.ids, this.marked, current); - } }