Skip to content

Commit

Permalink
Added hybrid mode
Browse files Browse the repository at this point in the history
  • Loading branch information
WebReflection committed Nov 29, 2023
1 parent 62019ec commit 6d19d97
Show file tree
Hide file tree
Showing 17 changed files with 288 additions and 180 deletions.
53 changes: 53 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,59 @@ Please read the [XWorker](#xworker) dedicated section to know more.
</details>


## Extra `config` features

It is possible to land in either the *main* world or the *worker* one native *JS* modules (aka: *ESM*).

In *polyscript*, this is possible by defining one or more `[js_modules.X]` fields in the config, where `X` is either *main* or *worker*:

* `[js_modules.main]` is a list of *source* -> *module name* pairs, similarly to how `[files]` field work, where the *module* name will then be reachable via `polyscript.js_modules.actual_name` in both *main* and *worker* world. As the *main* module lands on the main thread, where there is also likely some UI, it is also possible to define one or more related *CSS* to that module, as long as they target the very same name (see the example to better understand).
* `[js_modules.worker]` is a list of *source* -> *module name* pairs that actually land only in `<script type="x" worker>` cases. These modules are still reachable through the very same `polyscript.js_modules.actual_name` convention and this feature is meant to be used for modules that only works best, or work regardless, outside the *main* world. As example, if your *JS* module implies that `document` or `window` references, among other *DOM* related APIs, are globally available, it means that that module should be part of the `[js_modules.main]` list instead ... however, if the module works out of the box in a *Worker* environment, it is best for performance reasons to explicitly define such module under this field. Please note that *CSS* files are not accepted within this list because there's no way *CSS* can be useful or land in any meaningful way within a *Worker* environment.

### js_modules config example

```toml
[js_modules.main]
# this modules work best on main
"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js" = "leaflet"
"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" = "leaflet" # CSS
# this works in both main and worker
"https://cdn.jsdelivr.net/npm/html-escaper" = "html_escaper"

[js_modules.worker]
# this works out of the box in a worker too
"https://cdn.jsdelivr.net/npm/html-escaper" = "html_escaper"
# this works only in a worker
"https://cdn.jsdelivr.net/npm/worker-only" = "worker_only"
```

```html
<!-- main case -->
<script type="pyodide" config="./that.toml">
# these both works
from polyscript.js_modules import leaflet as L
from polyscript.js_modules import html_escaper
# this fails as it's not reachable in main
from polyscript.js_modules import worker_only
</script>
<!-- worker case -->
<script type="pyodide" config="./that.toml" worker>
# these works by proxying the main module and landing
# on main only when accessed, never before
# the CSS file also lands automatically on demand
from polyscript.js_modules import leaflet as L
# this works out of the box in the worker
from polyscript.js_modules import html_escaper
# this works only in a worker 👍
from polyscript.js_modules import worker_only
</script>
```
## How Events Work
The event should contain the *interpreter* or *custom type* prefix, followed by the *event* type it'd like to handle.
Expand Down
4 changes: 2 additions & 2 deletions docs/core.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/core.js.map

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions esm/custom.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '@ungap/with-resolvers';
import { $$ } from 'basic-devtools';

import { assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo } from './utils.js';
import { JSModules, assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo } from './utils.js';
import { getDetails } from './script-handler.js';
import { registry as defaultRegistry, prefixes, configs } from './interpreters.js';
import { getRuntimeID } from './loader.js';
Expand Down Expand Up @@ -99,7 +99,10 @@ export const handleCustomType = (node) => {
XWorker,
};

module.registerJSModule(interpreter, 'polyscript', { XWorker });
module.registerJSModule(interpreter, 'polyscript', {
js_modules: JSModules,
XWorker,
});

// patch methods accordingly to hooks (and only if needed)
for (const suffix of ['Run', 'RunAsync']) {
Expand Down
30 changes: 16 additions & 14 deletions esm/interpreter/_utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import '@ungap/with-resolvers';
import { UNDEFINED } from 'proxy-target/types';

import { getBuffer } from '../fetch-utils.js';
import { absoluteURL, all, entries, isArray } from '../utils.js';
import { absoluteURL, all, entries, importCSS, importJS, isArray, isCSS } from '../utils.js';

// REQUIRES INTEGRATION TEST
/* c8 ignore start */
Expand Down Expand Up @@ -167,19 +168,20 @@ export const fetchFiles = (module, interpreter, config_files) =>
),
);

