Skip to content

Commit c7f87d8

Browse files
committed
Breaking: refactory of all main/worker hooks
1 parent dcad916 commit c7f87d8

File tree

14 files changed

+312
-154
lines changed

14 files changed

+312
-154
lines changed

docs/core.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/core.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

esm/custom.js

Lines changed: 69 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import '@ungap/with-resolvers';
22
import { $$ } from 'basic-devtools';
33

4-
import { assign, create, defineProperty, nodeInfo } from './utils.js';
4+
import { assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo } from './utils.js';
55
import { getDetails } from './script-handler.js';
66
import { registry as defaultRegistry, prefixes, configs } from './interpreters.js';
77
import { getRuntimeID } from './loader.js';
8-
import { io } from './interpreter/_utils.js';
98
import { addAllListeners } from './listeners.js';
109
import { Hook, XWorker } from './xworker.js';
10+
import { polluteJS, js as jsHooks, code as codeHooks } from './hooks.js';
1111
import workerURL from './worker/url.js';
1212

1313
export const CUSTOM_SELECTORS = [];
@@ -44,15 +44,15 @@ export const handleCustomType = (node) => {
4444
config,
4545
version,
4646
env,
47-
onInterpreterReady,
4847
onerror,
48+
hooks,
4949
} = options;
5050

