Files
Julian Descottes c91fea99a7 Bug 1936770 - [remote] Allow to emit an event for several related contexts r=Sasha
To support realm events for shared and service workers - which can have more than 1 related context - we need to adapt our logic to match several browsing contexts to a single context descriptor.

On the bidi spec, this is expressed in the event is enabled algorithm (https://w3c.github.io/webdriver-bidi/#event-is-enabled )which takes an array of navigables as parameter. In order to enable this, a few event-related APIs are modified to support matching several browsing context ids.

We should be able to emit an event related to N contexts, and the event should be emitted if:
- any subscription monitors any of the related contexts
- any subscription monitors the user context of any of the related contexts
- any subscription is global

Note that this allows to emit an event related to 0 browsing contexts. In that case, global subscriptions should still get the event.
While this is handy given that we can't currently retrieve the contexts related to service and shared worker, this is also an important feature in the long run.
It is completely possible to have a worker instance for a service worker which. starts running even if no related context is open at the moment.

A dedicated browser mochitest is added to cover the new feature.

Differential Revision: https://phabricator.services.mozilla.com/D282748
2026-02-13 20:02:57 +00:00

250 lines
7.0 KiB
JavaScript

/* 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/. */
import { MessageHandler } from "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NavigationManager: "chrome://remote/content/shared/NavigationManager.sys.mjs",
registerWebDriverWorkerListenerActor:
"chrome://remote/content/shared/js-process-actors/WebDriverWorkerListenerActor.sys.mjs",
RootTransport:
"chrome://remote/content/shared/messagehandler/transports/RootTransport.sys.mjs",
SessionData:
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
SessionDataMethod:
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
unregisterWebDriverWorkerListenerActor:
"chrome://remote/content/shared/js-process-actors/WebDriverWorkerListenerActor.sys.mjs",
WindowGlobalMessageHandler:
"chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
});
/**
* A RootMessageHandler is the root node of a MessageHandler network. It lives
* in the parent process. It can forward commands to MessageHandlers in other
* layers (at the moment WindowGlobalMessageHandlers in content processes).
*/
export class RootMessageHandler extends MessageHandler {
#navigationManager;
#realms;
#rootTransport;
#sessionData;
/**
* Returns the RootMessageHandler module path.
*
* @returns {string}
*/
static get modulePath() {
return "root";
}
/**
* Returns the RootMessageHandler type.
*
* @returns {string}
*/
static get type() {
return "ROOT";
}
/**
* The ROOT MessageHandler is unique for a given MessageHandler network
* (ie for a given sessionId). Reuse the type as context id here.
*/
static getIdFromContext() {
return RootMessageHandler.type;
}
/**
* Create a new RootMessageHandler instance.
*
* @param {string} sessionId
* ID of the session the handler is used for.
*/
constructor(sessionId) {
super(sessionId, null);
this.#rootTransport = new lazy.RootTransport(this);
this.#sessionData = new lazy.SessionData(this);
this.#navigationManager = new lazy.NavigationManager();
this.#navigationManager.startMonitoring();
// Register js process actors to monitor worker creation and destruction in
// all processes.
lazy.registerWebDriverWorkerListenerActor();
// Map with inner window ids as keys, and sets of realm ids, associated with
// this window as values.
this.#realms = new Map();
// In the general case, we don't get notified that realms got destroyed,
// because there is no communication between content and parent process at this moment,
// so we have to listen to the this notification to clean up the internal
// map and trigger the events.
Services.obs.addObserver(this, "window-global-destroyed");
}
get navigationManager() {
return this.#navigationManager;
}
get realms() {
return this.#realms;
}
get sessionData() {
return this.#sessionData;
}
destroy() {
this.#sessionData.destroy();
this.#navigationManager.destroy();
lazy.unregisterWebDriverWorkerListenerActor();
Services.obs.removeObserver(this, "window-global-destroyed");
this.#realms = null;
super.destroy();
}
/**
* Add new session data items of a given module, category and
* contextDescriptor.
*
* Forwards the call to the SessionData instance owned by this
* RootMessageHandler and propagates the information via a command to existing
* MessageHandlers.
*/
addSessionDataItem(sessionData = {}) {
sessionData.method = lazy.SessionDataMethod.Add;
return this.updateSessionData([sessionData]);
}
emitEvent(name, eventPayload, relatedContexts) {
// Intercept realm created and destroyed events to update internal map.
if (name === "realm-created") {
this.#onRealmCreated(eventPayload);
}
// We receive this events in the case of moving the page to BFCache.
if (name === "windowglobal-pagehide") {
this.#cleanUpRealmsForWindow(
eventPayload.innerWindowId,
eventPayload.context
);
}
super.emitEvent(name, eventPayload, relatedContexts);
}
/**
* Emit a public protocol event. This event will be sent over to the client.
*
* @param {string} name
* Name of the event. Protocol level events should be of the
* form [module name].[event name].
* @param {object} data
* The event's data.
*/
emitProtocolEvent(name, data) {
this.emit("message-handler-protocol-event", {
name,
data,
sessionId: this.sessionId,
});
}
/**
* Forward the provided command to WINDOW_GLOBAL MessageHandlers via the
* RootTransport.
*
* @param {Command} command
* The command to forward. See type definition in MessageHandler.js
* @returns {Promise}
* Returns a promise that resolves with the result of the command.
*/
forwardCommand(command) {
switch (command.destination.type) {
case lazy.WindowGlobalMessageHandler.type:
return this.#rootTransport.forwardCommand(command);
default:
throw new Error(
`Cannot forward command to "${command.destination.type}" from "${this.constructor.type}".`
);
}
}
matchesContext() {
return true;
}
observe(subject, topic) {
if (topic !== "window-global-destroyed") {
return;
}
this.#cleanUpRealmsForWindow(
subject.innerWindowId,
subject.browsingContext
);
}
/**
* Remove session data items of a given module, category and
* contextDescriptor.
*
* Forwards the call to the SessionData instance owned by this
* RootMessageHandler and propagates the information via a command to existing
* MessageHandlers.
*/
removeSessionDataItem(sessionData = {}) {
sessionData.method = lazy.SessionDataMethod.Remove;
return this.updateSessionData([sessionData]);
}
/**
* Update session data items of a given module, category and
* contextDescriptor.
*
* Forwards the call to the SessionData instance owned by this
* RootMessageHandler.
*/
async updateSessionData(sessionData = []) {
await this.#sessionData.updateSessionData(sessionData);
}
#cleanUpRealmsForWindow(innerWindowId, context) {
const realms = this.#realms.get(innerWindowId);
if (!realms) {
return;
}
realms.forEach(realm => {
realms.delete(realm.realm);
this.emitEvent("realm-destroyed", {
context,
realm: realm.realm,
});
});
this.#realms.delete(innerWindowId);
}
#onRealmCreated = data => {
const { innerWindowId, realmInfo } = data;
if (!this.#realms.has(innerWindowId)) {
this.#realms.set(innerWindowId, new Set());
}
this.#realms
.get(innerWindowId)
.add({ realm: realmInfo.realm, sandbox: realmInfo.sandbox });
};
}