Files
infocom-systems-design/node_modules/chromium-bidi/lib/cjs/bidiMapper/modules/session/SubscriptionManager.js
2025-10-03 22:27:28 +03:00

280 lines
13 KiB
JavaScript

"use strict";
/**
* Copyright 2022 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubscriptionManager = void 0;
exports.cartesianProduct = cartesianProduct;
exports.unrollEvents = unrollEvents;
const protocol_js_1 = require("../../../protocol/protocol.js");
const events_js_1 = require("./events.js");
/**
* Returns the cartesian product of the given arrays.
*
* Example:
* cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
*/
function cartesianProduct(...a) {
return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
}
/** Expands "AllEvents" events into atomic events. */
function unrollEvents(events) {
const allEvents = new Set();
function addEvents(events) {
for (const event of events) {
allEvents.add(event);
}
}
for (const event of events) {
switch (event) {
case protocol_js_1.ChromiumBidi.BiDiModule.Bluetooth:
addEvents(Object.values(protocol_js_1.ChromiumBidi.Bluetooth.EventNames));
break;
case protocol_js_1.ChromiumBidi.BiDiModule.BrowsingContext:
addEvents(Object.values(protocol_js_1.ChromiumBidi.BrowsingContext.EventNames));
break;
case protocol_js_1.ChromiumBidi.BiDiModule.Log:
addEvents(Object.values(protocol_js_1.ChromiumBidi.Log.EventNames));
break;
case protocol_js_1.ChromiumBidi.BiDiModule.Network:
addEvents(Object.values(protocol_js_1.ChromiumBidi.Network.EventNames));
break;
case protocol_js_1.ChromiumBidi.BiDiModule.Script:
addEvents(Object.values(protocol_js_1.ChromiumBidi.Script.EventNames));
break;
default:
allEvents.add(event);
}
}
return [...allEvents.values()];
}
class SubscriptionManager {
#subscriptionPriority = 0;
// BrowsingContext `null` means the event has subscription across all the
// browsing contexts.
// Channel `null` means no `channel` should be added.
#channelToContextToEventMap = new Map();
#browsingContextStorage;
constructor(browsingContextStorage) {
this.#browsingContextStorage = browsingContextStorage;
}
getChannelsSubscribedToEvent(eventMethod, contextId) {
const prioritiesAndChannels = Array.from(this.#channelToContextToEventMap.keys())
.map((channel) => ({
priority: this.#getEventSubscriptionPriorityForChannel(eventMethod, contextId, channel),
channel,
}))
.filter(({ priority }) => priority !== null);
// Sort channels by priority.
return prioritiesAndChannels
.sort((a, b) => a.priority - b.priority)
.map(({ channel }) => channel);
}
#getEventSubscriptionPriorityForChannel(eventMethod, contextId, channel) {
const contextToEventMap = this.#channelToContextToEventMap.get(channel);
if (contextToEventMap === undefined) {
return null;
}
const maybeTopLevelContextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
// `null` covers global subscription.
const relevantContexts = [...new Set([null, maybeTopLevelContextId])];
// Get all the subscription priorities.
const priorities = relevantContexts
.map((context) => {
// Get the priority for exact event name
const priority = contextToEventMap.get(context)?.get(eventMethod);
// For CDP we can't provide specific event name when subscribing
// to the module directly.
// Because of that we need to see event `cdp` exists in the map.
if ((0, events_js_1.isCdpEvent)(eventMethod)) {
const cdpPriority = contextToEventMap
.get(context)
?.get(protocol_js_1.ChromiumBidi.BiDiModule.Cdp);
// If we subscribe to the event directly and `cdp` module as well
// priority will be different we take minimal priority
return priority && cdpPriority
? Math.min(priority, cdpPriority)
: // At this point we know that we have subscribed
// to only one of the two
(priority ?? cdpPriority);
}
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2844.
if ((0, events_js_1.isDeprecatedCdpEvent)(eventMethod)) {
const cdpPriority = contextToEventMap
.get(context)
?.get(protocol_js_1.ChromiumBidi.BiDiModule.DeprecatedCdp);
// If we subscribe to the event directly and `cdp` module as well
// priority will be different we take minimal priority
return priority && cdpPriority
? Math.min(priority, cdpPriority)
: // At this point we know that we have subscribed
// to only one of the two
(priority ?? cdpPriority);
}
return priority;
})
.filter((p) => p !== undefined);
if (priorities.length === 0) {
// Not subscribed, return null.
return null;
}
// Return minimal priority.
return Math.min(...priorities);
}
/**
* @param module BiDi+ module
* @param contextId `null` == globally subscribed
*
* @returns
*/
isSubscribedTo(moduleOrEvent, contextId = null) {
const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(contextId);
for (const browserContextToEventMap of this.#channelToContextToEventMap.values()) {
for (const [id, eventMap] of browserContextToEventMap.entries()) {
// Not subscribed to this context or globally
if (topLevelContext !== id && id !== null) {
continue;
}
for (const event of eventMap.keys()) {
// This also covers the `cdp` case where
// we don't unroll the event names
if (
// Event explicitly subscribed
event === moduleOrEvent ||
// Event subscribed via module
event === moduleOrEvent.split('.').at(0) ||
// Event explicitly subscribed compared to module
event.split('.').at(0) === moduleOrEvent) {
return true;
}
}
}
}
return false;
}
/**
* Subscribes to event in the given context and channel.
* @param {EventNames} event
* @param {BrowsingContext.BrowsingContext | null} contextId
* @param {BidiPlusChannel} channel
* @return {SubscriptionItem[]} List of
* subscriptions. If the event is a whole module, it will return all the specific
* events. If the contextId is null, it will return all the top-level contexts which were
* not subscribed before the command.
*/
subscribe(event, contextId, channel) {
// All the subscriptions are handled on the top-level contexts.
contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
// Check if subscribed event is a whole module
switch (event) {
case protocol_js_1.ChromiumBidi.BiDiModule.BrowsingContext:
return Object.values(protocol_js_1.ChromiumBidi.BrowsingContext.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case protocol_js_1.ChromiumBidi.BiDiModule.Log:
return Object.values(protocol_js_1.ChromiumBidi.Log.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case protocol_js_1.ChromiumBidi.BiDiModule.Network:
return Object.values(protocol_js_1.ChromiumBidi.Network.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case protocol_js_1.ChromiumBidi.BiDiModule.Script:
return Object.values(protocol_js_1.ChromiumBidi.Script.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case protocol_js_1.ChromiumBidi.BiDiModule.Bluetooth:
return Object.values(protocol_js_1.ChromiumBidi.Bluetooth.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
default:
// Intentionally left empty.
}
if (!this.#channelToContextToEventMap.has(channel)) {
this.#channelToContextToEventMap.set(channel, new Map());
}
const contextToEventMap = this.#channelToContextToEventMap.get(channel);
if (!contextToEventMap.has(contextId)) {
contextToEventMap.set(contextId, new Map());
}
const eventMap = contextToEventMap.get(contextId);
const affectedContextIds = (contextId === null
? this.#browsingContextStorage.getTopLevelContexts().map((c) => c.id)
: [contextId])
// There can be contexts that are already subscribed to the event. Do not include
// them to the output.
.filter((contextId) => !this.isSubscribedTo(event, contextId));
if (!eventMap.has(event)) {
// Add subscription only if it's not already subscribed.
eventMap.set(event, this.#subscriptionPriority++);
}
return affectedContextIds.map((contextId) => ({
event,
contextId,
}));
}
/**
* Unsubscribes atomically from all events in the given contexts and channel.
*/
unsubscribeAll(events, contextIds, channel) {
// Assert all contexts are known.
for (const contextId of contextIds) {
if (contextId !== null) {
this.#browsingContextStorage.getContext(contextId);
}
}
const eventContextPairs = cartesianProduct(unrollEvents(events), contextIds);
// Assert all unsubscriptions are valid.
// If any of the unsubscriptions are invalid, do not unsubscribe from anything.
eventContextPairs
.map(([event, contextId]) => this.#checkUnsubscribe(event, contextId, channel))
.forEach((unsubscribe) => unsubscribe());
}
/**
* Unsubscribes from the event in the given context and channel.
* Syntactic sugar for "unsubscribeAll".
*/
unsubscribe(eventName, contextId, channel) {
this.unsubscribeAll([eventName], [contextId], channel);
}
#checkUnsubscribe(event, contextId, channel) {
// All the subscriptions are handled on the top-level contexts.
contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
if (!this.#channelToContextToEventMap.has(channel)) {
throw new protocol_js_1.InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
}
const contextToEventMap = this.#channelToContextToEventMap.get(channel);
if (!contextToEventMap.has(contextId)) {
throw new protocol_js_1.InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
}
const eventMap = contextToEventMap.get(contextId);
if (!eventMap.has(event)) {
throw new protocol_js_1.InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
}
return () => {
eventMap.delete(event);
// Clean up maps if empty.
if (eventMap.size === 0) {
contextToEventMap.delete(event);
}
if (contextToEventMap.size === 0) {
this.#channelToContextToEventMap.delete(channel);
}
};
}
}
exports.SubscriptionManager = SubscriptionManager;
//# sourceMappingURL=SubscriptionManager.js.map