This commit is contained in:
nik
2025-10-03 22:27:28 +03:00
parent 829fad0e17
commit 871cf7e792
16520 changed files with 2967597 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
/**
* 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.
*/
import type { BidiPlusChannel } from '../../../protocol/chromium-bidi.js';
import { ChromiumBidi, type BrowsingContext } from '../../../protocol/protocol.js';
import { EventEmitter } from '../../../utils/EventEmitter.js';
import type { Result } from '../../../utils/result.js';
import { OutgoingMessage } from '../../OutgoingMessage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import { SubscriptionManager } from './SubscriptionManager.js';
export declare const enum EventManagerEvents {
Event = "event"
}
type EventManagerEventsMap = {
[EventManagerEvents.Event]: {
message: Promise<Result<OutgoingMessage>>;
event: string;
};
};
/**
* Subscription item is a pair of event name and context id.
*/
export type SubscriptionItem = {
contextId: BrowsingContext.BrowsingContext;
event: ChromiumBidi.EventNames;
};
export declare class EventManager extends EventEmitter<EventManagerEventsMap> {
#private;
constructor(browsingContextStorage: BrowsingContextStorage);
get subscriptionManager(): SubscriptionManager;
addSubscribeHook(event: ChromiumBidi.EventNames, hook: (contextId: BrowsingContext.BrowsingContext) => Promise<void>): void;
registerEvent(event: ChromiumBidi.Event, contextId: BrowsingContext.BrowsingContext | null): void;
registerPromiseEvent(event: Promise<Result<ChromiumBidi.Event>>, contextId: BrowsingContext.BrowsingContext | null, eventName: ChromiumBidi.EventNames): void;
subscribe(eventNames: ChromiumBidi.EventNames[], contextIds: (BrowsingContext.BrowsingContext | null)[], channel: BidiPlusChannel): Promise<void>;
unsubscribe(eventNames: ChromiumBidi.EventNames[], contextIds: (BrowsingContext.BrowsingContext | null)[], channel: BidiPlusChannel): Promise<void>;
toggleModulesIfNeeded(): Promise<void>;
clearBufferedEvents(contextId: string): void;
}
export {};

View File

