diff --git a/src/message.ts b/src/message.ts index 5ef74b6..19b4d4d 100644 --- a/src/message.ts +++ b/src/message.ts @@ -11,6 +11,10 @@ export const handleIncomingMessage = function (event: any) { return; } + if (event.origin.endsWith(".module.kernel.lumeweb.com")) { + return; + } + // Input validation. if (!("method" in event.data)) { logErr("handleIncomingMessage", "kernel request is missing 'method' field"); diff --git a/src/queries.ts b/src/queries.ts index 2b2fb3d..60f647a 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -23,10 +23,11 @@ import { store as moduleStore, } from "./modules.js"; import { blake3 } from "@noble/hashes/blake3"; +import Worker from "./worker.js"; // WorkerLaunchFn is the type signature of the function that launches the // worker to set up for processing a query. -type WorkerLaunchFn = () => [Worker, Err]; +type WorkerLaunchFn = () => Promise<[Worker, Err]>; // modules is a hashmap that maps from a domain to the module that handles // queries to that domain. It maintains the domain and URL of the module so @@ -36,7 +37,7 @@ type WorkerLaunchFn = () => [Worker, Err]; // a new worker gets launched for every query. interface Module { domain: string; - url: string; + code: Uint8Array; launchWorker: WorkerLaunchFn; worker?: Worker; } @@ -223,20 +224,16 @@ async function createModule( workerCode: Uint8Array, domain: string, ): Promise<[Module | null, Err]> { - // Generate the URL for the worker code. - const url = URL.createObjectURL(new Blob([workerCode])); - // Create the module object. const mod: Module = { domain, - url, - launchWorker: function (): [Worker, Err] { + code: workerCode, + launchWorker: function (): Promise<[Worker, Err]> { return launchWorker(mod); }, }; - // Start worker - const [worker, err] = mod.launchWorker(); + const [worker, err] = await mod.launchWorker(); if (err !== null) { return [{} as Module, addContextToErr(err, "unable to launch worker")]; } @@ -247,11 +244,12 @@ async function createModule( // launchWorker will launch a worker and perform all the setup so that the // worker is ready to receive a query. -function launchWorker(mod: Module): [Worker, Err] { +async function launchWorker(mod: Module): Promise<[Worker, Err]> { // Create and launch the worker. let worker: Worker; try { - worker = new Worker(mod.url); + worker = new Worker(mod.code, CID.decode(mod.domain)); + await worker.ready; } catch (err: any) { logErr("worker", mod.domain, "unable to create worker", mod.domain, err); return [ @@ -260,6 +258,9 @@ function launchWorker(mod: Module): [Worker, Err] { ]; } + // clean up memory, we don't need the code anymore + mod.code = new Uint8Array(); + // Set the onmessage and onerror functions. worker.onmessage = function (event: MessageEvent) { handleWorkerMessage(event, mod, worker); @@ -321,7 +322,6 @@ async function handleModuleCall( ); return; } - let validCid = false; let isResolver = false; if ( @@ -588,8 +588,12 @@ async function handleModuleCall( await (await moduleStore()).put(finalModule, moduleData); } + if (!(moduleData instanceof Uint8Array)) { + moduleData = new TextEncoder().encode(moduleData); + } + // Create a new module. - const [mod, errCM] = await createModule(moduleData, moduleDomain); + const [mod, errCM] = await createModule(moduleData, finalModule); if (errCM !== null) { const err = addContextToErr(errCM, "unable to create module"); respondErr(event, messagePortal, isWorker, isInternal, err); diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 0000000..1a7c2e3 --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,86 @@ +import { CID } from "@lumeweb/libs5"; +import { defer } from "@lumeweb/libkernel/module"; + +type WorkerMessage = (this: Worker, ev: MessageEvent) => any; +type WorkerError = (this: Worker, ev: ErrorEvent) => any; + +export default class Worker { + private _code: Uint8Array; + private _errorHandler?: WorkerError; + private _messageHandler?: WorkerMessage; + private _iframe: HTMLIFrameElement; + private _iframeDefer = defer(); + private _cid: CID; + + constructor(code: Uint8Array, cid: CID) { + this._code = code; + this._cid = cid; + + const iframe = document.createElement("iframe"); + iframe.src = this.getModuleUrl(); + iframe.onload = () => { + this._postMessage({ + method: "workerInit", + module: this._code, + }); + }; + + this._iframe = iframe; + + document.body.appendChild(iframe); + + window.addEventListener("message", (event: MessageEvent) => { + if (event.source !== iframe.contentWindow) { + return; + } + + if (event.data.method === "workerMessage") { + this._messageHandler?.( + new MessageEvent("worker", { data: event.data.data }), + ); + return; + } + + if (event.data.method === "workerError") { + this._errorHandler?.( + new ErrorEvent("worker", { + message: event.data.data.message, + error: event.data.data.error, + }), + ); + return; + } + + if (event.data.method === "workerInited") { + this._iframeDefer.resolve(); + return; + } + }); + } + + set onmessage(handler: (this: Worker, ev: MessageEvent) => any) { + this._messageHandler = handler; + } + + set onerror(handler: (this: Worker, ev: ErrorEvent) => any) { + this._errorHandler = handler; + } + + get ready(): Promise { + return this._iframeDefer.promise; + } + + postMessage(message: any) { + this._iframeDefer.promise.then(() => { + this._postMessage(message); + }); + } + + _postMessage(message: any) { + this._iframe.contentWindow!.postMessage(message, this.getModuleUrl()); + } + + private getModuleUrl() { + return `https://${this._cid.toBase32()}.module.kernel.lumeweb.com`; + } +}