Source code

Revision control

Copy as Markdown

Other Tools

/* 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 { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
ContextDescriptorType:
"chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
getWebDriverSessionById:
"chrome://remote/content/shared/webdriver/Session.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
RootMessageHandler:
"chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
class SessionModule extends RootBiDiModule {
#browsingContextIdEventMap;
#globalEventSet;
constructor(messageHandler) {
super(messageHandler);
// Map with top-level browsing context id keys and values
// that are a set of event names for events
// that are enabled in the given browsing context.
// TODO: Bug 1804417. Use navigable instead of browsing context id.
this.#browsingContextIdEventMap = new Map();
// Set of event names which are strings of the form [moduleName].[eventName]
// for events that are enabled for all browsing contexts.
// We should only add an actual event listener on the MessageHandler the
// first time an event is subscribed to.
this.#globalEventSet = new Set();
}
destroy() {
this.#browsingContextIdEventMap = null;
this.#globalEventSet = null;
}
/**
* Commands
*/
/**
* End the current session.
*
* Session clean up will happen later in WebDriverBiDiConnection class.
*/
async end() {
const session = lazy.getWebDriverSessionById(this.messageHandler.sessionId);
if (session.http) {
throw new lazy.error.UnsupportedOperationError(
"Ending a session started with WebDriver classic is not supported." +
' Use the WebDriver classic "Delete Session" command instead.'
);
}
}
/**
* Enable certain events either globally, or for a list of browsing contexts.
*
* @param {object=} params
* @param {Array<string>} params.events
* List of events to subscribe to.
* @param {Array<string>=} params.contexts
* Optional list of top-level browsing context ids
* to subscribe the events for.
*
* @throws {InvalidArgumentError}
* If <var>events</var> or <var>contexts</var> are not valid types.
*/
async subscribe(params = {}) {
const { events, contexts: contextIds = null } = params;
// Check input types until we run schema validation.
this.#assertNonEmptyArrayWithStrings(events, "events");
if (contextIds !== null) {
this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
}
const listeners = this.#updateEventMap(events, contextIds, true);
// TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8).
// Subscribe to the relevant engine-internal events.
await this.messageHandler.eventsDispatcher.update(listeners);
}
/**
* Disable certain events either globally, or for a list of browsing contexts.
*
* @param {object=} params
* @param {Array<string>} params.events
* List of events to unsubscribe from.
* @param {Array<string>=} params.contexts
* Optional list of top-level browsing context ids
* to unsubscribe the events from.
*
* @throws {InvalidArgumentError}
* If <var>events</var> or <var>contexts</var> are not valid types.
*/
async unsubscribe(params = {}) {
const { events, contexts: contextIds = null } = params;
// Check input types until we run schema validation.
this.#assertNonEmptyArrayWithStrings(events, "events");
if (contextIds !== null) {
this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
}
const listeners = this.#updateEventMap(events, contextIds, false);
// Unsubscribe from the relevant engine-internal events.
await this.messageHandler.eventsDispatcher.update(listeners);
}
#assertModuleSupportsEvent(moduleName, event) {
const rootModuleClass = this.#getRootModuleClass(moduleName);
if (!rootModuleClass?.supportsEvent(event)) {
throw new lazy.error.InvalidArgumentError(
`${event} is not a valid event name`
);
}
}
#assertNonEmptyArrayWithStrings(array, variableName) {
lazy.assert.array(
array,
`Expected "${variableName}" to be an array, ` + lazy.pprint`got ${array}`
);
lazy.assert.that(
array => !!array.length,
`Expected "${variableName}" array to have at least one item, ` +
lazy.pprint`got ${array}`
)(array);
array.forEach(item => {
lazy.assert.string(
item,
`Expected elements of "${variableName}" to be a string, ` +
lazy.pprint`got ${item}`
);
});
}
#getBrowserIdForContextId(contextId) {
const context = lazy.TabManager.getBrowsingContextById(contextId);
if (!context) {
throw new lazy.error.NoSuchFrameError(
`Browsing context with id ${contextId} not found`
);
}
return context.browserId;
}
#getRootModuleClass(moduleName) {
// Modules which support event subscriptions should have a root module
// defining supported events.
const rootDestination = { type: lazy.RootMessageHandler.type };
const moduleClasses = this.messageHandler.getAllModuleClasses(
moduleName,
rootDestination
);
if (!moduleClasses.length) {
throw new lazy.error.InvalidArgumentError(
`Module ${moduleName} does not exist`
);
}
return moduleClasses[0];
}
#getTopBrowsingContextId(contextId) {
const context = lazy.TabManager.getBrowsingContextById(contextId);
if (!context) {
throw new lazy.error.NoSuchFrameError(
`Browsing context with id ${contextId} not found`
);
}
const topContext = context.top;
return lazy.TabManager.getIdForBrowsingContext(topContext);
}
/**
* Obtain a set of events based on the given event name.
*
* Could contain a period for a specific event,
* or just the module name for all events.
*
* @param {string} event
* Name of the event to process.
*
* @returns {Set<string>}
* A Set with the expanded events in the form of `<module>.<event>`.
*
* @throws {InvalidArgumentError}
* If <var>event</var> does not reference a valid event.
*/
#obtainEvents(event) {
const events = new Set();
// Check if a period is present that splits the event name into the module,
// and the actual event. Hereby only care about the first found instance.
const index = event.indexOf(".");
if (index >= 0) {
const [moduleName] = event.split(".");
this.#assertModuleSupportsEvent(moduleName, event);
events.add(event);
} else {
// Interpret the name as module, and register all its available events
const rootModuleClass = this.#getRootModuleClass(event);
const supportedEvents = rootModuleClass?.supportedEvents;
for (const eventName of supportedEvents) {
events.add(eventName);
}
}
return events;
}
/**
* Obtain a list of event enabled browsing context ids.
*
*
* @param {string} eventName
* The name of the event.
*
* @returns {Set<string>} The set of browsing context.
*/
#obtainEventEnabledBrowsingContextIds(eventName) {
const contextIds = new Set();
for (const [
contextId,
events,
] of this.#browsingContextIdEventMap.entries()) {
if (events.has(eventName)) {
// Check that a browsing context still exists for a given id
const context = lazy.TabManager.getBrowsingContextById(contextId);
if (context) {
contextIds.add(contextId);
}
}
}
return contextIds;
}
#onMessageHandlerEvent = (name, event) => {
this.messageHandler.emitProtocolEvent(name, event);
};
/**
* Update global event state for top-level browsing contexts.
*
*
* @param {Array<string>} requestedEventNames
* The list of the event names to run the update for.
* @param {Array<string>|null} browsingContextIds
* The list of the browsing context ids to update or null.
* @param {boolean} enabled
* True, if events have to be enabled. Otherwise false.
*
* @returns {Array<Subscription>} subscriptions
* The list of information to subscribe/unsubscribe to.
*
* @throws {InvalidArgumentError}
* If failed unsubscribe from event from <var>requestedEventNames</var> for
* browsing context id from <var>browsingContextIds</var>, if present.
*/
#updateEventMap(requestedEventNames, browsingContextIds, enabled) {
const globalEventSet = new Set(this.#globalEventSet);
const eventMap = structuredClone(this.#browsingContextIdEventMap);
const eventNames = new Set();
requestedEventNames.forEach(name => {
this.#obtainEvents(name).forEach(event => eventNames.add(event));
});
const enabledEvents = new Map();
const subscriptions = [];
if (browsingContextIds === null) {
// Subscribe or unsubscribe events for all browsing contexts.
if (enabled) {
// Subscribe to each event.
// Get the list of all top level browsing context ids.
const allTopBrowsingContextIds = lazy.TabManager.allBrowserUniqueIds;
for (const eventName of eventNames) {
if (!globalEventSet.has(eventName)) {
const alreadyEnabledContextIds =
this.#obtainEventEnabledBrowsingContextIds(eventName);
globalEventSet.add(eventName);
for (const contextId of alreadyEnabledContextIds) {
eventMap.get(contextId).delete(eventName);
// Since we're going to subscribe to all top-level
// browsing context ids to not have duplicate subscriptions,
// we have to unsubscribe from already subscribed.
subscriptions.push({
event: eventName,
contextDescriptor: {
type: lazy.ContextDescriptorType.TopBrowsingContext,
id: this.#getBrowserIdForContextId(contextId),
},
callback: this.#onMessageHandlerEvent,
enable: false,
});
}
// Get a list of all top-level browsing context ids
// that are not contained in alreadyEnabledContextIds.
const newlyEnabledContextIds = allTopBrowsingContextIds.filter(
contextId => !alreadyEnabledContextIds.has(contextId)
);
enabledEvents.set(eventName, newlyEnabledContextIds);
subscriptions.push({
event: eventName,
contextDescriptor: {
type: lazy.ContextDescriptorType.All,
},
callback: this.#onMessageHandlerEvent,
enable: true,
});
}
}
} else {
// Unsubscribe each event which has a global subscription.
for (const eventName of eventNames) {
if (globalEventSet.has(eventName)) {
globalEventSet.delete(eventName);
subscriptions.push({
event: eventName,
contextDescriptor: {
type: lazy.ContextDescriptorType.All,
},
callback: this.#onMessageHandlerEvent,
enable: false,
});
} else {
throw new lazy.error.InvalidArgumentError(
`Failed to unsubscribe from event ${eventName}`
);
}
}
}
} else {
// Subscribe or unsubscribe events for given list of browsing context ids.
const targets = new Map();
for (const contextId of browsingContextIds) {
const topLevelContextId = this.#getTopBrowsingContextId(contextId);
if (!eventMap.has(topLevelContextId)) {
eventMap.set(topLevelContextId, new Set());
}
targets.set(topLevelContextId, eventMap.get(topLevelContextId));
}
for (const eventName of eventNames) {
// Do nothing if we want to subscribe,
// but the event has already a global subscription.
if (enabled && this.#globalEventSet.has(eventName)) {
continue;
}
for (const [contextId, target] of targets.entries()) {
// Subscribe if an event doesn't have a subscription for a specific context id.
if (enabled && !target.has(eventName)) {
target.add(eventName);
if (!enabledEvents.has(eventName)) {
enabledEvents.set(eventName, new Set());
}
enabledEvents.get(eventName).add(contextId);
subscriptions.push({
event: eventName,
contextDescriptor: {
type: lazy.ContextDescriptorType.TopBrowsingContext,
id: this.#getBrowserIdForContextId(contextId),
},
callback: this.#onMessageHandlerEvent,
enable: true,
});
} else if (!enabled) {
// Unsubscribe from each event for a specific context id if the event has a subscription.
if (target.has(eventName)) {
target.delete(eventName);
subscriptions.push({
event: eventName,
contextDescriptor: {
type: lazy.ContextDescriptorType.TopBrowsingContext,
id: this.#getBrowserIdForContextId(contextId),
},
callback: this.#onMessageHandlerEvent,
enable: false,
});
} else {
throw new lazy.error.InvalidArgumentError(
`Failed to unsubscribe from event ${eventName} for context ${contextId}`
);
}
}
}
}
}
this.#globalEventSet = globalEventSet;
this.#browsingContextIdEventMap = eventMap;
return subscriptions;
}
}
// To export the class as lower-case
export const session = SessionModule;