export const fetchJSModules = (module, interpreter, js_modules) => {
const modules = [];
for (const [source, name] of entries(js_modules)) {
modules.push(import(source).then(
esm => {
// { ...esm } is needed to avoid dealing w/ module records
module.registerJSModule(interpreter, name, { ...esm });
},
err => {
console.warn(`Unable to register ${name} due ${err}`);
}
));
const RUNNING_IN_WORKER = typeof document === UNDEFINED;

export const fetchJSModules = ({ main, worker }) => {
const promises = [];
if (worker && RUNNING_IN_WORKER) {
for (const [source, name] of entries(worker))
promises.push(importJS(source, name));
}
if (main && !RUNNING_IN_WORKER) {
for (const [source, name] of entries(main)) {
if (isCSS(source)) importCSS(source);
else promises.push(importJS(source, name));
}
}
return all(modules);
return all(promises);
};
/* c8 ignore stop */
2 changes: 1 addition & 1 deletion esm/interpreter/micropython.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default {
const interpreter = await get(loadMicroPython({ stderr, stdout, url }));
if (config.files) await fetchFiles(this, interpreter, config.files);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
if (config.js_modules) await fetchJSModules(this, interpreter, config.js_modules);
if (config.js_modules) await fetchJSModules(config.js_modules);
return interpreter;
},
registerJSModule,
Expand Down
2 changes: 1 addition & 1 deletion esm/interpreter/pyodide.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default {
);
if (config.files) await fetchFiles(this, interpreter, config.files);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
if (config.js_modules) await fetchJSModules(this, interpreter, config.js_modules);
if (config.js_modules) await fetchJSModules(config.js_modules);
if (config.packages) {
await interpreter.loadPackage('micropip');
const micropip = await interpreter.pyimport('micropip');
Expand Down
3 changes: 2 additions & 1 deletion esm/interpreter/ruby-wasm-wasi.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { dedent } from '../utils.js';
import { fetchFiles, fetchPaths } from './_utils.js';
import { fetchFiles, fetchJSModules, fetchPaths } from './_utils.js';

const type = 'ruby-wasm-wasi';
const jsType = type.replace(/\W+/g, '_');
Expand All @@ -24,6 +24,7 @@ export default {
const { vm: interpreter } = await DefaultRubyVM(module);
if (config.files) await fetchFiles(this, interpreter, config.files);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
if (config.js_modules) await fetchJSModules(config.js_modules);
return interpreter;
},
// Fallback to globally defined module fields (i.e. $xworker)
Expand Down
3 changes: 2 additions & 1 deletion esm/interpreter/wasmoon.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { dedent } from '../utils.js';
import { fetchFiles, fetchPaths, io, stdio, writeFileShim } from './_utils.js';
import { fetchFiles, fetchJSModules, fetchPaths, io, stdio, writeFileShim } from './_utils.js';

const type = 'wasmoon';

Expand All @@ -21,6 +21,7 @@ export default {
});
if (config.files) await fetchFiles(this, interpreter, config.files);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
if (config.js_modules) await fetchJSModules(config.js_modules);
return interpreter;
},
// Fallback to globally defined module fields
Expand Down
7 changes: 5 additions & 2 deletions esm/script-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import $xworker from './worker/class.js';
import workerURL from './worker/url.js';
import { getRuntime, getRuntimeID } from './loader.js';
import { registry } from './interpreters.js';
import { all, dispatch, resolve, defineProperty, nodeInfo } from './utils.js';
import { JSModules, all, dispatch, resolve, defineProperty, nodeInfo } from './utils.js';
import { getText } from './fetch-utils.js';

const getRoot = (script) => {
Expand Down Expand Up @@ -60,7 +60,10 @@ const execute = async (script, source, XWorker, isAsync) => {
configurable: true,
get: () => script,
});
module.registerJSModule(interpreter, 'polyscript', { XWorker });
module.registerJSModule(interpreter, 'polyscript', {
js_modules: JSModules,
XWorker,
});
dispatch(script, type, 'ready');
const result = module[isAsync ? 'runAsync' : 'run'](interpreter, content);
const done = dispatch.bind(null, script, type, 'done');
Expand Down
25 changes: 25 additions & 0 deletions esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,31 @@ export const createOverload = (module, name) => ($, pre) => {
module[name] = (interpreter, code, ...args) =>
method(interpreter, `${pre ? $ : code}\n${pre ? code : $}`, ...args);
};

export const js_modules = Symbol.for('polyscript.js_modules');

const jsModules = new Map;
defineProperty(globalThis, js_modules, { value: jsModules });

export const JSModules = new Proxy(jsModules, {
get: (map, name) => map.get(name),
});

export const importJS = (source, name) => import(source).then(esm => {
jsModules.set(name, { ...esm });
});

export const importCSS = href => new Promise((onload, onerror) => {
if (document.querySelector(`link[href="${href}"]`)) onload();
document.head.append(
assign(
document.createElement('link'),
{ rel: 'stylesheet', href, onload, onerror },
)
)
});

export const isCSS = source => /\.css/i.test(new URL(source).pathname);
/* c8 ignore stop */

export {
Expand Down
28 changes: 21 additions & 7 deletions esm/worker/_template.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import * as JSON from '@ungap/structured-clone/json';
import coincident from 'coincident/window';

import { assign, create, createFunction, createOverload, createResolved, dispatch } from '../utils.js';
import { registry } from '../interpreters.js';
import { assign, create, createFunction, createOverload, createResolved, dispatch, entries, isCSS, js_modules } from '../utils.js';
import { configs, registry } from '../interpreters.js';
import { getRuntime, getRuntimeID } from '../loader.js';
import { patch, polluteJS, js as jsHooks, code as codeHooks } from '../hooks.js';

Expand Down Expand Up @@ -68,12 +68,11 @@ add('message', ({ data: { options, config: baseURL, code, hooks } }) => {
interpreter = (async () => {
try {
const { id, tag, type, custom, version, config, async: isAsync } = options;
const runtimeID = getRuntimeID(type, version);

const interpreter = await getRuntime(
getRuntimeID(type, version),
baseURL,
config
);
const interpreter = await getRuntime(runtimeID, baseURL, config);

const mainModules = configs.get(runtimeID).js_modules?.main;

const details = create(registry.get(type));

Expand Down Expand Up @@ -142,6 +141,21 @@ add('message', ({ data: { options, config: baseURL, code, hooks } }) => {
// set the `xworker` global reference once
details.registerJSModule(interpreter, 'polyscript', {
xworker,
js_modules: new Proxy(globalThis[js_modules], {
get(map, name) {
if (!map.has(name) && mainModules) {
for (const [source, module] of entries(mainModules)) {
if (module !== name) continue;
if (isCSS(source)) sync.importCSS(source);
else {
sync.importJS(source, name);
map.set(name, window[js_modules].get(name));
}
}
}
return map.get(name);
}
}),
get target() {
if (!target && element) {
if (tag === 'SCRIPT') {
Expand Down
11 changes: 7 additions & 4 deletions esm/worker/class.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as JSON from '@ungap/structured-clone/json';
import coincident from 'coincident/window';
import xworker from './xworker.js';
import { getConfigURLAndType } from '../loader.js';
import { assign, create, defineProperties } from '../utils.js';
import { assign, create, defineProperties, importCSS, importJS } from '../utils.js';
import { getText } from '../fetch-utils.js';
import Hook from './hook.js';

Expand Down Expand Up @@ -43,16 +43,19 @@ export default (...args) =>
postMessage.call(worker, { options, config, code, hooks });
});

const sync = assign(
coincident(worker, JSON).proxy,
{ importJS, importCSS },
);

defineProperties(worker, {
sync: { value: sync },
postMessage: {
value: (data, ...rest) =>
bootstrap.then(() =>
postMessage.call(worker, data, ...rest),
),
},
sync: {
value: coincident(worker, JSON).proxy,
},
onerror: {
writable: true,
configurable: true,
Expand Down
Loading

0 comments on commit 6d19d97

Please sign in to comment.