blob: f0e1f135cf048a3dea061b4952a58d6398381be1 [file] [log] [blame]
"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;
exports.difference = difference;
const protocol_js_1 = require("../../../protocol/protocol.js");
const uuid_js_1 = require("../../../utils/uuid.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.Input:
addEvents(Object.values(protocol_js_1.ChromiumBidi.Input.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;
case protocol_js_1.ChromiumBidi.BiDiModule.Speculation:
addEvents(Object.values(protocol_js_1.ChromiumBidi.Speculation.EventNames));
break;
default:
allEvents.add(event);
}
}
return allEvents.values();
}
class SubscriptionManager {
#subscriptions = [];
#knownSubscriptionIds = new Set();
#browsingContextStorage;
constructor(browsingContextStorage) {
this.#browsingContextStorage = browsingContextStorage;
}
getGoogChannelsSubscribedToEvent(eventName, contextId) {
const googChannels = new Set();
for (const subscription of this.#subscriptions) {
if (this.#isSubscribedTo(subscription, eventName, contextId)) {
googChannels.add(subscription.googChannel);
}
}
return Array.from(googChannels);
}
getGoogChannelsSubscribedToEventGlobally(eventName) {
const googChannels = new Set();
for (const subscription of this.#subscriptions) {
if (this.#isSubscribedTo(subscription, eventName)) {
googChannels.add(subscription.googChannel);
}
}
return Array.from(googChannels);
}
#isSubscribedTo(subscription, moduleOrEvent, browsingContextId) {
let includesEvent = false;
for (const eventName of subscription.eventNames) {
// This also covers the `goog:cdp` case where
// we don't unroll the event names
if (
// Event explicitly subscribed
eventName === moduleOrEvent ||
// Event subscribed via module
eventName === moduleOrEvent.split('.').at(0) ||
// Event explicitly subscribed compared to module
eventName.split('.').at(0) === moduleOrEvent) {
includesEvent = true;
break;
}
}
if (!includesEvent) {
return false;
}
// user context subscription.
if (subscription.userContextIds.size !== 0) {
if (!browsingContextId) {
return false;
}
const context = this.#browsingContextStorage.findContext(browsingContextId);
if (!context) {
return false;
}
return subscription.userContextIds.has(context.userContext);
}
// context subscription.
if (subscription.topLevelTraversableIds.size !== 0) {
if (!browsingContextId) {
return false;
}
const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(browsingContextId);
return (topLevelContext !== null &&
subscription.topLevelTraversableIds.has(topLevelContext));
}
// global subscription.
return true;
}
isSubscribedTo(moduleOrEvent, contextId) {
for (const subscription of this.#subscriptions) {
if (this.#isSubscribedTo(subscription, moduleOrEvent, contextId)) {
return true;
}
}
return false;
}
/**
* Subscribes to event in the given context and goog: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(eventNames, contextIds, userContextIds, googChannel) {
// All the subscriptions are handled on the top-level contexts.
const subscription = {
id: (0, uuid_js_1.uuidv4)(),
eventNames: new Set(unrollEvents(eventNames)),
topLevelTraversableIds: new Set(contextIds.map((contextId) => {
const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(contextId);
if (!topLevelContext) {
throw new protocol_js_1.NoSuchFrameException(`Top-level navigable not found for context id ${contextId}`);
}
return topLevelContext;
})),
userContextIds: new Set(userContextIds),
googChannel,
};
this.#subscriptions.push(subscription);
this.#knownSubscriptionIds.add(subscription.id);
return subscription;
}
/**
* Unsubscribes atomically from all events in the given contexts and channel.
*
* This is a legacy spec branch to unsubscribe by attributes.
*/
unsubscribe(inputEventNames, googChannel) {
const eventNames = new Set(unrollEvents(inputEventNames));
const newSubscriptions = [];
const eventsMatched = new Set();
for (const subscription of this.#subscriptions) {
if (subscription.googChannel !== googChannel) {
newSubscriptions.push(subscription);
continue;
}
// Skip user context subscriptions.
if (subscription.userContextIds.size !== 0) {
newSubscriptions.push(subscription);
continue;
}
// Skip subscriptions when none of the event names match.
if (intersection(subscription.eventNames, eventNames).size === 0) {
newSubscriptions.push(subscription);
continue;
}
// Skip non-global subscriptions.
if (subscription.topLevelTraversableIds.size !== 0) {
newSubscriptions.push(subscription);
continue;
}
const subscriptionEventNames = new Set(subscription.eventNames);
for (const eventName of eventNames) {
if (subscriptionEventNames.has(eventName)) {
eventsMatched.add(eventName);
subscriptionEventNames.delete(eventName);
}
}
if (subscriptionEventNames.size !== 0) {
newSubscriptions.push({
...subscription,
eventNames: subscriptionEventNames,
});
}
}
// If some events did not match, it is an invalid request.
if (!equal(eventsMatched, eventNames)) {
throw new protocol_js_1.InvalidArgumentException('No subscription found');
}
// Committing the new subscriptions.
this.#subscriptions = newSubscriptions;
}
/**
* Unsubscribes by subscriptionId.
*/
unsubscribeById(subscriptionIds) {
const subscriptionIdsSet = new Set(subscriptionIds);
const unknownIds = difference(subscriptionIdsSet, this.#knownSubscriptionIds);
if (unknownIds.size !== 0) {
throw new protocol_js_1.InvalidArgumentException('No subscription found');
}
this.#subscriptions = this.#subscriptions.filter((subscription) => {
return !subscriptionIdsSet.has(subscription.id);
});
this.#knownSubscriptionIds = difference(this.#knownSubscriptionIds, subscriptionIdsSet);
}
}
exports.SubscriptionManager = SubscriptionManager;
/**
* Replace with Set.prototype.intersection once Node 20 is dropped.
*/
function intersection(setA, setB) {
const result = new Set();
for (const a of setA) {
if (setB.has(a)) {
result.add(a);
}
}
return result;
}
/**
* Replace with Set.prototype.difference once Node 20 is dropped.
*/
function difference(setA, setB) {
const result = new Set();
for (const a of setA) {
if (!setB.has(a)) {
result.add(a);
}
}
return result;
}
function equal(setA, setB) {
if (setA.size !== setB.size) {
return false;
}
for (const a of setA) {
if (!setB.has(a)) {
return false;
}
}
return true;
}
//# sourceMappingURL=SubscriptionManager.js.map