280 lines
13 KiB
JavaScript
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
|