From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- devtools/server/startup/content-process-script.js | 281 ++++++++++++++++++++++ devtools/server/startup/content-process.js | 33 +++ devtools/server/startup/content-process.sys.mjs | 104 ++++++++ devtools/server/startup/frame.js | 193 +++++++++++++++ devtools/server/startup/moz.build | 13 + devtools/server/startup/worker.js | 152 ++++++++++++ 6 files changed, 776 insertions(+) create mode 100644 devtools/server/startup/content-process-script.js create mode 100644 devtools/server/startup/content-process.js create mode 100644 devtools/server/startup/content-process.sys.mjs create mode 100644 devtools/server/startup/frame.js create mode 100644 devtools/server/startup/moz.build create mode 100644 devtools/server/startup/worker.js (limited to 'devtools/server/startup') diff --git a/devtools/server/startup/content-process-script.js b/devtools/server/startup/content-process-script.js new file mode 100644 index 0000000000..628c481685 --- /dev/null +++ b/devtools/server/startup/content-process-script.js @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/process-script */ + +"use strict"; + +/** + * Main entry point for DevTools in content processes. + * + * This module is loaded early when a content process is started. + * Note that (at least) JS XPCOM registered at app-startup, will be running before. + * It is used by the multiprocess browser toolbox in order to debug privileged resources. + * When debugging a Web page loaded in a Tab, DevToolsFrame JS Window Actor is used instead + * (DevToolsFrameParent.jsm and DevToolsFrameChild.jsm). + * + * This module won't do anything unless DevTools codebase starts adding some data + * in `Services.cpmm.sharedData` object or send a message manager message via `Services.cpmm`. + * Also, this module is only loaded, on-demand from process-helper if devtools are watching for process targets. + */ + +const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; + +class ContentProcessStartup { + constructor() { + // The map is indexed by the Watcher Actor ID. + // The values are objects containing the following properties: + // - connection: the DevToolsServerConnection itself + // - actor: the ContentProcessTargetActor instance + this._connections = new Map(); + + this.observe = this.observe.bind(this); + this.receiveMessage = this.receiveMessage.bind(this); + + this.addListeners(); + this.maybeCreateExistingTargetActors(); + } + + observe(subject, topic, data) { + switch (topic) { + case "xpcom-shutdown": { + this.destroy(); + break; + } + } + } + + destroy(options) { + this.removeListeners(); + + for (const [, connectionInfo] of this._connections) { + connectionInfo.connection.close(options); + } + this._connections.clear(); + } + + addListeners() { + Services.obs.addObserver(this.observe, "xpcom-shutdown"); + + Services.cpmm.addMessageListener( + "debug:instantiate-already-available", + this.receiveMessage + ); + Services.cpmm.addMessageListener( + "debug:destroy-target", + this.receiveMessage + ); + Services.cpmm.addMessageListener( + "debug:add-session-data-entry", + this.receiveMessage + ); + Services.cpmm.addMessageListener( + "debug:remove-session-data-entry", + this.receiveMessage + ); + Services.cpmm.addMessageListener( + "debug:destroy-process-script", + this.receiveMessage + ); + } + + removeListeners() { + Services.obs.removeObserver(this.observe, "xpcom-shutdown"); + + Services.cpmm.removeMessageListener( + "debug:instantiate-already-available", + this.receiveMessage + ); + Services.cpmm.removeMessageListener( + "debug:destroy-target", + this.receiveMessage + ); + Services.cpmm.removeMessageListener( + "debug:add-session-data-entry", + this.receiveMessage + ); + Services.cpmm.removeMessageListener( + "debug:remove-session-data-entry", + this.receiveMessage + ); + Services.cpmm.removeMessageListener( + "debug:destroy-process-script", + this.receiveMessage + ); + } + + receiveMessage(msg) { + switch (msg.name) { + case "debug:instantiate-already-available": + this.createTargetActor( + msg.data.watcherActorID, + msg.data.connectionPrefix, + msg.data.sessionData, + true + ); + break; + case "debug:destroy-target": + this.destroyTarget(msg.data.watcherActorID); + break; + case "debug:add-session-data-entry": + this.addSessionDataEntry( + msg.data.watcherActorID, + msg.data.type, + msg.data.entries + ); + break; + case "debug:remove-session-data-entry": + this.removeSessionDataEntry( + msg.data.watcherActorID, + msg.data.type, + msg.data.entries + ); + break; + case "debug:destroy-process-script": + this.destroy(msg.data.options); + break; + default: + throw new Error(`Unsupported message name ${msg.name}`); + } + } + + /** + * Called when the content process just started. + * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / WatcherRegistry.jsm) + * put some data in `sharedData` telling us to do so. + */ + maybeCreateExistingTargetActors() { + const { sharedData } = Services.cpmm; + + // Accessing `sharedData` right off the app-startup returns null. + // Spinning the event loop with dispatchToMainThread seems enough, + // but it means that we let some more Javascript code run before + // instantiating the target actor. + // So we may miss a few resources and will register the breakpoints late. + if (!sharedData) { + Services.tm.dispatchToMainThread( + this.maybeCreateExistingTargetActors.bind(this) + ); + return; + } + + const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); + if (!sessionDataByWatcherActor) { + return; + } + + // Create one Target actor for each prefix/client which listen to process + for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { + const { connectionPrefix, targets } = sessionData; + // This is where we only do something significant only if DevTools are opened + // and requesting to create target actor for content processes + if (targets?.includes("process")) { + this.createTargetActor(watcherActorID, connectionPrefix, sessionData); + } + } + } + + /** + * Instantiate a new ContentProcessTarget for the given connection. + * This is where we start doing some significant computation that only occurs when DevTools are opened. + * + * @param String watcherActorID + * The ID of the WatcherActor who requested to observe and create these target actors. + * @param String parentConnectionPrefix + * The prefix of the DevToolsServerConnection of the Watcher Actor. + * This is used to compute a unique ID for the target actor. + * @param Object sessionData + * All data managed by the Watcher Actor and WatcherRegistry.jsm, containing + * target types, resources types to be listened as well as breakpoints and any + * other data meant to be shared across processes and threads. + * @param Object options Dictionary with optional values: + * @param Boolean options.ignoreAlreadyCreated + * If true, do not throw if the target actor has already been created. + */ + createTargetActor( + watcherActorID, + parentConnectionPrefix, + sessionData, + ignoreAlreadyCreated = false + ) { + if (this._connections.get(watcherActorID)) { + if (ignoreAlreadyCreated) { + return; + } + throw new Error( + "ContentProcessStartup createTargetActor was called more than once" + + ` for the Watcher Actor (ID: "${watcherActorID}")` + ); + } + // Compute a unique prefix, just for this content process, + // which will be used to create a ChildDebuggerTransport pair between content and parent processes. + // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, + // but here, we can't have access to any DevTools connection as we are really early in the content process startup + const prefix = + parentConnectionPrefix + "contentProcess" + Services.appinfo.processID; + //TODO: probably merge content-process.jsm with this module + const { initContentProcessTarget } = ChromeUtils.importESModule( + "resource://devtools/server/startup/content-process.sys.mjs" + ); + const { actor, connection } = initContentProcessTarget({ + target: Services.cpmm, + data: { + watcherActorID, + parentConnectionPrefix, + prefix, + sessionContext: sessionData.sessionContext, + }, + }); + this._connections.set(watcherActorID, { + actor, + connection, + }); + + // Pass initialization data to the target actor + for (const type in sessionData) { + actor.addSessionDataEntry(type, sessionData[type]); + } + } + + destroyTarget(watcherActorID) { + const connectionInfo = this._connections.get(watcherActorID); + // This connection has already been cleaned? + if (!connectionInfo) { + throw new Error( + `Trying to destroy a content process target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` + ); + } + connectionInfo.connection.close(); + this._connections.delete(watcherActorID); + } + + async addSessionDataEntry(watcherActorID, type, entries) { + const connectionInfo = this._connections.get(watcherActorID); + if (!connectionInfo) { + throw new Error( + `No content process target actor for this Watcher Actor ID:"${watcherActorID}"` + ); + } + const { actor } = connectionInfo; + await actor.addSessionDataEntry(type, entries); + Services.cpmm.sendAsyncMessage("debug:add-session-data-entry-done", { + watcherActorID, + }); + } + + removeSessionDataEntry(watcherActorID, type, entries) { + const connectionInfo = this._connections.get(watcherActorID); + if (!connectionInfo) { + return; + } + const { actor } = connectionInfo; + actor.removeSessionDataEntry(type, entries); + } +} + +// Only start this component for content processes. +// i.e. explicitely avoid running it for the parent process +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + new ContentProcessStartup(); +} diff --git a/devtools/server/startup/content-process.js b/devtools/server/startup/content-process.js new file mode 100644 index 0000000000..5710220e44 --- /dev/null +++ b/devtools/server/startup/content-process.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/process-script */ + +"use strict"; + +/* + * Process script that listens for requests to start a `DevToolsServer` for an entire + * content process. Loaded into content processes by the main process during + * content-process-connector.js' `connectToContentProcess`. + * + * The actual server startup itself is in a JSM so that code can be cached. + */ + +function onInit(message) { + // Only reply if we are in a real content process + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + const { initContentProcessTarget } = ChromeUtils.importESModule( + "resource://devtools/server/startup/content-process.sys.mjs" + ); + initContentProcessTarget(message); + } +} + +function onClose() { + removeMessageListener("debug:init-content-server", onInit); + removeMessageListener("debug:close-content-server", onClose); +} + +addMessageListener("debug:init-content-server", onInit); +addMessageListener("debug:close-content-server", onClose); diff --git a/devtools/server/startup/content-process.sys.mjs b/devtools/server/startup/content-process.sys.mjs new file mode 100644 index 0000000000..fd974e8c4a --- /dev/null +++ b/devtools/server/startup/content-process.sys.mjs @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Module that listens for requests to start a `DevToolsServer` for an entire content + * process. Loaded into content processes by the main process during + * content-process-connector.js' `connectToContentProcess` via the process + * script `content-process.js`. + * + * The actual server startup itself is in this JSM so that code can be cached. + */ + +export function initContentProcessTarget(msg) { + const mm = msg.target; + const prefix = msg.data.prefix; + const watcherActorID = msg.data.watcherActorID; + + // Lazy load Loader.sys.mjs to prevent loading any devtools dependency too early. + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + + // Use a unique object to identify this one usage of the loader + const loaderRequester = {}; + + // Init a custom, invisible DevToolsServer, in order to not pollute the + // debugger with all devtools modules, nor break the debugger itself with + // using it in the same process. + const loader = useDistinctSystemPrincipalLoader(loaderRequester); + + const { DevToolsServer } = loader.require( + "resource://devtools/server/devtools-server.js" + ); + + DevToolsServer.init(); + // For browser content toolbox, we do need a regular root actor and all tab + // actors, but don't need all the "browser actors" that are only useful when + // debugging the parent process via the browser toolbox. + DevToolsServer.registerActors({ root: true, target: true }); + + // Connect both parent/child processes devtools servers RDP via message + // managers + const conn = DevToolsServer.connectToParent(prefix, mm); + + const { ContentProcessTargetActor } = loader.require( + "resource://devtools/server/actors/targets/content-process.js" + ); + + const actor = new ContentProcessTargetActor(conn, { + sessionContext: msg.data.sessionContext, + }); + actor.manage(actor); + + const response = { watcherActorID, prefix, actor: actor.form() }; + mm.sendAsyncMessage("debug:content-process-actor", response); + + function onDestroy(options) { + mm.removeMessageListener( + "debug:content-process-disconnect", + onContentProcessDisconnect + ); + actor.off("destroyed", onDestroy); + + // Notify the parent process that the actor is being destroyed + mm.sendAsyncMessage("debug:content-process-actor-destroyed", { + watcherActorID, + }); + + // Call DevToolsServerConnection.close to destroy all child actors. It should end up + // calling DevToolsServerConnection.onTransportClosed that would actually cleanup all actor + // pools. + conn.close(options); + + // Destroy the related loader when the target is destroyed + // and we were the last user of the special loader + releaseDistinctSystemPrincipalLoader(loaderRequester); + } + function onContentProcessDisconnect(message) { + if (message.data.prefix != prefix) { + // Several copies of this process script can be running for a single process if + // we are debugging the same process from multiple clients. + // If this disconnect request doesn't match a connection known here, ignore it. + return; + } + onDestroy(); + } + + // Clean up things when the client disconnects + mm.addMessageListener( + "debug:content-process-disconnect", + onContentProcessDisconnect + ); + // And also when the target actor is destroyed + actor.on("destroyed", onDestroy); + + return { + actor, + connection: conn, + }; +} diff --git a/devtools/server/startup/frame.js b/devtools/server/startup/frame.js new file mode 100644 index 0000000000..db14f03c15 --- /dev/null +++ b/devtools/server/startup/frame.js @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +/* global addEventListener */ + +/* + * Frame script that listens for requests to start a `DevToolsServer` for a frame in a + * content process. Loaded into content process frames by the main process during + * frame-connector.js' connectToFrame. + */ + +try { + var chromeGlobal = this; + + // Encapsulate in its own scope to allows loading this frame script more than once. + (function () { + // In most cases, we are debugging a tab in content process, without chrome + // privileges. But in some tests, we are attaching to privileged document. + // Because the debugger can't be running in the same compartment than its debuggee, + // we have to load the server in a dedicated Loader, flagged with + // invisibleToDebugger, which will force it to be loaded in another compartment. + let loader, + customLoader = false; + if (content.document.nodePrincipal.isSystemPrincipal) { + const { useDistinctSystemPrincipalLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + loader = useDistinctSystemPrincipalLoader(chromeGlobal); + customLoader = true; + } else { + // Otherwise, use the shared loader. + loader = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + } + const { require } = loader; + + const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); + const { + DevToolsServer, + } = require("resource://devtools/server/devtools-server.js"); + + DevToolsServer.init(); + // We want a special server without any root actor and only target-scoped actors. + // We are going to spawn a WindowGlobalTargetActor instance in the next few lines, + // it is going to act like a root actor without being one. + DevToolsServer.registerActors({ target: true }); + + const connections = new Map(); + + const onConnect = DevToolsUtils.makeInfallible(function (msg) { + const mm = msg.target; + const prefix = msg.data.prefix; + const addonId = msg.data.addonId; + const addonBrowsingContextGroupId = msg.data.addonBrowsingContextGroupId; + + // If we try to create several frame targets simultaneously, the frame script will be loaded several times. + // In this case a single "debug:connect" message might be received by all the already loaded frame scripts. + // Check if the DevToolsServer already knows the provided connection prefix, + // because it means that another framescript instance already handled this message. + // Another "debug:connect" message is guaranteed to be emitted for another prefix, + // so we keep the message listener and wait for this next message. + if (DevToolsServer.hasConnectionForPrefix(prefix)) { + return; + } + removeMessageListener("debug:connect", onConnect); + + const conn = DevToolsServer.connectToParent(prefix, mm); + connections.set(prefix, conn); + + let actor; + + if (addonId) { + const { + WebExtensionTargetActor, + } = require("resource://devtools/server/actors/targets/webextension.js"); + const { + createWebExtensionSessionContext, + } = require("resource://devtools/server/actors/watcher/session-context.js"); + const { browsingContext } = docShell; + actor = new WebExtensionTargetActor(conn, { + addonId, + addonBrowsingContextGroupId, + chromeGlobal, + isTopLevelTarget: true, + prefix, + sessionContext: createWebExtensionSessionContext( + { + addonId, + browsingContextID: browsingContext.id, + innerWindowId: browsingContext.currentWindowContext.innerWindowId, + }, + { + isServerTargetSwitchingEnabled: + msg.data.isServerTargetSwitchingEnabled, + } + ), + }); + } else { + const { + WindowGlobalTargetActor, + } = require("resource://devtools/server/actors/targets/window-global.js"); + const { + createBrowserElementSessionContext, + } = require("resource://devtools/server/actors/watcher/session-context.js"); + + const { docShell } = chromeGlobal; + // For a script loaded via loadFrameScript, the global is the content + // message manager. + // All WindowGlobalTarget actors created via the framescript are top-level + // targets. Non top-level WindowGlobalTarget actors are all created by the + // DevToolsFrameChild actor. + // + // createBrowserElementSessionContext only reads browserId attribute + const fakeBrowserElement = { + browserId: docShell.browsingContext.browserId, + }; + actor = new WindowGlobalTargetActor(conn, { + docShell, + isTopLevelTarget: true, + // This is only used when server target switching is off and we create + // the target from TabDescriptor. So all config attributes are false. + sessionContext: createBrowserElementSessionContext( + fakeBrowserElement, + {} + ), + }); + } + actor.manage(actor); + + sendAsyncMessage("debug:actor", { actor: actor.form(), prefix }); + }); + + addMessageListener("debug:connect", onConnect); + + const onDisconnect = DevToolsUtils.makeInfallible(function (msg) { + const prefix = msg.data.prefix; + const conn = connections.get(prefix); + if (!conn) { + // Several copies of this frame script can be running for a single frame since it + // is loaded once for each DevTools connection to the frame. If this disconnect + // request doesn't match a connection known here, ignore it. + return; + } + + removeMessageListener("debug:disconnect", onDisconnect); + // Call DevToolsServerConnection.close to destroy all child actors. It should end up + // calling DevToolsServerConnection.onTransportClosed that would actually cleanup all actor + // pools. + conn.close(); + connections.delete(prefix); + }); + addMessageListener("debug:disconnect", onDisconnect); + + // In non-e10s mode, the "debug:disconnect" message isn't always received before the + // messageManager connection goes away. Watching for "unload" here ensures we close + // any connections when the frame is unloaded. + addEventListener("unload", () => { + for (const conn of connections.values()) { + conn.close(); + } + connections.clear(); + }); + + // Destroy the server once its last connection closes. Note that multiple frame + // scripts may be running in parallel and reuse the same server. + function destroyLoader() { + // Only destroy the server if there is no more connections to it. It may be used + // to debug another tab running in the same process. + if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) { + return; + } + DevToolsServer.off("connectionchange", destroyLoader); + + // When debugging chrome pages, we initialized a dedicated loader, also destroy it + if (customLoader) { + const { releaseDistinctSystemPrincipalLoader } = + ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + releaseDistinctSystemPrincipalLoader(chromeGlobal); + } + } + DevToolsServer.on("connectionchange", destroyLoader); + })(); +} catch (e) { + dump(`Exception in DevTools frame startup: ${e}\n`); +} diff --git a/devtools/server/startup/moz.build b/devtools/server/startup/moz.build new file mode 100644 index 0000000000..4237d88599 --- /dev/null +++ b/devtools/server/startup/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "content-process-script.js", + "content-process.js", + "content-process.sys.mjs", + "frame.js", + "worker.js", +) diff --git a/devtools/server/startup/worker.js b/devtools/server/startup/worker.js new file mode 100644 index 0000000000..0171cc7ae0 --- /dev/null +++ b/devtools/server/startup/worker.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* eslint-env mozilla/chrome-worker */ +/* global worker, loadSubScript, global */ + +/* + * Worker debugger script that listens for requests to start a `DevToolsServer` for a + * worker in a process. Loaded into a specific worker during worker-connector.js' + * `connectToWorker` which is called from the same process as the worker. + */ + +// This function is used to do remote procedure calls from the worker to the +// main thread. It is exposed as a built-in global to every module by the +// worker loader. To make sure the worker loader can access it, it needs to be +// defined before loading the worker loader script below. +let nextId = 0; +this.rpc = function (method, ...params) { + return new Promise((resolve, reject) => { + const id = nextId++; + this.addEventListener("message", function onMessageForRpc(event) { + const packet = JSON.parse(event.data); + if (packet.type !== "rpc" || packet.id !== id) { + return; + } + if (packet.error) { + reject(packet.error); + } else { + resolve(packet.result); + } + this.removeEventListener("message", onMessageForRpc); + }); + + postMessage( + JSON.stringify({ + type: "rpc", + method, + params, + id, + }) + ); + }); +}.bind(this); + +loadSubScript("resource://devtools/shared/loader/worker-loader.js"); + +const { WorkerTargetActor } = worker.require( + "resource://devtools/server/actors/targets/worker.js" +); +const { DevToolsServer } = worker.require( + "resource://devtools/server/devtools-server.js" +); + +DevToolsServer.createRootActor = function () { + throw new Error("Should never get here!"); +}; + +// This file is only instanciated once for a given WorkerDebugger, which means that +// multiple toolbox could end up using the same instance of this script. In order to handle +// that, we handle a Map of the different connections, keyed by forwarding prefix. +const connections = new Map(); + +this.addEventListener("message", async function (event) { + const packet = JSON.parse(event.data); + switch (packet.type) { + case "connect": + const { forwardingPrefix } = packet; + + // Force initializing the server each time on connect + // as it may have been destroyed by a previous, now closed toolbox. + // Once the last connection drops, the server auto destroy itself. + DevToolsServer.init(); + + // Step 3: Create a connection to the parent. + const connection = DevToolsServer.connectToParent(forwardingPrefix, this); + + // Step 4: Create a WorkerTarget actor. + const workerTargetActor = new WorkerTargetActor( + connection, + global, + packet.workerDebuggerData, + packet.options.sessionContext + ); + // Make the worker manage itself so it is put in a Pool and assigned an actorID. + workerTargetActor.manage(workerTargetActor); + + workerTargetActor.on( + "worker-thread-attached", + function onThreadAttached() { + postMessage(JSON.stringify({ type: "worker-thread-attached" })); + } + ); + + // Step 5: Send a response packet to the parent to notify + // it that a connection has been established. + connections.set(forwardingPrefix, { + connection, + workerTargetActor, + }); + + postMessage( + JSON.stringify({ + type: "connected", + forwardingPrefix, + workerTargetForm: workerTargetActor.form(), + }) + ); + + // We might receive data to watch. + if (packet.options.sessionData) { + const promises = []; + for (const [type, entries] of Object.entries( + packet.options.sessionData + )) { + promises.push(workerTargetActor.addSessionDataEntry(type, entries)); + } + await Promise.all(promises); + } + + break; + + case "add-session-data-entry": + await connections + .get(packet.forwardingPrefix) + .workerTargetActor.addSessionDataEntry( + packet.dataEntryType, + packet.entries + ); + postMessage(JSON.stringify({ type: "session-data-entry-added" })); + break; + + case "remove-session-data-entry": + await connections + .get(packet.forwardingPrefix) + .workerTargetActor.removeSessionDataEntry( + packet.dataEntryType, + packet.entries + ); + break; + + case "disconnect": + // This will destroy the associate WorkerTargetActor (and the actors it manages). + if (connections.has(packet.forwardingPrefix)) { + connections.get(packet.forwardingPrefix).connection.close(); + connections.delete(packet.forwardingPrefix); + } + break; + } +}); -- cgit v1.2.3