@@ -0,0 +1,226 @@
/**
* 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.
*/
var _a;
import { ChromiumBidi, } from '../../../protocol/protocol.js';
import { Buffer } from '../../../utils/Buffer.js';
import { DefaultMap } from '../../../utils/DefaultMap.js';
import { distinctValues } from '../../../utils/DistinctValues.js';
import { EventEmitter } from '../../../utils/EventEmitter.js';
import { IdWrapper } from '../../../utils/IdWrapper.js';
import { OutgoingMessage } from '../../OutgoingMessage.js';
import { assertSupportedEvent } from './events.js';
import { SubscriptionManager } from './SubscriptionManager.js';
class EventWrapper {
#idWrapper = new IdWrapper();
#contextId;
#event;
constructor(event, contextId) {
this.#event = event;
this.#contextId = contextId;
}
get id() {
return this.#idWrapper.id;
}
get contextId() {
return this.#contextId;
}
get event() {
return this.#event;
}
}
/**
* Maps event name to a desired buffer length.
*/
const eventBufferLength = new Map([[ChromiumBidi.Log.EventNames.LogEntryAdded, 100]]);
export class EventManager extends EventEmitter {
/**
* Maps event name to a set of contexts where this event already happened.
* Needed for getting buffered events from all the contexts in case of
* subscripting to all contexts.
*/
#eventToContextsMap = new DefaultMap(() => new Set());
/**
* Maps `eventName` + `browsingContext` to buffer. Used to get buffered events
* during subscription. Channel-agnostic.
*/
#eventBuffers = new Map();
/**
* Maps `eventName` + `browsingContext` to Map of channel to last id
* Used to avoid sending duplicated events when user
* subscribes -> unsubscribes -> subscribes.
*/
#lastMessageSent = new Map();
#subscriptionManager;
#browsingContextStorage;
/**
* Map of event name to hooks to be called when client is subscribed to the event.
*/
#subscribeHooks;
constructor(browsingContextStorage) {
super();
this.#browsingContextStorage = browsingContextStorage;
this.#subscriptionManager = new SubscriptionManager(browsingContextStorage);
this.#subscribeHooks = new DefaultMap(() => []);
}
get subscriptionManager() {
return this.#subscriptionManager;
}
/**
* Returns consistent key to be used to access value maps.
*/
static #getMapKey(eventName, browsingContext) {
return JSON.stringify({ eventName, browsingContext });
}
addSubscribeHook(event, hook) {
this.#subscribeHooks.get(event).push(hook);
}
registerEvent(event, contextId) {
this.registerPromiseEvent(Promise.resolve({
kind: 'success',
value: event,
}), contextId, event.method);
}
registerPromiseEvent(event, contextId, eventName) {
const eventWrapper = new EventWrapper(event, contextId);
const sortedChannels = this.#subscriptionManager.getChannelsSubscribedToEvent(eventName, contextId);
this.#bufferEvent(eventWrapper, eventName);
// Send events to channels in the subscription priority.
for (const channel of sortedChannels) {
this.emit("event" /* EventManagerEvents.Event */, {
message: OutgoingMessage.createFromPromise(event, channel),
event: eventName,
});
this.#markEventSent(eventWrapper, channel, eventName);
}
}
async subscribe(eventNames, contextIds, channel) {
for (const name of eventNames) {
assertSupportedEvent(name);
}
// First check if all the contexts are known.
for (const contextId of contextIds) {
if (contextId !== null) {
// Assert the context is known. Throw exception otherwise.
this.#browsingContextStorage.getContext(contextId);
}
}
// List of the subscription items that were actually added. Each contains a specific
// event and context. No module event (like "network") or global context subscription
// (like null) are included.
const addedSubscriptionItems = [];
for (const eventName of eventNames) {
for (const contextId of contextIds) {
addedSubscriptionItems.push(...this.#subscriptionManager.subscribe(eventName, contextId, channel));
for (const eventWrapper of this.#getBufferedEvents(eventName, contextId, channel)) {
// The order of the events is important.
this.emit("event" /* EventManagerEvents.Event */, {
message: OutgoingMessage.createFromPromise(eventWrapper.event, channel),
event: eventName,
});
this.#markEventSent(eventWrapper, channel, eventName);
}
}
}
// Iterate over all new subscription items and call hooks if any. There can be
// duplicates, e.g. when subscribing to the whole module and some specific event in
// the same time ("network", "network.responseCompleted"). `distinctValues` guarantees
// that hooks are called only once per pair event + context.
distinctValues(addedSubscriptionItems).forEach(({ contextId, event }) => {
this.#subscribeHooks.get(event).forEach((hook) => hook(contextId));
});
await this.toggleModulesIfNeeded();
}
async unsubscribe(eventNames, contextIds, channel) {
for (const name of eventNames) {
assertSupportedEvent(name);
}
this.#subscriptionManager.unsubscribeAll(eventNames, contextIds, channel);
await this.toggleModulesIfNeeded();
}
async toggleModulesIfNeeded() {
// TODO(1): Only update changed subscribers
// TODO(2): Enable for Worker Targets
await Promise.all(this.#browsingContextStorage.getAllContexts().map(async (context) => {
return await context.toggleModulesIfNeeded();
}));
}
clearBufferedEvents(contextId) {
for (const eventName of eventBufferLength.keys()) {
const bufferMapKey = _a.#getMapKey(eventName, contextId);
this.#eventBuffers.delete(bufferMapKey);
}
}
/**
* If the event is buffer-able, put it in the buffer.
*/
#bufferEvent(eventWrapper, eventName) {
if (!eventBufferLength.has(eventName)) {
// Do nothing if the event is no buffer-able.
return;
}
const bufferMapKey = _a.#getMapKey(eventName, eventWrapper.contextId);
if (!this.#eventBuffers.has(bufferMapKey)) {
this.#eventBuffers.set(bufferMapKey, new Buffer(eventBufferLength.get(eventName)));
}
this.#eventBuffers.get(bufferMapKey).add(eventWrapper);
// Add the context to the list of contexts having `eventName` events.
this.#eventToContextsMap.get(eventName).add(eventWrapper.contextId);
}
/**
* If the event is buffer-able, mark it as sent to the given contextId and channel.
*/
#markEventSent(eventWrapper, channel, eventName) {
if (!eventBufferLength.has(eventName)) {
// Do nothing if the event is no buffer-able.
return;
}
const lastSentMapKey = _a.#getMapKey(eventName, eventWrapper.contextId);
const lastId = Math.max(this.#lastMessageSent.get(lastSentMapKey)?.get(channel) ?? 0, eventWrapper.id);
const channelMap = this.#lastMessageSent.get(lastSentMapKey);
if (channelMap) {
channelMap.set(channel, lastId);
}
else {
this.#lastMessageSent.set(lastSentMapKey, new Map([[channel, lastId]]));
}
}
/**
* Returns events which are buffered and not yet sent to the given channel events.
*/
#getBufferedEvents(eventName, contextId, channel) {
const bufferMapKey = _a.#getMapKey(eventName, contextId);
const lastSentMessageId = this.#lastMessageSent.get(bufferMapKey)?.get(channel) ?? -Infinity;
const result = this.#eventBuffers
.get(bufferMapKey)
?.get()
.filter((wrapper) => wrapper.id > lastSentMessageId) ?? [];
if (contextId === null) {
// For global subscriptions, events buffered in each context should be sent back.
Array.from(this.#eventToContextsMap.get(eventName).keys())
.filter((_contextId) =>
// Events without context are already in the result.
_contextId !== null &&
// Events from deleted contexts should not be sent.
this.#browsingContextStorage.hasContext(_contextId))
.map((_contextId) => this.#getBufferedEvents(eventName, _contextId, channel))
.forEach((events) => result.push(...events));
}
return result.sort((e1, e2) => e1.id - e2.id);
}
}
_a = EventManager;
//# sourceMappingURL=EventManager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
/**
* Copyright 2023 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.
*/
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { BidiPlusChannel } from '../../../protocol/chromium-bidi.js';
import { Session, type EmptyResult } from '../../../protocol/protocol.js';
import type { MapperOptions } from '../../BidiServer.js';
import type { EventManager } from './EventManager.js';
export declare class SessionProcessor {
#private;
constructor(eventManager: EventManager, browserCdpClient: CdpClient, initConnection: (opts: MapperOptions) => Promise<void>);
status(): Session.StatusResult;
new(params: Session.NewParameters): Promise<Session.NewResult>;
subscribe(params: Session.SubscriptionRequest, channel?: BidiPlusChannel): Promise<EmptyResult>;
unsubscribe(params: Session.SubscriptionRequest, channel?: BidiPlusChannel): Promise<EmptyResult>;
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright 2023 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.
*/
import { InvalidArgumentException, } from '../../../protocol/protocol.js';
export class SessionProcessor {
#eventManager;
#browserCdpClient;
#initConnection;
#created = false;
constructor(eventManager, browserCdpClient, initConnection) {
this.#eventManager = eventManager;
this.#browserCdpClient = browserCdpClient;
this.#initConnection = initConnection;
}
status() {
return { ready: false, message: 'already connected' };
}
#mergeCapabilities(capabilitiesRequest) {
// Roughly following https://www.w3.org/TR/webdriver2/#dfn-capabilities-processing.
// Validations should already be done by the parser.
const mergedCapabilities = [];
for (const first of capabilitiesRequest.firstMatch ?? [{}]) {
const result = {
...capabilitiesRequest.alwaysMatch,
};
for (const key of Object.keys(first)) {
if (result[key] !== undefined) {
throw new InvalidArgumentException(`Capability ${key} in firstMatch is already defined in alwaysMatch`);
}
result[key] = first[key];
}
mergedCapabilities.push(result);
}
const match = mergedCapabilities.find((c) => c.browserName === 'chrome') ??
mergedCapabilities[0] ??
{};
match.unhandledPromptBehavior = this.#getUnhandledPromptBehavior(match.unhandledPromptBehavior);
return match;
}
#getUnhandledPromptBehavior(capabilityValue) {
if (capabilityValue === undefined) {
return undefined;
}
if (typeof capabilityValue === 'object') {
// Do not validate capabilities. Incorrect ones will be ignored by Mapper.
return capabilityValue;
}
if (typeof capabilityValue !== 'string') {
throw new InvalidArgumentException(`Unexpected 'unhandledPromptBehavior' type: ${typeof capabilityValue}`);
}
switch (capabilityValue) {
case 'accept':
case 'accept and notify':
return { default: "accept" /* Session.UserPromptHandlerType.Accept */ };
case 'dismiss':
case 'dismiss and notify':
return { default: "dismiss" /* Session.UserPromptHandlerType.Dismiss */ };
case 'ignore':
return { default: "ignore" /* Session.UserPromptHandlerType.Ignore */ };
default:
throw new InvalidArgumentException(`Unexpected 'unhandledPromptBehavior' value: ${capabilityValue}`);
}
}
async new(params) {
if (this.#created) {
throw new Error('Session has been already created.');
}
this.#created = true;
const matchedCapabitlites = this.#mergeCapabilities(params.capabilities);
await this.#initConnection(matchedCapabitlites);
const version = await this.#browserCdpClient.sendCommand('Browser.getVersion');
return {
sessionId: 'unknown',
capabilities: {
...matchedCapabitlites,
acceptInsecureCerts: matchedCapabitlites.acceptInsecureCerts ?? false,
browserName: version.product,
browserVersion: version.revision,
platformName: '',
setWindowRect: false,
webSocketUrl: '',
userAgent: version.userAgent,
},
};
}
async subscribe(params, channel = null) {
await this.#eventManager.subscribe(params.events, params.contexts ?? [null], channel);
return {};
}
async unsubscribe(params, channel = null) {
await this.#eventManager.unsubscribe(params.events, params.contexts ?? [null], channel);
return {};
}
}
//# sourceMappingURL=SessionProcessor.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SessionProcessor.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/session/SessionProcessor.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,61 @@
/**
* 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.
*/
import type { BidiPlusChannel } from '../../../protocol/chromium-bidi.js';
import { type BrowsingContext, ChromiumBidi } from '../../../protocol/protocol.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { SubscriptionItem } from './EventManager.js';
/**
* Returns the cartesian product of the given arrays.
*
* Example:
* cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
*/
export declare function cartesianProduct(...a: any[][]): any[];
/** Expands "AllEvents" events into atomic events. */
export declare function unrollEvents(events: ChromiumBidi.EventNames[]): ChromiumBidi.EventNames[];
export declare class SubscriptionManager {
#private;
constructor(browsingContextStorage: BrowsingContextStorage);
getChannelsSubscribedToEvent(eventMethod: ChromiumBidi.EventNames, contextId: BrowsingContext.BrowsingContext | null): BidiPlusChannel[];
/**
* @param module BiDi+ module
* @param contextId `null` == globally subscribed
*
* @returns
*/
isSubscribedTo(moduleOrEvent: ChromiumBidi.EventNames, contextId?: BrowsingContext.BrowsingContext | null): boolean;
/**
* 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: ChromiumBidi.EventNames, contextId: BrowsingContext.BrowsingContext | null, channel: BidiPlusChannel): SubscriptionItem[];
/**
* Unsubscribes atomically from all events in the given contexts and channel.
*/
unsubscribeAll(events: ChromiumBidi.EventNames[], contextIds: (BrowsingContext.BrowsingContext | null)[], channel: BidiPlusChannel): void;
/**
* Unsubscribes from the event in the given context and channel.
* Syntactic sugar for "unsubscribeAll".
*/
unsubscribe(eventName: ChromiumBidi.EventNames, contextId: BrowsingContext.BrowsingContext | null, channel: BidiPlusChannel): void;
}

