/* 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",
generateUUID: "chrome://remote/content/shared/UUID.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",
UserContextManager:
"chrome://remote/content/shared/UserContextManager.sys.mjs",
});
class SessionModule extends RootBiDiModule {
#knownSubscriptionIds;
#subscriptions;
/**
* An object that holds information about the subscription,
* if the topLevelTraversableIds and the userContextIds
* are both empty, the subscription is considered global.
*
* @typedef Subscription
*
* @property {Set} eventNames
* A set of event names related to this subscription.
* @property {string} subscriptionId
* A unique subscription identifier.
* @property {Set} topLevelTraversableIds
* A set of top level traversable ids related to this subscription.
* @property {Set} userContextIds
* A set of user context ids related to this subscription.
*/
constructor(messageHandler) {
super(messageHandler);
// Set of subscription ids.
this.#knownSubscriptionIds = new Set();
// List of subscription objects type Subscription.
this.#subscriptions = [];
}
destroy() {
this.#knownSubscriptionIds = null;
this.#subscriptions = 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.'
);
}
}
/**
* An object that holds a unique subscription identifier.
*
* @typedef SubscribeResult
*
* @property {string} subscription
* A unique subscription identifier.
*/
/**
* Enable certain events either globally, or for a list of browsing contexts.
*
* @param {object=} params
* @param {Array} params.events
* List of events to subscribe to.
* @param {Array=} params.contexts
* Optional list of top-level browsing context ids
* to subscribe the events for.
* @param {Array=} params.userContexts
* Optional list of user context ids
* to subscribe the events for.
*
* @returns {SubscribeResult}
* A unique subscription identifier.
* @throws {InvalidArgumentError}
* If events or contexts are not valid types.
*/
async subscribe(params = {}) {
const { events, contexts: contextIds = null, userContexts = null } = params;
// Check input types until we run schema validation.
this.#assertNonEmptyArrayWithStrings(events, "events");
if (contextIds !== null) {
this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
}
if (userContexts !== null) {
this.#assertNonEmptyArrayWithStrings(userContexts, "userContexts");
}
const eventNames = new Set();
events.forEach(name => {
this.#obtainEvents(name).forEach(event => eventNames.add(event));
});
const inputUserContextIds = new Set(userContexts);
const inputContextIds = new Set(contextIds);
if (inputUserContextIds.size > 0 && inputContextIds.size > 0) {
throw new lazy.error.InvalidArgumentError(
`Providing both "userContexts" and "contexts" arguments is not supported`
);
}
let subscriptionNavigables = new Set();
const topLevelTraversableContextIds = new Set();
const userContextIds = new Set();
if (inputContextIds.size !== 0) {
const navigables = this.#getValidNavigablesByIds(inputContextIds);
subscriptionNavigables = this.#getTopLevelTraversables(navigables);
for (const navigable of subscriptionNavigables) {
topLevelTraversableContextIds.add(
lazy.TabManager.getIdForBrowsingContext(navigable)
);
}
} else if (inputUserContextIds.size !== 0) {
for (const userContextId of inputUserContextIds) {
const internalId =
lazy.UserContextManager.getInternalIdById(userContextId);
if (internalId === null) {
throw new lazy.error.NoSuchUserContextError(
`User context with id: ${userContextId} doesn't exist`
);
}
lazy.UserContextManager.getTabsForUserContext(internalId).forEach(
item => subscriptionNavigables.add(item)
);
userContextIds.add(internalId);
}
} else {
for (const tab of lazy.TabManager.tabs) {
subscriptionNavigables.add(tab);
}
}
const subscription = {
eventNames,
subscriptionId: lazy.generateUUID(),
topLevelTraversableIds: topLevelTraversableContextIds,
userContextIds,
};
const subscribeStepEvents = new Map();
for (const eventName of eventNames) {
const existingNavigables =
this.#getEnabledTopLevelTraversables(eventName);
subscribeStepEvents.set(
eventName,
subscriptionNavigables.difference(existingNavigables)
);
}
this.#subscriptions.push(subscription);
this.#knownSubscriptionIds.add(subscription.subscriptionId);
// TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8).
const includeGlobal = this.#isSubscriptionGlobal(subscription);
const listeners = this.#getListenersToSubscribe(
eventNames,
includeGlobal,
subscribeStepEvents,
userContextIds
);
// Subscribe to the relevant engine-internal events.
await this.messageHandler.eventsDispatcher.update(listeners);
return { subscription: subscription.subscriptionId };
}
/**
* Disable certain events either globally, for a list of browsing contexts
* or for a list of subscription ids.
*
* @param {object=} params
* @param {Array=} params.events
* List of events to unsubscribe from.
* @param {Array=} params.contexts
* Optional list of top-level browsing context ids
* to unsubscribe the events from.
* @param {Array=} params.subscriptions
* List of subscription identifiers to unsubscribe from.
*
* @throws {InvalidArgumentError}
* If events or contexts are not valid types.
*/
async unsubscribe(params = {}) {
const { events = null, contexts = null, subscriptions = null } = params;
const listeners =
subscriptions === null
? this.#unsubscribeByAttributes(events, contexts)
: this.#unsubscribeById(subscriptions);
// 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.isNonEmptyArray(
array,
`Expected "${variableName}" to be a non-empty array, ` +
lazy.pprint`got ${array}`
);
array.forEach(item => {
lazy.assert.string(
item,
`Expected elements of "${variableName}" to be a string, ` +
lazy.pprint`got ${item}`
);
});
}
#createListener(
enable,
{ eventName, traversableId = null, userContextId = null }
) {
let contextDescriptor;
if (traversableId === null && userContextId === null) {
contextDescriptor = {
type: lazy.ContextDescriptorType.All,
};
} else if (userContextId !== null) {
contextDescriptor = {
type: lazy.ContextDescriptorType.UserContext,
id: userContextId,
};
} else {
const traversable = lazy.TabManager.getBrowsingContextById(traversableId);
if (traversable === null) {
return null;
}
contextDescriptor = {
type: lazy.ContextDescriptorType.TopBrowsingContext,
id: traversable.browserId,
};
}
return {
event: eventName,
contextDescriptor,
callback: this.#onMessageHandlerEvent,
enable,
};
}
#createListenerToSubscribe(params) {
return this.#createListener(true, params);
}
#createListenerToUnsubscribe(params) {
return this.#createListener(false, params);
}
/**
* Get a set of top-level traversables for which an event is enabled.
*
* @see https://w3c.github.io/webdriver-bidi/#set-of-top-level-traversables-for-which-an-event-is-enabled
*
* @param {string} eventName
* The name of the event.
*
* @returns {Array}
* The list of top-level traversables.
*/
#getEnabledTopLevelTraversables(eventName) {
let result = new Set();
for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
const { topLevelTraversableIds } = subscription;
if (this.#isSubscriptionGlobal(subscription)) {
for (const traversable of lazy.TabManager.tabs) {
result.add(traversable);
}
break;
}
result = this.#getNavigablesByIds(topLevelTraversableIds);
}
return result;
}
#getListenersToSubscribe(
eventNames,
includeGlobal,
subscribeStepEvents,
userContextIds
) {
const listeners = [];
for (const eventName of eventNames) {
if (includeGlobal) {
// Since we're going to subscribe to all top-level
// traversable ids to not have duplicate subscriptions,
// we have to unsubscribe from already subscribed.
const alreadyEnabledTraversableIds =
this.#obtainEventEnabledTraversableIds(eventName);
for (const traversableId of alreadyEnabledTraversableIds) {
listeners.push(
this.#createListenerToUnsubscribe({
eventName,
traversableId,
})
);
}
// Also unsubscribe from already subscribed user contexts.
const alreadyEnabledUserContextIds =
this.#obtainEventEnabledUserContextIds(eventName);
for (const userContextId of alreadyEnabledUserContextIds) {
listeners.push(
this.#createListenerToUnsubscribe({
eventName,
userContextId,
})
);
}
listeners.push(this.#createListenerToSubscribe({ eventName }));
} else if (userContextIds.size !== 0) {
for (const userContextId of userContextIds) {
// Do nothing if the event has already a global subscription.
if (this.#hasGlobalEventSubscription(eventName)) {
continue;
}
// Since we're going to subscribe to all top-level
// traversable ids which belongs to the certain user context
// to not have duplicate subscriptions,
// we have to unsubscribe from already subscribed.
const alreadyEnabledTraversableIds =
this.#obtainEventEnabledTraversableIds(eventName, userContextId);
for (const traversableId of alreadyEnabledTraversableIds) {
listeners.push(
this.#createListenerToUnsubscribe({
eventName,
traversableId,
})
);
}
listeners.push(
this.#createListenerToSubscribe({ eventName, userContextId })
);
}
} else {
for (const navigable of subscribeStepEvents.get(eventName)) {
// Do nothing if the event has already a global subscription
// or subscription to the associated user context.
if (
this.#hasGlobalEventSubscription(eventName) ||
this.#hasSubscriptionByAssociatedUserContext(eventName, navigable)
) {
continue;
}
const traversableId =
lazy.TabManager.getIdForBrowsingContext(navigable);
listeners.push(
this.#createListenerToSubscribe({
eventName,
traversableId,
})
);
}
}
}
return listeners;
}
#getListenersToUnsubscribe(subscription) {
const { eventNames, topLevelTraversableIds, userContextIds } = subscription;
const listeners = [];
for (const eventName of eventNames) {
// Do nothing if there is a global subscription.
if (this.#hasGlobalEventSubscription(eventName)) {
continue;
}
if (this.#isSubscriptionGlobal(subscription)) {
listeners.push(
...this.#getListenersToUnsubscribeFromGlobalSubscription(eventName)
);
} else if (userContextIds.size !== 0) {
for (const userContextId of userContextIds) {
listeners.push(
...this.#getListenersToUnsubscribeFromUserContext(
eventName,
userContextId
)
);
}
} else {
for (const traversableId of topLevelTraversableIds) {
listeners.push(
this.#getListenersToUnsubscribeFromTraversable(
eventName,
traversableId
)
);
}
}
}
return listeners;
}
#getListenersToUnsubscribeFromGlobalSubscription(eventName) {
// Unsubscribe from the global subscription.
const listeners = [this.#createListenerToUnsubscribe({ eventName })];
// Subscribe again to user contexts which have a subscription and
// to traversables which have individual subscriptions,
// but are not associated with subscribed user contexts.
for (const item of this.#getSubscriptionsForEvent(eventName)) {
for (const userContextId of item.userContextIds) {
listeners.push(
this.#createListenerToSubscribe({
eventName,
userContextId,
})
);
}
for (const traversableId of item.topLevelTraversableIds) {
const traversable =
lazy.TabManager.getBrowsingContextById(traversableId);
// Do nothing if traversable doesn't exist anymore or
// there is already a subscription to the associated user context.
if (
traversable === null ||
this.#hasSubscriptionByAssociatedUserContext(eventName, traversable)
) {
continue;
}
listeners.push(
this.#createListenerToSubscribe({
eventName,
traversableId,
})
);
}
}
return listeners;
}
#getListenersToUnsubscribeFromTraversable(eventName, traversableId) {
// Do nothing if traversable is already closed or still has another subscription.
const traversable = lazy.TabManager.getBrowsingContextById(traversableId);
if (
traversable === null ||
this.#hasSubscriptionByAssociatedUserContext(eventName, traversable) ||
this.#hasSubscriptionByTraversableId(eventName, traversableId)
) {
return null;
}
return this.#createListenerToUnsubscribe({
eventName,
traversableId,
});
}
#getListenersToUnsubscribeFromUserContext(eventName, userContextId) {
// Do nothing if there is another subscription for this user context.
if (this.#hasSubscriptionByUserContextId(eventName, userContextId)) {
return [];
}
// Unsubscribe from the user context.
const listeners = [
this.#createListenerToUnsubscribe({ eventName, userContextId }),
];
// Resubscribe to traversables which are associated with this user context and
// have individual subscriptions.
const alreadyEnabledTraversableIds = this.#obtainEventEnabledTraversableIds(
eventName,
userContextId
);
for (const traversableId of alreadyEnabledTraversableIds) {
listeners.push(
this.#createListenerToSubscribe({
eventName,
traversableId,
})
);
}
return listeners;
}
/**
* Retrieves a navigable based on its id.
*
* @see https://w3c.github.io/webdriver-bidi/#get-a-navigable
*
* @param {number} navigableId
* Id of the navigable.
*
* @returns {BrowsingContext=}
* The navigable or null if navigableId is null.
* @throws {NoSuchFrameError}
* If the navigable cannot be found.
*/
#getNavigable(navigableId) {
if (navigableId === null) {
return null;
}
const navigable = lazy.TabManager.getBrowsingContextById(navigableId);
if (!navigable) {
throw new lazy.error.NoSuchFrameError(
`Browsing context with id ${navigableId} not found`
);
}
return navigable;
}
/**
* Get a list of navigables by provided ids.
*
* @see https://w3c.github.io/webdriver-bidi/#get-navigables-by-ids
*
* @param {Set} navigableIds
* The set of the navigable ids.
*
* @returns {Set}
* The set of navigables.
*/
#getNavigablesByIds(navigableIds) {
const result = new Set();
for (const navigableId of navigableIds) {
const navigable = lazy.TabManager.getBrowsingContextById(navigableId);
if (navigable !== null) {
result.add(navigable);
}
}
return result;
}
#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];
}
#getSubscriptionsForEvent(eventName) {
return this.#subscriptions.filter(({ eventNames }) =>
eventNames.has(eventName)
);
}
#getTopLevelTraversableContextIds(contextIds) {
const topLevelTraversableContextIds = new Set();
const inputContextIds = new Set(contextIds);
if (inputContextIds.size !== 0) {
const navigables = this.#getValidNavigablesByIds(inputContextIds);
const topLevelTraversable = this.#getTopLevelTraversables(navigables);
for (const navigable of topLevelTraversable) {
topLevelTraversableContextIds.add(
lazy.TabManager.getIdForBrowsingContext(navigable)
);
}
}
return topLevelTraversableContextIds;
}
/**
* Get a list of top-level traversables for provided navigables.
*
* @see https://w3c.github.io/webdriver-bidi/#get-top-level-traversables
*
* @param {Array} navigables
* The list of the navigables.
*
* @returns {Set}
* The set of top-level traversables.
*/
#getTopLevelTraversables(navigables) {
const result = new Set();
for (const { top } of navigables) {
result.add(top);
}
return result;
}
/**
* Get a list of valid navigables by provided ids.
*
* @see https://w3c.github.io/webdriver-bidi/#get-valid-navigables-by-ids
*
* @param {Set} navigableIds
* The set of the navigable ids.
*
* @returns {Set}
* The set of navigables.
* @throws {NoSuchFrameError}
* If the navigable cannot be found.
*/
#getValidNavigablesByIds(navigableIds) {
const result = new Set();
for (const navigableId of navigableIds) {
result.add(this.#getNavigable(navigableId));
}
return result;
}
#hasGlobalEventSubscription(eventName) {
for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
if (this.#isSubscriptionGlobal(subscription)) {
return true;
}
}
return false;
}
// Check if for a given event name and traversable there is
// a subscription for a user context associated with this traversable.
#hasSubscriptionByAssociatedUserContext(eventName, traversable) {
if (traversable === null) {
return false;
}
return this.#hasSubscriptionByUserContextId(
eventName,
traversable.originAttributes.userContextId
);
}
#hasSubscriptionByTraversableId(eventName, traversableId) {
for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
const { topLevelTraversableIds } = subscription;
for (const topLevelTraversableId of topLevelTraversableIds) {
if (topLevelTraversableId === traversableId) {
return true;
}
}
}
return false;
}
#hasSubscriptionByUserContextId(eventName, userContextId) {
for (const subscription of this.#getSubscriptionsForEvent(eventName)) {
const { userContextIds } = subscription;
if (userContextIds.has(userContextId)) {
return true;
}
}
return false;
}
/**
* Identify if a provided subscription is global.
*
* @see https://w3c.github.io/webdriver-bidi/#subscription-global
*
* @param {Subscription} subscription
* A subscription object.
*
* @returns {boolean}
* Return true if the subscription is global, false otherwise.
*/
#isSubscriptionGlobal(subscription) {
return (
subscription.topLevelTraversableIds.size === 0 &&
subscription.userContextIds.size === 0
);
}
/**
* Obtain a list of event enabled traversable ids.
*
* @param {string} eventName
* The name of the event.
* @param {string=} userContextId
* The user context id.
*
* @returns {Set}
* The set of traversable ids.
*/
#obtainEventEnabledTraversableIds(eventName, userContextId = null) {
let traversableIds = new Set();
for (const { topLevelTraversableIds } of this.#getSubscriptionsForEvent(
eventName
)) {
if (topLevelTraversableIds.size === 0) {
continue;
}
if (userContextId === null) {
traversableIds = traversableIds.union(topLevelTraversableIds);
continue;
}
for (const traversableId of topLevelTraversableIds) {
const traversable =
lazy.TabManager.getBrowsingContextById(traversableId);
if (traversable === null) {
continue;
}
if (traversable.originAttributes.userContextId === userContextId) {
traversableIds.add(traversableId);
}
}
}
return traversableIds;
}
#obtainEventEnabledUserContextIds(eventName) {
let enabledUserContextIds = new Set();
for (const { userContextIds } of this.#getSubscriptionsForEvent(
eventName
)) {
enabledUserContextIds = enabledUserContextIds.union(userContextIds);
}
return enabledUserContextIds;
}
/**
* 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}
* A Set with the expanded events in the form of `.`.
*
* @throws {InvalidArgumentError}
* If event 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;
}
#onMessageHandlerEvent = (name, event) => {
this.messageHandler.emitProtocolEvent(name, event);
};
#unsubscribeByAttributes(events, contextIds) {
const listeners = [];
// Check input types until we run schema validation.
this.#assertNonEmptyArrayWithStrings(events, "events");
if (contextIds !== null) {
this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
}
const eventNames = new Set();
events.forEach(name => {
this.#obtainEvents(name).forEach(event => eventNames.add(event));
});
const topLevelTraversableContextIds =
this.#getTopLevelTraversableContextIds(contextIds);
const newSubscriptions = [];
const matchedEvents = new Set();
const matchedContexts = new Set();
for (const subscription of this.#subscriptions) {
// Keep subscription if it doesn't contain any target events.
if (subscription.eventNames.intersection(eventNames).size === 0) {
newSubscriptions.push(subscription);
continue;
}
// Unsubscribe globally.
if (topLevelTraversableContextIds.size === 0) {
// Keep subscription if verified subscription is not global.
if (!this.#isSubscriptionGlobal(subscription)) {
newSubscriptions.push(subscription);
continue;
}
// Delete event names from the subscription.
const subscriptionEventNames = new Set(subscription.eventNames);
for (const eventName of eventNames) {
if (subscriptionEventNames.has(eventName)) {
matchedEvents.add(eventName);
subscriptionEventNames.delete(eventName);
listeners.push(this.#createListenerToUnsubscribe({ eventName }));
}
}
// If the subscription still contains some event,
// save a new partial subscription.
if (subscriptionEventNames.size !== 0) {
const clonedSubscription = {
subscriptionId: subscription.subscriptionId,
eventNames: new Set(subscriptionEventNames),
topLevelTraversableIds: new Set(),
userContextIds: new Set(subscription.userContextIds),
};
newSubscriptions.push(clonedSubscription);
}
}
// Keep the subscription if it's global but we want to unsubscribe only from some contexts.
else if (this.#isSubscriptionGlobal(subscription)) {
newSubscriptions.push(subscription);
} else {
// Map with an event name as a key and the set of subscribed traversable ids as a value.
const eventMap = new Map();
// Populate the map.
for (const eventName of subscription.eventNames) {
eventMap.set(eventName, new Set(subscription.topLevelTraversableIds));
}
for (const eventName of eventNames) {
// Skip if there is no subscription related to this event.
if (!eventMap.has(eventName)) {
continue;
}
for (const topLevelTraversableId of topLevelTraversableContextIds) {
// Skip if there is no subscription related to this event and this traversable id.
if (!eventMap.get(eventName).has(topLevelTraversableId)) {
continue;
}
matchedContexts.add(topLevelTraversableId);
matchedEvents.add(eventName);
eventMap.get(eventName).delete(topLevelTraversableId);
listeners.push(
this.#createListenerToUnsubscribe({
eventName,
traversableId: topLevelTraversableId,
})
);
}
if (eventMap.get(eventName).size === 0) {
eventMap.delete(eventName);
}
}
// Build new partial subscriptions based on the remaining data in eventMap.
for (const [
eventName,
remainingTopLevelTraversableIds,
] of eventMap.entries()) {
const partialSubscription = {
subscriptionId: subscription.subscriptionId,
eventNames: new Set([eventName]),
topLevelTraversableIds: remainingTopLevelTraversableIds,
userContextIds: new Set(subscription.userContextIds),
};
newSubscriptions.push(partialSubscription);
const traversableIdsToUnsubscribe =
subscription.topLevelTraversableIds.difference(
remainingTopLevelTraversableIds
);
for (const traversableId of traversableIdsToUnsubscribe) {
listeners.push(
this.#createListenerToUnsubscribe({ eventName, traversableId })
);
}
}
}
}
if (matchedEvents.symmetricDifference(eventNames).size > 0) {
throw new lazy.error.InvalidArgumentError(
`Failed to unsubscribe from events: ${Array.from(eventNames).join(", ")}`
);
}
if (
topLevelTraversableContextIds.size > 0 &&
matchedContexts.symmetricDifference(topLevelTraversableContextIds).size >
0
) {
throw new lazy.error.InvalidArgumentError(
`Failed to unsubscribe from events: ${Array.from(eventNames).join(", ")} for context ids: ${Array.from(topLevelTraversableContextIds).join(", ")}`
);
}
this.#subscriptions = newSubscriptions;
return listeners;
}
#unsubscribeById(subscriptionIds) {
this.#assertNonEmptyArrayWithStrings(subscriptionIds, "subscriptions");
const subscriptions = new Set(subscriptionIds);
const unknownSubscriptionIds = subscriptions.difference(
this.#knownSubscriptionIds
);
if (unknownSubscriptionIds.size !== 0) {
throw new lazy.error.InvalidArgumentError(
`Failed to unsubscribe from subscriptions with ids: ${Array.from(subscriptionIds).join(", ")} ` +
`(unknown ids: ${Array.from(unknownSubscriptionIds).join(", ")})`
);
}
const listeners = [];
const subscriptionIdsToRemove = new Set();
const subscriptionsToRemove = new Set();
for (const subscription of this.#subscriptions) {
const { subscriptionId } = subscription;
if (!subscriptions.has(subscriptionId)) {
continue;
}
subscriptionIdsToRemove.add(subscriptionId);
subscriptionsToRemove.add(subscription);
}
this.#knownSubscriptionIds =
this.#knownSubscriptionIds.difference(subscriptions);
this.#subscriptions = this.#subscriptions.filter(
({ subscriptionId }) => !subscriptionIdsToRemove.has(subscriptionId)
);
for (const subscription of subscriptionsToRemove) {
listeners.push(...this.#getListenersToUnsubscribe(subscription));
}
return listeners;
}
}
// To export the class as lower-case
export const session = SessionModule;