Skip to content

Commit

Permalink
feat(core): introduce async version of plugin dispatch mechanism (#3994)
Browse files Browse the repository at this point in the history
Refs #3993
  • Loading branch information
char0n committed Apr 2, 2024
1 parent 7dd0bbd commit 36011bc
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 51 deletions.
2 changes: 1 addition & 1 deletion packages/apidom-ast/src/index.ts
Expand Up @@ -71,7 +71,6 @@ export { default as ParseResult } from './ParseResult';
export { isParseResult, isLiteral, isPoint, isPosition } from './predicates';
// AST traversal related exports
export {
customPromisifySymbol,
BREAK,
mergeAll as mergeAllVisitors,
getVisitFn,
Expand All @@ -80,3 +79,4 @@ export {
isNode,
cloneNode,
} from './traversal/visitor';
export type { MergeAllSync, MergeAllAsync } from './traversal/visitor';
32 changes: 22 additions & 10 deletions packages/apidom-ast/src/traversal/visitor.ts
Expand Up @@ -6,8 +6,6 @@ import { ApiDOMStructuredError } from '@swagger-api/apidom-error';
* SPDX-License-Identifier: MIT
*/

export const customPromisifySymbol: unique symbol = Symbol.for('nodejs.util.promisify.custom');

// getVisitFn :: (Visitor, String, Boolean) -> Function
export const getVisitFn = (visitor: any, type: string, isLeaving: boolean) => {
const typeVisitor = visitor[type];
Expand Down Expand Up @@ -60,7 +58,7 @@ export const cloneNode = (node: any) =>
* `exposeEdits=true` can be used to exoise the edited node from the previous visitors.
*/

interface MergeAllBase {
export interface MergeAllSync {
(
visitors: any[],
options?: {
Expand All @@ -75,15 +73,27 @@ interface MergeAllBase {
enter: (node: any, ...rest: any[]) => any;
leave: (node: any, ...rest: any[]) => any;
};
[key: symbol]: MergeAllAsync;
}

interface MergeAllPromisify {
[customPromisifySymbol]: MergeAllBase;
export interface MergeAllAsync {
(
visitors: any[],
options?: {
visitFnGetter?: typeof getVisitFn;
nodeTypeGetter?: typeof getNodeType;
breakSymbol?: typeof BREAK;
deleteNodeSymbol?: any;
skipVisitingNodeSymbol?: boolean;
exposeEdits?: boolean;
},
): {
enter: (node: any, ...rest: any[]) => Promise<any>;
leave: (node: any, ...rest: any[]) => Promise<any>;
};
}

type MergeAll = MergeAllBase & MergeAllPromisify;

export const mergeAll: MergeAll = ((
export const mergeAll: MergeAllSync = ((
visitors: any[],
{
visitFnGetter = getVisitFn,
Expand Down Expand Up @@ -150,9 +160,9 @@ export const mergeAll: MergeAll = ((
return undefined;
},
};
}) as MergeAll;
}) as MergeAllSync;

mergeAll[customPromisifySymbol] = (
const mergeAllAsync: MergeAllAsync = (
visitors: any[],
{
visitFnGetter = getVisitFn,
Expand Down Expand Up @@ -223,6 +233,8 @@ mergeAll[customPromisifySymbol] = (
};
};

mergeAll[Symbol.for('nodejs.util.promisify.custom')] = mergeAllAsync;

/* eslint-disable no-continue, no-param-reassign */
/**
* visit() will walk through an AST using a preorder depth first traversal, calling
Expand Down
Expand Up @@ -60,7 +60,7 @@ class OpenAPI31ToOpenAPI30ConvertStrategy extends ConvertStrategy {

async convert(file: IFile): Promise<ParseResultElement> {
const annotations: AnnotationElement[] = [];
const parseResultElement = dispatchRefractorPlugins(
const parseResultElement: ParseResultElement = dispatchRefractorPlugins(
cloneDeep(file.parseResult),
[
openAPIVersionRefractorPlugin(),
Expand Down
8 changes: 7 additions & 1 deletion packages/apidom-core/src/index.ts
@@ -1,4 +1,10 @@
export { dispatchPlugins as dispatchRefractorPlugins } from './refractor/plugins/utils';
export { dispatchPluginsSync as dispatchRefractorPlugins } from './refractor/plugins/dispatcher';
export type {
DispatchPluginsSync,
DispatchPluginsAsync,
DispatchPluginsOptions,
} from './refractor/plugins/dispatcher';

export { default as refractorPluginElementIdentity } from './refractor/plugins/element-identity';
export { default as refractorPluginSemanticElementIdentity } from './refractor/plugins/semantic-element-identity';

Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-core/src/refractor/index.ts
@@ -1,6 +1,6 @@
import { Element } from 'minim';

import { dispatchPlugins } from './plugins/utils';
import { dispatchPluginsSync } from './plugins/dispatcher';
import { getNodeType } from '../traversal/visitor';
import { cloneDeep } from '../clone';
import { isElement } from '../predicates';
Expand Down Expand Up @@ -32,7 +32,7 @@ const refract = (value: any, { Type, plugins = [] }: RefractOptions): Element =>
* Run plugins only when necessary.
* Running plugins visitors means extra single traversal === performance hit.
*/
return dispatchPlugins(element, plugins, {
return dispatchPluginsSync(element, plugins, {
toolboxCreator: createToolbox,
visitorOptions: { nodeTypeGetter: getNodeType },
});
Expand Down
87 changes: 87 additions & 0 deletions packages/apidom-core/src/refractor/plugins/dispatcher/index.ts
@@ -0,0 +1,87 @@
import { Element } from 'minim';
import { mergeDeepRight, propOr } from 'ramda';
import { invokeArgs } from 'ramda-adjunct';

import createToolbox from '../../toolbox';
import { getNodeType, mergeAllVisitors, visit } from '../../../traversal/visitor';

export interface DispatchPluginsOptions {
toolboxCreator: typeof createToolbox;
visitorOptions: {
nodeTypeGetter: typeof getNodeType;
exposeEdits: boolean;
};
}

export interface DispatchPluginsSync {
<T extends Element, U extends Element = Element>(
element: T,
plugins: ((toolbox: any) => object)[],
options?: Record<string, unknown>,
): U;
[key: symbol]: DispatchPluginsAsync;
}

export interface DispatchPluginsAsync {
<T extends Element, U extends Element = Element>(
element: T,
plugins: ((toolbox: any) => object)[],
options?: Record<string, unknown>,
): Promise<U>;
}

const defaultDispatchPluginsOptions: DispatchPluginsOptions = {
toolboxCreator: createToolbox,
visitorOptions: {
nodeTypeGetter: getNodeType,
exposeEdits: true,
},
};

export const dispatchPluginsSync: DispatchPluginsSync = ((element, plugins, options = {}) => {
if (plugins.length === 0) return element;

const mergedOptions = mergeDeepRight(
defaultDispatchPluginsOptions,
options,
) as DispatchPluginsOptions;
const { toolboxCreator, visitorOptions } = mergedOptions;
const toolbox = toolboxCreator();
const pluginsSpecs = plugins.map((plugin) => plugin(toolbox));
const mergedPluginsVisitor = mergeAllVisitors(pluginsSpecs.map(propOr({}, 'visitor')), {
...visitorOptions,
});

pluginsSpecs.forEach(invokeArgs(['pre'], []));
const newElement = visit(element, mergedPluginsVisitor, visitorOptions as any);
pluginsSpecs.forEach(invokeArgs(['post'], []));
return newElement;
}) as DispatchPluginsSync;

export const dispatchPluginsAsync: DispatchPluginsAsync = async (
element,
plugins,
options = {},
) => {
if (plugins.length === 0) return element;

const mergedOptions = mergeDeepRight(
defaultDispatchPluginsOptions,
options,
) as DispatchPluginsOptions;
const { toolboxCreator, visitorOptions } = mergedOptions;
const toolbox = toolboxCreator();
const pluginsSpecs = plugins.map((plugin) => plugin(toolbox));
const mergeAllVisitorsAsync = mergeAllVisitors[Symbol.for('nodejs.util.promisify.custom')];
const visitAsync = (visit as any)[Symbol.for('nodejs.util.promisify.custom')];
const mergedPluginsVisitor = mergeAllVisitorsAsync(pluginsSpecs.map(propOr({}, 'visitor')), {
...visitorOptions,
});

await Promise.allSettled(pluginsSpecs.map(invokeArgs(['pre'], [])));
const newElement = await visitAsync(element, mergedPluginsVisitor, visitorOptions as any);
await Promise.allSettled(pluginsSpecs.map(invokeArgs(['post'], [])));
return newElement;
};

dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')] = dispatchPluginsAsync;
36 changes: 0 additions & 36 deletions packages/apidom-core/src/refractor/plugins/utils/index.ts

This file was deleted.

59 changes: 59 additions & 0 deletions packages/apidom-core/test/refractor/plugins/dispatcher/index.ts
@@ -0,0 +1,59 @@
import sinon from 'sinon';
import { assert } from 'chai';

import {
NumberElement,
ObjectElement,
toValue,
dispatchRefractorPlugins as dispatchPluginsSync,
} from '../../../../src';

const dispatchPluginsAsync = dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')];

describe('refrator', function () {
context('plugins', function () {
context('dispatcher', function () {
context('dispatchPluginsSync', function () {
specify('should dispatch plugins synchronously', function () {
const preSpy = sinon.spy();
const postSpy = sinon.spy();
const NumberElementSpy = sinon.spy(() => new NumberElement(2));
const plugin = () => ({
pre: preSpy,
visitor: {
NumberElement: NumberElementSpy,
},
post: postSpy,
});
const objectElement = new ObjectElement({ a: 1 });
const result = dispatchPluginsSync(objectElement, [plugin]);

assert.isTrue(preSpy.calledBefore(NumberElementSpy));
assert.isTrue(postSpy.calledAfter(NumberElementSpy));
assert.deepEqual(toValue(result), { a: 2 });
});
});

context('dispatchPluginsASync', function () {
specify('should dispatch plugins asynchronously', async function () {
const preSpy = sinon.spy();
const postSpy = sinon.spy();
const NumberElementSpy = sinon.spy(async () => new NumberElement(2));
const plugin = () => ({
pre: preSpy,
visitor: {
NumberElement: NumberElementSpy,
},
post: postSpy,
});
const objectElement = new ObjectElement({ a: 1 });
const result = await dispatchPluginsAsync(objectElement, [plugin]);

assert.isTrue(preSpy.calledBefore(NumberElementSpy));
assert.isTrue(postSpy.calledAfter(NumberElementSpy));
assert.deepEqual(toValue(result), { a: 2 });
});
});
});
});
});

0 comments on commit 36011bc

Please sign in to comment.