5151
let error;
5252
try {
5353
const worker = workerURL(node);
5454
if (worker) {
55-
const xworker = XWorker.call(new Hook(null, options), worker, {
55+
const xworker = XWorker.call(new Hook(null, hooks), worker, {
5656
...nodeInfo(node, type),
5757
version,
5858
type: runtime,
@@ -83,71 +83,66 @@ export const handleCustomType = (node) => {
8383
engine.then((interpreter) => {
8484
const module = create(defaultRegistry.get(runtime));
8585

86-
const {
87-
onBeforeRun,
88-
onBeforeRunAsync,
89-
onAfterRun,
90-
onAfterRunAsync,
91-
} = options;
92-
93-
const hooks = new Hook(interpreter, options);
86+
const hook = new Hook(interpreter, hooks);
9487

9588
const XWorker = function XWorker(...args) {
96-
return Worker.apply(hooks, args);
89+
return Worker.apply(hook, args);
9790
};
9891

99-
// These two loops mimic a `new Map(arrayContent)` without needing
100-
// the new Map overhead so that [name, [before, after]] can be easily destructured
101-
// and new sync or async patches become easy to add (when the logic is the same).
102-
103-
// patch sync
104-
for (const [name, [before, after]] of [
105-
['run', [onBeforeRun, onAfterRun]],
106-
]) {
107-
const method = module[name];
108-
module[name] = function (interpreter, code, ...args) {
109-
if (before) before.call(this, resolved, node);
110-
const result = method.call(this, interpreter, code, ...args);
111-
if (after) after.call(this, resolved, node);
112-
return result;
113-
};
114-
}
115-
116-
// patch async
117-
for (const [name, [before, after]] of [
118-
['runAsync', [onBeforeRunAsync, onAfterRunAsync]],
119-
]) {
120-
const method = module[name];
121-
module[name] = async function (interpreter, code, ...args) {
122-
if (before) await before.call(this, resolved, node);
123-
const result = await method.call(
124-
this,
125-
interpreter,
126-
code,
127-
...args
128-
);
129-
if (after) await after.call(this, resolved, node);
130-
return result;
131-
};
132-
}
133-
134-
module.registerJSModule(interpreter, 'polyscript', { XWorker });
135-
13692
const resolved = {
137-
type,
138-
interpreter,
93+
...createResolved(
94+
module,
95+
type,
96+
structuredClone(configs.get(name)),
97+
interpreter,
98+
),
13999
XWorker,
140-
io: io.get(interpreter),
141-
config: structuredClone(configs.get(name)),
142-
run: module.run.bind(module, interpreter),
143-
runAsync: module.runAsync.bind(module, interpreter),
144-
runEvent: module.runEvent.bind(module, interpreter),
145100
};
146101

102+
module.registerJSModule(interpreter, 'polyscript', { XWorker });
103+
104+
// patch methods accordingly to hooks (and only if needed)
105+
for (const suffix of ['Run', 'RunAsync']) {
106+
const overload = createOverload(module, `r${suffix.slice(1)}`);
107+
108+
let before = '';
109+
let after = '';
110+
111+
for (const key of codeHooks) {
112+
const value = hooks?.main?.[key];
113+
if (value && key.endsWith(suffix)) {
114+
if (key.startsWith('codeBefore'))
115+
before = dedent(value());
116+
else
117+
after = dedent(value());
118+
}
119+
}
120+
121+
// append code that should be executed *after* first
122+
if (after) overload(after, false);
123+
124+
// prepend code that should be executed *before* (so that after is post-patched)
125+
if (before) overload(before, true);
126+
127+
let beforeCB, afterCB;
128+
// ignore onReady and onWorker
129+
for (let i = 2; i < jsHooks.length; i++) {
130+
const key = jsHooks[i];
131+
const value = hooks?.main?.[key];
132+
if (value && key.endsWith(suffix)) {
133+
if (key.startsWith('onBefore'))
134+
beforeCB = value;
135+
else
136+
afterCB = value;
137+
}
138+
}
139+
polluteJS(module, resolved, node, suffix.endsWith('Async'), beforeCB, afterCB);
140+
}
141+
147142
details.queue = details.queue.then(() => {
148143
resolve(resolved);
149144
if (error) onerror?.(error, node);
150-
return onInterpreterReady?.(resolved, node);
145+
return hooks?.main?.onReady?.(resolved, node);
151146
});
152147
});
153148
}
@@ -165,7 +160,6 @@ const registry = new Map();
165160
* @prop {'pyodide' | 'micropython' | 'wasmoon' | 'ruby-wasm-wasi'} interpreter the interpreter to use
166161
* @prop {string} [version] the optional interpreter version to use
167162
* @prop {string} [config] the optional config to use within such interpreter
168-
* @prop {(environment: object, node: Element) => void} [onInterpreterReady] the callback that will be invoked once
169163
*/
170164

171165
let dontBotherCount = 0;
@@ -198,17 +192,24 @@ export const define = (type, options) => {
198192

199193
if (dontBother) {
200194
// add a script then cleanup everything once that's ready
201-
const { onInterpreterReady } = options;
195+
const { hooks } = options;
196+
const onReady = hooks?.main?.onReady;
202197
options = {
203198
...options,
204-
onInterpreterReady(resolved, node) {
205-
CUSTOM_SELECTORS.splice(CUSTOM_SELECTORS.indexOf(type), 1);
206-
defaultRegistry.delete(type);
207-
registry.delete(type);
208-
waitList.delete(type);
209-
node.remove();
210-
onInterpreterReady?.(resolved);
211-
}
199+
hooks: {
200+
...hooks,
201+
main: {
202+
...hooks?.main,
203+
onReady(resolved, node) {
204+
CUSTOM_SELECTORS.splice(CUSTOM_SELECTORS.indexOf(type), 1);
205+
defaultRegistry.delete(type);
206+
registry.delete(type);
207+
waitList.delete(type);
208+
node.remove();
209+
onReady?.(resolved);
210+
}
211+
}
212+
},
212213
};
213214
document.head.append(
214215
assign(document.createElement('script'), { type })

esm/hooks.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const beforeRun = 'BeforeRun';
2+
const afterRun = 'AfterRun';
3+
4+
export const code = [
5+
`code${beforeRun}`,
6+
`code${beforeRun}Async`,
7+
`code${afterRun}`,
8+
`code${afterRun}Async`,
9+
];
10+
11+
export const js = [
12+
'onWorker',
13+
'onReady',
14+
`on${beforeRun}`,
15+
`on${beforeRun}Async`,
16+
`on${afterRun}`,
17+
`on${afterRun}Async`,
18+
];
19+
20+
/* c8 ignore start */
21+
/**
22+
* Created the wrapper to pass along hooked callbacks.
23+
* @param {object} module the details module
24+
* @param {object} ref the node or reference to pass as second argument
25+
* @param {boolean} isAsync if run should be async
26+
* @param {function?} before callback to run before
27+
* @param {function?} after callback to run after
28+
* @returns {object}
29+
*/
30+
export const polluteJS = (module, resolved, ref, isAsync, before, after) => {
31+
if (before || after) {
32+
const name = isAsync ? 'runAsync' : 'run';
33+
const method = module[name];
34+
module[name] = isAsync ?
35+
async function (interpreter, code, ...args) {
36+
if (before) await before.call(this, resolved, ref);
37+
const result = await method.call(
38+
this,
39+
interpreter,
40+
code,
41+
...args
42+
);
43+
if (after) await after.call(this, resolved, ref);
44+
return result;
45+
} :
46+
function (interpreter, code, ...args) {
47+
if (before) before.call(this, resolved, ref);
48+
const result = method.call(this, interpreter, code, ...args);
49+
if (after) after.call(this, resolved, ref);
50+
return result;
51+
}
52+
;
53+
}
54+
};
55+
/* c8 ignore stop */

esm/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dedent from 'codedent';
22
import { unescape } from 'html-escaper';
3+
import { io } from './interpreter/_utils.js';
34

45
const { isArray } = Array;
56

@@ -35,6 +36,24 @@ const dispatch = (target, type, what, worker = false, CE = CustomEvent) => {
3536
})
3637
);
3738
};
39+
40+
export const createFunction = value => Function(`'use strict';return (${value})`)();
41+
42+
export const createResolved = (module, type, config, interpreter) => ({
43+
type,
44+
config,
45+
interpreter,
46+
io: io.get(interpreter),
47+
run: (code, ...args) => module.run(interpreter, code, ...args),
48+
runAsync: (code, ...args) => module.runAsync(interpreter, code, ...args),
49+
runEvent: (...args) => module.runEvent(interpreter, ...args),
50+
});
51+
52+
export const createOverload = (module, name) => ($, pre) => {
53+
const method = module[name].bind(module);
54+
module[name] = (interpreter, code, ...args) =>
55+
method(interpreter, `${pre ? $ : code}\n${pre ? code : $}`, ...args);
56+
};
3857
/* c8 ignore stop */
3958

4059
export {

esm/worker/_template.js

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
import * as JSON from '@ungap/structured-clone/json';
88
import coincident from 'coincident/window';
99

10-
import { assign, create, dispatch } from '../utils.js';
10+
import { assign, create, createFunction, createOverload, createResolved, dispatch } from '../utils.js';
1111
import { registry } from '../interpreters.js';
1212
import { getRuntime, getRuntimeID } from '../loader.js';
13+
import { polluteJS, js as jsHooks, code as codeHooks } from '../hooks.js';
1314

1415
// bails out out of the box with a native/meaningful error
1516
// in case the SharedArrayBuffer is not available
@@ -67,35 +68,69 @@ add('message', ({ data: { options, config: baseURL, code, hooks } }) => {
6768
interpreter = (async () => {
6869
try {
6970
const { id, tag, type, custom, version, config, async: isAsync } = options;
71+
7072
const interpreter = await getRuntime(
7173
getRuntimeID(type, version),
7274
baseURL,
7375
config
7476
);
77+
7578
const details = create(registry.get(type));
76-
const name = `run${isAsync ? 'Async' : ''}`;
7779

78-
if (hooks) {
79-
// patch code if needed
80-
const { beforeRun, beforeRunAsync, afterRun, afterRunAsync } =
81-
hooks;
80+
const resolved = createResolved(
81+
details,
82+
type,
83+
config,
84+
interpreter
85+
);
8286

83-
const after = isAsync ? afterRunAsync : afterRun;
84-
const before = isAsync ? beforeRunAsync : beforeRun;
87+
let name = 'run';
88+
if (isAsync) name += 'Async';
8589

86-
// append code that should be executed *after* first
87-
if (after) {
88-
const method = details[name].bind(details);
89-
details[name] = (interpreter, code, ...args) =>
90-
method(interpreter, `${code}\n${after}`, ...args);
90+
if (hooks) {
91+
const overload = createOverload(details, name);
92+
93+
let before = '';
94+
let after = '';
95+
96+
for (const key of codeHooks) {
97+
const value = hooks[key];
98+
if (value) {
99+
const asyncCode = key.endsWith('Async');
100+
// either async hook and this worker is async
101+
// or sync hook and this worker is sync
102+
// other shared options possible cases are ignored
103+
if ((asyncCode && isAsync) || (!asyncCode && !isAsync)) {
104+
if (key.startsWith('codeBefore'))
105+
before = value;
106+
else
107+
after = value;
108+
}
109+
}
91110
}
92111

112+
// append code that should be executed *after* first
113+
if (after) overload(after, false);
114+
93115
// prepend code that should be executed *before* (so that after is post-patched)
94-
if (before) {
95-
const method = details[name].bind(details);
96-
details[name] = (interpreter, code, ...args) =>
97-
method(interpreter, `${before}\n${code}`, ...args);
116+
if (before) overload(before, true);
117+
118+
let beforeCB, afterCB;
119+
// exclude onWorker and onReady
120+
for (const key of jsHooks.slice(2)) {
121+
const value = hooks[key];
122+
if (value) {
123+
const asyncCode = key.endsWith('Async');
124+
if ((asyncCode && isAsync) || (!asyncCode && !isAsync)) {
125+
const cb = createFunction(value);
126+
if (key.startsWith('onBefore'))
127+
beforeCB = cb;
128+
else
129+
afterCB = cb;
130+
}
131+
}
98132
}
133+
polluteJS(details, resolved, xworker, isAsync, beforeCB, afterCB);
99134
}
100135

101136
const { CustomEvent, document } = window;
@@ -134,6 +169,10 @@ add('message', ({ data: { options, config: baseURL, code, hooks } }) => {
134169
// notify worker ready to execute
135170
if (element) notify('ready');
136171

172+
// evaluate the optional `onReady` callback
173+
if (hooks?.onReady)
174+
createFunction(hooks?.onReady).call(details, resolved, xworker);
175+
137176
// run either sync or async code in the worker
138177
await details[name](interpreter, code);
139178

0 commit comments

Comments
 (0)