diff --git a/rollup.config.js b/rollup.config.js index c8e8e832..2ca6a10b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -391,8 +391,8 @@ function managlePropsPlugin() { $documentReferrer$: '', $documentTitle$: '', $errors$: '', - $extraInstructions$: '', $firstScriptId$: '', + $immediateSetters$: '', $importScripts$: '', $instanceId$: '', $instanceIdByInstance$: '', @@ -405,6 +405,7 @@ function managlePropsPlugin() { $location$: '', $memberPath$: '', $msgId$: '', + $newInstanceId$: '', $nextId$: '', $nodeName$: '', $parentWinId$: '', diff --git a/src/lib/sandbox/main-access-handler.ts b/src/lib/sandbox/main-access-handler.ts index ab478900..a4367dc7 100644 --- a/src/lib/sandbox/main-access-handler.ts +++ b/src/lib/sandbox/main-access-handler.ts @@ -1,14 +1,8 @@ -import { - AccessType, - ExtraInstruction, - MainAccessRequest, - MainAccessResponse, - MainWindowContext, -} from '../types'; +import { AccessType, MainAccessRequest, MainAccessResponse, MainWindowContext } from '../types'; import { deserializeFromWorker, serializeForWorker } from './main-serialization'; -import { EMPTY_ARRAY, isPromise, len, PT_SCRIPT_TYPE } from '../utils'; +import { EMPTY_ARRAY, isPromise, len } from '../utils'; import { forwardToWinAccessHandler } from './messenger'; -import { getInstance } from './main-instances'; +import { getInstance, setInstanceId } from './main-instances'; export const mainAccessHandler = async ( winCtx: MainWindowContext, @@ -30,13 +24,14 @@ export const mainAccessHandler = async ( let memberPath = accessReqTask.$memberPath$; let memberPathLength = len(memberPath); let lastMemberName = memberPath[memberPathLength - 1]; - let extraInstructions = accessReqTask.$extraInstructions$ || EMPTY_ARRAY; + let immediateSetters = accessReqTask.$immediateSetters$ || EMPTY_ARRAY; let instance: any; let rtnValue: any; let data: any; let i: number; let count: number; let tmr: any; + let immediateSetterName: string; try { instance = getInstance(winCtx, instanceId); @@ -49,7 +44,7 @@ export const mainAccessHandler = async ( data = deserializeFromWorker(winCtx, instanceId, accessReqTask.$data$); if (accessType === AccessType.Get) { - if (extraInstructions.includes(ExtraInstruction.WAIT_FOR_INSTANCE_MEMBER)) { + if (lastMemberName === 'partyWinId') { await new Promise((resolve) => { count = 0; tmr = setInterval(() => { @@ -66,14 +61,19 @@ export const mainAccessHandler = async ( instance[lastMemberName] = data; } else if (accessType === AccessType.CallMethod) { rtnValue = instance[lastMemberName].apply(instance, data); - extraInstructions.forEach((extra, i) => { - if (extra === ExtraInstruction.SET_INERT_SCRIPT) { - (rtnValue as HTMLScriptElement).type = PT_SCRIPT_TYPE; - } - if (extra === ExtraInstruction.SET_IFRAME_SRCDOC) { - (rtnValue as HTMLIFrameElement).srcdoc = extraInstructions[i + 1] as any; - } + + immediateSetters.map((immediateSetter) => { + immediateSetterName = immediateSetter[0][0]; + rtnValue[immediateSetterName] = deserializeFromWorker( + winCtx, + instanceId, + immediateSetter[1] + ); }); + + if (accessReqTask.$newInstanceId$) { + setInstanceId(winCtx, rtnValue, accessReqTask.$newInstanceId$); + } } if (isPromise(rtnValue)) { diff --git a/src/lib/sandbox/messenger.ts b/src/lib/sandbox/messenger.ts index 05a7972d..392aad53 100644 --- a/src/lib/sandbox/messenger.ts +++ b/src/lib/sandbox/messenger.ts @@ -58,7 +58,7 @@ const onMessageFromWebWorker = ( forwardMsgResolve(accessRsp); readNextScript(winCtx); } - } else if (msgType === WorkerMessageType.RunStateProp) { + } 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/types.ts b/src/lib/types.ts index cec34983..5f06a921 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -7,7 +7,7 @@ export type MessageFromWorkerToSandbox = | [WorkerMessageType.InitializedWorkerScript, number, string] | [WorkerMessageType.InitializeNextWorkerScript] | [WorkerMessageType.ForwardMainDataResponse, MainAccessResponse] - | [WorkerMessageType.RunStateProp, RunStatePropData]; + | [WorkerMessageType.RunStateHandlers, RunStatePropData]; export type MessageFromSandboxToWorker = | [WorkerMessageType.MainDataResponseToWorker, InitWebWorkerData] @@ -20,7 +20,7 @@ export type MessageFromSandboxToWorker = ] | [WorkerMessageType.ForwardMainDataRequest, MainAccessRequest] | [WorkerMessageType.ForwardEvent, string, any[] | undefined] - | [WorkerMessageType.RunStateProp, RunStatePropData]; + | [WorkerMessageType.RunStateHandlers, RunStatePropData]; export const enum WorkerMessageType { MainDataRequestFromWorker, @@ -31,7 +31,7 @@ export const enum WorkerMessageType { ForwardMainDataRequest, ForwardMainDataResponse, ForwardEvent, - RunStateProp, + RunStateHandlers, } export type RunStatePropData = { @@ -43,6 +43,7 @@ export type RunStatePropData = { export const enum StateProp { errorHandlers, loadHandlers, + isSuccessfulLoad, url, partyWinId, } @@ -157,14 +158,11 @@ export interface MainAccessRequestTask { $accessType$: AccessType; $memberPath$: string[]; $data$?: SerializedTransfer; - $extraInstructions$?: ExtraInstruction[]; + $immediateSetters$?: ImmediateSetter[]; + $newInstanceId$?: number; } -export const enum ExtraInstruction { - SET_INERT_SCRIPT, - SET_IFRAME_SRCDOC, - WAIT_FOR_INSTANCE_MEMBER, -} +export type ImmediateSetter = [string[], SerializedTransfer | undefined]; export interface MainAccessResponse { $msgId$: number; @@ -300,6 +298,7 @@ export const enum NodeName { Document = '#document', DocumentElement = 'HTML', DocumentFragment = '#document-fragment', + IFrame = 'IFRAME', Head = 'HEAD', Script = 'SCRIPT', Text = '#text', diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d2e63c6b..7dedef3a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -228,3 +228,5 @@ export const PT_SCRIPT = ``; export const TOP_WIN_ID = 1; + +export const randomId = () => Math.round(Math.random() * 999999999); diff --git a/src/lib/web-worker/index.ts b/src/lib/web-worker/index.ts index e946957c..46cb15c1 100644 --- a/src/lib/web-worker/index.ts +++ b/src/lib/web-worker/index.ts @@ -10,10 +10,10 @@ import { RunStatePropData, WorkerMessageType, } from '../types'; +import { runStateHandlers } from './worker-instance'; import { webWorkerCtx } from './worker-constants'; import { workerAccessHandler } from './worker-access-handler'; import { workerEventForwarding } from './worker-event-forwarding'; -import { runStateProp } from './worker-instance'; const queuedEvents: MessageEvent[] = []; @@ -34,9 +34,9 @@ const onMessage = (ev: MessageEvent) => { workerAccessHandler(msgData as MainAccessRequest); } else if (msgType === WorkerMessageType.ForwardEvent) { workerEventForwarding(msgData as any, msg[2] as any); - } else if (msgType === WorkerMessageType.RunStateProp) { + } else if (msgType === WorkerMessageType.RunStateHandlers) { const data: RunStatePropData = msgData as any; - runStateProp(data.$winId$, data.$instanceId$, data.$stateProp$); + runStateHandlers(data.$winId$, data.$instanceId$, data.$stateProp$); } } else if (msgType === WorkerMessageType.MainDataResponseToWorker) { // initialize the web worker with the received the main data diff --git a/src/lib/web-worker/worker-access-handler.ts b/src/lib/web-worker/worker-access-handler.ts index 25a3a35e..29a739af 100644 --- a/src/lib/web-worker/worker-access-handler.ts +++ b/src/lib/web-worker/worker-access-handler.ts @@ -1,4 +1,11 @@ -import { AccessType, MainAccessRequest, MainAccessResponse, WorkerMessageType } from '../types'; +import { + AccessType, + MainAccessRequest, + MainAccessResponse, + PlatformInstanceId, + WorkerMessageType, +} from '../types'; +import { callMethod } from './worker-proxy'; import { constructSerializedInstance, deserializeFromMain, @@ -39,10 +46,23 @@ export const workerAccessHandler = (accessReq: MainAccessRequest) => { } else if (accessType === AccessType.Set) { instance[lastMemberName] = data; } else if (accessType === AccessType.CallMethod) { - rtnValue = instance[lastMemberName].apply(instance, data); + if ( + accessReqTask.$instanceId$ === PlatformInstanceId.document && + lastMemberName === 'createElement' + ) { + rtnValue = callMethod( + instance, + memberPath, + data, + accessReqTask.$immediateSetters$, + accessReqTask.$newInstanceId$ + ); + } else { + rtnValue = instance[lastMemberName].apply(instance, data); + } } - accessRsp.$rtnValue$ = serializeForMain(rtnValue, new Set()); + accessRsp.$rtnValue$ = serializeForMain(rtnValue); } catch (e: any) { accessRsp.$errors$.push(String(e.stack || e)); } diff --git a/src/lib/web-worker/worker-constants.ts b/src/lib/web-worker/worker-constants.ts index a2bc4e5a..154f6192 100644 --- a/src/lib/web-worker/worker-constants.ts +++ b/src/lib/web-worker/worker-constants.ts @@ -5,4 +5,5 @@ export const InstanceIdKey = Symbol(); export const InterfaceTypeKey = Symbol(); export const NodeNameKey = Symbol(); export const ProxyKey = Symbol(); +export const ImmediateSettersKey = Symbol(); export const webWorkerCtx: WebWorkerContext = {} as any; diff --git a/src/lib/web-worker/worker-constructors.ts b/src/lib/web-worker/worker-constructors.ts new file mode 100644 index 00000000..90730e40 --- /dev/null +++ b/src/lib/web-worker/worker-constructors.ts @@ -0,0 +1,47 @@ +import { InterfaceType, NodeName } from '../types'; +import { WorkerAnchorElement, WorkerDocumentElementChild, WorkerElement } from './worker-element'; +import { WorkerContentWindow, WorkerIFrameElement } from './worker-iframe'; +import { WorkerDocument } from './worker-document'; +import { WorkerInstance } from './worker-instance'; +import { WorkerNode } from './worker-node'; +import { WorkerScriptElement } from './worker-script'; + +export const constructInstance = ( + interfaceType: InterfaceType, + instanceId: number, + winId?: number, + nodeName?: string +) => { + nodeName = + interfaceType === InterfaceType.Document + ? NodeName.Document + : interfaceType === InterfaceType.TextNode + ? NodeName.Text + : nodeName; + + const Cstr = getConstructor(interfaceType, nodeName); + return new Cstr(interfaceType, instanceId, winId, nodeName); +}; + +const getConstructor = (interfaceType: InterfaceType, nodeName?: string): typeof WorkerInstance => { + if (interfaceType === InterfaceType.Element) { + return getElementConstructor(nodeName!); + } else if (interfaceType === InterfaceType.Document) { + return WorkerDocument; + } else if (interfaceType === InterfaceType.Window) { + return WorkerContentWindow; + } else if (interfaceType === InterfaceType.TextNode) { + return WorkerNode; + } else { + return WorkerInstance; + } +}; + +export const getElementConstructor = (nodeName: string): typeof WorkerElement => + ({ + A: WorkerAnchorElement, + BODY: WorkerDocumentElementChild, + HEAD: WorkerDocumentElementChild, + IFRAME: WorkerIFrameElement, + SCRIPT: WorkerScriptElement, + }[nodeName] || WorkerElement); diff --git a/src/lib/web-worker/worker-document.ts b/src/lib/web-worker/worker-document.ts index 61f8f19e..5cb9c767 100644 --- a/src/lib/web-worker/worker-document.ts +++ b/src/lib/web-worker/worker-document.ts @@ -1,8 +1,16 @@ import { callMethod, getter, setter } from './worker-proxy'; -import { constructInstance } from './worker-serialization'; -import { ExtraInstruction, InterfaceType, NodeName, PlatformInstanceId } from '../types'; -import { logWorkerGetter, logWorkerSetter, PT_SCRIPT, toLower } from '../utils'; -import { webWorkerCtx, WinIdKey } from './worker-constants'; +import { constructInstance, getElementConstructor } from './worker-constructors'; +import { ImmediateSettersKey, webWorkerCtx, WinIdKey } from './worker-constants'; +import { InterfaceType, NodeName, PlatformInstanceId } from '../types'; +import { + logWorkerGetter, + logWorkerSetter, + PT_SCRIPT, + PT_SCRIPT_TYPE, + randomId, + toUpper, +} from '../utils'; +import { serializeForMain } from './worker-serialization'; import { WorkerElement } from './worker-element'; export class WorkerDocument extends WorkerElement { @@ -27,16 +35,23 @@ export class WorkerDocument extends WorkerElement { setter(this, ['cookie'], (webWorkerCtx.$documentCookie$ = cookie)); } - createElement(tagName: string, $extraInstructions$?: ExtraInstruction[]) { - tagName = toLower(tagName); + createElement(tagName: string) { + tagName = toUpper(tagName); - if (tagName === 'script') { - $extraInstructions$ = [ExtraInstruction.SET_INERT_SCRIPT]; - } else if (tagName === 'iframe') { - $extraInstructions$ = [ExtraInstruction.SET_IFRAME_SRCDOC, PT_SCRIPT as any]; + const winId = this[WinIdKey]; + const instanceId = randomId(); + const ElementCstr = getElementConstructor(tagName); + const elm = new ElementCstr(InterfaceType.Element, instanceId, winId, tagName); + + if (tagName === NodeName.Script) { + elm[ImmediateSettersKey] = [[['type'], serializeForMain(PT_SCRIPT_TYPE)]]; + } else if (tagName === NodeName.IFrame) { + elm[ImmediateSettersKey] = [[['srcdoc'], serializeForMain(PT_SCRIPT)]]; + } else { + elm[ImmediateSettersKey] = []; } - return callMethod(this, ['createElement'], [tagName], $extraInstructions$); + return elm; } get createEventObject() { @@ -70,14 +85,12 @@ export class WorkerDocument extends WorkerElement { } getElementsByTagName(tagName: string) { - tagName = toLower(tagName); - if (tagName === 'body') { + tagName = toUpper(tagName); + if (tagName === NodeName.Body) { return [this.body]; - } - if (tagName === 'head') { + } else if (tagName === NodeName.Head) { return [this.head]; - } - if (tagName === 'script') { + } else if (tagName === NodeName.Script) { return [ constructInstance( InterfaceType.Element, @@ -86,8 +99,9 @@ export class WorkerDocument extends WorkerElement { NodeName.Script ), ]; + } else { + return callMethod(this, ['getElementsByTagName'], [tagName]); } - return callMethod(this, ['getElementsByTagName'], [tagName]); } get head() { diff --git a/src/lib/web-worker/worker-element.ts b/src/lib/web-worker/worker-element.ts index 8ef61901..3fe0b91c 100644 --- a/src/lib/web-worker/worker-element.ts +++ b/src/lib/web-worker/worker-element.ts @@ -1,6 +1,5 @@ import { EventHandler, StateProp } from '../types'; import { getInstanceStateValue, setInstanceStateValue } from './worker-instance'; -import { InstanceIdKey, WinIdKey } from './worker-constants'; import { resolveUrl } from './worker-exec'; import { toLower } from '../utils'; import { WorkerNode } from './worker-node'; diff --git a/src/lib/web-worker/worker-exec.ts b/src/lib/web-worker/worker-exec.ts index 57042fb8..16f15a00 100644 --- a/src/lib/web-worker/worker-exec.ts +++ b/src/lib/web-worker/worker-exec.ts @@ -1,74 +1,68 @@ -import { debug, logWorker, nextTick } from '../utils'; -import { InitializeScriptData, StateProp, WorkerMessageType } from '../types'; -import { getInstanceStateValue, setStateValue } from './worker-instance'; -import { InstanceIdKey, webWorkerCtx, WinIdKey } from './worker-constants'; -import type { WorkerScriptElement } from './worker-script'; +import { debug, logWorker } from '../utils'; +import { EventHandler, InitializeScriptData, StateProp, WorkerMessageType } from '../types'; +import { getInstanceStateValue, runStateHandlers, setStateValue } from './worker-instance'; +import { webWorkerCtx } from './worker-constants'; +import type { WorkerNode } from './worker-node'; export const initNextScriptsInWebWorker = (initScript: InitializeScriptData) => { - const winId = initScript.$winId$; - const instanceId = initScript.$instanceId$; - const content = initScript.$content$; - const url = initScript.$url$; + let $winId$ = initScript.$winId$; + let $instanceId$ = initScript.$instanceId$; + let content = initScript.$content$; + let url = initScript.$url$; let errorMsg = ''; + let handlersType = StateProp.loadHandlers; - try { - if (url) { - importScriptUrl(winId, instanceId, url); - } else if (content) { - if (debug && webWorkerCtx.$config$.logScriptExecution) { - logWorker(`Execute script[data-pt-id="${winId}.${instanceId}"]`); + if (url) { + try { + url = resolveUrl(url) + ''; + setStateValue($winId$, $instanceId$, StateProp.url, url); + setCurrentScript($instanceId$, url); + + if (debug && $winId$ !== webWorkerCtx.$winId$) { + console.error( + `Incorrect window context, winId: ${$winId$}, instanceId: ${$instanceId$}, url: ${url}` + ); } - setCurrentScript(instanceId, ''); - const runScript = new Function(content); - runScript(); - } - } catch (e) { - console.error('Party foul,', e, '\n' + (url || content)); - errorMsg = e + ''; - } - setCurrentScript(-1, ''); - webWorkerCtx.$postMessage$([WorkerMessageType.InitializedWorkerScript, instanceId, errorMsg]); -}; + if (debug && webWorkerCtx.$config$.logScriptExecution) { + logWorker(`Execute script[data-pt-id="${$winId$}.${$instanceId$}"], src: ${url}`); + } -export const scriptElementSetSrc = (script: WorkerScriptElement) => { - nextTick(() => { - let $winId$ = script[WinIdKey]; - let $instanceId$ = script[InstanceIdKey]; - let $stateProp$ = StateProp.loadHandlers; + webWorkerCtx.$importScripts$(url); + } catch (urlError) { + console.error(name, urlError, '\n' + url); + handlersType = StateProp.errorHandlers; + errorMsg = urlError + ''; + } + if (!runStateHandlers($winId$, $instanceId$, handlersType)) { + webWorkerCtx.$postMessage$([ + WorkerMessageType.RunStateHandlers, + { + $winId$, + $instanceId$: $instanceId$, + $stateProp$: handlersType, + }, + ]); + } + } else if (content) { try { - importScriptUrl($winId$, $instanceId$, script.src); - } catch (e) { - $stateProp$ = StateProp.errorHandlers; - console.error(e); + if (debug && webWorkerCtx.$config$.logScriptExecution) { + logWorker(`Execute script[data-pt-id="${$winId$}.${$instanceId$}"]`); + } + setCurrentScript($instanceId$, ''); + const runScript = new Function(content); + runScript(); + } catch (contentError) { + console.error(name, contentError, '\n' + content); + handlersType = StateProp.errorHandlers; + errorMsg = contentError + ''; } - - webWorkerCtx.$postMessage$([ - WorkerMessageType.RunStateProp, - { $winId$, $instanceId$, $stateProp$ }, - ]); - - setCurrentScript(-1, ''); - }, 50); -}; - -const importScriptUrl = (winId: number, instanceId: number, scriptUrl: string) => { - scriptUrl = resolveUrl(scriptUrl) + ''; - setStateValue(winId, instanceId, StateProp.url, scriptUrl); - - if (debug && winId !== webWorkerCtx.$winId$) { - console.error( - `Incorrect window context, winId: ${winId}, instanceId: ${instanceId}, scriptUrl: ${scriptUrl}` - ); } - setCurrentScript(instanceId, scriptUrl); + setCurrentScript(-1, ''); - if (debug && webWorkerCtx.$config$.logScriptExecution) { - logWorker(`Execute script[data-pt-id="${winId}.${instanceId}"], src: ${scriptUrl}`); - } - webWorkerCtx.$importScripts$(scriptUrl); + webWorkerCtx.$postMessage$([WorkerMessageType.InitializedWorkerScript, $instanceId$, errorMsg]); }; const setCurrentScript = (instanceId: number, src: string) => { @@ -76,6 +70,22 @@ const setCurrentScript = (instanceId: number, src: string) => { webWorkerCtx.$currentScriptUrl$ = src; }; +export const insertIframe = (iframe: WorkerNode) => { + let handlers: EventHandler[]; + + if (getInstanceStateValue(iframe, StateProp.isSuccessfulLoad)) { + handlers = getInstanceStateValue(iframe, StateProp.loadHandlers); + if (handlers) { + handlers.forEach((onLoad) => onLoad({ type: 'load' })); + } + } else { + handlers = getInstanceStateValue(iframe, StateProp.loadHandlers); + if (handlers) { + handlers.forEach((onError) => onError({ type: 'error' })); + } + } +}; + export const resolveUrl = (url?: string) => new URL(url || '', webWorkerCtx.$location$ + ''); export const sendBeacon = (url: string, data?: any) => { diff --git a/src/lib/web-worker/worker-global.ts b/src/lib/web-worker/worker-global.ts index 64d2d0a2..4f191d69 100644 --- a/src/lib/web-worker/worker-global.ts +++ b/src/lib/web-worker/worker-global.ts @@ -1,5 +1,5 @@ import { callMethod } from './worker-proxy'; -import { constructInstance } from './worker-serialization'; +import { constructInstance } from './worker-constructors'; import { Image } from './worker-image'; import { InstanceIdKey, webWorkerCtx, WinIdKey } from './worker-constants'; import { InterfaceType, MemberTypeInfo, PlatformInstanceId } from '../types'; diff --git a/src/lib/web-worker/worker-iframe.ts b/src/lib/web-worker/worker-iframe.ts index a5cdf6bb..a4e3a5cb 100644 --- a/src/lib/web-worker/worker-iframe.ts +++ b/src/lib/web-worker/worker-iframe.ts @@ -1,17 +1,12 @@ -import { constructInstance } from './worker-serialization'; +import { constructInstance } from './worker-constructors'; import { getter, setter } from './worker-proxy'; import { getInstanceStateValue, setInstanceStateValue, WorkerInstance } from './worker-instance'; -import { - EventHandler, - ExtraInstruction, - InterfaceType, - PlatformInstanceId, - StateProp, -} from '../types'; -import { nextTick, PT_SCRIPT, PT_SCRIPT_TYPE } from '../utils'; +import { InterfaceType, PlatformInstanceId, StateProp } from '../types'; +import { PT_SCRIPT, PT_SCRIPT_TYPE } from '../utils'; import { resolveUrl } from './worker-exec'; +import { ImmediateSettersKey, webWorkerCtx, WinIdKey } from './worker-constants'; +import { serializeForMain } from './worker-serialization'; import { WorkerSrcElement } from './worker-element'; -import { webWorkerCtx, WinIdKey } from './worker-constants'; export class WorkerIFrameElement extends WorkerSrcElement { get contentDocument() { @@ -21,7 +16,7 @@ export class WorkerIFrameElement extends WorkerSrcElement { get contentWindow() { let winId = getInstanceStateValue(this, StateProp.partyWinId); if (!winId) { - winId = getter(this, ['partyWinId'], [ExtraInstruction.WAIT_FOR_INSTANCE_MEMBER]); + winId = getter(this, ['partyWinId']); setInstanceStateValue(this, StateProp.partyWinId, winId); } return new WorkerContentWindow(InterfaceType.Window, PlatformInstanceId.window, winId); @@ -32,27 +27,25 @@ export class WorkerIFrameElement extends WorkerSrcElement { } set src(url: string) { let xhr = new XMLHttpRequest(); - let callbacks: EventHandler[]; + let iframeContent: string; + let isSuccessfulLoad: boolean; url = resolveUrl(url) + ''; - if (this.src !== url) { setInstanceStateValue(this, StateProp.url, url); xhr.open('GET', url, false); xhr.send(); - if (xhr.status > 199 && xhr.status < 300) { - setter(this, ['srcdoc'], updateIframeContent(url, xhr.responseText)); + isSuccessfulLoad = xhr.status > 199 && xhr.status < 300; + setInstanceStateValue(this, StateProp.isSuccessfulLoad, isSuccessfulLoad); - callbacks = getInstanceStateValue(this, StateProp.loadHandlers); - if (callbacks) { - nextTick(() => callbacks.forEach((onload) => onload({ type: 'load' }))); - } - } else { - callbacks = getInstanceStateValue(this, StateProp.errorHandlers); - if (callbacks) { - nextTick(() => callbacks.forEach((onerror) => onerror({ type: 'error' }))); + if (isSuccessfulLoad) { + iframeContent = updateIframeContent(url, xhr.responseText); + if (this[ImmediateSettersKey]) { + this[ImmediateSettersKey]!.push([['srcdoc'], serializeForMain(iframeContent)]); + } else { + setter(this, ['srcdoc'], iframeContent); } } } diff --git a/src/lib/web-worker/worker-instance.ts b/src/lib/web-worker/worker-instance.ts index d1202114..2ebe0e58 100644 --- a/src/lib/web-worker/worker-instance.ts +++ b/src/lib/web-worker/worker-instance.ts @@ -1,23 +1,27 @@ +import { EventHandler, ImmediateSetter, InterfaceType, StateProp } from '../types'; import { + ImmediateSettersKey, InstanceIdKey, InterfaceTypeKey, NodeNameKey, webWorkerCtx, WinIdKey, } from './worker-constants'; -import { EventHandler, InterfaceType, StateProp } from '../types'; +import { nextTick } from '../utils'; import { proxy } from './worker-proxy'; export class WorkerInstance { [WinIdKey]: number; [InstanceIdKey]: number; [InterfaceTypeKey]: number; - [NodeNameKey]?: string; + [NodeNameKey]: string | undefined; + [ImmediateSettersKey]: ImmediateSetter[] | undefined; constructor(interfaceType: InterfaceType, instanceId: number, winId?: number, nodeName?: string) { this[WinIdKey] = winId || webWorkerCtx.$winId$; this[InstanceIdKey] = instanceId!; this[NodeNameKey] = nodeName; + this[ImmediateSettersKey] = undefined; return proxy((this[InterfaceTypeKey] = interfaceType), this, []); } } @@ -27,7 +31,7 @@ export const instanceState = new Map(); export const getInstanceStateValue = (instance: WorkerInstance, propName: StateProp) => getStateValue(instance[WinIdKey], instance[InstanceIdKey], propName); -const getStateValue = ( +export const getStateValue = ( winId: number, instanceId: number, propName: StateProp, @@ -56,16 +60,19 @@ export const setStateValue = ( const getKey = (winId: number, instanceId: number) => winId + '.' + instanceId; -export const runStateProp = ( +export const runStateHandlers = ( winId: number, instanceId: number, - stateProp: StateProp, - value?: any + handlerType: StateProp, + handlers?: EventHandler[] ) => { - value = getStateValue(winId, instanceId, stateProp); - if (value) { - (value as EventHandler[]).forEach((cb) => - cb({ type: stateProp === StateProp.errorHandlers ? 'error' : 'load' }) + handlers = getStateValue(winId, instanceId, handlerType); + if (handlers) { + nextTick(() => + handlers!.map((cb) => + cb({ type: handlerType === StateProp.errorHandlers ? 'error' : 'load' }) + ) ); } + return !!handlers; }; diff --git a/src/lib/web-worker/worker-node.ts b/src/lib/web-worker/worker-node.ts index c647e47a..f04b0e4b 100644 --- a/src/lib/web-worker/worker-node.ts +++ b/src/lib/web-worker/worker-node.ts @@ -1,9 +1,16 @@ -import { InterfaceTypeKey, NodeNameKey } from './worker-constants'; -import { len } from '../utils'; +import { applyBeforeSyncSetters, callMethod } from './worker-proxy'; +import { EMPTY_ARRAY, len } from '../utils'; +import { insertIframe } from './worker-exec'; +import { InterfaceTypeKey, NodeNameKey, webWorkerCtx, WinIdKey } from './worker-constants'; +import { NodeName, WorkerMessageType } from '../types'; import type { WorkerDocument } from './worker-document'; import { WorkerInstance } from './worker-instance'; export class WorkerNode extends WorkerInstance { + appendChild(node: WorkerNode) { + return this.insertBefore(node, null); + } + get ownerDocument(): WorkerDocument { return document as any; } @@ -13,6 +20,20 @@ export class WorkerNode extends WorkerInstance { } set href(_: any) {} + insertBefore(newNode: WorkerNode, referenceNode: WorkerNode | null) { + applyBeforeSyncSetters(this[WinIdKey], newNode); + + newNode = callMethod(this, ['insertBefore'], [newNode, referenceNode], EMPTY_ARRAY); + + if (newNode[NodeNameKey] === NodeName.IFrame) { + insertIframe(newNode); + } else if (newNode[NodeNameKey] === NodeName.Script) { + webWorkerCtx.$postMessage$([WorkerMessageType.InitializeNextWorkerScript]); + } + + return newNode; + } + get nodeName() { return this[NodeNameKey]; } diff --git a/src/lib/web-worker/worker-proxy.ts b/src/lib/web-worker/worker-proxy.ts index ce9422f6..e552ce84 100644 --- a/src/lib/web-worker/worker-proxy.ts +++ b/src/lib/web-worker/worker-proxy.ts @@ -1,11 +1,14 @@ import { AccessType, - ExtraInstruction, + ImmediateSetter, InterfaceType, MainAccessRequest, MainAccessRequestTask, MainAccessResponse, + NodeName, + PlatformInstanceId, } from '../types'; +import { constructInstance } from './worker-constructors'; import { debug, len, @@ -13,9 +16,11 @@ import { logWorkerGetter, logWorkerSetter, PT_PROXY_URL, + randomId, } from '../utils'; import { deserializeFromMain, serializeForMain } from './worker-serialization'; import { + ImmediateSettersKey, InstanceIdKey, InterfaceTypeKey, NodeNameKey, @@ -23,13 +28,15 @@ import { webWorkerCtx, WinIdKey, } from './worker-constants'; +import type { WorkerInstance } from './worker-instance'; const queueTask = ( - target: any, + target: WorkerInstance, $accessType$: AccessType, $memberPath$: string[], data?: any, - $extraInstructions$?: ExtraInstruction[] + $immediateSetters$?: ImmediateSetter[], + $newInstanceId$?: number ) => { const winId: number = target[WinIdKey]; const winQueue = (webWorkerCtx.$tasks$[winId] = webWorkerCtx.$tasks$[winId] || []); @@ -42,8 +49,9 @@ const queueTask = ( $nodeName$: target[NodeNameKey], $accessType$, $memberPath$, - $data$: serializeForMain(data, new Set()), - $extraInstructions$, + $data$: serializeForMain(data), + $immediateSetters$, + $newInstanceId$, }; winQueue.push(accessReqTask); @@ -51,14 +59,14 @@ const queueTask = ( }; const drainQueue = ( - target: any, + target: WorkerInstance, $memberPath$: string[], $forwardToWin$: boolean, queue: MainAccessRequestTask[] ) => { if (len(queue)) { const accessReq: MainAccessRequest = { - $msgId$: Math.random(), + $msgId$: randomId(), $winId$: target[WinIdKey], $forwardToWin$, $tasks$: [...queue], @@ -93,32 +101,69 @@ const drainQueue = ( } }; -export const getter = ( - target: any, - memberPath: string[], - extraInstructions?: ExtraInstruction[] -) => { - const rtn = queueTask(target, AccessType.Get, memberPath, undefined, extraInstructions); +export const getter = (target: WorkerInstance, memberPath: string[]) => { + applyBeforeSyncSetters(target[WinIdKey], target); + + const rtn = queueTask(target, AccessType.Get, memberPath, undefined); logWorkerGetter(target, memberPath, rtn); return rtn; }; -export const setter = (target: any, memberPath: string[], value: any) => { +export const setter = (target: WorkerInstance, memberPath: string[], value: any) => { logWorkerSetter(target, memberPath, value); - queueTask(target, AccessType.Set, memberPath, value); + + if (target[ImmediateSettersKey]) { + target[ImmediateSettersKey]!.push([memberPath, serializeForMain(value)]); + } else { + queueTask(target, AccessType.Set, memberPath, value); + } }; export const callMethod = ( - target: any, + target: WorkerInstance, memberPath: string[], args: any[], - extraInstructions?: ExtraInstruction[] + immediateSetters?: ImmediateSetter[], + newInstanceId?: number ) => { - const rtn = queueTask(target, AccessType.CallMethod, memberPath, args, extraInstructions); + applyBeforeSyncSetters(target[WinIdKey], target); + const rtn = queueTask( + target, + AccessType.CallMethod, + memberPath, + args, + immediateSetters, + newInstanceId + ); logWorkerCall(target, memberPath, args, rtn); return rtn; }; +export const applyBeforeSyncSetters = (winId: number, target: WorkerInstance) => { + const beforeSyncValues = target[ImmediateSettersKey]; + if (beforeSyncValues) { + target[ImmediateSettersKey] = undefined; + + const winDoc = constructInstance( + InterfaceType.Document, + PlatformInstanceId.document, + winId, + NodeName.Document + ); + const syncedTarget = callMethod( + winDoc, + ['createElement'], + [target[NodeNameKey]], + beforeSyncValues, + target[InstanceIdKey] + ); + + if (debug && target[InstanceIdKey] !== syncedTarget[InstanceIdKey]) { + console.error('Main and web worker instance ids do not match', target, syncedTarget); + } + } +}; + const createComplexMember = (interfaceType: InterfaceType, target: any, memberPath: string[]) => { const interfaceInfo = webWorkerCtx.$interfaces$.find((i) => i[0] === interfaceType); if (interfaceInfo) { diff --git a/src/lib/web-worker/worker-script.ts b/src/lib/web-worker/worker-script.ts index deb3cf73..581dab91 100644 --- a/src/lib/web-worker/worker-script.ts +++ b/src/lib/web-worker/worker-script.ts @@ -1,8 +1,9 @@ import { getInstanceStateValue, setInstanceStateValue } from './worker-instance'; import { getter, setter } from './worker-proxy'; -import { scriptElementSetSrc } from './worker-exec'; +import { ImmediateSettersKey, webWorkerCtx, WinIdKey } from './worker-constants'; +import { resolveUrl } from './worker-exec'; +import { serializeForMain } from './worker-serialization'; import { StateProp } from '../types'; -import { webWorkerCtx, WinIdKey } from './worker-constants'; import { WorkerSrcElement } from './worker-element'; export class WorkerScriptElement extends WorkerSrcElement { @@ -14,9 +15,10 @@ export class WorkerScriptElement extends WorkerSrcElement { } set src(url: string) { if (this[WinIdKey] === webWorkerCtx.$winId$) { - if (getInstanceStateValue(this, StateProp.url) !== url) { - setInstanceStateValue(this, StateProp.url, url); - scriptElementSetSrc(this); + url = resolveUrl(url) + ''; + setInstanceStateValue(this, StateProp.url, url); + if (this[ImmediateSettersKey]) { + this[ImmediateSettersKey]!.push([['src'], serializeForMain(url)]); } } else { setter(this, ['src'], url); diff --git a/src/lib/web-worker/worker-serialization.ts b/src/lib/web-worker/worker-serialization.ts index 83fe32ce..72af284c 100644 --- a/src/lib/web-worker/worker-serialization.ts +++ b/src/lib/web-worker/worker-serialization.ts @@ -1,8 +1,8 @@ import { callMethod } from './worker-proxy'; -import { debug, logWorker } from '../utils'; +import { constructInstance } from './worker-constructors'; +import { debug, logWorker, randomId } from '../utils'; import { InterfaceType, - NodeName, PlatformInstanceId, SerializedInstance, SerializedRefTransfer, @@ -10,15 +10,11 @@ import { SerializedType, } from '../types'; import { InstanceIdKey, InterfaceTypeKey, NodeNameKey, WinIdKey } from './worker-constants'; -import { WorkerAnchorElement, WorkerDocumentElementChild, WorkerElement } from './worker-element'; -import { WorkerNode, WorkerNodeList } from './worker-node'; -import { WorkerContentWindow, WorkerIFrameElement } from './worker-iframe'; -import { WorkerDocument } from './worker-document'; -import { WorkerInstance } from './worker-instance'; -import { WorkerScriptElement } from './worker-script'; - -export const serializeForMain = (value: any, added: Set): SerializedTransfer | undefined => { +import { WorkerNodeList } from './worker-node'; + +export const serializeForMain = (value: any, added?: Set): SerializedTransfer | undefined => { if (value !== undefined) { + added = added || new Set(); const type = typeof value; if (type === 'string' || type === 'boolean' || type === 'number' || value == null) { return [SerializedType.Primitive, value]; @@ -122,51 +118,13 @@ export const constructSerializedInstance = ({ } }; -export const constructInstance = ( - interfaceType: InterfaceType, - instanceId: number, - winId?: number, - nodeName?: string -) => { - nodeName = - interfaceType === InterfaceType.Document - ? NodeName.Document - : interfaceType === InterfaceType.TextNode - ? NodeName.Text - : nodeName; - - const Cstr = getConstructor(interfaceType, nodeName); - return new Cstr(interfaceType, instanceId, winId, nodeName); -}; - -const getConstructor = (interfaceType: InterfaceType, nodeName?: string): typeof WorkerInstance => { - if (interfaceType === InterfaceType.Element) { - const Cstrs: { [nodeName: string]: any } = { - A: WorkerAnchorElement, - BODY: WorkerDocumentElementChild, - HEAD: WorkerDocumentElementChild, - IFRAME: WorkerIFrameElement, - SCRIPT: WorkerScriptElement, - }; - return Cstrs[nodeName!] || WorkerElement; - } else if (interfaceType === InterfaceType.Document) { - return WorkerDocument; - } else if (interfaceType === InterfaceType.Window) { - return WorkerContentWindow; - } else if (interfaceType === InterfaceType.TextNode) { - return WorkerNode; - } else { - return WorkerInstance; - } -}; - const refsByRefId = new Map(); const refIdsByRef = new WeakMap(); const serializeRef = (ref: any, refId?: number): SerializedRefTransfer => { refId = refIdsByRef.get(ref); if (!refId) { - refIdsByRef.set(ref, (refId = Math.random())); + refIdsByRef.set(ref, (refId = randomId())); refsByRefId.set(refId, ref); } return [SerializedType.Ref, refId]; diff --git a/tests/element/element.spec.ts b/tests/element/element.spec.ts index d8d1546b..d0faf895 100644 --- a/tests/element/element.spec.ts +++ b/tests/element/element.spec.ts @@ -45,4 +45,10 @@ test('element', async ({ page }) => { const testParentElement = page.locator('#testParentElement'); await expect(testParentElement).toHaveText('hasParentElement'); + + const testSyncSetters = page.locator('#testSyncSetters'); + await expect(testSyncSetters).toHaveText('101'); + + const testSyncSettersCall = page.locator('#testSyncSettersCall'); + await expect(testSyncSettersCall).toHaveText('some-id'); }); diff --git a/tests/element/index.html b/tests/element/index.html index 0c85cfb3..cfc2c0ff 100644 --- a/tests/element/index.html +++ b/tests/element/index.html @@ -216,6 +216,32 @@

Element

+
  • + before sync setters, get + + +
  • + +
  • + before sync setters, call + + +
  • + +
  • + different document.createElement + + +
  • +