"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.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` exits 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); } 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(); 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