[next] Improve the whole events story (#1584)

This commit is contained in:
Andrea Giammarchi
2023-07-10 15:36:48 +02:00
committed by GitHub
parent c6b5ce7f55
commit 0b0e03456c
16 changed files with 101 additions and 113 deletions

View File

@@ -455,7 +455,8 @@ In few words, while every *interpreter* is literally passed along to unlock its
| io | `wrap.io` | Allow to lazily define different `stdout` or `stderr` via the running *interpreter*. This `io` field can be lazily defined and restored back for any element currently running the code. |
| config | `wrap.config` | It is the resolved *JSON* config and it is an own clone per each element running the code, usable also as "_state_" reference for the specific element, as changing it at run time will never affect any other element. |
| run | `wrap.run(code)` | It abstracts away the need to know the exact method name used to run code synchronously, whenever the *interpreter* allows such operation, facilitating future migrations from an interpreter to another. |
| runAsync | `wrap.run(code)` | It abstracts away the need to know the exact method name used to run code asynchronously, whenever the *interpreter* allows such operation, facilitating future migrations from an interpreter to another. |
| runAsync | `wrap.runAsync(code)` | It abstracts away the need to know the exact method name used to run code asynchronously, whenever the *interpreter* allows such operation, facilitating future migrations from an interpreter to another. |
| runEvent | `wrap.runEvent(code, event)` | It abstracts away the need to know how an *interpreter* retrieves paths to execute an event handler. |
This is the `wrap` mentioned with most hooks and initializers previously described, and we're more than happy to learn if we are not passing along some extra helper.

View File

@@ -119,6 +119,7 @@ export const handleCustomType = (node) => {
config: structuredClone(configs.get(name)),
run: module.run.bind(module, interpreter),
runAsync: module.runAsync.bind(module, interpreter),
runEvent: module.runEvent.bind(module, interpreter),
};
resolve(resolved);

View File

@@ -2,23 +2,23 @@ import { clean, writeFile as writeFileUtil } from "./_utils.js";
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export const registerJSModule = (interpreter, name, value) => {
interpreter.registerJsModule(name, value);
};
export const run = (interpreter, code) => interpreter.runPython(clean(code));
export const runAsync = (interpreter, code) =>
interpreter.runPythonAsync(clean(code));
export const getGlobal = (interpreter, name) => interpreter.globals.get(name);
export const setGlobal = (interpreter, name, value) => {
interpreter.globals.set(name, value);
};
export const deleteGlobal = (interpreter, name) => {
interpreter.globals.delete(name);
};
export const registerJSModule = (interpreter, name, value) => {
interpreter.registerJsModule(name, value);
export const runEvent = async (interpreter, code, event) => {
// allows method(event) as well as namespace.method(event)
// it does not allow fancy brackets names for now
const [name, ...keys] = code.split(".");
let target = interpreter.globals.get(name);
let context;
for (const key of keys) [context, target] = [target, target[key]];
target.call(context, event);
};
export const writeFile = ({ FS }, path, buffer) =>

View File

@@ -1,7 +1,7 @@
import "@ungap/with-resolvers";
import { getBuffer } from "../fetch-utils.js";
import { absoluteURL, entries } from "../utils.js";
import { absoluteURL } from "../utils.js";
/**
* Trim code only if it's a single line that prettier or other tools might have modified.
@@ -131,12 +131,4 @@ export const fetchPaths = (module, interpreter, config_fetch) =>
.then((buffer) => module.writeFile(interpreter, path, buffer)),
),
);
// this is a fallback for interpreters unable to register JS modules
// all defined keys will end up as globally available references
// REQUIRES INTEGRATION TEST
/* c8 ignore start */
export function registerJSModule(interpreter, _, value) {
for (const [k, v] of entries(value)) this.setGlobal(interpreter, k, v);
}
/* c8 ignore stop */

View File

@@ -1,10 +1,9 @@
import { fetchPaths, stdio } from "./_utils.js";
import {
run,
getGlobal,
setGlobal,
deleteGlobal,
registerJSModule,
run,
runAsync,
runEvent,
writeFile,
} from "./_python.js";
@@ -23,16 +22,10 @@ export default {
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
return interpreter;
},
getGlobal,
setGlobal,
deleteGlobal,
registerJSModule,
run,
// TODO: MicroPython doesn't have a Pyodide like top-level await,
// this method should still not throw errors once invoked
async runAsync(...args) {
return this.run(...args);
},
runAsync,
runEvent,
writeFile,
};
/* c8 ignore stop */

View File

@@ -1,11 +1,9 @@
import { fetchPaths, stdio } from "./_utils.js";
import {
registerJSModule,
run,
runAsync,
getGlobal,
setGlobal,
deleteGlobal,
registerJSModule,
runEvent,
writeFile,
} from "./_python.js";
@@ -32,12 +30,10 @@ export default {
}
return interpreter;
},
getGlobal,
setGlobal,
deleteGlobal,
registerJSModule,
run,
runAsync,
runEvent,
writeFile,
};
/* c8 ignore stop */

