"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 = exports.unrollEvents = exports.cartesianProduct = void 0; 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()))); } exports.cartesianProduct = cartesianProduct; /** 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()]; } exports.unrollEvents = unrollEvents; 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); } 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: Object.values(protocol_js_1.ChromiumBidi.BrowsingContext.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel)); return; case protocol_js_1.ChromiumBidi.BiDiModule.Log: Object.values(protocol_js_1.ChromiumBidi.Log.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel)); return; case protocol_js_1.ChromiumBidi.BiDiModule.Network: Object.values(protocol_js_1.ChromiumBidi.Network.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel)); return; case protocol_js_1.ChromiumBidi.BiDiModule.Script: Object.values(protocol_js_1.ChromiumBidi.Script.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel)); return; 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); // Do not re-subscribe to events to keep the priority. if (eventMap.has(event)) { return; } eventMap.set(event, this.#subscriptionPriority++); } /** * 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