Skip to content

Commit

Permalink
Deduping reference generation; Fix defer
Browse files Browse the repository at this point in the history
  • Loading branch information
lxsmnsyc committed Oct 17, 2023
1 parent 0c86fe6 commit 987cef7
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 181 deletions.
10 changes: 0 additions & 10 deletions packages/seroval/src/core/base/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ import type {
SerovalResponseNode,
SerovalSetNode,
SerovalDataViewNode,
SerovalIndexedValueNode,
SerovalReferenceNode,
} from '../types';
import { createURLNode, createURLSearchParamsNode, createDOMExceptionNode } from '../web-api';

Expand All @@ -71,14 +69,6 @@ type ObjectLikeNode =
| SerovalPromiseNode;

export default abstract class BaseAsyncParserContext extends BaseParserContext {
protected abstract getReference<T>(
current: T,
): number | SerovalIndexedValueNode | SerovalReferenceNode;

protected abstract getStrictReference<T>(
current: T,
): SerovalIndexedValueNode | SerovalReferenceNode;

private async parseItems(
current: unknown[],
): Promise<SerovalNode[]> {
Expand Down
10 changes: 0 additions & 10 deletions packages/seroval/src/core/base/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ import type {
SerovalTypedArrayNode,
SerovalBigIntTypedArrayNode,
SerovalDataViewNode,
SerovalIndexedValueNode,
SerovalReferenceNode,
} from '../types';
import { createDOMExceptionNode, createURLNode, createURLSearchParamsNode } from '../web-api';

Expand All @@ -65,14 +63,6 @@ export interface BaseSyncParserContextOptions extends BaseParserContextOptions {
}

export default abstract class BaseSyncParserContext extends BaseParserContext {
protected abstract getReference<T>(
current: T,
): number | SerovalIndexedValueNode | SerovalReferenceNode;

protected abstract getStrictReference<T>(
current: T,
): SerovalIndexedValueNode | SerovalReferenceNode;

protected parseItems(
current: unknown[],
): SerovalNode[] {
Expand Down
17 changes: 0 additions & 17 deletions packages/seroval/src/core/cross/async.ts
Original file line number Diff line number Diff line change
@@ -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<unknown, number>;

constructor(options: CrossAsyncParserContextOptions) {
super(options);
this.refs = options.refs || new Map<unknown, number>();
}

protected getReference<T>(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode {
return getCrossReference(this.refs, current);
}

protected getStrictReference<T>(current: T): SerovalIndexedValueNode | SerovalReferenceNode {
return getStrictCrossReference(this.refs, current);
}
}
32 changes: 0 additions & 32 deletions packages/seroval/src/core/cross/cross-parser.ts
Original file line number Diff line number Diff line change
@@ -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<unknown, number>;
Expand All @@ -10,32 +7,3 @@ export interface CrossParserContextOptions extends BaseParserContextOptions {
export interface CrossContextOptions {
scopeId?: string;
}

export function getCrossReference<T>(
refs: Map<unknown, number>,
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<T>(
refs: Map<unknown, number>,
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);
}
15 changes: 13 additions & 2 deletions packages/seroval/src/core/cross/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ export function crossSerialize<T>(
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);
}
Expand All @@ -33,12 +38,17 @@ export async function crossSerializeAsync<T>(
source: T,
options: CrossSerializeAsyncOptions = {},
): Promise<string> {
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);
}
Expand Down Expand Up @@ -106,6 +116,7 @@ export function crossSerializeStream<T>(
plugins: options.plugins,
features: ctx.features,
scopeId: options.scopeId,
markedRefs: ctx.marked,
});

options.onSerialize(
Expand Down
4 changes: 0 additions & 4 deletions packages/seroval/src/core/cross/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ']';
}
Expand Down
20 changes: 0 additions & 20 deletions packages/seroval/src/core/cross/stream.ts
Original file line number Diff line number Diff line change
@@ -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<unknown, number>;

constructor(options: CrossStreamParserContextOptions) {
super(options);
this.refs = options.refs || new Map<unknown, number>();
}

protected getReference<T>(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode {
return getCrossReference(this.refs, current);
}

protected getStrictReference<T>(current: T): SerovalIndexedValueNode | SerovalReferenceNode {
return getStrictCrossReference(this.refs, current);
}
}
18 changes: 1 addition & 17 deletions packages/seroval/src/core/cross/sync.ts
Original file line number Diff line number Diff line change
@@ -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<unknown, number>;

constructor(options: CrossSyncParserContextOptions) {
super(options);
this.refs = options.refs || new Map<unknown, number>();
}

protected getReference<T>(current: T): number | SerovalIndexedValueNode | SerovalReferenceNode {
return getCrossReference(this.refs, current);
}

protected getStrictReference<T>(current: T): SerovalIndexedValueNode | SerovalReferenceNode {
return getStrictCrossReference(this.refs, current);
}
}
47 changes: 42 additions & 5 deletions packages/seroval/src/core/parser-context.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,64 @@
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<unknown, number>;
marked: Set<number>;
}
import type { SerovalIndexedValueNode, SerovalReferenceNode } from './types';

export interface BaseParserContextOptions extends PluginAccessOptions {
disabledFeatures?: number;
refs?: Map<unknown, number>;
}

export abstract class BaseParserContext implements PluginAccessOptions {
abstract readonly mode: SerovalMode;

features: number;

marked = new Set<number>();

refs: Map<unknown, number>;

plugins?: Plugin<any, any>[] | undefined;

constructor(options: BaseParserContextOptions) {
this.plugins = options.plugins;
this.features = ALL_ENABLED ^ (options.disabledFeatures || 0);
this.refs = options.refs || new Map<unknown, number>();
}

protected markRef(id: number): void {
this.marked.add(id);
}

protected isMarked(id: number): boolean {
return this.marked.has(id);
}

protected getReference<T>(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<T>(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);
}

/**
Expand Down
43 changes: 40 additions & 3 deletions packages/seroval/src/core/serializer-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const PROMISE_REJECT = 'Promise.reject';

export interface BaseSerializerContextOptions extends PluginAccessOptions {
features: number;
markedRefs: number[] | Set<number>;
}

export default abstract class BaseSerializerContext implements PluginAccessOptions {
Expand Down Expand Up @@ -94,9 +95,16 @@ export default abstract class BaseSerializerContext implements PluginAccessOptio

plugins?: Plugin<any, any>[] | undefined;

/**
* Refs that are...referenced
* @private
*/
marked: Set<number>;

constructor(options: BaseSerializerContextOptions) {
this.plugins = options.plugins;
this.features = options.features;
this.marked = new Set(options.markedRefs);
}

abstract readonly mode: SerovalMode;
Expand All @@ -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
Expand Down Expand Up @@ -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<number, string>();

defer(id: number, value: string): void {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 987cef7

Please sign in to comment.