View File

@@ -1,6 +1,8 @@
import { clean, fetchPaths, registerJSModule } from "./_utils.js";
import { clean, fetchPaths } from "./_utils.js";
import { entries } from "../utils.js";
const type = "ruby-wasm-wasi";
const jsType = type.replace(/\W+/g, "_");
// MISSING:
// * there is no VFS apparently or I couldn't reach any
@@ -23,31 +25,35 @@ export default {
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
return interpreter;
},
registerJSModule,
getGlobal(interpreter, name) {
try {
return this.run(interpreter, name);
} catch (_) {
const method = this.run(interpreter, `method(:${name})`);
return (...args) =>
method.call(
name,
...args.map((value) => interpreter.wrap(value)),
);
// Fallback to globally defined module fields (i.e. $xworker)
registerJSModule(interpreter, _, value) {
const code = ['require "js"'];
for (const [k, v] of entries(value)) {
const id = `__module_${jsType}_${k}`;
globalThis[id] = v;
code.push(`$${k}=JS.global[:${id}]`);
}
},
setGlobal(interpreter, name, value) {
const id = `__pyscript_ruby_wasm_wasi_${name}`;
globalThis[id] = value;
this.run(interpreter, `require "js";$${name}=JS::eval("return ${id}")`);
},
deleteGlobal(interpreter, name) {
const id = `__pyscript_ruby_wasm_wasi_${name}`;
this.run(interpreter, `$${name}=nil`);
delete globalThis[id];
this.run(interpreter, code.join(";"));
},
run: (interpreter, code) => interpreter.eval(clean(code)),
runAsync: (interpreter, code) => interpreter.evalAsync(clean(code)),
runEvent(interpreter, code, event) {
// patch common xworker.onmessage/onerror cases
if (/^xworker\.(on\w+)$/.test(code)) {
const { $1: name } = RegExp;
const id = `__module_${jsType}_event`;
globalThis[id] = event;
this.run(
interpreter,
`require "js";$xworker.call("${name}",JS.global[:${id}])`,
);
delete globalThis[id];
} else {
// Experimental: allows only events by fully qualified method name
const method = this.run(interpreter, `method(:${code})`);
method.call(code, interpreter.wrap(event));
}
},
writeFile: () => {
throw new Error(`writeFile is not supported in ${type}`);
},

View File

@@ -1,10 +1,6 @@
import {
clean,
fetchPaths,
stdio,
registerJSModule,
writeFileShim,
} from "./_utils.js";
import { clean, fetchPaths, stdio, writeFileShim } from "./_utils.js";
import { entries } from "../utils.js";
const type = "wasmoon";
@@ -27,16 +23,21 @@ export default {
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
return interpreter;
},
registerJSModule,
getGlobal: (interpreter, name) => interpreter.global.get(name),
setGlobal(interpreter, name, value) {
interpreter.global.set(name, value);
},
deleteGlobal(interpreter, name) {
interpreter.global.set(name, void 0);
// Fallback to globally defined module fields
registerJSModule: (interpreter, _, value) => {
for (const [k, v] of entries(value)) interpreter.global.set(k, v);
},
run: (interpreter, code) => interpreter.doStringSync(clean(code)),
runAsync: (interpreter, code) => interpreter.doString(clean(code)),
runEvent: (interpreter, code, event) => {
// allows method(event) as well as namespace.method(event)
// it does not allow fancy brackets names for now
const [name, ...keys] = code.split(".");
let target = interpreter.global.get(name);
let context;
for (const key of keys) [context, target] = [target, target[key]];
target.call(context, event);
},
writeFile: (
{
cmodule: {

View File

@@ -41,8 +41,7 @@ export const listener = async (event) => {
el.getAttribute(`${name}-env`) || name,
);
const handler = registry.get(name);
const callback = handler.getGlobal(interpreter, value);
callback(event);
handler.runEvent(interpreter, value, event);
}
};

View File

@@ -24,15 +24,14 @@ try {
);
}
let interpreter, run, interpreterEvent;
let interpreter, runEvent;
const add = (type, fn) => {
addEventListener(
type,
fn ||
(async (event) => {
await interpreter;
interpreterEvent = event;
run(`xworker.on${type}(xworker.event);`, xworker);
runEvent(`xworker.on${type}`, event);
}),
!!fn && { once: true },
);
@@ -52,17 +51,6 @@ const xworker = {
onmessage() {},
onmessageerror() {},
postMessage: postMessage.bind(self),
// this getter exists so that arbitrarily access to xworker.event
// would always fail once an event has been dispatched, as that's not
// meant to be accessed in the wild, respecting the one-off event nature of JS.
// because xworker is a unique well defined globally shared reference,
// there's also no need to bother setGlobal and deleteGlobal every single time.
get event() {
const event = interpreterEvent;
if (!event) throw new Error("Unauthorized event access");
interpreterEvent = void 0;
return event;
},
};
add("message", ({ data: { options, code, hooks } }) => {
@@ -99,10 +87,10 @@ add("message", ({ data: { options, code, hooks } }) => {
}
// set the `xworker` global reference once
details.registerJSModule(interpreter, "xworker", { xworker });
// simplify run calls after possible patches
run = details[name].bind(details, interpreter);
// execute the content of the worker file
run(code);
// simplify runEvent calls
runEvent = details.runEvent.bind(details, interpreter);
// run either sync or async code in the worker
await details[name](interpreter, code);
return interpreter;
})();
add("error");

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/core",
"version": "0.0.7",
"version": "0.0.8",
"description": "PyScript Next core",
"main": "./cjs/index.js",
"types": "./types/index.d.ts",
@@ -66,6 +66,6 @@
"coincident": "^0.8.3"
},
"worker": {
"blob": "sha256-BqPm4/IdGDQduhprGUnwdf5iumpMmkkGNsPrPZXl+mU="
"blob": "sha256-JuwqC8WVlEqybo1Q4UB76PQ260TCqm7b5sSEj1/Tuc8="
}
}

File diff suppressed because one or more lines are too long

View File

@@ -14,10 +14,16 @@
import sys
print(event.type)
print(sys.version)
class Printer:
def version(self, event):
print_version(event)
printer = Printer()
</script>
<button
pyodide-pointerdown="print_version"
pyodide-click="print_version"
pyodide-click="printer.version"
>
pyodide version
</button>
@@ -28,9 +34,10 @@
print(event.type)
print(sys.version)
</script>
<!-- ⚠️ MicroPython bug: it fails the printer.version case -->
<button
micropython-pointerdown="print_version"
micropython-click="print_version"
micropython-click="printer.version"
>
micropython version
</button>

View File

@@ -64,5 +64,17 @@
w.postMessage('MicroPython: Hello Lua 👋')
w.onmessage = handle_message
</script>
<!-- XWorker - MicroPython to Ruby -->
<script type="micropython">
from xworker import XWorker
def handle_message(event):
print(event.data)
w = XWorker('./worker.rb', type='ruby-wasm-wasi')
w.postMessage('MicroPython: Hello Ruby 👋')
w.onmessage = handle_message
</script>
</body>
</html>

View File

@@ -1,6 +1,2 @@
require "js"
xworker = JS::eval("return xworker")
puts "What is 2 + 3?"
puts xworker.sync.input("What is 2 + 3?")
puts $xworker[:sync].call("input", "What is 2 + 3?")

View File

@@ -1,10 +1,6 @@
require "js"
xworker = JS::eval("return xworker")
def on_message(event)
puts event[:data]
xworker.postMessage('Ruby: Hello MicroPython 👋')
$xworker.call('postMessage', 'Ruby: Hello MicroPython 👋')
end
xworker.onmessage = on_message
$xworker[:onmessage] = -> (event) { on_message event }