View File

@@ -0,0 +1,274 @@
/**
* 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.
*/
import { ChromiumBidi, InvalidArgumentException, } from '../../../protocol/protocol.js';
import { isCdpEvent, isDeprecatedCdpEvent } from './events.js';
/**
* Returns the cartesian product of the given arrays.
*
* Example:
* cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
*/
export function cartesianProduct(...a) {
return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
}
/** Expands "AllEvents" events into atomic events. */
export 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 ChromiumBidi.BiDiModule.Bluetooth:
addEvents(Object.values(ChromiumBidi.Bluetooth.EventNames));
break;
case ChromiumBidi.BiDiModule.BrowsingContext:
addEvents(Object.values(ChromiumBidi.BrowsingContext.EventNames));
break;
case ChromiumBidi.BiDiModule.Log:
addEvents(Object.values(ChromiumBidi.Log.EventNames));
break;
case ChromiumBidi.BiDiModule.Network:
addEvents(Object.values(ChromiumBidi.Network.EventNames));
break;
case ChromiumBidi.BiDiModule.Script:
addEvents(Object.values(ChromiumBidi.Script.EventNames));
break;
default:
allEvents.add(event);
}
}
return [...allEvents.values()];
}
export 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 (isCdpEvent(eventMethod)) {
const cdpPriority = contextToEventMap
.get(context)
?.get(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 (isDeprecatedCdpEvent(eventMethod)) {
const cdpPriority = contextToEventMap
.get(context)
?.get(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 ChromiumBidi.BiDiModule.BrowsingContext:
return Object.values(ChromiumBidi.BrowsingContext.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case ChromiumBidi.BiDiModule.Log:
return Object.values(ChromiumBidi.Log.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case ChromiumBidi.BiDiModule.Network:
return Object.values(ChromiumBidi.Network.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case ChromiumBidi.BiDiModule.Script:
return Object.values(ChromiumBidi.Script.EventNames)
.map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
.flat();
case ChromiumBidi.BiDiModule.Bluetooth:
return Object.values(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 InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
}
const contextToEventMap = this.#channelToContextToEventMap.get(channel);
if (!contextToEventMap.has(contextId)) {
throw new InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
}
const eventMap = contextToEventMap.get(contextId);
if (!eventMap.has(event)) {
throw new 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);
}
};
}
}
//# sourceMappingURL=SubscriptionManager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
/**
* Copyright 2023 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.
*/
import { ChromiumBidi } from '../../../protocol/protocol.js';
/**
* Returns true if the given event is a CDP event.
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export declare function isCdpEvent(name: string): boolean;
/**
* Returns true if the given event is a deprecated CDP event.
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export declare function isDeprecatedCdpEvent(name: string): boolean;
/**
* Asserts that the given event is known to BiDi or BiDi+, or throws otherwise.
*/
export declare function assertSupportedEvent(name: string): asserts name is ChromiumBidi.EventNames;

View File

@@ -0,0 +1,43 @@
/**
* Copyright 2023 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.
*/
import { ChromiumBidi, InvalidArgumentException, } from '../../../protocol/protocol.js';
/**
* Returns true if the given event is a CDP event.
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export function isCdpEvent(name) {
return (name.split('.').at(0)?.startsWith(ChromiumBidi.BiDiModule.Cdp) ?? false);
}
/**
* Returns true if the given event is a deprecated CDP event.
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export function isDeprecatedCdpEvent(name) {
return (name.split('.').at(0)?.startsWith(ChromiumBidi.BiDiModule.DeprecatedCdp) ??
false);
}
/**
* Asserts that the given event is known to BiDi or BiDi+, or throws otherwise.
*/
export function assertSupportedEvent(name) {
if (!ChromiumBidi.EVENT_NAMES.has(name) &&
!isCdpEvent(name) &&
!isDeprecatedCdpEvent(name)) {
throw new InvalidArgumentException(`Unknown event: ${name}`);
}
}
//# sourceMappingURL=events.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/session/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,YAAY,EACZ,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,CACL,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CACxE,CAAC;AACJ,CAAC;AACD;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,CACL,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,aAAa,CAAC;QACxE,KAAK,CACN,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY;IAEZ,IACE,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,CAAC,IAAa,CAAC;QAC5C,CAAC,UAAU,CAAC,IAAI,CAAC;QACjB,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAC3B,CAAC;QACD,MAAM,IAAI,wBAAwB,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC"}