Skip to content

Commit 36011bc

Browse files
authored
feat(core): introduce async version of plugin dispatch mechanism (#3994)
Refs #3993
1 parent 7dd0bbd commit 36011bc

File tree

8 files changed

+179
-51
lines changed

8 files changed

+179
-51
lines changed

packages/apidom-ast/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ export { default as ParseResult } from './ParseResult';
7171
export { isParseResult, isLiteral, isPoint, isPosition } from './predicates';
7272
// AST traversal related exports
7373
export {
74-
customPromisifySymbol,
7574
BREAK,
7675
mergeAll as mergeAllVisitors,
7776
getVisitFn,
@@ -80,3 +79,4 @@ export {
8079
isNode,
8180
cloneNode,
8281
} from './traversal/visitor';
82+
export type { MergeAllSync, MergeAllAsync } from './traversal/visitor';

packages/apidom-ast/src/traversal/visitor.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { ApiDOMStructuredError } from '@swagger-api/apidom-error';
66
* SPDX-License-Identifier: MIT
77
*/
88

9-
export const customPromisifySymbol: unique symbol = Symbol.for('nodejs.util.promisify.custom');
10-
119
// getVisitFn :: (Visitor, String, Boolean) -> Function
1210
export const getVisitFn = (visitor: any, type: string, isLeaving: boolean) => {
1311
const typeVisitor = visitor[type];
@@ -60,7 +58,7 @@ export const cloneNode = (node: any) =>
6058
* `exposeEdits=true` can be used to exoise the edited node from the previous visitors.
6159
*/
6260

63-
interface MergeAllBase {
61+
export interface MergeAllSync {
6462
(
6563
visitors: any[],
6664
options?: {
@@ -75,15 +73,27 @@ interface MergeAllBase {
7573
enter: (node: any, ...rest: any[]) => any;
7674
leave: (node: any, ...rest: any[]) => any;
7775
};
76+
[key: symbol]: MergeAllAsync;
7877
}
7978

80-
interface MergeAllPromisify {
81-
[customPromisifySymbol]: MergeAllBase;
79+
export interface MergeAllAsync {
80+
(
81+
visitors: any[],
82+
options?: {
83+
visitFnGetter?: typeof getVisitFn;
84+
nodeTypeGetter?: typeof getNodeType;
85+
breakSymbol?: typeof BREAK;
86+
deleteNodeSymbol?: any;
87+
skipVisitingNodeSymbol?: boolean;
88+
exposeEdits?: boolean;
89+
},
90+
): {
91+
enter: (node: any, ...rest: any[]) => Promise<any>;
92+
leave: (node: any, ...rest: any[]) => Promise<any>;
93+
};
8294
}
8395

84-
type MergeAll = MergeAllBase & MergeAllPromisify;
85-
86-
export const mergeAll: MergeAll = ((
96+
export const mergeAll: MergeAllSync = ((
8797
visitors: any[],
8898
{
8999
visitFnGetter = getVisitFn,
@@ -150,9 +160,9 @@ export const mergeAll: MergeAll = ((
150160
return undefined;
151161
},
152162
};
153-
}) as MergeAll;
163+
}) as MergeAllSync;
154164

155-
mergeAll[customPromisifySymbol] = (
165+
const mergeAllAsync: MergeAllAsync = (
156166
visitors: any[],
157167
{
158168
visitFnGetter = getVisitFn,
@@ -223,6 +233,8 @@ mergeAll[customPromisifySymbol] = (
223233
};
224234
};
225235

236+
mergeAll[Symbol.for('nodejs.util.promisify.custom')] = mergeAllAsync;
237+
226238
/* eslint-disable no-continue, no-param-reassign */
227239
/**
228240
* visit() will walk through an AST using a preorder depth first traversal, calling

packages/apidom-converter/src/strategies/openapi-3-1-to-openapi-3-0-3/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class OpenAPI31ToOpenAPI30ConvertStrategy extends ConvertStrategy {
6060

6161
async convert(file: IFile): Promise<ParseResultElement> {
6262
const annotations: AnnotationElement[] = [];
63-
const parseResultElement = dispatchRefractorPlugins(
63+
const parseResultElement: ParseResultElement = dispatchRefractorPlugins(
6464
cloneDeep(file.parseResult),
6565
[
6666
openAPIVersionRefractorPlugin(),

packages/apidom-core/src/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export { dispatchPlugins as dispatchRefractorPlugins } from './refractor/plugins/utils';
1+
export { dispatchPluginsSync as dispatchRefractorPlugins } from './refractor/plugins/dispatcher';
2+
export type {
3+
DispatchPluginsSync,
4+
DispatchPluginsAsync,
5+
DispatchPluginsOptions,
6+
} from './refractor/plugins/dispatcher';
7+
28
export { default as refractorPluginElementIdentity } from './refractor/plugins/element-identity';
39
export { default as refractorPluginSemanticElementIdentity } from './refractor/plugins/semantic-element-identity';
410

packages/apidom-core/src/refractor/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Element } from 'minim';
22

3-
import { dispatchPlugins } from './plugins/utils';
3+
import { dispatchPluginsSync } from './plugins/dispatcher';
44
import { getNodeType } from '../traversal/visitor';
55
import { cloneDeep } from '../clone';
66
import { isElement } from '../predicates';
@@ -32,7 +32,7 @@ const refract = (value: any, { Type, plugins = [] }: RefractOptions): Element =>
3232
* Run plugins only when necessary.
3333
* Running plugins visitors means extra single traversal === performance hit.
3434
*/
35-
return dispatchPlugins(element, plugins, {
35+
return dispatchPluginsSync(element, plugins, {
3636
toolboxCreator: createToolbox,
3737
visitorOptions: { nodeTypeGetter: getNodeType },
3838
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Element } from 'minim';
2+
import { mergeDeepRight, propOr } from 'ramda';
3+
import { invokeArgs } from 'ramda-adjunct';
4+
5+
import createToolbox from '../../toolbox';
6+
import { getNodeType, mergeAllVisitors, visit } from '../../../traversal/visitor';
7+
8+
export interface DispatchPluginsOptions {
9+
toolboxCreator: typeof createToolbox;
10+
visitorOptions: {
11+
nodeTypeGetter: typeof getNodeType;
12+
exposeEdits: boolean;
13+
};
14+
}
15+
16+
export interface DispatchPluginsSync {
17+
<T extends Element, U extends Element = Element>(
18+
element: T,
19+
plugins: ((toolbox: any) => object)[],
20+
options?: Record<string, unknown>,
21+
): U;
22+
[key: symbol]: DispatchPluginsAsync;
23+
}
24+
25+
export interface DispatchPluginsAsync {
26+
<T extends Element, U extends Element = Element>(
27+
element: T,
28+
plugins: ((toolbox: any) => object)[],
29+
options?: Record<string, unknown>,
30+
): Promise<U>;
31+
}
32+
33+
const defaultDispatchPluginsOptions: DispatchPluginsOptions = {
34+
toolboxCreator: createToolbox,
35+
visitorOptions: {
36+
nodeTypeGetter: getNodeType,
37+
exposeEdits: true,
38+
},
39+
};
40+
41+
export const dispatchPluginsSync: DispatchPluginsSync = ((element, plugins, options = {}) => {
42+
if (plugins.length === 0) return element;
43+
44+
const mergedOptions = mergeDeepRight(
45+
defaultDispatchPluginsOptions,
46+
options,
47+
) as DispatchPluginsOptions;
48+
const { toolboxCreator, visitorOptions } = mergedOptions;
49+
const toolbox = toolboxCreator();
50+
const pluginsSpecs = plugins.map((plugin) => plugin(toolbox));
51+
const mergedPluginsVisitor = mergeAllVisitors(pluginsSpecs.map(propOr({}, 'visitor')), {
52+
...visitorOptions,
53+
});
54+
55+
pluginsSpecs.forEach(invokeArgs(['pre'], []));
56+
const newElement = visit(element, mergedPluginsVisitor, visitorOptions as any);
57+
pluginsSpecs.forEach(invokeArgs(['post'], []));
58+
return newElement;
59+
}) as DispatchPluginsSync;
60+
61+
export const dispatchPluginsAsync: DispatchPluginsAsync = async (
62+
element,
63+
plugins,
64+
options = {},
65+
) => {
66+
if (plugins.length === 0) return element;
67+
68+
const mergedOptions = mergeDeepRight(
69+
defaultDispatchPluginsOptions,
70+
options,
71+
) as DispatchPluginsOptions;
72+
const { toolboxCreator, visitorOptions } = mergedOptions;
73+
const toolbox = toolboxCreator();
74+
const pluginsSpecs = plugins.map((plugin) => plugin(toolbox));
75+
const mergeAllVisitorsAsync = mergeAllVisitors[Symbol.for('nodejs.util.promisify.custom')];
76+
const visitAsync = (visit as any)[Symbol.for('nodejs.util.promisify.custom')];
77+
const mergedPluginsVisitor = mergeAllVisitorsAsync(pluginsSpecs.map(propOr({}, 'visitor')), {
78+
...visitorOptions,
79+
});
80+
81+
await Promise.allSettled(pluginsSpecs.map(invokeArgs(['pre'], [])));
82+
const newElement = await visitAsync(element, mergedPluginsVisitor, visitorOptions as any);
83+
await Promise.allSettled(pluginsSpecs.map(invokeArgs(['post'], [])));
84+
return newElement;
85+
};
86+
87+
dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')] = dispatchPluginsAsync;

packages/apidom-core/src/refractor/plugins/utils/index.ts

-36
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import sinon from 'sinon';
2+
import { assert } from 'chai';
3+
4+
import {
5+
NumberElement,
6+
ObjectElement,
7+
toValue,
8+
dispatchRefractorPlugins as dispatchPluginsSync,
9+
} from '../../../../src';
10+
11+
const dispatchPluginsAsync = dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')];
12+
13+
describe('refrator', function () {
14+
context('plugins', function () {
15+
context('dispatcher', function () {
16+
context('dispatchPluginsSync', function () {
17+
specify('should dispatch plugins synchronously', function () {
18+
const preSpy = sinon.spy();
19+
const postSpy = sinon.spy();
20+
const NumberElementSpy = sinon.spy(() => new NumberElement(2));
21+
const plugin = () => ({
22+
pre: preSpy,
23+
visitor: {
24+
NumberElement: NumberElementSpy,
25+
},
26+
post: postSpy,
27+
});
28+
const objectElement = new ObjectElement({ a: 1 });
29+
const result = dispatchPluginsSync(objectElement, [plugin]);
30+
31+
assert.isTrue(preSpy.calledBefore(NumberElementSpy));
32+
assert.isTrue(postSpy.calledAfter(NumberElementSpy));
33+
assert.deepEqual(toValue(result), { a: 2 });
34+
});
35+
});
36+
37+
context('dispatchPluginsASync', function () {
38+
specify('should dispatch plugins asynchronously', async function () {
39+
const preSpy = sinon.spy();
40+
const postSpy = sinon.spy();
41+
const NumberElementSpy = sinon.spy(async () => new NumberElement(2));
42+
const plugin = () => ({
43+
pre: preSpy,
44+
visitor: {
45+
NumberElement: NumberElementSpy,
46+
},
47+
post: postSpy,
48+
});
49+
const objectElement = new ObjectElement({ a: 1 });
50+
const result = await dispatchPluginsAsync(objectElement, [plugin]);
51+
52+
assert.isTrue(preSpy.calledBefore(NumberElementSpy));
53+
assert.isTrue(postSpy.calledAfter(NumberElementSpy));
54+
assert.deepEqual(toValue(result), { a: 2 });
55+
});
56+
});
57+
});
58+
});
59+
});

0 commit comments

Comments
 (0)