From 32c758b86cc2c2aec379bb1ef832c164e1f76f8e Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 18 Oct 2021 22:21:42 -0500 Subject: [PATCH] refactor: iframe windows share web worker --- package.json | 2 +- playwright.config.ts | 4 +- scripts/build-atomics.ts | 3 +- scripts/minify.ts | 27 +- .../sync-send-message-to-main-atomics.ts | 2 +- src/lib/main/loader.ts | 7 +- src/lib/sandbox/create-web-worker.ts | 25 -- src/lib/sandbox/index.ts | 3 +- src/lib/sandbox/init-sandbox.ts | 89 +++---- src/lib/sandbox/main-access-handler.ts | 71 ++---- src/lib/sandbox/main-constants.ts | 11 +- src/lib/sandbox/main-forward-trigger.ts | 14 +- src/lib/sandbox/main-instances.ts | 76 ++---- src/lib/sandbox/main-register-window.ts | 73 ++++++ src/lib/sandbox/main-serialization.ts | 161 +++++++----- src/lib/sandbox/messenger.ts | 62 ----- src/lib/sandbox/on-messenge-from-worker.ts | 43 ++++ src/lib/sandbox/read-main-interfaces.ts | 36 ++- src/lib/sandbox/read-main-scripts.ts | 81 +++--- src/lib/service-worker/fetch.ts | 1 - src/lib/types.ts | 150 ++++++----- src/lib/utils.ts | 107 ++++---- src/lib/web-worker/index.ts | 47 ++-- src/lib/web-worker/init-web-worker.ts | 42 +++ src/lib/web-worker/init-worker.ts | 33 --- src/lib/web-worker/worker-access-handler.ts | 79 ------ src/lib/web-worker/worker-anchor.ts | 2 +- src/lib/web-worker/worker-constants.ts | 6 +- src/lib/web-worker/worker-constructors.ts | 43 ++-- src/lib/web-worker/worker-document.ts | 117 ++++----- src/lib/web-worker/worker-environment.ts | 240 ++++++++++++++++++ src/lib/web-worker/worker-exec.ts | 229 +++++++++++------ .../web-worker/worker-forwarded-trigger.ts | 10 +- src/lib/web-worker/worker-global.ts | 92 ------- src/lib/web-worker/worker-iframe.ts | 100 +++----- src/lib/web-worker/worker-image.ts | 106 ++++---- src/lib/web-worker/worker-location.ts | 20 +- src/lib/web-worker/worker-navigator.ts | 34 +++ src/lib/web-worker/worker-node-list.ts | 6 +- src/lib/web-worker/worker-node.ts | 39 ++- .../web-worker/worker-proxy-constructor.ts | 5 +- src/lib/web-worker/worker-proxy.ts | 155 ++++++----- src/lib/web-worker/worker-script.ts | 47 ++-- src/lib/web-worker/worker-serialization.ts | 197 ++++++++------ 44 files changed, 1481 insertions(+), 1216 deletions(-) delete mode 100644 src/lib/sandbox/create-web-worker.ts create mode 100644 src/lib/sandbox/main-register-window.ts delete mode 100644 src/lib/sandbox/messenger.ts create mode 100644 src/lib/sandbox/on-messenge-from-worker.ts create mode 100644 src/lib/web-worker/init-web-worker.ts delete mode 100644 src/lib/web-worker/init-worker.ts delete mode 100644 src/lib/web-worker/worker-access-handler.ts create mode 100644 src/lib/web-worker/worker-environment.ts delete mode 100644 src/lib/web-worker/worker-global.ts create mode 100644 src/lib/web-worker/worker-navigator.ts diff --git a/package.json b/package.json index fdbca1e7..3f3abe68 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "build.prod": "tsc && rollup -c scripts/rollup.config.js --configApi", "build.watch": "rollup -c scripts/rollup.config.js -w --configDev", "dev": "tsc && concurrently \"npm:build.watch\" \"npm:tsc.watch\" -n build,tsc -c magenta,yellow", - "playwright": "playwright test --browser=chromium", + "playwright": "playwright test tests --browser=chromium", "playwright.webkit": "playwright test --browser=webkit", "release": "npm run build && npm test && np --no-2fa --no-tests", "serve": "sirv tests --port 4000 --dev", diff --git a/playwright.config.ts b/playwright.config.ts index 8ed78081..011da7a7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -4,7 +4,7 @@ const config: PlaywrightTestConfig = { use: { baseURL: 'http://localhost:5000/', viewport: { - width: 480, + width: 520, height: 600, }, contextOptions: { @@ -12,6 +12,8 @@ const config: PlaywrightTestConfig = { dir: 'tests/videos/', }, }, + geolocation: { latitude: 88, longitude: 99 }, + permissions: ['geolocation'], }, }; diff --git a/scripts/build-atomics.ts b/scripts/build-atomics.ts index 4052dc9e..8b605641 100644 --- a/scripts/build-atomics.ts +++ b/scripts/build-atomics.ts @@ -12,9 +12,10 @@ import { writeFile } from 'fs-extra'; import { webWorkerBlobUrlPlugin } from './build-web-worker'; export function buildAtomics(opts: BuildOptions): RollupOptions[] { - const rollups: RollupOptions[] = [buildAtomicsDebug(opts)]; + const rollups: RollupOptions[] = []; if (!opts.isDev) { + rollups.push(buildAtomicsDebug(opts)); rollups.push(buildAtomicsMin(opts)); } diff --git a/scripts/minify.ts b/scripts/minify.ts index 5fd783c2..969ca163 100644 --- a/scripts/minify.ts +++ b/scripts/minify.ts @@ -63,45 +63,48 @@ function managlePropsPlugin(): Plugin { const mangleProps: { [key: string]: string } = { $accessType$: '', $args$: '', - $cleanupInc$: '', + $assignInstanceId$: '', + $body$: '', $config$: '', $content$: '', - $contextWinId$: '', $currentScriptId$: '', $currentScriptUrl$: '', $data$: '', - $documentCompatMode$: '', - $documentReadyState$: '', - $documentReferrer$: '', + $document$: '', + $documentElement$: '', $error$: '', - $firstScriptId$: '', $forward$: '', - $forwardToWorkerAccess$: '', + $forwardedTriggers$: '', + $head$: '', $htmlConstructors$: '', $immediateSetters$: '', - $importScripts$: '', + $implementation$: '', + $interfaces$: '', $instanceId$: '', + $instanceIds$: '', $instances$: '', - $interfaces$: '', $interfaceType$: '', - $items: '', $isInitialized$: '', $isPromise$: '', + $isTop$: '', + $items$: '', $libPath$: '', $location$: '', + $memberName$: '', $memberPath$: '', $msgId$: '', - $newInstanceId$: '', $nodeName$: '', $parentWinId$: '', $postMessage$: '', $refId$: '', $rtnValue$: '', + $run$: '', $thisArg$: '', $url$: '', $window$: '', + $windowMembers$: '', + $windowMemberNames$: '', $winId$: '', - $worker$: '', }; if (chars.length < Object.keys(mangleProps).length) { diff --git a/src/lib/atomics/sync-send-message-to-main-atomics.ts b/src/lib/atomics/sync-send-message-to-main-atomics.ts index 24c33ff2..7f00cbd8 100644 --- a/src/lib/atomics/sync-send-message-to-main-atomics.ts +++ b/src/lib/atomics/sync-send-message-to-main-atomics.ts @@ -6,7 +6,7 @@ const syncSendMessageToMainAtomics = ( ): MainAccessResponse => { const accessRsp: MainAccessResponse = { $msgId$: accessReq.$msgId$, - $winId$: webWorkerCtx.$winId$, + $winId$: accessReq.$winId$, $error$: `Atomics not implemented (yet)`, }; return accessRsp; diff --git a/src/lib/main/loader.ts b/src/lib/main/loader.ts index 6ce051ee..a009f4b5 100644 --- a/src/lib/main/loader.ts +++ b/src/lib/main/loader.ts @@ -1,4 +1,4 @@ -import { debug, PT_INITIALIZED_EVENT, SCRIPT_TYPE } from '../utils'; +import { debug, PT_IFRAME_APPENDED, PT_INITIALIZED_EVENT, SCRIPT_TYPE } from '../utils'; import type { MainWindow } from '../types'; export function loader( @@ -44,8 +44,9 @@ export function loader( scripts = doc.querySelectorAll(`script[type="${SCRIPT_TYPE}"]`); - if (location !== parent.location) { - (parent as MainWindow)._ptWin!(win); + if (top !== win) { + // this is an iframe + top!.dispatchEvent(new CustomEvent(PT_IFRAME_APPENDED, { detail: win })); } else { if (scripts!.length) { timeout = setTimeout(fallback, debug ? 60000 : 10000); diff --git a/src/lib/sandbox/create-web-worker.ts b/src/lib/sandbox/create-web-worker.ts deleted file mode 100644 index 453a2469..00000000 --- a/src/lib/sandbox/create-web-worker.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { debug, logMain } from '../utils'; -import type { MainWindowContext } from '../types'; -import { onMessageFromWebWorker } from './messenger'; -import WebWorkerBlob from '@web-worker-blob'; -import WebWorkerUrl from '@web-worker-url'; - -export const createWebWorker = (winCtx: MainWindowContext) => { - winCtx.$worker$ = new Worker( - debug - ? WebWorkerUrl - : URL.createObjectURL( - new Blob([WebWorkerBlob], { - type: 'text/javascript', - }) - ), - { name: `Partytown (${winCtx.$winId$}) 🎉` } - ); - - winCtx.$worker$.onmessage = (ev) => onMessageFromWebWorker(winCtx, ev.data); - - if (debug) { - logMain(winCtx, `Created Web Worker (${winCtx.$winId$})`); - winCtx.$worker$.onerror = (ev) => console.error(`Web Worker (${winCtx.$winId$}) Error`, ev); - } -}; diff --git a/src/lib/sandbox/index.ts b/src/lib/sandbox/index.ts index 1ec2bb79..ab21adcd 100644 --- a/src/lib/sandbox/index.ts +++ b/src/lib/sandbox/index.ts @@ -1,4 +1,3 @@ import { initSandbox } from './init-sandbox'; -import { TOP_WIN_ID } from '../utils'; -initSandbox(window, TOP_WIN_ID); +initSandbox(window); diff --git a/src/lib/sandbox/init-sandbox.ts b/src/lib/sandbox/init-sandbox.ts index 508c69eb..8da20b78 100644 --- a/src/lib/sandbox/init-sandbox.ts +++ b/src/lib/sandbox/init-sandbox.ts @@ -1,62 +1,55 @@ -import { createWebWorker } from './create-web-worker'; -import { debug } from '../utils'; +import { debug, logMain, PT_IFRAME_APPENDED } from '../utils'; +import { getAndSetInstanceId } from './main-instances'; import { mainAccessHandler } from './main-access-handler'; -import { +import type { MainWindow, - MainWindowContext, + MessageFromWorkerToSandbox, MessengerRequestCallback, - PlatformInstanceId, + PartytownWebWorker, } from '../types'; -import { readNextScript } from './read-main-scripts'; -import { setInstanceId } from './main-instances'; +import { onMessageFromWebWorker } from './on-messenge-from-worker'; +import { registerWindow } from './main-register-window'; import syncCreateMessenger from '@sync-create-messenger'; -import { winCtxs, windows } from './main-constants'; +import WebWorkerBlob from '@web-worker-blob'; +import WebWorkerUrl from '@web-worker-url'; +import { winCtxs, windowIds } from './main-constants'; -export const initSandbox = async (sandboxWindow: Window, winIds: number) => { - const mainWindow: MainWindow = sandboxWindow.parent as any; - const $config$ = mainWindow.partytown || {}; - const $libPath$ = ($config$.lib || '/~partytown/') + (debug ? 'debug/' : ''); +export const initSandbox = async (sandboxWindow: any) => { + let worker: PartytownWebWorker; - const registerWindow = (win: MainWindow) => { - if (!windows.has(win)) { - windows.add(win); + const mainWindow: MainWindow = sandboxWindow.parent; - const parentWin = win.parent; - const winCtx: MainWindowContext = { - $winId$: (win._ptId = winIds++), - $parentWinId$: parentWin._ptId!, - $cleanupInc$: 0, - $config$, - $libPath$, - $url$: win.document.baseURI, - $window$: win, - }; - - winCtxs.set(winCtx.$winId$, winCtx); - - if (debug) { - winCtx.$startTime$ = performance.now(); - } - - setInstanceId(winCtx, win, PlatformInstanceId.window); - - createWebWorker(winCtx); - - win.addEventListener('load', () => readNextScript(winCtx)); - } - }; - - mainWindow._ptWin = registerWindow; - - const receiveMessage: MessengerRequestCallback = (accessReq, responseCallback) => { - const accessWinId = accessReq.$winId$; - const winCtx = winCtxs.get(accessWinId)!; - mainAccessHandler(winCtx, accessReq).then(responseCallback); - }; + const receiveMessage: MessengerRequestCallback = (accessReq, responseCallback) => + mainAccessHandler(worker, accessReq).then(responseCallback); const success = await syncCreateMessenger(sandboxWindow, receiveMessage); if (success) { - registerWindow(mainWindow); + worker = new Worker( + debug + ? WebWorkerUrl + : URL.createObjectURL( + new Blob([WebWorkerBlob], { + type: 'text/javascript', + }) + ), + { name: `Partytown 🎉` } + ); + + worker.onmessage = (ev: MessageEvent) => + onMessageFromWebWorker(worker, mainWindow, ev.data); + + if (debug) { + logMain(`Created web worker`); + worker.onerror = (ev) => console.error(`Web Worker Error`, ev); + } + + mainWindow.addEventListener(PT_IFRAME_APPENDED, (ev: CustomEvent) => { + const win: MainWindow = ev.detail; + const parentWinId = windowIds.get(win.parent); + const parentWinCtx = winCtxs[parentWinId!]!; + const winId = getAndSetInstanceId(parentWinCtx, win.frameElement); + registerWindow(worker, winId, win); + }); } }; diff --git a/src/lib/sandbox/main-access-handler.ts b/src/lib/sandbox/main-access-handler.ts index 0e11846c..b82d5ddf 100644 --- a/src/lib/sandbox/main-access-handler.ts +++ b/src/lib/sandbox/main-access-handler.ts @@ -1,70 +1,49 @@ -import { - AccessType, - MainAccessRequest, - MainAccessResponse, - MainWindowContext, - WorkerMessageType, -} from '../types'; +import { AccessType, MainAccessRequest, MainAccessResponse, PartytownWebWorker } from '../types'; import { deserializeFromWorker, serializeForWorker } from './main-serialization'; import { EMPTY_ARRAY, isPromise, len } from '../utils'; -import { forwardMsgResolves, winCtxs } from './main-constants'; import { getInstance, setInstanceId } from './main-instances'; +import { getWinCtx } from './main-register-window'; export const mainAccessHandler = async ( - winCtx: MainWindowContext, + worker: PartytownWebWorker, accessReq: MainAccessRequest ) => { + let $winId$ = accessReq.$winId$; let accessRsp: MainAccessResponse = { $msgId$: accessReq.$msgId$, - $winId$: accessReq.$winId$, + $winId$, }; - let instanceId = accessReq.$instanceId$; let accessType = accessReq.$accessType$; let memberPath = accessReq.$memberPath$; let memberPathLength = len(memberPath); let lastMemberName = memberPath[memberPathLength - 1]; let immediateSetters = accessReq.$immediateSetters$ || EMPTY_ARRAY; + let winCtx = await getWinCtx($winId$); let instance: any; let rtnValue: any; let data: any; let i: number; - let count: number; - let tmr: any; let immediateSetterTarget: any; let immediateSetterMemberPath; let immediateSetterMemberNameLen; try { // deserialize the data, such as a getter value or function arguments - data = deserializeFromWorker(accessReq.$data$); + data = deserializeFromWorker(worker, accessReq.$data$); - if (accessReq.$forwardToWorkerAccess$) { - // same as continue; - } else if (accessType === AccessType.GlobalConstructor) { + if (accessType === AccessType.GlobalConstructor) { // create a new instance of a global constructor - setInstanceId(winCtx, new (winCtx.$window$ as any)[lastMemberName](...data), instanceId); + setInstanceId(winCtx, new (winCtx!.$window$ as any)[lastMemberName](...data), instanceId); } else { // get the existing instance - instance = getInstance(accessRsp.$winId$, instanceId); + instance = getInstance($winId$, instanceId); if (instance) { for (i = 0; i < memberPathLength - 1; i++) { instance = instance[memberPath[i]]; } if (accessType === AccessType.Get) { - if (lastMemberName === '_ptId') { - await new Promise((resolve) => { - count = 0; - tmr = setInterval(() => { - if (isMemberInInstance(instance, memberPath) || count > 99) { - clearInterval(tmr); - resolve(); - } - count++; - }, 40); - }); - } rtnValue = instance[lastMemberName]; } else if (accessType === AccessType.Set) { instance[lastMemberName] = data; @@ -81,11 +60,11 @@ export const mainAccessHandler = async ( } immediateSetterTarget[immediateSetterMemberPath[immediateSetterMemberNameLen - 1]] = - deserializeFromWorker(immediateSetter[1]); + deserializeFromWorker(worker, immediateSetter[1]); }); - if (accessReq.$newInstanceId$) { - setInstanceId(winCtx, rtnValue, accessReq.$newInstanceId$); + if (accessReq.$assignInstanceId$) { + setInstanceId(winCtx, rtnValue, accessReq.$assignInstanceId$); } } @@ -93,32 +72,14 @@ export const mainAccessHandler = async ( rtnValue = await rtnValue; accessRsp.$isPromise$ = true; } - accessRsp.$rtnValue$ = serializeForWorker(winCtx, rtnValue); + accessRsp.$rtnValue$ = serializeForWorker($winId$, rtnValue); } else { - accessRsp.$error$ = `${instanceId} not found`; + accessRsp.$error$ = instanceId + ' not found'; } } } catch (e: any) { accessRsp.$error$ = String(e.stack || e); } - if (accessReq.$forwardToWorkerAccess$) { - return new Promise((resolve) => { - const forwardToWinId = accessReq.$contextWinId$ || accessReq.$winId$; - const otherWinCtx = winCtxs.get(forwardToWinId); - - tmr = setTimeout(() => { - forwardMsgResolves.delete(accessReq.$msgId$); - accessRsp.$error$ = `Timeout`; - resolve(accessRsp); - }, 30000); - - forwardMsgResolves.set(accessReq.$msgId$, [resolve, tmr]); - otherWinCtx!.$worker$!.postMessage([WorkerMessageType.ForwardWorkerAccessRequest, accessReq]); - }); - } else { - return accessRsp; - } + return accessRsp; }; - -const isMemberInInstance = (instance: any, memberPath: string[]) => memberPath[0] in instance; diff --git a/src/lib/sandbox/main-constants.ts b/src/lib/sandbox/main-constants.ts index 19b8d036..75e6fc4a 100644 --- a/src/lib/sandbox/main-constants.ts +++ b/src/lib/sandbox/main-constants.ts @@ -1,8 +1,5 @@ -import type { MainAccessResponse, MainWindow, MainWindowContext } from '../types'; +import type { MainWindow, MainWindowContext, WinId } from '../types'; -export const forwardMsgResolves = new Map void, any]>(); -export const mainInstanceIdByInstance = new WeakMap(); -export const mainInstances: [number, any][] = []; -export const mainInstanceRefs: { [instanceId: number]: { [key: string]: Function } } = {}; -export const winCtxs = new Map(); -export const windows = new WeakSet(); +export const mainRefs = new Map(); +export const windowIds = new WeakMap(); +export const winCtxs: { [winId: WinId]: MainWindowContext | undefined } = {}; diff --git a/src/lib/sandbox/main-forward-trigger.ts b/src/lib/sandbox/main-forward-trigger.ts index dfebabde..1dbfb091 100644 --- a/src/lib/sandbox/main-forward-trigger.ts +++ b/src/lib/sandbox/main-forward-trigger.ts @@ -1,27 +1,31 @@ import { len } from '../utils'; import { MainWindow, - MainWindowContext, PartytownForwardProperty, + PartytownWebWorker, PlatformInstanceId, WorkerMessageType, } from '../types'; import { serializeForWorker } from './main-serialization'; -export const mainForwardTrigger = (winCtx: MainWindowContext, win: MainWindow) => { +export const mainForwardTrigger = ( + worker: PartytownWebWorker, + $winId$: number, + win: MainWindow +) => { let existingTriggers = win._ptf; let forwardTriggers: any = (win._ptf = []); let i = 0; // see src/lib/main/snippet.ts and src/lib/web-worker/worker-forwarded-trigger.ts (forwardTriggers as any).push = ($forward$: PartytownForwardProperty, $args$: any[]) => - winCtx.$worker$!.postMessage([ + worker.postMessage([ WorkerMessageType.ForwardMainTrigger, { - $winId$: winCtx.$winId$, + $winId$, $instanceId$: PlatformInstanceId.window, $forward$, - $args$: serializeForWorker(winCtx, Array.from($args$)), + $args$: serializeForWorker($winId$, Array.from($args$)), }, ]); diff --git a/src/lib/sandbox/main-instances.ts b/src/lib/sandbox/main-instances.ts index e30ebcde..782a79fd 100644 --- a/src/lib/sandbox/main-instances.ts +++ b/src/lib/sandbox/main-instances.ts @@ -1,8 +1,8 @@ -import { len, randomId } from '../utils'; -import { mainInstanceIdByInstance, mainInstances, winCtxs } from './main-constants'; +import { randomId } from '../utils'; import { MainWindowContext, NodeName, PlatformInstanceId } from '../types'; +import { winCtxs } from './main-constants'; -export const getInstanceId = (instance: InstanceType | null | undefined) => { +const getInstanceId = (winCtx: MainWindowContext, instance: InstanceType | null | undefined) => { if (instance) { const nodeName = (instance as any as Node).nodeName; if (nodeName === NodeName.Document) { @@ -17,7 +17,7 @@ export const getInstanceId = (instance: InstanceType | null | undefined) => { if (nodeName === NodeName.Body) { return PlatformInstanceId.body; } - return mainInstanceIdByInstance.get(instance); + return winCtx.$instanceIds$.get(instance); } return -1; }; @@ -28,7 +28,7 @@ export const getAndSetInstanceId = ( instanceId?: number ) => { if (instance) { - instanceId = getInstanceId(instance); + instanceId = getInstanceId(winCtx, instance); if (typeof instanceId !== 'number') { setInstanceId(winCtx, instance, (instanceId = randomId())); } @@ -40,29 +40,25 @@ export const getAndSetInstanceId = ( export const getInstance = ( winId: number, instanceId: number, - instanceItem?: any, - winCtx?: MainWindowContext, - doc?: Document + instanceItem?: any ): T | undefined => { - winCtx = winCtxs.get(winId)!; - if (winCtx) { - doc = winCtx.$window$.document; - if (instanceId === PlatformInstanceId.document) { - return doc as any; - } - if (instanceId === PlatformInstanceId.documentElement) { - return doc.documentElement as any; - } - if (instanceId === PlatformInstanceId.head) { - return doc.head as any; - } - if (instanceId === PlatformInstanceId.body) { - return doc.body as any; - } - instanceItem = mainInstances.find((i) => i[0] === instanceId); - if (instanceItem) { - return instanceItem[1]; - } + const winCtx = winCtxs[winId]!; + const doc: Document = winCtx.$window$.document; + if (instanceId === PlatformInstanceId.document) { + return doc as any; + } + if (instanceId === PlatformInstanceId.documentElement) { + return doc.documentElement as any; + } + if (instanceId === PlatformInstanceId.head) { + return doc.head as any; + } + if (instanceId === PlatformInstanceId.body) { + return doc.body as any; + } + instanceItem = winCtx.$instances$.find((i) => i[0] === instanceId); + if (instanceItem) { + return instanceItem[1]; } }; @@ -72,30 +68,8 @@ export const setInstanceId = ( instanceId: number ) => { if (instance) { - mainInstances.push([instanceId, instance]); - mainInstanceIdByInstance.set(instance, instanceId); - - winCtx.$cleanupInc$++; - if (winCtx.$cleanupInc$ > 99) { - winCtx.$cleanupInc$ = 0; - while (true) { - let disconnectedNodes = mainInstances.filter( - (i) => (i[1] as InstanceNode).nodeType && !(i[1] as InstanceNode).isConnected - ); - let i: number; - let l: number; - if (len(disconnectedNodes) > 99) { - for (i = 0, l = len(mainInstances); i < l; i++) { - if (!(mainInstances[i][1] as InstanceNode).isConnected) { - mainInstances.slice(i, 1); - break; - } - } - } else { - break; - } - } - } + winCtx.$instances$.push([instanceId, instance]); + winCtx.$instanceIds$.set(instance, instanceId); } }; diff --git a/src/lib/sandbox/main-register-window.ts b/src/lib/sandbox/main-register-window.ts new file mode 100644 index 00000000..950a4596 --- /dev/null +++ b/src/lib/sandbox/main-register-window.ts @@ -0,0 +1,73 @@ +import { debug, logMain, normalizedWinId } from '../utils'; +import { + InitializeEnvironmentData, + MainWindow, + MainWindowContext, + PartytownWebWorker, + PlatformInstanceId, + WorkerMessageType, +} from '../types'; +import { setInstanceId } from './main-instances'; +import { winCtxs, windowIds } from './main-constants'; + +export const registerWindow = ( + worker: PartytownWebWorker, + $winId$: number, + $window$: MainWindow, + $isTop$?: number +) => { + if (!windowIds.has($window$)) { + windowIds.set($window$, $winId$); + + const doc = $window$.document; + const $url$ = doc.baseURI; + + const envData: InitializeEnvironmentData = { + $winId$, + $parentWinId$: windowIds.get($window$.parent)!, + $isTop$, + $url$, + }; + + const sendInitEnvData = () => + worker.postMessage([WorkerMessageType.InitializeEnvironment, envData]); + + const winCtx = (winCtxs[$winId$] = { + $winId$, + $window$, + $url$, + $instanceIds$: new WeakMap(), + $instances$: [], + }); + if (debug) { + winCtxs[$winId$]!.$startTime$ = performance.now(); + } + + setInstanceId(winCtx, $window$, PlatformInstanceId.window); + + if (debug) { + const winType = envData.$isTop$ ? 'top' : 'iframe'; + logMain(`Registered ${winType} window ${normalizedWinId($winId$)} (${$winId$})`); + } + + if (doc.readyState === 'complete') { + sendInitEnvData(); + } else { + $window$.addEventListener('load', sendInitEnvData); + } + } +}; + +export const getWinCtx = (winId: number) => { + let i = 0; + return new Promise((resolve) => { + const callback = () => { + if (winCtxs[winId] || i++ > 999) { + resolve(winCtxs[winId]!); + } else { + setTimeout(callback, 9); + } + }; + callback(); + }); +}; diff --git a/src/lib/sandbox/main-serialization.ts b/src/lib/sandbox/main-serialization.ts index e3279fa4..8e8a357a 100644 --- a/src/lib/sandbox/main-serialization.ts +++ b/src/lib/sandbox/main-serialization.ts @@ -2,24 +2,24 @@ import { getConstructorName, isValidMemberName } from '../utils'; import { getInstance, getAndSetInstanceId } from './main-instances'; import { InterfaceType, - MainWindowContext, + PartytownWebWorker, PlatformInstanceId, RefHandlerCallbackData, SerializedInstance, + SerializedObject, SerializedRefTransferData, SerializedTransfer, SerializedType, WorkerMessageType, } from '../types'; -import { mainInstanceRefs, winCtxs } from './main-constants'; +import { mainRefs, winCtxs } from './main-constants'; export const serializeForWorker = ( - winCtx: MainWindowContext, + $winId$: number, value: any, added?: Set, type?: string, - obj?: { [key: string]: SerializedTransfer | undefined }, - key?: string + cstrName?: string ): SerializedTransfer | undefined => { if (value !== undefined) { type = typeof value; @@ -36,60 +36,80 @@ export const serializeForWorker = ( if (Array.isArray(value)) { if (!added.has(value)) { added.add(value); - return [SerializedType.Array, value.map((v) => serializeForWorker(winCtx, v, added))]; + return [SerializedType.Array, value.map((v) => serializeForWorker($winId$, v, added))]; } return [SerializedType.Array, []]; } if (type === 'object') { if (value.nodeType) { - const nodeInstance: SerializedInstance = { - $winId$: winCtx.$winId$, - $interfaceType$: value.nodeType, - $instanceId$: getAndSetInstanceId(winCtx, value), - $nodeName$: value.nodeName, - }; - return [SerializedType.Instance, nodeInstance]; + return [ + SerializedType.Instance, + { + $winId$, + $interfaceType$: value.nodeType, + $instanceId$: getAndSetInstanceId(winCtxs[$winId$]!, value), + $nodeName$: value.nodeName, + }, + ]; } - if (value === value.window) { - const winInstance: SerializedInstance = { - $winId$: winCtx.$winId$, - $interfaceType$: InterfaceType.Window, - $instanceId$: PlatformInstanceId.window, - }; - return [SerializedType.Instance, winInstance]; + cstrName = getConstructorName(value); + if (cstrName === 'Window') { + return [ + SerializedType.Instance, + { + $winId$, + $interfaceType$: InterfaceType.Window, + $instanceId$: PlatformInstanceId.window, + }, + ]; } - if (isNodeList(getConstructorName(value))) { - const nodeList: SerializedInstance = { - $winId$: winCtx.$winId$, - $interfaceType$: InterfaceType.NodeList, - $items$: Array.from(value).map((v) => serializeForWorker(winCtx, v, added)![1]) as any, - }; - return [SerializedType.Instance, nodeList]; + if (cstrName === 'HTMLCollection' || cstrName === 'NodeList') { + return [ + SerializedType.Instance, + { + $winId$, + $interfaceType$: InterfaceType.NodeList, + $data$: Array.from(value).map((v) => serializeForWorker($winId$, v, added)![1]) as any, + }, + ]; } - obj = {}; - if (!added.has(value)) { - added.add(value); - for (key in value) { - if (isValidMemberName(key)) { - obj[key] = serializeForWorker(winCtx, value[key], added); - } - } + if (cstrName === 'Event') { + return [SerializedType.Event, serializeObjectForWorker($winId$, value, false, added)]; } - return [SerializedType.Object, obj]; + + return [SerializedType.Object, serializeObjectForWorker($winId$, value, true, added)]; } } }; +const serializeObjectForWorker = ( + winId: number, + obj: any, + includeFunctions: boolean, + added: Set, + serializedObj?: SerializedObject, + propName?: string, + propValue?: any +) => { + serializedObj = {}; + for (propName in obj) { + propValue = obj[propName]; + if (isValidMemberName(propName) && (includeFunctions || typeof propValue !== 'function')) { + serializedObj[propName] = serializeForWorker(winId, propValue, added); + } + } + return serializedObj; +}; + export const deserializeFromWorker = ( + worker: PartytownWebWorker, serializedTransfer: SerializedTransfer | undefined, serializedType?: SerializedType, - serializedValue?: any, - obj?: { [key: string]: any }, - key?: string + serializedValue?: any ): any => { if (serializedTransfer) { serializedType = serializedTransfer[0]; @@ -100,50 +120,65 @@ export const deserializeFromWorker = ( } if (serializedType === SerializedType.Ref) { - return deserializeRefFromWorker(serializedValue); + return deserializeRefFromWorker(worker, serializedValue); } if (serializedType === SerializedType.Array) { - return (serializedValue as SerializedTransfer[]).map((v) => deserializeFromWorker(v)); + return (serializedValue as SerializedTransfer[]).map((v) => deserializeFromWorker(worker, v)); } if (serializedType === SerializedType.Instance) { - const serializedInstance: SerializedInstance = serializedValue; - return getInstance(serializedInstance.$winId$, serializedInstance.$instanceId$!); + return getInstance( + (serializedValue as SerializedInstance).$winId$, + (serializedValue as SerializedInstance).$instanceId$! + ); + } + + if (serializedType === SerializedType.Event) { + return constructEvent(deserializeObjectFromWorker(worker, serializedValue)); } if (serializedType === SerializedType.Object) { - obj = {}; - for (key in serializedValue) { - obj[key] = deserializeFromWorker(serializedValue[key]); - } - return obj; + return deserializeObjectFromWorker(worker, serializedValue); } } }; -const isNodeList = (cstrName: string) => cstrName === 'HTMLCollection' || cstrName === 'NodeList'; +const deserializeRefFromWorker = ( + worker: PartytownWebWorker, + { $winId$, $instanceId$, $refId$ }: SerializedRefTransferData +) => { + let ref = mainRefs.get($refId$); -const deserializeRefFromWorker = ({ - $winId$, - $instanceId$, - $refId$, -}: SerializedRefTransferData) => { - let mainRefHandlerMap = (mainInstanceRefs[$instanceId$] = mainInstanceRefs[$instanceId$] || {}); - - if (!mainRefHandlerMap[$refId$]) { - mainRefHandlerMap[$refId$] = function (this: any, ...args: any[]) { - const winCtx = winCtxs.get($winId$)!; + if (!ref) { + ref = function (this: any, ...args: any[]) { const refHandlerData: RefHandlerCallbackData = { $winId$, $instanceId$, $refId$, - $thisArg$: serializeForWorker(winCtx, this), - $args$: serializeForWorker(winCtx, args), + $thisArg$: serializeForWorker($winId$, this), + $args$: serializeForWorker($winId$, args), }; - winCtx.$worker$!.postMessage([WorkerMessageType.RefHandlerCallback, refHandlerData]); + worker.postMessage([WorkerMessageType.RefHandlerCallback, refHandlerData]); }; + mainRefs.set($refId$, ref); } - return mainRefHandlerMap[$refId$]; + return ref; +}; + +const constructEvent = (eventProps: any) => + new ('detail' in eventProps ? CustomEvent : Event)(eventProps.type, eventProps); + +const deserializeObjectFromWorker = ( + worker: PartytownWebWorker, + serializedValue: any, + obj?: any, + key?: string +) => { + obj = {}; + for (key in serializedValue) { + obj[key] = deserializeFromWorker(worker, serializedValue[key]); + } + return obj; }; diff --git a/src/lib/sandbox/messenger.ts b/src/lib/sandbox/messenger.ts deleted file mode 100644 index 98b2f73a..00000000 --- a/src/lib/sandbox/messenger.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { forwardMsgResolves, winCtxs } from './main-constants'; -import { getAndSetInstanceId } from './main-instances'; -import { - InitWebWorkerData, - MainAccessResponse, - MainWindowContext, - MessageFromWorkerToSandbox, - WorkerMessageType, -} from '../types'; -import { initializedWorkerScript, readNextScript } from './read-main-scripts'; -import { readMainInterfaces } from './read-main-interfaces'; - -export const onMessageFromWebWorker = ( - winCtx: MainWindowContext, - msg: MessageFromWorkerToSandbox -) => { - const msgType = msg[0]; - const win = winCtx.$window$; - const doc = win.document; - - if (msgType === WorkerMessageType.MainDataRequestFromWorker) { - // web worker has requested data from the main thread - const initWebWorkerData: InitWebWorkerData = { - $winId$: winCtx.$winId$, - $parentWinId$: winCtx.$parentWinId$, - $config$: winCtx.$config$ || {}, - $documentCompatMode$: doc.compatMode, - $documentReadyState$: doc.readyState, - $documentReferrer$: doc.referrer, - $firstScriptId$: getAndSetInstanceId(winCtx, doc.querySelector('script')), - $htmlConstructors$: Object.getOwnPropertyNames(win).filter((c) => c.startsWith('HTML')), - $interfaces$: readMainInterfaces(win, doc), - $libPath$: new URL(winCtx.$libPath$, winCtx.$url$) + '', - $url$: winCtx.$url$, - }; - - // send to the web worker the main data - winCtx.$worker$!.postMessage([WorkerMessageType.MainDataResponseToWorker, initWebWorkerData]); - } else if (msgType === WorkerMessageType.InitializeNextWorkerScript) { - // web worker has been initialized with the main data - readNextScript(winCtx); - } else if (msgType === WorkerMessageType.InitializedWorkerScript) { - // web worker has finished initializing the script, and has another one to do - // doing this postMessage back-and-forth so we don't have long running tasks - initializedWorkerScript(winCtx, doc, msg[1] as number, msg[2] as string); - } else if (msgType === WorkerMessageType.ForwardWorkerAccessResponse) { - const accessRsp = msg[1] as MainAccessResponse; - - const forwardMsgResolveData = forwardMsgResolves.get(accessRsp.$msgId$); - if (forwardMsgResolveData) { - clearTimeout(forwardMsgResolveData[1]); - forwardMsgResolves.delete(accessRsp.$msgId$); - - forwardMsgResolveData[0](accessRsp); - readNextScript(winCtx); - } - } else if (msgType === WorkerMessageType.RunStateHandlers) { - // run this state prop on all web workers (only one of them actually has it) - // this is used for script onload, when the function was created in another window - winCtxs.forEach((winCtx) => winCtx.$worker$!.postMessage(msg)); - } -}; diff --git a/src/lib/sandbox/on-messenge-from-worker.ts b/src/lib/sandbox/on-messenge-from-worker.ts new file mode 100644 index 00000000..68ab8110 --- /dev/null +++ b/src/lib/sandbox/on-messenge-from-worker.ts @@ -0,0 +1,43 @@ +import { initializedWorkerScript, readNextScript } from './read-main-scripts'; +import { + MainWindow, + MessageFromWorkerToSandbox, + PartytownWebWorker, + WorkerMessageType, +} from '../types'; +import { randomId } from '../utils'; +import { readMainInterfaces } from './read-main-interfaces'; +import { registerWindow } from './main-register-window'; +import { winCtxs } from './main-constants'; + +export const onMessageFromWebWorker = ( + worker: PartytownWebWorker, + mainWindow: MainWindow, + msg: MessageFromWorkerToSandbox +) => { + const msgType = msg[0]; + + if (msgType === WorkerMessageType.MainDataRequestFromWorker) { + // web worker has requested data from the main thread + // collect up all the info about the main thread interfaces + const initWebWorkerData = readMainInterfaces(mainWindow); + + // send the main thread interface data to the web worker + worker.postMessage([WorkerMessageType.MainDataResponseToWorker, initWebWorkerData]); + } else if (msgType === WorkerMessageType.InitializedWebWorker) { + // web worker has finished initializing and ready to run scripts + registerWindow(worker, randomId(), mainWindow, 1); + } else { + const winCtx = winCtxs[msg[1]]!; + if (winCtx) { + if (msgType === WorkerMessageType.InitializeNextScript) { + // web worker has been initialized with the main data + readNextScript(worker, winCtx); + } else if (msgType === WorkerMessageType.InitializedEnvironmentScript) { + // web worker has finished initializing the script, and has another one to do + // doing this postMessage back-and-forth so we don't have long running tasks + initializedWorkerScript(worker, winCtx, msg[2] as number, msg[3] as string); + } + } + } +}; diff --git a/src/lib/sandbox/read-main-interfaces.ts b/src/lib/sandbox/read-main-interfaces.ts index 3ac8a0b6..dfc056d8 100644 --- a/src/lib/sandbox/read-main-interfaces.ts +++ b/src/lib/sandbox/read-main-interfaces.ts @@ -1,7 +1,19 @@ -import { getConstructorName, isValidMemberName, noop } from '../utils'; -import { InterfaceInfo, InterfaceType, MembersInterfaceTypeInfo } from '../types'; +import { debug, getConstructorName, isValidMemberName, logMain, noop } from '../utils'; +import { + InitWebWorkerData, + InterfaceInfo, + InterfaceType, + MainWindow, + MembersInterfaceTypeInfo, +} from '../types'; + +export const readMainInterfaces = (win: MainWindow) => { + // web worker has requested data from the main thread + const doc = win.document; + const $config$ = win.partytown || {}; + const $libPath$ = ($config$.lib || '/~partytown/') + (debug ? 'debug/' : ''); + const $url$ = win.location + ''; -export const readMainInterfaces = (win: Window, doc: Document) => { const docImpl = doc.implementation.createHTMLDocument(); const docHead = docImpl.head; @@ -14,6 +26,7 @@ export const readMainInterfaces = (win: Window, doc: Document) => { [InterfaceType.DOMTokenList, docHead.classList], [InterfaceType.Element, docHead], [InterfaceType.History, win.history], + [InterfaceType.Location, win.location], [InterfaceType.MutationObserver, new MutationObserver(noop)], [InterfaceType.NamedNodeMap, docHead.attributes], [InterfaceType.NodeList, docHead.childNodes], @@ -22,7 +35,7 @@ export const readMainInterfaces = (win: Window, doc: Document) => { [InterfaceType.TextNode, docImpl.createTextNode('')], ].map((i) => [...i, getConstructorName(i[1] as any)]) as any; - return implementations.map(([interfaceType, impl, cstrName]) => { + const $interfaces$ = implementations.map(([interfaceType, impl, cstrName]) => { let members: MembersInterfaceTypeInfo = {}; let memberName: string; let value: any; @@ -50,6 +63,21 @@ export const readMainInterfaces = (win: Window, doc: Document) => { const interfaceInfo: InterfaceInfo = [interfaceType, cstrName, members]; return interfaceInfo; }); + + const initWebWorkerData: InitWebWorkerData = { + $config$, + $htmlConstructors$: Object.getOwnPropertyNames(win).filter((c) => /^HT.*t$/i.test(c)), + $interfaces$, + $libPath$: new URL($libPath$, $url$) + '', + }; + + if (debug) { + logMain( + `Read main window, interfaces: ${initWebWorkerData.$interfaces$.length}, HTML Constructors: ${initWebWorkerData.$htmlConstructors$.length}` + ); + } + + return initWebWorkerData; }; type MainImplementation = [InterfaceType, any, string]; diff --git a/src/lib/sandbox/read-main-scripts.ts b/src/lib/sandbox/read-main-scripts.ts index 8dd69cae..54c55859 100644 --- a/src/lib/sandbox/read-main-scripts.ts +++ b/src/lib/sandbox/read-main-scripts.ts @@ -1,68 +1,91 @@ -import { debug, logMain, PT_INITIALIZED_EVENT, SCRIPT_TYPE } from '../utils'; +import { + debug, + logMain, + normalizedWinId, + PT_INITIALIZED_EVENT, + SCRIPT_TYPE, + SCRIPT_TYPE_EXEC, +} from '../utils'; import { getAndSetInstanceId } from './main-instances'; -import { MainWindowContext, InitializeScriptData, WorkerMessageType } from '../types'; +import { + InitializeScriptData, + MainWindowContext, + PartytownWebWorker, + WorkerMessageType, +} from '../types'; import { mainForwardTrigger } from './main-forward-trigger'; -export const readNextScript = (winCtx: MainWindowContext) => { - const $winId$ = winCtx.$winId$; - const win = winCtx.$window$; - const doc = win.document; - const scriptElm = doc.querySelector( - `script[type="${SCRIPT_TYPE}"]:not([data-pt-id]):not([data-pt-error])` - ); +export const readNextScript = (worker: PartytownWebWorker, winCtx: MainWindowContext) => { + let $winId$ = winCtx.$winId$; + let win = winCtx.$window$; + let doc = win.document; + let scriptSelector = `script[type="${SCRIPT_TYPE}"]:not([data-ptid]):not([data-pterror])`; + let blockingScriptSelector = scriptSelector + `:not([async]):not([defer])`; + let scriptElm = doc.querySelector(blockingScriptSelector); + let $instanceId$: number; + let scriptData: InitializeScriptData; + + if (!scriptElm) { + // first query for partytown scripts are blocking scripts that + // do not include async or defer attribute that should run first + // if no blocking scripts are found + // query again for all scripts which includes async / defer + scriptElm = doc.querySelector(scriptSelector); + } if (scriptElm) { // read the next script found - const $instanceId$ = getAndSetInstanceId(winCtx, scriptElm, $winId$); + scriptElm.dataset.ptid = $instanceId$ = getAndSetInstanceId(winCtx, scriptElm, $winId$) as any; - const scriptData: InitializeScriptData = { + scriptData = { $winId$, $instanceId$, }; - scriptElm.dataset.ptId = $winId$ + '.' + $instanceId$; - if (scriptElm.src) { scriptData.$url$ = scriptElm.src; } else { scriptData.$content$ = scriptElm.innerHTML; } - winCtx.$worker$!.postMessage([WorkerMessageType.InitializeNextWorkerScript, scriptData]); + worker.postMessage([WorkerMessageType.InitializeNextScript, scriptData]); } else if (!winCtx.$isInitialized$) { - // finished startup - winCtx.$isInitialized$ = true; - - if (win.frameElement) { - win.frameElement._ptId = $winId$; - } + // finished environment initialization + winCtx.$isInitialized$ = 1; - mainForwardTrigger(winCtx, win); + mainForwardTrigger(worker, $winId$, win); doc.dispatchEvent(new CustomEvent(PT_INITIALIZED_EVENT)); if (debug) { - logMain(winCtx, `Startup ${(performance.now() - winCtx.$startTime$!).toFixed(1)}ms`); + const winType = win === win.top ? 'top' : 'iframe'; + logMain( + `Executed ${winType} window ${normalizedWinId($winId$)} environment scripts in ${( + performance.now() - winCtx.$startTime$! + ).toFixed(1)}ms` + ); } + + worker.postMessage([WorkerMessageType.InitializedEnvironment, $winId$]); } }; export const initializedWorkerScript = ( + worker: PartytownWebWorker, winCtx: MainWindowContext, - doc: Document, instanceId: number, errorMsg: string, script?: HTMLScriptElement | null ) => { - script = doc.querySelector( - '[data-pt-id="' + winCtx.$winId$ + '.' + instanceId + '"]' - ); + script = winCtx.$window$.document.querySelector(`[data-ptid="${instanceId}"]`); + if (script) { if (errorMsg) { - script.dataset.ptError = errorMsg; + script.dataset.pterror = errorMsg; } else { - script.type += '-init'; + script.type += SCRIPT_TYPE_EXEC; } } - readNextScript(winCtx); + + readNextScript(worker, winCtx); }; diff --git a/src/lib/service-worker/fetch.ts b/src/lib/service-worker/fetch.ts index 43f2ed7d..6ca6bead 100644 --- a/src/lib/service-worker/fetch.ts +++ b/src/lib/service-worker/fetch.ts @@ -73,7 +73,6 @@ const httpRequestFromWebWorker = (self: ServiceWorkerGlobalScope, req: Request) new Promise(async (resolve) => { const accessReq: MainAccessRequest = await req.clone().json(); const responseData = await sendMessageToSandboxFromServiceWorker(self, accessReq); - resolve(response(JSON.stringify(responseData), 'application/json')); }); diff --git a/src/lib/types.ts b/src/lib/types.ts index 7caebaaa..4aeafca7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,3 +1,5 @@ +import type { HTMLDocument } from './web-worker/worker-document'; +import type { HTMLElement } from './web-worker/worker-element'; import type { Location } from './web-worker/worker-location'; export type CreateWorker = (workerName: string) => Worker; @@ -14,31 +16,32 @@ export type MessengerRequestCallback = ( export type MessengerResponseCallback = (accessRsp: MainAccessResponse) => void; +export type WinId = number; + export type MessageFromWorkerToSandbox = | [WorkerMessageType.MainDataRequestFromWorker] - | [WorkerMessageType.InitializedWorkerScript, number, string] - | [WorkerMessageType.InitializeNextWorkerScript] - | [WorkerMessageType.ForwardWorkerAccessResponse, MainAccessResponse] - | [WorkerMessageType.RunStateHandlers, number, StateProp]; + | [WorkerMessageType.InitializedWebWorker] + | [WorkerMessageType.InitializedEnvironmentScript, WinId, number, string] + | [WorkerMessageType.InitializeNextScript, WinId]; export type MessageFromSandboxToWorker = | [WorkerMessageType.MainDataResponseToWorker, InitWebWorkerData] - | [WorkerMessageType.InitializeNextWorkerScript, InitializeScriptData] + | [WorkerMessageType.InitializeEnvironment, InitializeEnvironmentData] + | [WorkerMessageType.InitializedEnvironment, WinId] + | [WorkerMessageType.InitializeNextScript, InitializeScriptData] | [WorkerMessageType.RefHandlerCallback, RefHandlerCallbackData] - | [WorkerMessageType.ForwardWorkerAccessRequest, MainAccessRequest] - | [WorkerMessageType.ForwardMainTrigger, ForwardMainTriggerData] - | [WorkerMessageType.RunStateHandlers, number, StateProp]; + | [WorkerMessageType.ForwardMainTrigger, ForwardMainTriggerData]; export const enum WorkerMessageType { MainDataRequestFromWorker, MainDataResponseToWorker, - InitializedWorkerScript, - InitializeNextWorkerScript, + InitializedWebWorker, + InitializeEnvironment, + InitializedEnvironment, + InitializedEnvironmentScript, + InitializeNextScript, RefHandlerCallback, - ForwardWorkerAccessRequest, - ForwardWorkerAccessResponse, ForwardMainTrigger, - RunStateHandlers, } export interface ForwardMainTriggerData { @@ -60,16 +63,12 @@ export type PostMessageToWorker = (msg: MessageFromSandboxToWorker) => void; export interface MainWindowContext { $winId$: number; - $parentWinId$: number; - $cleanupInc$: number; - $config$: PartytownConfig | undefined; - $interfaces$?: InterfaceInfo[]; - $isInitialized$?: boolean; - $libPath$: string; + $isInitialized$?: number; $startTime$?: number; $url$: string; $window$: MainWindow; - $worker$?: PartytownWebWorker; + $instanceIds$: WeakMap; + $instances$: [number, any][]; } export interface PartytownWebWorker extends Worker { @@ -77,28 +76,49 @@ export interface PartytownWebWorker extends Worker { } export interface InitWebWorkerData { - $winId$: number; - $parentWinId$: number; $config$: PartytownConfig; - $documentCompatMode$: string; - $documentReadyState$: string; - $documentReferrer$: string; - $firstScriptId$: number; $htmlConstructors$: string[]; $interfaces$: InterfaceInfo[]; $libPath$?: string; - $url$: string; } export interface InitWebWorkerContext { - $currentScriptId$: number; - $currentScriptUrl$: string; - $importScripts$: (...urls: string[]) => void; - $isInitialized$?: boolean; - $location$: Location; + $isInitialized$?: number; $postMessage$: (msg: MessageFromWorkerToSandbox) => void; } +export interface WebWorkerContext extends InitWebWorkerData, InitWebWorkerContext { + $forwardedTriggers$: string[]; + $windowMembers$: MembersInterfaceTypeInfo; + $windowMemberNames$: string[]; +} + +export interface InitializeEnvironmentData { + $winId$: number; + $parentWinId$: number; + $isTop$?: number; + $url$: string; +} + +export interface WebWorkerEnvironment extends Omit { + $window$: Window; + $document$: HTMLDocument; + $documentElement$: HTMLElement; + $head$: HTMLElement; + $body$: HTMLElement; + $location$: Location; + $run$: (content: string) => void; + $currentScriptId$?: number; + $currentScriptUrl$?: string; + $isInitialized$?: number; +} + +export interface WebWorkerGlobal { + $memberName$: string; + $interfaceType$: InterfaceType; + $implementation$: any; +} + export type InterfaceInfo = [InterfaceType, string, MembersInterfaceTypeInfo]; export interface MembersInterfaceTypeInfo { @@ -121,16 +141,18 @@ export const enum InterfaceType { DocumentFragmentNode = 11, // Global Constructors and window function implementations - Function = 12, - CSSStyleDeclaration = 13, - DOMStringMap = 14, - DOMTokenList = 15, - History = 16, - MutationObserver = 17, - NodeList = 18, - NamedNodeMap = 19, - Screen = 20, - Storage = 21, + Property = 12, + Function = 13, + CSSStyleDeclaration = 14, + DOMStringMap = 15, + DOMTokenList = 16, + History = 17, + Location = 18, + MutationObserver = 19, + NodeList = 20, + NamedNodeMap = 21, + Screen = 22, + Storage = 23, } export const enum PlatformInstanceId { @@ -141,8 +163,6 @@ export const enum PlatformInstanceId { body, } -export interface WebWorkerContext extends InitWebWorkerData, InitWebWorkerContext {} - export interface InitializeScriptData { $winId$: number; $instanceId$: number; @@ -161,7 +181,6 @@ export interface MainAccessRequest { $msgId$: number; $winId$: number; $contextWinId$?: number; - $forwardToWorkerAccess$: boolean; $instanceId$: number; $interfaceType$: InterfaceType; $nodeName$?: string; @@ -169,7 +188,7 @@ export interface MainAccessRequest { $memberPath$: string[]; $data$?: SerializedTransfer; $immediateSetters$?: ImmediateSetter[]; - $newInstanceId$?: number; + $assignInstanceId$?: number; } export type ImmediateSetter = [string[], SerializedTransfer | undefined]; @@ -185,8 +204,9 @@ export interface MainAccessResponse { export const enum SerializedType { Array, - Instance, + Event, Function, + Instance, Object, Primitive, Ref, @@ -194,10 +214,12 @@ export const enum SerializedType { export type SerializedArrayTransfer = [SerializedType.Array, (SerializedTransfer | undefined)[]]; -export type SerializedInstanceTransfer = [SerializedType.Instance, SerializedInstance]; +export type SerializedEventTransfer = [SerializedType.Event, SerializedObject]; export type SerializedFunctionTransfer = [SerializedType.Function]; +export type SerializedInstanceTransfer = [SerializedType.Instance, SerializedInstance]; + export type SerializedObjectTransfer = [ SerializedType.Object, { [key: string]: SerializedTransfer | undefined } @@ -210,28 +232,26 @@ export type SerializedPrimitiveTransfer = export type SerializedRefTransfer = [SerializedType.Ref, SerializedRefTransferData]; export interface SerializedRefTransferData { - /** - * The window the reference "meant" to be on - */ $winId$: number; - /** - * The window the reference is "actually" persisted in - */ - $contextWinId$: number; $instanceId$: number; $refId$: number; } export type SerializedTransfer = | SerializedArrayTransfer - | SerializedInstanceTransfer + | SerializedEventTransfer | SerializedFunctionTransfer + | SerializedInstanceTransfer | SerializedObjectTransfer | SerializedPrimitiveTransfer | SerializedPrimitiveTransfer | SerializedRefTransfer | []; +export interface SerializedObject { + [key: string]: SerializedTransfer | undefined; +} + export interface SerializedInstance { $winId$: number; $instanceId$?: number; @@ -244,9 +264,9 @@ export interface SerializedInstance { */ $nodeName$?: string; /** - * Node list data + * Instance data */ - $items$?: any[]; + $data$?: any; } /** @@ -353,17 +373,8 @@ export type PartytownForwardProperty = [ ]; export interface MainWindow extends Window { - frameElement: MainFrameElement | null; partytown?: PartytownConfig; - parent: MainWindow; - top: MainWindow; _ptf?: any[]; - _ptWin?: (win: MainWindow) => void; - _ptId?: number; -} - -export interface MainFrameElement extends HTMLIFrameElement { - _ptId?: number; } export const enum NodeName { @@ -388,9 +399,8 @@ export const enum NodeName { export const enum StateProp { errorHandlers = 'error', loadHandlers = 'load', - href = 'href', - loadError = 1, - partyWinId = 2, + loadErrorStatus = 1, + innerHTML = 2, url = 3, } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5131f0e1..f944c82c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,10 +1,4 @@ -import { - AccessType, - InterfaceType, - MainWindowContext, - NodeName, - PlatformInstanceId, -} from './types'; +import { AccessType, InterfaceType, NodeName, PlatformInstanceId } from './types'; import { InstanceIdKey, InterfaceTypeKey, @@ -21,23 +15,17 @@ export const toLower = (str: string) => str.toLowerCase(); export const toUpper = (str: string) => str.toUpperCase(); -export const logMain = (winCtx: MainWindowContext, msg: string) => { +export const logMain = (msg: string) => { if (debug) { - let prefix: string; - if (winCtx.$winId$ === TOP_WIN_ID) { - prefix = `Main (${winCtx.$winId$}) 🌎`; - } else { - prefix = `Iframe (${winCtx.$winId$}) 👾`; - } console.debug.apply(console, [ - `%c${prefix}`, + `%cMain 🌎`, `background: #717171; color: white; padding: 2px 3px; border-radius: 2px; font-size: 0.8em;`, msg, ]); } }; -export const logWorker = (msg: string) => { +export const logWorker = (msg: string, winId = -1) => { if (debug) { try { const config = webWorkerCtx.$config$; @@ -48,32 +36,54 @@ export const logWorker = (msg: string) => { msg += '\n' + frames.slice(i + 1).join('\n'); } + let prefix: string; + let color: string; + if (winId > -1) { + prefix = `Worker (${normalizedWinId(winId)}) 🎉`; + color = winColor(winId); + } else { + prefix = self.name; + color = `#9844bf`; + } + console.debug.apply(console, [ - `%c${self.name}`, - `background: #3498db; color: white; padding: 2px 3px; border-radius: 2px; font-size: 0.8em;`, + `%c${prefix}`, + `background: ${color}; color: white; padding: 2px 3px; border-radius: 2px; font-size: 0.8em;`, msg, ]); } catch (e) {} } }; +const winIds: number[] = []; +export const normalizedWinId = (winId: number) => { + if (!winIds.includes(winId)) { + winIds.push(winId); + } + return winIds.indexOf(winId) + 1; +}; + +const winColor = (winId: number) => { + const colors = ['#00309e', '#ea3655', '#eea727']; + const index = normalizedWinId(winId) - 1; + return colors[index] || colors[colors.length - 1]; +}; + export const logWorkerGetter = ( target: any, memberPath: string[], rtnValue: any, - skipOtherWindow = true + restrictedToWorker = false ) => { if (debug && webWorkerCtx.$config$.logGetters) { try { - if (target && target[WinIdKey] !== webWorkerCtx.$winId$ && skipOtherWindow) { - return; + const msg = `Get ${logTargetProp(target, AccessType.Get, memberPath)}, returned: ${logValue( + memberPath, + rtnValue + )}${restrictedToWorker ? ' (restricted to worker)' : ''}`; + if (!msg.includes('Symbol(')) { + logWorker(msg, target[WinIdKey]); } - logWorker( - `Get ${logTargetProp(target, AccessType.Get, memberPath)}, returned: ${logValue( - memberPath, - rtnValue - )} ` - ); } catch (e) {} } }; @@ -82,51 +92,38 @@ export const logWorkerSetter = ( target: any, memberPath: string[], value: any, - skipOtherWindow = true + restrictedToWorker = false ) => { if (debug && webWorkerCtx.$config$.logSetters) { try { - if (target && target[WinIdKey] !== webWorkerCtx.$winId$ && skipOtherWindow) { - return; - } logWorker( `Set ${logTargetProp(target, AccessType.Set, memberPath)}, value: ${logValue( memberPath, value - )}` + )}${restrictedToWorker ? ' (restricted to worker)' : ''}`, + target[WinIdKey] ); } catch (e) {} } }; -export const logWorkerCall = ( - target: any, - memberPath: string[], - args: any[], - rtnValue: any, - skipOtherWindow = true -) => { +export const logWorkerCall = (target: any, memberPath: string[], args: any[], rtnValue: any) => { if (debug && webWorkerCtx.$config$.logCalls) { try { - if (target && target[WinIdKey] !== webWorkerCtx.$winId$ && skipOtherWindow) { - return; - } logWorker( `Call ${logTargetProp(target, AccessType.CallMethod, memberPath)}(${args .map((v) => logValue(memberPath, v)) - .join(', ')}), returned: ${logValue(memberPath, rtnValue)}` + .join(', ')}), returned: ${logValue(memberPath, rtnValue)}`, + target[WinIdKey] ); } catch (e) {} } }; -export const logWorkerGlobalConstructor = (target: any, cstrName: string, args: any[]) => { +export const logWorkerGlobalConstructor = (winId: number, cstrName: string, args: any[]) => { if (debug && webWorkerCtx.$config$.logCalls) { try { - if (target && target[WinIdKey] !== webWorkerCtx.$winId$) { - return; - } - logWorker(`Construct new ${cstrName}(${args.map((v) => logValue([], v)).join(', ')})`); + logWorker(`Construct new ${cstrName}(${args.map((v) => logValue([], v)).join(', ')})`, winId); } catch (e) {} } }; @@ -136,7 +133,7 @@ const logTargetProp = (target: any, accessType: AccessType, memberPath: string[] if (target) { const instanceId = target[InstanceIdKey]; if (instanceId === PlatformInstanceId.window) { - n = 'window.'; + n = ''; } else if (instanceId === PlatformInstanceId.document) { n = 'document.'; } else if (instanceId === PlatformInstanceId.documentElement) { @@ -264,6 +261,11 @@ export const isValidMemberName = (memberName: string) => { } }; +export const defineConstructorName = (Cstr: any, value: string) => + Object.defineProperty(Cstr, 'name', { + value, + }); + export const nextTick = (cb: Function, ms?: number) => setTimeout(cb, ms); export const EMPTY_ARRAY = []; @@ -271,9 +273,8 @@ if (debug) { Object.freeze(EMPTY_ARRAY); } -export const PT_INITIALIZED_EVENT = `ptinit`; - -export const TOP_WIN_ID = 1; +export const PT_INITIALIZED_EVENT = `pt0`; +export const PT_IFRAME_APPENDED = `pt1`; export const randomId = () => Math.round(Math.random() * 9999999999 + PlatformInstanceId.body); @@ -286,3 +287,5 @@ export const randomId = () => Math.round(Math.random() * 9999999999 + PlatformIn * @public */ export const SCRIPT_TYPE = `text/partytown`; + +export const SCRIPT_TYPE_EXEC = `-x`; diff --git a/src/lib/web-worker/index.ts b/src/lib/web-worker/index.ts index a0fa1727..98be1842 100644 --- a/src/lib/web-worker/index.ts +++ b/src/lib/web-worker/index.ts @@ -1,19 +1,15 @@ import { callWorkerRefHandler } from './worker-serialization'; -import { debug, logWorker, nextTick } from '../utils'; -import { initNextScriptsInWebWorker } from './worker-exec'; -import { initWebWorker } from './init-worker'; +import { createEnvironment } from './worker-environment'; +import { debug, logWorker, nextTick, normalizedWinId } from '../utils'; +import { environments, webWorkerCtx } from './worker-constants'; import { ForwardMainTriggerData, - InitializeScriptData, InitWebWorkerData, - MainAccessRequest, MessageFromSandboxToWorker, - RefHandlerCallbackData, WorkerMessageType, } from '../types'; -import { runStateHandlers } from './worker-exec'; -import { webWorkerCtx } from './worker-constants'; -import { workerAccessHandler } from './worker-access-handler'; +import { initNextScriptsInWebWorker } from './worker-exec'; +import { initWebWorker } from './init-web-worker'; import { workerForwardedTriggerHandle } from './worker-forwarded-trigger'; const queuedEvents: MessageEvent[] = []; @@ -21,28 +17,35 @@ const queuedEvents: MessageEvent[] = []; const receiveMessageFromSandboxToWorker = (ev: MessageEvent) => { const msg = ev.data; const msgType = msg[0]; - const msgData1 = msg[1]; - const msgData2 = msg[2]; if (webWorkerCtx.$isInitialized$) { - if (msgType === WorkerMessageType.InitializeNextWorkerScript) { + if (msgType === WorkerMessageType.InitializeNextScript) { // message from main to web worker that it should initialize the next script - initNextScriptsInWebWorker(msgData1 as InitializeScriptData); + initNextScriptsInWebWorker(msg[1]); } else if (msgType === WorkerMessageType.RefHandlerCallback) { // main has called a worker ref handler - callWorkerRefHandler(msgData1 as RefHandlerCallbackData); - } else if (msgType === WorkerMessageType.ForwardWorkerAccessRequest) { - // message forwarded from another window, like the main window accessing data from an iframe - workerAccessHandler(msgData1 as MainAccessRequest); + callWorkerRefHandler(msg[1]); } else if (msgType === WorkerMessageType.ForwardMainTrigger) { - workerForwardedTriggerHandle(msgData1 as ForwardMainTriggerData); - } else if (msgType === WorkerMessageType.RunStateHandlers) { - runStateHandlers(msgData1 as number, msgData2 as any); + workerForwardedTriggerHandle(msg[1] as ForwardMainTriggerData); + } else if (msgType === WorkerMessageType.InitializeEnvironment) { + createEnvironment(msg[1]); + } else if (msgType === WorkerMessageType.InitializedEnvironment) { + environments[msg[1]].$isInitialized$ = 1; + + if (debug) { + const winId = msg[1]; + const env = environments[winId]; + const winType = env.$isTop$ ? 'top' : 'iframe'; + logWorker( + `Initialized ${winType} window ${normalizedWinId(winId)} environment (${winId}) 🎉`, + winId + ); + } } } else if (msgType === WorkerMessageType.MainDataResponseToWorker) { // initialize the web worker with the received the main data - initWebWorker(self as any, msgData1 as InitWebWorkerData); - webWorkerCtx.$postMessage$([WorkerMessageType.InitializeNextWorkerScript]); + initWebWorker(msg[1] as InitWebWorkerData); + webWorkerCtx.$postMessage$([WorkerMessageType.InitializedWebWorker]); nextTick(() => { if (debug && queuedEvents.length) { diff --git a/src/lib/web-worker/init-web-worker.ts b/src/lib/web-worker/init-web-worker.ts new file mode 100644 index 00000000..0b8c2ec2 --- /dev/null +++ b/src/lib/web-worker/init-web-worker.ts @@ -0,0 +1,42 @@ +import type { InitWebWorkerData } from '../types'; +import { defineConstructorName, EMPTY_ARRAY, logWorker } from '../utils'; +import { elementConstructors, getTagNameFromConstructor } from './worker-constructors'; +import { HTMLAnchorElement } from './worker-anchor'; +import { HTMLElement } from './worker-element'; +import { HTMLIFrameElement } from './worker-iframe'; +import { HTMLScriptElement } from './worker-script'; +import { webWorkerCtx } from './worker-constants'; + +export const initWebWorker = (initWebWorkerData: InitWebWorkerData) => { + Object.assign(webWorkerCtx, initWebWorkerData); + + webWorkerCtx.$forwardedTriggers$ = (webWorkerCtx.$config$.forward || EMPTY_ARRAY).map( + (f) => f[0] + ); + + webWorkerCtx.$windowMembers$ = webWorkerCtx.$interfaces$[0][2]; + webWorkerCtx.$windowMemberNames$ = Object.keys(webWorkerCtx.$windowMembers$).filter( + (m) => !webWorkerCtx.$forwardedTriggers$.includes(m) + ); + + webWorkerCtx.$postMessage$ = postMessage.bind(self); + + (self as any).postMessage = (self as any).importScripts = undefined; + + // create the same HTMLElement constructors that were found on main's window + // and add each constructor to the elementConstructors map, to be used by windows later + webWorkerCtx.$htmlConstructors$.map( + (htmlCstrName) => + (elementConstructors[getTagNameFromConstructor(htmlCstrName)] = defineConstructorName( + class extends HTMLElement {}, + htmlCstrName + )) + ); + elementConstructors.A = HTMLAnchorElement; + elementConstructors.IFRAME = HTMLIFrameElement; + elementConstructors.SCRIPT = HTMLScriptElement; + + webWorkerCtx.$isInitialized$ = 1; + + logWorker(`Initialized web worker`); +}; diff --git a/src/lib/web-worker/init-worker.ts b/src/lib/web-worker/init-worker.ts deleted file mode 100644 index d98cb0e0..00000000 --- a/src/lib/web-worker/init-worker.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { InitWebWorkerData } from '../types'; -import { initWebWorkerGlobal } from './worker-global'; -import { Location } from './worker-location'; -import { logWorker, TOP_WIN_ID } from '../utils'; -import { webWorkerCtx } from './worker-constants'; - -export const initWebWorker = (self: Worker, initWebWorkerData: InitWebWorkerData) => { - Object.assign(webWorkerCtx, initWebWorkerData); - - logWorker( - `Loaded web worker, winId: ${webWorkerCtx.$winId$}${ - webWorkerCtx.$winId$ > TOP_WIN_ID ? `, parentId: ` + webWorkerCtx.$parentWinId$ : `` - }` - ); - - webWorkerCtx.$importScripts$ = importScripts.bind(self); - (self as any).importScripts = undefined; - - webWorkerCtx.$postMessage$ = postMessage.bind(self); - (self as any).postMessage = (msg: any, targetOrigin: string) => - logWorker(`postMessage(${JSON.stringify(msg)}, "${targetOrigin}"})`); - - webWorkerCtx.$location$ = new Location(initWebWorkerData.$url$); - - initWebWorkerGlobal( - self, - initWebWorkerData.$interfaces$[0][2], - initWebWorkerData.$interfaces$, - initWebWorkerData.$htmlConstructors$ - ); - - webWorkerCtx.$isInitialized$ = true; -}; diff --git a/src/lib/web-worker/worker-access-handler.ts b/src/lib/web-worker/worker-access-handler.ts deleted file mode 100644 index 4dc6c891..00000000 --- a/src/lib/web-worker/worker-access-handler.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - AccessType, - MainAccessRequest, - MainAccessResponse, - PlatformInstanceId, - WorkerMessageType, -} from '../types'; -import { callMethod } from './worker-proxy'; -import { - constructSerializedInstance, - deserializeFromMain, - serializeForMain, -} from './worker-serialization'; -import { getStateValue } from './worker-state'; -import { len, logWorkerCall, logWorkerGetter, logWorkerSetter } from '../utils'; -import { webWorkerCtx } from './worker-constants'; - -export const workerAccessHandler = (accessReq: MainAccessRequest) => { - let $winId$ = accessReq.$winId$; - let instanceId = accessReq.$instanceId$; - let accessType = accessReq.$accessType$; - let memberPath = accessReq.$memberPath$; - - let accessRsp: MainAccessResponse = { - $msgId$: accessReq.$msgId$, - $winId$, - }; - - let memberPathLength = len(memberPath); - let lastMemberName = memberPath[memberPathLength - 1]; - - let instance: any; - let rtnValue: any; - let data: any; - let i: number; - let stateValue: any; - - try { - instance = constructSerializedInstance({ ...accessReq, $winId$ }); - - stateValue = getStateValue(instanceId, memberPath[0]); - if (stateValue) { - instance[memberPath[0]] = deserializeFromMain($winId$, instanceId, memberPath, stateValue); - } - - for (i = 0; i < memberPathLength - 1; i++) { - instance = instance[memberPath[i]]; - } - - data = deserializeFromMain($winId$, instanceId, memberPath, accessReq.$data$); - - if (accessType === AccessType.Get) { - rtnValue = instance[lastMemberName]; - logWorkerGetter(instance, memberPath, rtnValue, false); - } else if (accessType === AccessType.Set) { - logWorkerSetter(instance, memberPath, data, false); - instance[lastMemberName] = data; - } else if (accessType === AccessType.CallMethod) { - if (instanceId === PlatformInstanceId.document && lastMemberName === 'createElement') { - rtnValue = callMethod( - instance, - memberPath, - data, - accessReq.$immediateSetters$, - accessReq.$newInstanceId$ - ); - } else { - rtnValue = instance[lastMemberName].apply(instance, data); - } - logWorkerCall(instance, memberPath, data, rtnValue, false); - } - - accessRsp.$rtnValue$ = serializeForMain($winId$, instanceId, rtnValue); - } catch (e: any) { - accessRsp.$error$ = String(e.stack || e); - } - - webWorkerCtx.$postMessage$([WorkerMessageType.ForwardWorkerAccessResponse, accessRsp]); -}; diff --git a/src/lib/web-worker/worker-anchor.ts b/src/lib/web-worker/worker-anchor.ts index b540754f..3cc7aaef 100644 --- a/src/lib/web-worker/worker-anchor.ts +++ b/src/lib/web-worker/worker-anchor.ts @@ -18,7 +18,7 @@ export class HTMLAnchorElement extends HTMLElement { return getUrl(this) + ''; } set href(href: string) { - setInstanceStateValue(this, StateProp.href, href); + setInstanceStateValue(this, StateProp.url, href); setter(this, ['href'], href); } get origin() { diff --git a/src/lib/web-worker/worker-constants.ts b/src/lib/web-worker/worker-constants.ts index 908251d4..08c2fec2 100644 --- a/src/lib/web-worker/worker-constants.ts +++ b/src/lib/web-worker/worker-constants.ts @@ -1,4 +1,4 @@ -import type { RefHandler, StateMap, WebWorkerContext } from '../types'; +import type { RefHandler, StateMap, WebWorkerContext, WebWorkerEnvironment } from '../types'; export const WinIdKey = Symbol(); export const InstanceIdKey = Symbol(); @@ -7,8 +7,10 @@ export const NodeNameKey = Symbol(); export const ProxyKey = Symbol(); export const ImmediateSettersKey = Symbol(); -export const webWorkerRefsByRefId: { [refId: number]: RefHandler | undefined } = {}; +export const webWorkerRefsByRefId: { [refId: number]: RefHandler } = {}; export const webWorkerRefIdsByRef = new WeakMap(); export const webWorkerState: StateMap = {}; export const webWorkerCtx: WebWorkerContext = {} as any; + +export const environments: { [winId: number]: WebWorkerEnvironment } = {}; diff --git a/src/lib/web-worker/worker-constructors.ts b/src/lib/web-worker/worker-constructors.ts index d76cb02c..fb369c18 100644 --- a/src/lib/web-worker/worker-constructors.ts +++ b/src/lib/web-worker/worker-constructors.ts @@ -1,21 +1,17 @@ +import type { HTMLElement } from './worker-element'; import { InterfaceType, NodeName } from '../types'; -import { HTMLDocument } from './worker-document'; -import { HTMLElement } from './worker-element'; import { Node } from './worker-node'; import { toUpper } from '../utils'; -import { Window } from './worker-iframe'; import { WorkerProxy } from './worker-proxy-constructor'; export const constructInstance = ( interfaceType: InterfaceType, instanceId: number, - winId?: number, + winId: number, nodeName?: string ) => { nodeName = - interfaceType === InterfaceType.Document - ? NodeName.Document - : interfaceType === InterfaceType.TextNode + interfaceType === InterfaceType.TextNode ? NodeName.Text : interfaceType === InterfaceType.CommentNode ? NodeName.Comment @@ -32,10 +28,6 @@ export const constructInstance = ( const getConstructor = (interfaceType: InterfaceType, nodeName?: string): typeof WorkerProxy => { if (interfaceType === InterfaceType.Element) { return getElementConstructor(nodeName!); - } else if (interfaceType === InterfaceType.Document) { - return HTMLDocument; - } else if (interfaceType === InterfaceType.Window) { - return Window; } else if (interfaceType <= InterfaceType.DocumentFragmentNode) { return Node; } else { @@ -44,21 +36,26 @@ const getConstructor = (interfaceType: InterfaceType, nodeName?: string): typeof }; export const getElementConstructor = (nodeName: string): typeof HTMLElement => - elementConstructors[nodeName] || HTMLElement; + elementConstructors[nodeName] || elementConstructors.UNKNOWN; export const elementConstructors: { [tagName: string]: typeof HTMLElement } = {}; export const getTagNameFromConstructor = (t: string) => { t = toUpper(t.substr(4).replace('Element', '')); - if (t === 'IMAGE') { - return 'IMG'; - } else if (t === 'PARAGRAPH') { - return 'P'; - } else if (t === 'TABLEROW') { - return 'TR'; - } else if (t === 'TableCell') { - return 'TD'; - } else { - return t; - } + return ( + { + IMAGE: 'IMG', + OLIST: 'OL', + PARAGRAPH: 'P', + TABLECELL: 'TD', + TABLEROW: 'TR', + ULIST: 'UL', + }[t] || t + ); }; + +export const constructEvent = (eventProps: any) => + new Proxy(new Event(eventProps.type, eventProps), { + get: (target: any, propName) => + propName in eventProps ? eventProps[propName] : target[String(propName)], + }); diff --git a/src/lib/web-worker/worker-document.ts b/src/lib/web-worker/worker-document.ts index 4f3ecaef..573853a8 100644 --- a/src/lib/web-worker/worker-document.ts +++ b/src/lib/web-worker/worker-document.ts @@ -1,24 +1,16 @@ -import { callMethod, getter, setter } from './worker-proxy'; +import { callMethod } from './worker-proxy'; import { constructInstance, getElementConstructor } from './worker-constructors'; +import { createEnvironment, getEnv, getEnvWindow } from './worker-environment'; import { getPartytownScript } from './worker-exec'; import { HTMLElement } from './worker-element'; -import { ImmediateSettersKey, webWorkerCtx, WinIdKey } from './worker-constants'; -import { InterfaceType, NodeName, PlatformInstanceId } from '../types'; -import { logWorkerGetter, logWorkerSetter, SCRIPT_TYPE, randomId, toUpper } from '../utils'; +import { ImmediateSetter, InterfaceType, NodeName } from '../types'; +import { ImmediateSettersKey, WinIdKey } from './worker-constants'; +import { SCRIPT_TYPE, randomId, toUpper, defineConstructorName } from '../utils'; import { serializeForMain } from './worker-serialization'; export class HTMLDocument extends HTMLElement { get body() { - return constructInstance( - InterfaceType.Element, - PlatformInstanceId.body, - this[WinIdKey], - NodeName.Body - ); - } - - get compatMode() { - return webWorkerCtx.$documentCompatMode$; + return getEnv(this).$body$; } createElement(tagName: string) { @@ -28,30 +20,31 @@ export class HTMLDocument extends HTMLElement { const instanceId = randomId(); const ElementCstr = getElementConstructor(tagName); const elm = new ElementCstr(InterfaceType.Element, instanceId, winId, tagName); + const immediateSetter: ImmediateSetter[] = (elm[ImmediateSettersKey] = []); - if (tagName === NodeName.Script) { - elm[ImmediateSettersKey] = [[['type'], serializeForMain(winId, instanceId, SCRIPT_TYPE)]]; - } else if (tagName === NodeName.IFrame) { - elm[ImmediateSettersKey] = [ - [['srcdoc'], serializeForMain(winId, instanceId, getPartytownScript())], - ]; - } else { - elm[ImmediateSettersKey] = []; + if (tagName === NodeName.IFrame) { + // an iframe element's instanceId is the same as its contentWindow's winId + // and the contentWindow's parentWinId is the iframe element's winId + createEnvironment({ $winId$: instanceId, $parentWinId$: winId, $url$: 'about:blank' }); + + immediateSetter.push([['srcdoc'], serializeForMain(winId, instanceId, getPartytownScript())]); + } else if (tagName === NodeName.Script) { + immediateSetter.push([['type'], serializeForMain(winId, instanceId, SCRIPT_TYPE)]); } return elm; } - get createEventObject() { - // common check we can just avoid - return undefined; + createEvent(type: string) { + return new Event(type); } get currentScript() { - if (webWorkerCtx.$currentScriptId$ > 0) { + const currentScriptId = getEnv(this).$currentScriptId$!; + if (currentScriptId > 0) { return constructInstance( InterfaceType.Element, - webWorkerCtx.$currentScriptId$, + currentScriptId, this[WinIdKey], NodeName.Script ); @@ -60,16 +53,11 @@ export class HTMLDocument extends HTMLElement { } get defaultView() { - return self; + return getEnvWindow(this); } get documentElement() { - return constructInstance( - InterfaceType.Element, - PlatformInstanceId.documentElement, - this[WinIdKey], - NodeName.DocumentElement - ); + return getEnv(this).$documentElement$; } getElementsByTagName(tagName: string) { @@ -78,27 +66,13 @@ export class HTMLDocument extends HTMLElement { return [this.body]; } else if (tagName === NodeName.Head) { return [this.head]; - } else if (tagName === NodeName.Script) { - return [ - constructInstance( - InterfaceType.Element, - webWorkerCtx.$firstScriptId$, - this[WinIdKey], - NodeName.Script - ), - ]; } else { return callMethod(this, ['getElementsByTagName'], [tagName]); } } get head() { - return constructInstance( - InterfaceType.Element, - PlatformInstanceId.head, - this[WinIdKey], - NodeName.Head - ); + return getEnv(this).$head$; } get implementation() { @@ -108,12 +82,10 @@ export class HTMLDocument extends HTMLElement { } get location() { - logWorkerGetter(this, ['location'], webWorkerCtx.$location$); - return webWorkerCtx.$location$; + return getEnv(this).$location$; } set location(url: any) { - logWorkerSetter(this, ['location'], url); - webWorkerCtx.$location$!.href = url + ''; + getEnv(this).$location$.href = url + ''; } get parentNode() { @@ -125,25 +97,26 @@ export class HTMLDocument extends HTMLElement { } get readyState() { - if (webWorkerCtx.$documentReadyState$ !== 'complete') { - webWorkerCtx.$documentReadyState$ = getter(this, ['readyState']); - } else { - logWorkerGetter(this, ['readyState'], webWorkerCtx.$documentReadyState$); - } - return webWorkerCtx.$documentReadyState$; - } - - get referrer() { - logWorkerGetter(this, ['referrer'], webWorkerCtx.$documentReferrer$); - return webWorkerCtx.$documentReferrer$; + return 'complete'; } } -export class WorkerDocumentElementChild extends HTMLElement { - get parentElement() { - return document.documentElement; - } - get parentNode() { - return document.documentElement; - } -} +export const constructDocumentElementChild = ( + winId: number, + instanceId: number, + titleCaseNodeName: 'Body' | 'Head', + documentElement: HTMLElement +) => { + const HtmlCstr: typeof HTMLElement = defineConstructorName( + class extends HTMLElement { + get parentElement() { + return documentElement; + } + get parentNode() { + return documentElement; + } + }, + `HTML${titleCaseNodeName}Element` + ); + return new HtmlCstr(InterfaceType.Element, instanceId, winId, toUpper(titleCaseNodeName)); +}; diff --git a/src/lib/web-worker/worker-environment.ts b/src/lib/web-worker/worker-environment.ts new file mode 100644 index 00000000..d093fb6a --- /dev/null +++ b/src/lib/web-worker/worker-environment.ts @@ -0,0 +1,240 @@ +import { callMethod, createGlobalConstructorProxy, getter, proxy, setter } from './worker-proxy'; +import { constructDocumentElementChild, HTMLDocument } from './worker-document'; +import { constructInstance, elementConstructors } from './worker-constructors'; +import { createImageConstructor } from './worker-image'; +import { createNavigator } from './worker-navigator'; +import { debug, logWorker, normalizedWinId } from '../utils'; +import { + environments, + InstanceIdKey, + InterfaceTypeKey, + webWorkerCtx, + WinIdKey, +} from './worker-constants'; +import { HTMLElement } from './worker-element'; +import { + InitializeEnvironmentData, + InterfaceType, + NodeName, + PlatformInstanceId, + WorkerMessageType, +} from '../types'; +import { Location } from './worker-location'; +import { Node } from './worker-node'; + +export const createEnvironment = ({ + $winId$, + $parentWinId$, + $isTop$, + $url$, +}: InitializeEnvironmentData) => { + if (environments[$winId$]) { + // this environment (iframe) is already initialized + environments[$winId$].$location$.href = $url$; + } else { + // create a simulated global environment for this window + + class Window { + [WinIdKey]: number; + [InstanceIdKey]: number; + [InterfaceTypeKey]: InterfaceType; + + constructor() { + initWindowInstance(this); + return proxy(InterfaceType.Window, this, []); + } + + get document() { + return $document$; + } + + get frameElement() { + if ($isTop$) { + return null; + } + // the winId of an iframe's window is the same + // as the instanceId of the containing iframe element + const env = getEnv(this); + const iframeElementInstanceId = this[WinIdKey]; + const iframeElementWinId = env.$parentWinId$; + return constructInstance( + InterfaceType.Element, + iframeElementInstanceId, + iframeElementWinId, + NodeName.IFrame + ); + } + + get globalThis() { + return getEnvWindow(this); + } + + get location() { + return $location$; + } + set location(loc: any) { + $location$.href = loc + ''; + } + + get parent() { + return environments[$parentWinId$].$window$; + } + + get self() { + return getEnvWindow(this); + } + + get top() { + for (const envWinId in environments) { + if (environments[envWinId].$isTop$) { + return environments[envWinId].$window$; + } + } + } + + get window() { + return getEnvWindow(this); + } + } + + const $document$ = new HTMLDocument( + InterfaceType.Document, + PlatformInstanceId.document, + $winId$, + NodeName.Document + ); + + const $documentElement$ = new elementConstructors.HTML( + InterfaceType.Element, + PlatformInstanceId.documentElement, + $winId$, + NodeName.DocumentElement + ); + + const $head$ = constructDocumentElementChild( + $winId$, + PlatformInstanceId.head, + 'Head', + $documentElement$ + ); + + const $body$ = constructDocumentElementChild( + $winId$, + PlatformInstanceId.body, + 'Body', + $documentElement$ + ); + + const $location$ = new Location($url$); + + const windowFunctionWhiteList = + 'addEventListener,removeEventListener,dispatchEvent,postMessage'.split(','); + + const windowPropertyWhiteList = 'onmessage,onload,onerror'.split(','); + + const initWindowInstance = (win: any) => { + win[WinIdKey] = $winId$; + + // InterfaceType.Window and PlatformInstanceId.window both = 0 + win[InstanceIdKey] = win[InterfaceTypeKey] = PlatformInstanceId.window; + + // bind web worker global functions to the environment window + // window.atob = self.atob.bind(self); + for (const globalName in self) { + if (typeof self[globalName] === 'function' && !(globalName in win)) { + win[globalName] = (self as any)[globalName].bind(self); + } + } + + // assign web worker global properties to the environment window + // window.Promise = self.Promise + Object.getOwnPropertyNames(self).map((globalName) => { + if (!(globalName in win)) { + win[globalName] = (self as any)[globalName]; + } + }); + + // create the same HTMLElement constructors that were found on main's window + // and add each constructor to the windown environment + // window.HTMLParagraphElement = class extends HTMLElement {...} + Object.keys(elementConstructors).map( + (tagName) => (win[elementConstructors[tagName].name] = elementConstructors[tagName]) + ); + + // create interface properties found on window + // window.history = {...} + webWorkerCtx.$windowMemberNames$.map((memberName) => { + const $interfaceType$ = webWorkerCtx.$windowMembers$[memberName]; + const isFunctionInterface = $interfaceType$ === InterfaceType.Function; + const isValidInterface = + isFunctionInterface || $interfaceType$ > InterfaceType.DocumentFragmentNode; + + if ( + isValidInterface && + (!(memberName in win) || windowFunctionWhiteList.includes(memberName)) + ) { + win[memberName] = isFunctionInterface + ? (...args: any[]) => callMethod(win, [memberName], args) + : proxy($interfaceType$, win, ['window', memberName]); + } + }); + + // create global constructor proxies + // window.MutationObserver = class {...} + webWorkerCtx.$interfaces$.map((i) => { + const interfaceType = i[0]; + const memberName = i[1]; + win[memberName] = createGlobalConstructorProxy($winId$, interfaceType, memberName); + }); + + win.Document = HTMLDocument; + win.Element = win.HTMLElement = HTMLElement; + win.Image = createImageConstructor($winId$); + win.Node = Node; + win.Window = Window; + + win.performance = self.performance; + win.name = name + (debug ? `${normalizedWinId($winId$)} (${$winId$})` : ($winId$ as any)); + win.navigator = createNavigator($winId$); + + windowPropertyWhiteList.map((propName) => + Object.defineProperty(win, propName, { + get: () => getter($window$, [propName]), + set: (value) => setter($window$, [propName], value), + }) + ); + }; + + const $window$: any = new Window(); + + environments[$winId$] = { + $winId$, + $parentWinId$, + $window$, + $document$, + $documentElement$, + $head$, + $body$, + $location$, + $isTop$, + $run$: (script: string) => { + const runInEnv = new Function(`with(this){${script}}`); + runInEnv.apply($window$); + }, + }; + + if (debug) { + const winType = $isTop$ ? 'top' : 'iframe'; + logWorker( + `Created ${winType} window ${normalizedWinId($winId$)} environment (${$winId$})`, + $winId$ + ); + } + } + + webWorkerCtx.$postMessage$([WorkerMessageType.InitializeNextScript, $winId$]); +}; + +export const getEnv = (instance: { [WinIdKey]: number }) => environments[instance[WinIdKey]]; + +export const getEnvWindow = (instance: { [WinIdKey]: number }) => getEnv(instance).$window$; diff --git a/src/lib/web-worker/worker-exec.ts b/src/lib/web-worker/worker-exec.ts index 6ead038b..14124572 100644 --- a/src/lib/web-worker/worker-exec.ts +++ b/src/lib/web-worker/worker-exec.ts @@ -1,97 +1,185 @@ -import { debug, logWorker, nextTick, SCRIPT_TYPE } from '../utils'; -import { EventHandler, InitializeScriptData, StateProp, WorkerMessageType } from '../types'; +import { debug, logWorker, nextTick, SCRIPT_TYPE, SCRIPT_TYPE_EXEC } from '../utils'; +import { + environments, + ImmediateSettersKey, + InstanceIdKey, + webWorkerCtx, + WinIdKey, +} from './worker-constants'; +import { + EventHandler, + InitializeScriptData, + WebWorkerEnvironment, + StateProp, + WorkerMessageType, +} from '../types'; +import { getEnv } from './worker-environment'; import { getInstanceStateValue, getStateValue, setStateValue } from './worker-state'; import type { HTMLElement } from './worker-element'; +import type { Location } from './worker-location'; import type { Node } from './worker-node'; -import { webWorkerCtx } from './worker-constants'; +import { serializeForMain } from './worker-serialization'; -export const initNextScriptsInWebWorker = (initScript: InitializeScriptData) => { +export const initNextScriptsInWebWorker = async (initScript: InitializeScriptData) => { let winId = initScript.$winId$; let instanceId = initScript.$instanceId$; - let content = initScript.$content$; - let url = initScript.$url$; + let scriptContent = initScript.$content$; + let scriptSrc = initScript.$url$; let errorMsg = ''; - let handlersType = StateProp.loadHandlers; + let env = environments[winId]; + let rsp: Response; - if (url) { + if (scriptSrc) { try { - url = resolveUrl(url) + ''; - setStateValue(instanceId, StateProp.url, url); - setCurrentScript(instanceId, url); - - if (debug && winId !== webWorkerCtx.$winId$) { - console.error( - `Incorrect window context, winId: ${winId}, instanceId: ${instanceId}, url: ${url}` - ); - } + scriptSrc = resolveUrl(env, scriptSrc); + setStateValue(instanceId, StateProp.url, scriptSrc); if (debug && webWorkerCtx.$config$.logScriptExecution) { - logWorker(`Execute script[data-pt-id="${winId}.${instanceId}"], src: ${url}`); + logWorker(`Execute script (${instanceId}) src: ${scriptSrc}`, winId); } - webWorkerCtx.$importScripts$(url); + rsp = await fetch(scriptSrc, { mode: 'no-cors' }); + if (rsp.ok) { + scriptContent = await rsp.text(); + + env.$currentScriptId$ = instanceId; + env.$currentScriptUrl$ = scriptSrc; + env.$run$(scriptContent); + runStateLoadHandlers(instanceId, StateProp.loadHandlers); + } else { + console.error(rsp.status, 'url:', scriptSrc); + errorMsg = rsp.statusText; + runStateLoadHandlers(instanceId, StateProp.errorHandlers); + } } catch (urlError: any) { - console.error(name, urlError, '\n' + url); - handlersType = StateProp.errorHandlers; + console.error('url:', scriptSrc, urlError); errorMsg = String(urlError.stack || urlError) + ''; + runStateLoadHandlers(instanceId, StateProp.errorHandlers); } - - if (!runStateHandlers(instanceId, handlersType)) { - webWorkerCtx.$postMessage$([WorkerMessageType.RunStateHandlers, instanceId, handlersType]); - } - } else if (content) { - try { - if (debug && webWorkerCtx.$config$.logScriptExecution) { - logWorker(`Execute script[data-pt-id="${winId}.${instanceId}"]`); - } - setCurrentScript(instanceId, ''); - new Function(content)(); - } catch (contentError: any) { - console.error(name, contentError, '\n' + content); - handlersType = StateProp.errorHandlers; - errorMsg = String(contentError.stack || contentError) + ''; - } + } else if (scriptContent) { + errorMsg = runScriptContent(env, instanceId, scriptContent, winId); } - setCurrentScript(-1, ''); + env.$currentScriptId$ = -1; + env.$currentScriptUrl$ = ''; - webWorkerCtx.$postMessage$([WorkerMessageType.InitializedWorkerScript, instanceId, errorMsg]); + webWorkerCtx.$postMessage$([ + WorkerMessageType.InitializedEnvironmentScript, + winId, + instanceId, + errorMsg, + ]); }; -export const runStateHandlers = ( +export const runScriptContent = ( + env: WebWorkerEnvironment, instanceId: number, - handlerType: StateProp, - handlers?: EventHandler[] + scriptContent: string, + winId: number ) => { - handlers = getStateValue(instanceId, handlerType); - if (handlers) { - nextTick(() => - handlers!.map((cb) => - cb({ type: handlerType === StateProp.errorHandlers ? 'error' : 'load' }) - ) - ); + let errorMsg = ''; + try { + if (debug && webWorkerCtx.$config$.logScriptExecution) { + logWorker( + `Execute script (${instanceId}): ${scriptContent + .substr(0, 100) + .split('\n') + .map((l) => l.trim()) + .join(' ') + .trim() + .substr(0, 60)}...`, + winId + ); + } + + env.$currentScriptId$ = instanceId; + env.$currentScriptUrl$ = ''; + env.$run$(scriptContent); + } catch (contentError: any) { + console.error(scriptContent, contentError); + errorMsg = String(contentError.stack || contentError) + ''; } - return !!handlers; + + env.$currentScriptId$ = -1; + env.$currentScriptUrl$ = ''; + + return errorMsg; }; -const setCurrentScript = (instanceId: number, src: string) => { - webWorkerCtx.$currentScriptId$ = instanceId; - webWorkerCtx.$currentScriptUrl$ = src; +const runStateLoadHandlers = (instanceId: number, type: StateProp, handlers?: EventHandler[]) => { + handlers = getStateValue(instanceId, type); + if (handlers) { + nextTick(() => handlers!.map((cb) => cb({ type }))); + } }; export const insertIframe = (iframe: Node) => { - let loadError = getInstanceStateValue(iframe, StateProp.loadError); - let handlersType = loadError ? StateProp.errorHandlers : StateProp.loadHandlers; + // and iframe element's instanceId is also + // the winId of it's contentWindow + let i = 0; + const winId = iframe[InstanceIdKey]; + + const callback = () => { + if (environments[winId] && environments[winId].$isInitialized$) { + let type = getInstanceStateValue(iframe, StateProp.loadErrorStatus) + ? StateProp.errorHandlers + : StateProp.loadHandlers; + + let handlers = getInstanceStateValue(iframe, type); + if (handlers) { + handlers.map((handler) => handler({ type })); + } + } else if (i++ > 2000) { + let errorHandlers = getInstanceStateValue(iframe, StateProp.errorHandlers); + if (errorHandlers) { + errorHandlers.map((handler) => handler({ type: StateProp.errorHandlers })); + } + console.error(`Timeout`); + } else { + setTimeout(callback, 9); + } + }; - let handlers = getInstanceStateValue(iframe, handlersType); - if (handlers) { - handlers.map((handler) => handler({ type: StateProp.loadHandlers })); + callback(); +}; + +export const insertScriptContent = (script: Node) => { + const scriptContent = getInstanceStateValue(script, StateProp.innerHTML); + + if (scriptContent) { + const winId = script[WinIdKey]; + const instanceId = script[InstanceIdKey]; + const immediateSetters = script[ImmediateSettersKey]; + const errorMsg = runScriptContent(getEnv(script), instanceId, scriptContent, winId); + const datasetType = errorMsg ? 'pterror' : 'ptid'; + const datasetValue = errorMsg || instanceId; + + if (immediateSetters) { + immediateSetters.push( + [['type'], serializeForMain(winId, instanceId, SCRIPT_TYPE + SCRIPT_TYPE_EXEC)], + [['dataset', datasetType], serializeForMain(winId, instanceId, datasetValue)], + [['innerHTML'], serializeForMain(winId, instanceId, scriptContent)] + ); + } } }; -export const resolveUrl = (url?: string) => new URL(url || '', webWorkerCtx.$location$ + ''); +const resolveToUrl = (env: WebWorkerEnvironment, url?: string, baseLocation?: Location) => { + baseLocation = env.$location$; + while (!baseLocation.host) { + env = environments[env.$parentWinId$]; + baseLocation = env.$location$; + if (env.$isTop$) { + break; + } + } + return new URL(url || '', baseLocation); +}; -export const getUrl = (elm: HTMLElement) => resolveUrl(getInstanceStateValue(elm, StateProp.href)); +export const resolveUrl = (env: WebWorkerEnvironment, url?: string) => resolveToUrl(env, url) + ''; + +export const getUrl = (elm: HTMLElement) => + resolveToUrl(getEnv(elm), getInstanceStateValue(elm, StateProp.url)); export const updateIframeContent = (url: string, html: string) => `` + @@ -103,24 +191,3 @@ export const updateIframeContent = (url: string, html: string) => export const getPartytownScript = () => ``; - -export const sendBeacon = (url: string, data?: any) => { - if (debug && webWorkerCtx.$config$.logSendBeaconRequests) { - try { - logWorker(`sendBeacon: ${resolveUrl(url)}${data ? ', data: ' + JSON.stringify(data) : ''}`); - } catch (e) { - console.error(e); - } - } - try { - fetch(url, { - method: 'POST', - mode: 'no-cors', - keepalive: true, - }); - return true; - } catch (e) { - console.error(e); - return false; - } -}; diff --git a/src/lib/web-worker/worker-forwarded-trigger.ts b/src/lib/web-worker/worker-forwarded-trigger.ts index 1cd355ba..36a431ed 100644 --- a/src/lib/web-worker/worker-forwarded-trigger.ts +++ b/src/lib/web-worker/worker-forwarded-trigger.ts @@ -1,4 +1,5 @@ import { deserializeFromMain } from './worker-serialization'; +import { environments } from './worker-constants'; import type { ForwardMainTriggerData } from '../types'; export const workerForwardedTriggerHandle = ({ @@ -7,12 +8,13 @@ export const workerForwardedTriggerHandle = ({ $forward$, $args$, }: ForwardMainTriggerData) => { - let args = deserializeFromMain($winId$, $instanceId$, [], $args$); - let target = self as any; - let globalProperty = target[$forward$[0]]; - // see src/lib/main/snippet.ts and src/lib/sandbox/main-forward-trigger.ts try { + const win = environments[$winId$].$window$; + const target = $forward$[0] in win ? win : $forward$[0] in self ? (self as any) : {}; + const args = deserializeFromMain($winId$, $instanceId$, [], $args$); + const globalProperty = target[$forward$[0]]; + if (Array.isArray(globalProperty)) { globalProperty.push(...args); } else if (typeof globalProperty === 'function') { diff --git a/src/lib/web-worker/worker-global.ts b/src/lib/web-worker/worker-global.ts deleted file mode 100644 index 47530d34..00000000 --- a/src/lib/web-worker/worker-global.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { callMethod, createGlobalConstructorProxy, proxy } from './worker-proxy'; -import { - constructInstance, - elementConstructors, - getTagNameFromConstructor, -} from './worker-constructors'; -import { HTMLAnchorElement } from './worker-anchor'; -import { HTMLDocument, WorkerDocumentElementChild } from './worker-document'; -import { HTMLElement } from './worker-element'; -import { HTMLIFrameElement, Window } from './worker-iframe'; -import { HTMLImageElement } from './worker-image'; -import { HTMLScriptElement } from './worker-script'; -import { InstanceIdKey, webWorkerCtx, WinIdKey } from './worker-constants'; -import { - InterfaceInfo, - InterfaceType, - MembersInterfaceTypeInfo, - PlatformInstanceId, -} from '../types'; -import { nextTick, TOP_WIN_ID } from '../utils'; -import { Node } from './worker-node'; -import { sendBeacon } from './worker-exec'; - -export const initWebWorkerGlobal = ( - self: any, - windowMemberTypeInfo: MembersInterfaceTypeInfo, - interfaces: InterfaceInfo[], - htmlCstrNames: string[] -) => { - self[WinIdKey] = webWorkerCtx.$winId$; - self[InstanceIdKey] = PlatformInstanceId.window; - - Object.keys(windowMemberTypeInfo).map((memberName) => { - const interfaceType = windowMemberTypeInfo[memberName]; - - if (!self[memberName] && interfaceType > InterfaceType.DocumentFragmentNode) { - // this global doesn't already exist in the worker - // and the interface type isn't a DOM Node or Window object - if (interfaceType === InterfaceType.Function) { - // this is a global function, like alert() - self[memberName] = (...args: any[]) => callMethod(self, [memberName], args); - } else { - // this is a global implementation, like localStorage - self[memberName] = proxy(interfaceType, self, [memberName]); - } - } - }); - - interfaces.map((i) => createGlobalConstructorProxy(self, i[0], i[1])); - - Object.defineProperty(self, 'location', { - get: () => webWorkerCtx.$location$, - set: (href) => (webWorkerCtx.$location$.href = href + ''), - }); - - self.document = constructInstance(InterfaceType.Document, PlatformInstanceId.document); - - navigator.sendBeacon = sendBeacon; - - self.self = self.window = self; - - if (webWorkerCtx.$winId$ === TOP_WIN_ID) { - self.parent = self.top = self; - } else { - self.parent = constructInstance( - InterfaceType.Window, - PlatformInstanceId.window, - webWorkerCtx.$parentWinId$ - ); - - self.top = constructInstance(InterfaceType.Window, PlatformInstanceId.window, TOP_WIN_ID); - } - - self.Document = HTMLDocument; - self.HTMLElement = self.Element = HTMLElement; - self.Image = HTMLImageElement; - self.Node = Node; - - htmlCstrNames.map((htmlCstrName) => { - if (!self[htmlCstrName]) { - elementConstructors[getTagNameFromConstructor(htmlCstrName)] = self[htmlCstrName] = - Object.defineProperty(class extends HTMLElement {}, 'name', { - value: htmlCstrName, - }); - } - }); - - elementConstructors.A = self.HTMLAnchorElement = HTMLAnchorElement; - elementConstructors.BODY = elementConstructors.HEAD = WorkerDocumentElementChild; - elementConstructors.IFRAME = self.HTMLIFrameElement = HTMLIFrameElement; - elementConstructors.SCRIPT = self.HTMLScriptElement = HTMLScriptElement; -}; diff --git a/src/lib/web-worker/worker-iframe.ts b/src/lib/web-worker/worker-iframe.ts index da108d45..35aaead3 100644 --- a/src/lib/web-worker/worker-iframe.ts +++ b/src/lib/web-worker/worker-iframe.ts @@ -1,29 +1,23 @@ -import { constructInstance } from './worker-constructors'; +import { environments, ImmediateSettersKey, InstanceIdKey } from './worker-constants'; +import { getEnv } from './worker-environment'; import { getInstanceStateValue, setInstanceStateValue } from './worker-state'; -import { getter, setter } from './worker-proxy'; import { HTMLSrcElement } from './worker-element'; -import { ImmediateSettersKey, InstanceIdKey, webWorkerCtx, WinIdKey } from './worker-constants'; -import { InterfaceType, PlatformInstanceId, StateProp } from '../types'; -import { Location } from './worker-location'; import { resolveUrl, updateIframeContent } from './worker-exec'; -import { serializeForMain } from './worker-serialization'; -import { WorkerProxy } from './worker-proxy-constructor'; +import { serializeInstanceForMain } from './worker-serialization'; +import { setter } from './worker-proxy'; +import { StateProp } from '../types'; export class HTMLIFrameElement extends HTMLSrcElement { get contentDocument() { - return this.contentWindow!.document; + return this.contentWindow.document; } get contentWindow() { - let win: Window; - let winId = getInstanceStateValue(this, StateProp.partyWinId); - if (!winId) { - winId = getter(this, ['_ptId']); - setInstanceStateValue(this, StateProp.partyWinId, winId); - } - win = new Window(InterfaceType.Window, PlatformInstanceId.window, winId); - win.location = this.src; - return win; + // the winId of an iframe's window is the same + // as the instanceId of the containing iframe element + const iframeContentWinId = this[InstanceIdKey]; + const env = environments[iframeContentWinId]; + return env.$window$; } get src() { @@ -32,65 +26,29 @@ export class HTMLIFrameElement extends HTMLSrcElement { set src(url: string) { let xhr = new XMLHttpRequest(); let iframeContent: string; + let xhrStatus: number; + + url = resolveUrl(getEnv(this), url); - url = resolveUrl(url) + ''; - if (this.src !== url) { - setInstanceStateValue(this, StateProp.loadError, undefined); - setInstanceStateValue(this, StateProp.url, url); + setInstanceStateValue(this, StateProp.loadErrorStatus, undefined); + setInstanceStateValue(this, StateProp.url, url); - xhr.open('GET', url, false); - xhr.send(); + xhr.open('GET', url, false); + xhr.send(); + xhrStatus = xhr.status; - if (xhr.status > 199 && xhr.status < 300) { - iframeContent = updateIframeContent(url, xhr.responseText); - if (this[ImmediateSettersKey]) { - this[ImmediateSettersKey]!.push([ - ['srcdoc'], - serializeForMain(this[WinIdKey], this[InstanceIdKey], iframeContent), - ]); - } else { - setter(this, ['srcdoc'], iframeContent); - } + if (xhrStatus > 199 && xhrStatus < 300) { + iframeContent = updateIframeContent(url, xhr.responseText); + if (this[ImmediateSettersKey]) { + this[ImmediateSettersKey]!.push([ + ['srcdoc'], + serializeInstanceForMain(this, iframeContent), + ]); } else { - setInstanceStateValue(this, StateProp.loadError, xhr.status); + setter(this, ['srcdoc'], iframeContent); } + } else { + setInstanceStateValue(this, StateProp.loadErrorStatus, xhrStatus); } } } - -export class Window extends WorkerProxy { - get document() { - return constructInstance(InterfaceType.Document, PlatformInstanceId.document, this[WinIdKey]); - } - - get location(): Location { - let location = getInstanceStateValue(this, StateProp.url); - if (!location) { - setInstanceStateValue(this, StateProp.url, (location = new Location('about:blank'))); - } - return location; - } - set location(url: any) { - this.location.href = !url || url === '' ? 'about:blank' : url; - } - - get parent() { - return constructInstance( - InterfaceType.Window, - PlatformInstanceId.window, - webWorkerCtx.$parentWinId$ - ); - } - - get self() { - return this; - } - - get top() { - return top; - } - - get window() { - return this; - } -} diff --git a/src/lib/web-worker/worker-image.ts b/src/lib/web-worker/worker-image.ts index cbbeb22d..3445e0ba 100644 --- a/src/lib/web-worker/worker-image.ts +++ b/src/lib/web-worker/worker-image.ts @@ -1,61 +1,65 @@ import { debug, logWorker } from '../utils'; -import { resolveUrl } from './worker-exec'; -import { webWorkerCtx } from './worker-constants'; +import { environments, webWorkerCtx } from './worker-constants'; import type { EventHandler } from '../types'; +import { resolveUrl } from './worker-exec'; -export class HTMLImageElement { - s: string; - l: EventHandler[]; - e: EventHandler[]; +export const createImageConstructor = (winId: number) => { + return class HTMLImageElement { + s: string; + l: EventHandler[]; + e: EventHandler[]; - constructor() { - this.s = ''; - this.l = []; - this.e = []; - } + constructor() { + this.s = ''; + this.l = []; + this.e = []; + } - get src() { - return this.s; - } - set src(src: string) { - if (debug && webWorkerCtx.$config$.logImageRequests) { - logWorker(`Image() request: ${resolveUrl(src)}`); - } - fetch(resolveUrl(src) + '', { - mode: 'no-cors', - keepalive: true, - }).then( - (rsp) => { - if (rsp.ok) { - this.l.forEach((cb) => cb({ type: 'load' })); - } else { - this.e.forEach((cb) => cb({ type: 'error' })); - } - }, - () => this.e.forEach((cb) => cb({ type: 'error' })) - ); - } + get src() { + return this.s; + } + set src(src: string) { + const env = environments[winId]; + if (debug && webWorkerCtx.$config$.logImageRequests) { + logWorker(`Image() request: ${resolveUrl(env, src)}`, winId); + } - addEventListener(eventName: 'load' | 'error', cb: EventHandler) { - if (eventName === 'load') { - this.l.push(cb); + fetch(resolveUrl(env, src), { + mode: 'no-cors', + keepalive: true, + }).then( + (rsp) => { + if (rsp.ok) { + this.l.map((cb) => cb({ type: 'load' })); + } else { + this.e.map((cb) => cb({ type: 'error' })); + } + }, + () => this.e.forEach((cb) => cb({ type: 'error' })) + ); } - if (eventName === 'error') { - this.e.push(cb); + + addEventListener(eventName: 'load' | 'error', cb: EventHandler) { + if (eventName === 'load') { + this.l.push(cb); + } + if (eventName === 'error') { + this.e.push(cb); + } } - } - get onload() { - return this.l[0]; - } - set onload(cb: EventHandler) { - this.l = [cb]; - } + get onload() { + return this.l[0]; + } + set onload(cb: EventHandler) { + this.l = [cb]; + } - get onerror() { - return this.e[0]; - } - set onerror(cb: EventHandler) { - this.e = [cb]; - } -} + get onerror() { + return this.e[0]; + } + set onerror(cb: EventHandler) { + this.e = [cb]; + } + }; +}; diff --git a/src/lib/web-worker/worker-location.ts b/src/lib/web-worker/worker-location.ts index 2ba85901..779b4098 100644 --- a/src/lib/web-worker/worker-location.ts +++ b/src/lib/web-worker/worker-location.ts @@ -1,5 +1,19 @@ +import { debug, logWorker } from '../utils'; + export class Location extends URL { - assign() {} - reload() {} - replace() {} + assign() { + if (debug) { + logWorker(`location.assign(), noop`); + } + } + reload() { + if (debug) { + logWorker(`location.reload(), noop`); + } + } + replace() { + if (debug) { + logWorker(`location.replace(), noop`); + } + } } diff --git a/src/lib/web-worker/worker-navigator.ts b/src/lib/web-worker/worker-navigator.ts new file mode 100644 index 00000000..624c476c --- /dev/null +++ b/src/lib/web-worker/worker-navigator.ts @@ -0,0 +1,34 @@ +import { debug, logWorker } from '../utils'; +import { environments, webWorkerCtx } from './worker-constants'; +import { resolveUrl } from './worker-exec'; + +export const createNavigator = (winId: number) => { + const navigator = self.navigator as any; + + navigator.sendBeacon = (url: string, body?: any) => { + const env = environments[winId]; + if (debug && webWorkerCtx.$config$.logSendBeaconRequests) { + try { + logWorker( + `sendBeacon: ${resolveUrl(env, url)}${body ? ', data: ' + JSON.stringify(body) : ''}` + ); + } catch (e) { + console.error(e); + } + } + try { + fetch(resolveUrl(env, url), { + method: 'POST', + body, + mode: 'no-cors', + keepalive: true, + }); + return true; + } catch (e) { + console.error(e); + return false; + } + }; + + return navigator; +}; diff --git a/src/lib/web-worker/worker-node-list.ts b/src/lib/web-worker/worker-node-list.ts index a2ab55ac..bf4a05f9 100644 --- a/src/lib/web-worker/worker-node-list.ts +++ b/src/lib/web-worker/worker-node-list.ts @@ -4,14 +4,14 @@ import type { Node } from './worker-node'; export class NodeList { private _: Node[]; - constructor(workerNodes: Node[]) { - (this._ = workerNodes).forEach((node, index) => ((this as any)[index] = node)); + constructor(nodes: Node[]) { + (this._ = nodes).map((node, index) => ((this as any)[index] = node)); } entries() { return this._.entries(); } forEach(cb: (value: Node, index: number) => void, thisArg?: any) { - this._.forEach(cb, thisArg); + this._.map(cb, thisArg); } item(index: number) { return (this as any)[index]; diff --git a/src/lib/web-worker/worker-node.ts b/src/lib/web-worker/worker-node.ts index 1e5453ee..feed9bf6 100644 --- a/src/lib/web-worker/worker-node.ts +++ b/src/lib/web-worker/worker-node.ts @@ -1,8 +1,14 @@ import { applyBeforeSyncSetters, callMethod } from './worker-proxy'; -import { EMPTY_ARRAY } from '../utils'; +import { getEnv } from './worker-environment'; import type { HTMLDocument } from './worker-document'; -import { insertIframe } from './worker-exec'; -import { InterfaceTypeKey, NodeNameKey, webWorkerCtx, WinIdKey } from './worker-constants'; +import { insertIframe, insertScriptContent } from './worker-exec'; +import { + InstanceIdKey, + InterfaceTypeKey, + NodeNameKey, + webWorkerCtx, + WinIdKey, +} from './worker-constants'; import { NodeName, WorkerMessageType } from '../types'; import { WorkerProxy } from './worker-proxy-constructor'; @@ -12,23 +18,36 @@ export class Node extends WorkerProxy { } get ownerDocument(): HTMLDocument { - return document as any; + return getEnv(this).$document$; } get href() { - return undefined; + return; } set href(_: any) {} insertBefore(newNode: Node, referenceNode: Node | null) { - applyBeforeSyncSetters(this[WinIdKey], newNode); + // ensure the node being added to the window's document + // is given the same winId as the window it's being added to + const winId = (newNode[WinIdKey] = this[WinIdKey]); + const instanceId = newNode[InstanceIdKey]; + const nodeName = newNode[NodeNameKey]; + const isScript = nodeName === NodeName.Script; + const isIFrame = nodeName === NodeName.IFrame; + + if (isScript) { + insertScriptContent(newNode); + } + + applyBeforeSyncSetters(newNode); - newNode = callMethod(this, ['insertBefore'], [newNode, referenceNode], EMPTY_ARRAY); + newNode = callMethod(this, ['insertBefore'], [newNode, referenceNode], undefined, instanceId); - if (newNode[NodeNameKey] === NodeName.IFrame) { + if (isIFrame) { insertIframe(newNode); - } else if (newNode[NodeNameKey] === NodeName.Script) { - webWorkerCtx.$postMessage$([WorkerMessageType.InitializeNextWorkerScript]); + } + if (isScript) { + webWorkerCtx.$postMessage$([WorkerMessageType.InitializeNextScript, winId]); } return newNode; diff --git a/src/lib/web-worker/worker-proxy-constructor.ts b/src/lib/web-worker/worker-proxy-constructor.ts index 28b3f7ee..a73754b9 100644 --- a/src/lib/web-worker/worker-proxy-constructor.ts +++ b/src/lib/web-worker/worker-proxy-constructor.ts @@ -4,7 +4,6 @@ import { InstanceIdKey, InterfaceTypeKey, NodeNameKey, - webWorkerCtx, WinIdKey, } from './worker-constants'; import { proxy } from './worker-proxy'; @@ -12,12 +11,12 @@ import { proxy } from './worker-proxy'; export class WorkerProxy { [WinIdKey]: number; [InstanceIdKey]: number; - [InterfaceTypeKey]: number; + [InterfaceTypeKey]: InterfaceType; [NodeNameKey]: string | undefined; [ImmediateSettersKey]: ImmediateSetter[] | undefined; constructor(interfaceType: InterfaceType, instanceId: number, winId?: number, nodeName?: string) { - this[WinIdKey] = winId || webWorkerCtx.$winId$; + this[WinIdKey] = winId!; this[InstanceIdKey] = instanceId!; this[NodeNameKey] = nodeName; this[ImmediateSettersKey] = undefined; diff --git a/src/lib/web-worker/worker-proxy.ts b/src/lib/web-worker/worker-proxy.ts index 86e59f60..895baa9a 100644 --- a/src/lib/web-worker/worker-proxy.ts +++ b/src/lib/web-worker/worker-proxy.ts @@ -4,13 +4,10 @@ import { InterfaceType, MainAccessRequest, MainAccessResponse, - NodeName, - PlatformInstanceId, SerializedTransfer, } from '../types'; -import { constructInstance } from './worker-constructors'; import { - debug, + defineConstructorName, len, logWorkerCall, logWorkerGlobalConstructor, @@ -18,8 +15,13 @@ import { logWorkerSetter, randomId, } from '../utils'; -import { deserializeFromMain, serializeForMain } from './worker-serialization'; -import { getInstanceStateValue, setStateValue } from './worker-state'; +import { + deserializeFromMain, + serializeForMain, + serializeInstanceForMain, +} from './worker-serialization'; +import { getEnv } from './worker-environment'; +import { getInstanceStateValue } from './worker-state'; import { ImmediateSettersKey, InstanceIdKey, @@ -38,18 +40,14 @@ const syncMessage = ( $memberPath$: string[], $data$?: SerializedTransfer | undefined, $immediateSetters$?: ImmediateSetter[], - $newInstanceId$?: number, - $contextWinId$?: number + $assignInstanceId$?: number ) => { const $winId$ = instance[WinIdKey]; const $instanceId$ = instance[InstanceIdKey]; - const $forwardToWorkerAccess$ = webWorkerCtx.$winId$ !== $winId$ || !!$contextWinId$; - const accessReq: MainAccessRequest = { $msgId$: randomId(), $winId$, - $forwardToWorkerAccess$, $instanceId$: instance[InstanceIdKey], $interfaceType$: instance[InterfaceTypeKey], $nodeName$: instance[NodeNameKey], @@ -57,8 +55,7 @@ const syncMessage = ( $memberPath$, $data$, $immediateSetters$, - $newInstanceId$, - $contextWinId$, + $assignInstanceId$, }; const accessRsp: MainAccessResponse = syncSendMessage(webWorkerCtx, accessReq); @@ -67,9 +64,6 @@ const syncMessage = ( const rtnValue = deserializeFromMain($winId$, $instanceId$, $memberPath$, accessRsp.$rtnValue$!); if (accessRsp.$error$) { - if (debug) { - console.error(self.name, JSON.stringify(accessReq)); - } if (isPromise) { return Promise.reject(accessRsp.$error$); } @@ -83,7 +77,7 @@ const syncMessage = ( }; export const getter = (instance: WorkerProxy, memberPath: string[]) => { - applyBeforeSyncSetters(instance[WinIdKey], instance); + applyBeforeSyncSetters(instance); const rtnValue = syncMessage(instance, AccessType.Get, memberPath); logWorkerGetter(instance, memberPath, rtnValue); @@ -91,10 +85,8 @@ export const getter = (instance: WorkerProxy, memberPath: string[]) => { }; export const setter = (instance: WorkerProxy, memberPath: string[], value: any) => { - const winId = instance[WinIdKey]; - const instanceId = instance[InstanceIdKey]; const immediateSetters = instance[ImmediateSettersKey]; - const serializedValue = serializeForMain(winId, instanceId, value); + const serializedValue = serializeInstanceForMain(instance, value); logWorkerSetter(instance, memberPath, value); @@ -102,16 +94,9 @@ export const setter = (instance: WorkerProxy, memberPath: string[], value: any) // queue up setters to be applied immediately after the // node is added to the dom immediateSetters.push([memberPath, serializedValue]); - return; - } - - if (instanceId === PlatformInstanceId.window) { - if (typeof value === 'function' || (typeof value === 'object' && value)) { - setStateValue(instanceId, memberPath[0], serializedValue); - } + } else { + syncMessage(instance, AccessType.Set, memberPath, serializedValue); } - - syncMessage(instance, AccessType.Set, memberPath, serializedValue); }; export const callMethod = ( @@ -119,48 +104,34 @@ export const callMethod = ( memberPath: string[], args: any[], immediateSetters?: ImmediateSetter[], - newInstanceId?: number, - contextWinId?: number + assignInstanceId?: number ) => { - const winId = instance[WinIdKey]; - - applyBeforeSyncSetters(winId, instance); - - args.forEach((arg) => { - if (arg) { - applyBeforeSyncSetters(winId, arg); - } - }); + applyBeforeSyncSetters(instance); + args.map(applyBeforeSyncSetters); const rtnValue = syncMessage( instance, AccessType.CallMethod, memberPath, - serializeForMain(winId, instance[InstanceIdKey], args), + serializeInstanceForMain(instance, args), immediateSetters, - newInstanceId, - contextWinId + assignInstanceId ); logWorkerCall(instance, memberPath, args, rtnValue); return rtnValue; }; export const createGlobalConstructorProxy = ( - self: any, + winId: number, interfaceType: InterfaceType, cstrName: string ) => { const GlobalCstr = class { constructor(...args: any[]) { - const winId = webWorkerCtx.$winId$; const instanceId = randomId(); const workerProxy = new WorkerProxy(interfaceType, instanceId, winId); - args.forEach((arg) => { - if (arg) { - applyBeforeSyncSetters(winId, arg); - } - }); + args.map(applyBeforeSyncSetters); syncMessage( workerProxy, @@ -169,38 +140,28 @@ export const createGlobalConstructorProxy = ( serializeForMain(winId, instanceId, args) ); - logWorkerGlobalConstructor(workerProxy, cstrName, args); + logWorkerGlobalConstructor(winId, cstrName, args); return workerProxy; } }; - self[cstrName] = Object.defineProperty(GlobalCstr, 'name', { - value: cstrName, - }); + return defineConstructorName(GlobalCstr, cstrName); }; -export const applyBeforeSyncSetters = (winId: number, instance: WorkerProxy) => { - const beforeSyncValues = instance[ImmediateSettersKey]; - if (beforeSyncValues) { - instance[ImmediateSettersKey] = undefined; - - const winDoc = constructInstance( - InterfaceType.Document, - PlatformInstanceId.document, - winId, - NodeName.Document - ); - const syncedTarget = callMethod( - winDoc, - ['createElement'], - [instance[NodeNameKey]], - beforeSyncValues, - instance[InstanceIdKey] - ); - - if (debug && instance[InstanceIdKey] !== syncedTarget[InstanceIdKey]) { - console.error('Main and web worker instance ids do not match', instance, syncedTarget); +export const applyBeforeSyncSetters = (instance: WorkerProxy) => { + if (instance) { + const beforeSyncValues = instance[ImmediateSettersKey]; + if (beforeSyncValues) { + instance[ImmediateSettersKey] = undefined; + + callMethod( + getEnv(instance).$document$, + ['createElement'], + [instance[NodeNameKey]], + beforeSyncValues, + instance[InstanceIdKey] + ); } } }; @@ -233,7 +194,7 @@ const createComplexMember = ( if (typeof stateValue === 'function') { return (...args: any[]) => { const rtnValue = stateValue.apply(instance, args); - logWorkerCall(instance, memberPath, args, rtnValue, false); + logWorkerCall(instance, memberPath, args, rtnValue); return rtnValue; }; } @@ -254,13 +215,23 @@ export const proxy = ( } return new Proxy(target, { - get(target, propKey) { - if (propKey === ProxyKey) { - return true; + get(target, propKey, receiver) { + if (typeof propKey === 'symbol' || Reflect.has(target, propKey)) { + return Reflect.get(target, propKey, receiver); } - if (Reflect.has(target, propKey)) { - return Reflect.get(target, propKey); + if (shouldRestrictToWorker(interfaceType, propKey)) { + // this getter is for the Window instance, but it's not one of the known window members + // the value should only be read from the window environment and + // should not get the value from the main thread's window + // if the member is in the environment window instance, get the value + if (Reflect.has(self, propKey)) { + return Reflect.get(self, propKey, receiver); + } + + const globalRtnValue = target[propKey]; + logWorkerGetter(target, [propKey], globalRtnValue, true); + return globalRtnValue; } const memberPath = [...initMemberPath, String(propKey)]; @@ -273,12 +244,30 @@ export const proxy = ( }, set(target, propKey, value, receiver) { - if (Reflect.has(target, propKey)) { + if (typeof propKey === 'symbol' || Reflect.has(target, propKey)) { Reflect.set(target, propKey, value, receiver); + } else if (shouldRestrictToWorker(interfaceType, propKey)) { + // this value should only be set within the web worker world + // it does not get passed and set to the main thread's window + // set the value to just the window environment + target[propKey] = value; + logWorkerSetter(target, [propKey], value, true); } else { - setter(target, [...initMemberPath, String(propKey)], value); + setter(target, [...initMemberPath, propKey], value); } return true; }, + + has(target, propKey) { + if (interfaceType === InterfaceType.Window) { + return true; + } + return Reflect.has(target, propKey); + }, }); }; + +const shouldRestrictToWorker = (interfaceType: InterfaceType, propKey: string) => + interfaceType === InterfaceType.Window && + (!webWorkerCtx.$windowMemberNames$.includes(propKey) || + webWorkerCtx.$forwardedTriggers$.includes(propKey)); diff --git a/src/lib/web-worker/worker-script.ts b/src/lib/web-worker/worker-script.ts index b2b63c21..78d8b92b 100644 --- a/src/lib/web-worker/worker-script.ts +++ b/src/lib/web-worker/worker-script.ts @@ -1,33 +1,46 @@ +import { getEnv } from './worker-environment'; import { getInstanceStateValue, setInstanceStateValue } from './worker-state'; import { getter, setter } from './worker-proxy'; import { HTMLSrcElement } from './worker-element'; -import { ImmediateSettersKey, InstanceIdKey, webWorkerCtx, WinIdKey } from './worker-constants'; +import { ImmediateSettersKey } from './worker-constants'; import { resolveUrl } from './worker-exec'; -import { serializeForMain } from './worker-serialization'; +import { serializeInstanceForMain } from './worker-serialization'; import { StateProp } from '../types'; export class HTMLScriptElement extends HTMLSrcElement { + get innerHTML() { + return getInstanceStateValue(this, StateProp.innerHTML) || ''; + } + set innerHTML(scriptContent: string) { + setInstanceStateValue(this, StateProp.innerHTML, scriptContent); + } + + get innerText() { + return this.innerHTML; + } + set innerText(content: string) { + this.innerHTML = content; + } + get src() { - if (this[WinIdKey] === webWorkerCtx.$winId$) { - return getInstanceStateValue(this, StateProp.url) || ''; - } - return getter(this, ['src']); + return getInstanceStateValue(this, StateProp.url) || ''; } set src(url: string) { - if (this[WinIdKey] === webWorkerCtx.$winId$) { - url = resolveUrl(url) + ''; - setInstanceStateValue(this, StateProp.url, url); - if (this[ImmediateSettersKey]) { - this[ImmediateSettersKey]!.push([ - ['src'], - serializeForMain(this[WinIdKey], this[InstanceIdKey], url), - ]); - } - } else { - setter(this, ['src'], url); + url = resolveUrl(getEnv(this), url); + setInstanceStateValue(this, StateProp.url, url); + + if (this[ImmediateSettersKey]) { + this[ImmediateSettersKey]!.push([['src'], serializeInstanceForMain(this, url)]); } } + get textContent() { + return this.innerHTML; + } + set textContent(content: string) { + this.innerHTML = content; + } + get type() { return getter(this, ['type']); } diff --git a/src/lib/web-worker/worker-serialization.ts b/src/lib/web-worker/worker-serialization.ts index ca58984b..984d09d8 100644 --- a/src/lib/web-worker/worker-serialization.ts +++ b/src/lib/web-worker/worker-serialization.ts @@ -1,24 +1,24 @@ import { callMethod } from './worker-proxy'; -import { constructInstance } from './worker-constructors'; +import { constructEvent, constructInstance } from './worker-constructors'; +import { + environments, + InstanceIdKey, + InterfaceTypeKey, + NodeNameKey, + webWorkerRefIdsByRef, + webWorkerRefsByRefId, + WinIdKey, +} from './worker-constants'; import { InterfaceType, PlatformInstanceId, - RefHandler, RefHandlerCallbackData, SerializedInstance, + SerializedObject, SerializedRefTransferData, SerializedTransfer, SerializedType, } from '../types'; -import { - InstanceIdKey, - InterfaceTypeKey, - NodeNameKey, - webWorkerCtx, - webWorkerRefIdsByRef, - webWorkerRefsByRefId, - WinIdKey, -} from './worker-constants'; import { NodeList } from './worker-node-list'; import { setWorkerRef } from './worker-state'; @@ -30,21 +30,20 @@ export const serializeForMain = ( ): SerializedTransfer | undefined => { if (value !== undefined) { let type = typeof value; - let key: string; - let obj: { [key: string]: SerializedTransfer | undefined }; if (type === 'string' || type === 'boolean' || type === 'number' || value == null) { return [SerializedType.Primitive, value]; } if (type === 'function') { - const refData: SerializedRefTransferData = { - $winId$, - $contextWinId$: webWorkerCtx.$winId$, - $instanceId$, - $refId$: setWorkerRef(value), - }; - return [SerializedType.Ref, refData]; + return [ + SerializedType.Ref, + { + $winId$, + $instanceId$, + $refId$: setWorkerRef(value), + }, + ]; } added = added || new Set(); @@ -60,37 +59,67 @@ export const serializeForMain = ( if (type === 'object') { if (typeof value[InstanceIdKey] === 'number') { - const serializeInstance: SerializedInstance = { - $winId$: value[WinIdKey], - $interfaceType$: value[InterfaceTypeKey], - $instanceId$: value[InstanceIdKey], - $nodeName$: value[NodeNameKey], - }; - return [SerializedType.Instance, serializeInstance]; + return [ + SerializedType.Instance, + { + $winId$: value[WinIdKey], + $interfaceType$: value[InterfaceTypeKey], + $instanceId$: value[InstanceIdKey], + $nodeName$: value[NodeNameKey], + }, + ]; } - obj = {}; - if (!added.has(value)) { - added.add(value); - for (key in value) { - obj[key] = serializeForMain($winId$, $instanceId$, value[key], added); - } + if (value instanceof Event) { + return [ + SerializedType.Event, + serializeObjectForMain($winId$, $instanceId$, value, false, added), + ]; } - return [SerializedType.Object, obj]; + return [ + SerializedType.Object, + serializeObjectForMain($winId$, $instanceId$, value, true, added), + ]; + } + } +}; + +const serializeObjectForMain = ( + winId: number, + instanceId: number, + obj: any, + includeFunctions: boolean, + added: Set, + serializedObj?: SerializedObject, + propName?: string, + propValue?: any +) => { + serializedObj = {}; + for (propName in obj) { + propValue = obj[propName]; + if (includeFunctions || typeof propValue !== 'function') { + serializedObj[propName] = serializeForMain(winId, instanceId, propValue, added); } } + return serializedObj; }; +export const serializeInstanceForMain = ( + instance: any, + value: any +): SerializedTransfer | undefined => + instance + ? serializeForMain(instance[WinIdKey], instance[InstanceIdKey], value) + : [SerializedType.Primitive, value]; + export const deserializeFromMain = ( winId: number, instanceId: number | undefined | null, memberPath: string[], serializedValueTransfer?: SerializedTransfer, serializedType?: SerializedType, - serializedValue?: any, - obj?: { [key: string]: any }, - key?: string + serializedValue?: any ): any => { if (serializedValueTransfer) { serializedType = serializedValueTransfer[0]; @@ -105,56 +134,79 @@ export const deserializeFromMain = ( } if (serializedType === SerializedType.Instance) { - const serializedInstance: SerializedInstance = serializedValue; - return constructSerializedInstance(serializedInstance); + return constructSerializedInstance(serializedValue); } if (serializedType === SerializedType.Array) { - const serializedArray: SerializedTransfer[] = serializedValue; - return serializedArray.map((v) => deserializeFromMain(winId, instanceId, memberPath, v)); + return (serializedValue as SerializedTransfer[]).map((v) => + deserializeFromMain(winId, instanceId, memberPath, v) + ); + } + + if (serializedType === SerializedType.Event) { + return constructEvent( + deserializeObjectFromMain(winId, instanceId!, memberPath, serializedValue) + ); } if (serializedType === SerializedType.Object) { - obj = {}; - for (key in serializedValue) { - obj[key] = deserializeFromMain( - winId, - instanceId, - [...memberPath, key], - serializedValue[key] - ); - } - return obj; + return deserializeObjectFromMain(winId, instanceId!, memberPath, serializedValue); } } }; +const deserializeObjectFromMain = ( + winId: number, + instanceId: number, + memberPath: string[], + serializedValue: any, + obj?: any, + key?: string +) => { + obj = {}; + for (key in serializedValue) { + obj[key] = deserializeFromMain(winId, instanceId, [...memberPath, key], serializedValue[key]); + } + return obj; +}; + export const constructSerializedInstance = ({ $interfaceType$, $instanceId$, $winId$, $nodeName$, - $items$, + $data$, }: SerializedInstance): any => { + const env = environments[$winId$]; if ($instanceId$ === PlatformInstanceId.window) { - return self; + return env.$window$; + } else if ($instanceId$ === PlatformInstanceId.document) { + return env.$document$; + } else if ($instanceId$ === PlatformInstanceId.documentElement) { + return env.$documentElement$; + } else if ($instanceId$ === PlatformInstanceId.head) { + return env.$head$; + } else if ($instanceId$ === PlatformInstanceId.body) { + return env.$body$; } else if ($interfaceType$ === InterfaceType.NodeList) { - return new NodeList($items$!.map(constructSerializedInstance)); + return new NodeList($data$!.map(constructSerializedInstance)); } else { - return constructInstance($interfaceType$, $instanceId$!, $winId$, $nodeName$!); + return constructInstance($interfaceType$, $instanceId$!, $winId$, $nodeName$); } }; -export const callWorkerRefHandler = ( - { $winId$, $instanceId$, $refId$, $thisArg$, $args$ }: RefHandlerCallbackData, - workerRef?: RefHandler -) => { - workerRef = webWorkerRefsByRefId[$refId$]; - if (workerRef) { +export const callWorkerRefHandler = ({ + $winId$, + $instanceId$, + $refId$, + $thisArg$, + $args$, +}: RefHandlerCallbackData) => { + if (webWorkerRefsByRefId[$refId$]) { try { const thisArg = deserializeFromMain($winId$, $instanceId$, [], $thisArg$); const args = deserializeFromMain($winId$, $instanceId$, [], $args$); - workerRef.apply(thisArg, args); + webWorkerRefsByRefId[$refId$].apply(thisArg, args); } catch (e) { console.error(e); } @@ -164,18 +216,17 @@ export const callWorkerRefHandler = ( const deserializeRefFromMain = ( instanceId: number, memberPath: string[], - { $winId$, $contextWinId$, $refId$ }: SerializedRefTransferData + { $winId$, $refId$ }: SerializedRefTransferData ) => { - let workerRefHandler = webWorkerRefsByRefId[$refId$]; - - if (!workerRefHandler) { - webWorkerRefsByRefId[$refId$] = workerRefHandler = function (this: any, ...args: any[]) { - const instance = constructInstance(InterfaceType.Window, instanceId, $winId$); - return callMethod(instance, memberPath, args, undefined, undefined, $contextWinId$); - }; - - webWorkerRefIdsByRef.set(workerRefHandler, $refId$); + if (!webWorkerRefsByRefId[$refId$]) { + webWorkerRefIdsByRef.set( + (webWorkerRefsByRefId[$refId$] = function (this: any, ...args: any[]) { + const instance = constructInstance(InterfaceType.Window, instanceId, $winId$); + return callMethod(instance, memberPath, args); + }), + $refId$ + ); } - return workerRefHandler; + return webWorkerRefsByRefId[$refId$]; };