'use strict'; require('@ungap/with-resolvers'); const {MAIN, THREAD} = require('./channel.js'); const {APPLY} = require('./shared/traps.js'); const {assign} = require('./shared/utils.js'); const $coincident = (require('./structured.js')); const JSON = (require('./json.js')); const main = (require('./window/main.js')); const thread = (require('./window/thread.js')); const serverMain = (require('./server/main.js')); const serverThread = (require('./server/thread.js')); const Worker = (require('./shared/worker.js')); const {notify, wait} = Atomics; const {parse, stringify} = JSON; const isServer = !!globalThis.process; const proxies = new WeakMap; /** * @typedef {object} Coincident * @property {ProxyHandler} proxy * @property {ProxyHandler} window * @property {(value: any) => boolean} isWindowProxy */ /** * @typedef {object & Coincident} CoincidentWorker * @property {ProxyHandler} proxy * @property {ProxyHandler} window * @property {(value: any) => boolean} isWindowProxy * @property {ProxyHandler} server * @property {(value: any) => boolean} isServerProxy */ /** * @callback CoincidentServer * @param {WebSocketServer} wss the WebSocketServer to use to handle Worker/Server calls * @param {object} [globals] optional globals to expose through the Worker via the proxy * @returns {WebSocketServer} */ /** * @callback CoincidentWeb * @param {globalThis | Worker} self either the main thread (Worker)or a worker (in main thread UI) * @param {WebSocket} [ws] the optional WebSocket to use when `self` is `globalThis` * @returns {Coincident | CoincidentWorker} */ const parseData = data => { let id; if (/^!(-?\d+)?/.test(data)) { id = RegExp.$1; data = data.slice(1 + id.length); } return {id, result: data ? parse(data) : void 0}; }; const coincident = isServer ? /** @type {CoincidentServer} */ (wss, globals) => wss.on('connection', ws => { // wait for the cross-hand-shake from the main thread ws.once('message', buffer => { let id = 0; const [SERVER_MAIN, SERVER_THREAD] = parse(buffer); const resolvers = new Map; // bootstrap a dedicated channel for this socket const {[SERVER_MAIN]: __main__} = serverMain( {[SERVER_THREAD]: async (trap, ...args) => { const data = stringify([trap, ...args]); // allow return values from thread's callbacks if (trap === APPLY) { const {promise, resolve} = Promise.withResolvers(); const uid = String(id++); resolvers.set(uid, resolve); ws.send('!' + uid + data); return await promise; } ws.send('!' + data); }}, SERVER_MAIN, SERVER_THREAD, globals ).proxy; // setup the socket and terminate the hand-shake ws // resolve dangling promises on close .on('close', () => { for (const [_, resolve] of resolvers) resolve(); }) // resolve or forward results .on('message', buffer => { const {id, result} = parseData(String(buffer)); if (id) { const resolve = resolvers.get(id); resolvers.delete(id); resolve(result); } else ws.send(stringify(__main__(...result))); }) // end cross-hand-shake .send(''); }); }) : /** @type {CoincidentWeb} */ (self, ws, ...args) => { const proxy = $coincident(self, ...args); if (!proxies.has(proxy)) { const util = self instanceof Worker ? mainBridge : threadBridge; proxies.set(proxy, util(self, proxy, MAIN, THREAD, ws)); } return proxies.get(proxy); } ; if (!isServer) coincident.transfer = $coincident.transfer; module.exports = coincident; const mainBridge = (self, thread, MAIN, THREAD, ws) => { // wait for the cross-hand-shake from the worker self.addEventListener('message', ({data: [CHANNEL, i32]}) => { const SERVER_MAIN = 'M' + CHANNEL; const SERVER_THREAD = 'T' + CHANNEL; const {[SERVER_THREAD]: __thread__} = thread; let resolve; thread[SERVER_MAIN] = (...args) => new Promise($ => { resolve = $; ws.send(stringify(args)); }); // wait for the cross-hand-shake from the server ws.addEventListener('message', () => { // setup regular listener for all socket's interactions ws.addEventListener('message', async ({data}) => { const {id, result} = parseData(data); if (id != null) { const invoke = __thread__(...result); if (id) { const out = await invoke; ws.send('!' + id + (out === void 0 ? '' : stringify(out))); } } else if (resolve) resolve = resolve(result); }); // unlock the Worker now that all channels have been defined i32[0] = 1; notify(i32, 0); }, {once: true}); ws.send(stringify([SERVER_MAIN, SERVER_THREAD])); }, {once: true}); return main(thread, MAIN, THREAD); }; const threadBridge = (self, proxy, MAIN, THREAD) => { // create a unique channel to reflect on the server // through the main WebSocket (cross-hand-shake) const CHANNEL = 'S' + crypto.randomUUID(); const i32 = new Int32Array(new SharedArrayBuffer(4)); self.postMessage([CHANNEL, i32]); wait(i32, 0); // merge server and worker namespace return assign( serverThread(proxy, 'M' + CHANNEL, 'T' + CHANNEL), thread(proxy, MAIN, THREAD) ); };