Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement subscription IDs #2954

Merged
merged 6 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ Refer to the documentation at [.pre-commit-config.yaml](.pre-commit-config.yaml)
pre-commit install --hook-type pre-push
```

Re-installing pre-commit locally:

```
pre-commit clean && pip install pre-commit
```

### Starting WebDriver BiDi Server

This will run the server on port `8080`:
Expand Down
6 changes: 4 additions & 2 deletions src/bidiMapper/modules/cdp/CdpTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import type {Protocol} from 'devtools-protocol';

import type {CdpClient} from '../../../cdp/CdpClient.js';
import {BiDiModule} from '../../../protocol/chromium-bidi.js';
import {Bluetooth} from '../../../protocol/chromium-bidi.js';
import type {ChromiumBidi, Session} from '../../../protocol/protocol.js';
import {Deferred} from '../../../utils/Deferred.js';
import {EventEmitter} from '../../../utils/EventEmitter.js';
Expand Down Expand Up @@ -364,7 +364,9 @@ export class CdpTarget extends EventEmitter<TargetEventMap> {
}

async toggleDeviceAccessIfNeeded(): Promise<void> {
const enabled = this.isSubscribedTo(BiDiModule.Bluetooth);
const enabled = this.isSubscribedTo(
Bluetooth.EventNames.RequestDevicePromptUpdated,
);
if (this.#deviceAccessEnabled === enabled) {
return;
}
Expand Down
5 changes: 4 additions & 1 deletion src/bidiMapper/modules/context/BrowsingContextStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export class BrowsingContextStorage {
return null;
}
const maybeContext = this.findContext(id);
const parentId = maybeContext?.parentId ?? null;
if (!maybeContext) {
return null;
}
const parentId = maybeContext.parentId ?? null;
if (parentId === null) {
return id;
}
Expand Down
20 changes: 14 additions & 6 deletions src/bidiMapper/modules/network/NetworkRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,12 +808,20 @@ export class NetworkRequest {
this.#phaseChanged();

this.#emittedEvents[event.method] = true;
this.#eventManager.registerEvent(
Object.assign(event, {
type: 'event' as const,
}),
this.#context,
);
if (this.#context) {
this.#eventManager.registerEvent(
Object.assign(event, {
type: 'event' as const,
}),
this.#context,
);
} else {
this.#eventManager.registerGlobalEvent(
Object.assign(event, {
type: 'event' as const,
}),
);
}
}

#getBaseEventParams(phase?: Network.InterceptPhase): Network.BaseParameters {
Expand Down
4 changes: 2 additions & 2 deletions src/bidiMapper/modules/network/NetworkStorage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ describe('NetworkStorage', () => {
);
// Subscribe to the `network` module globally
eventManager.subscriptionManager.subscribe(
ChromiumBidi.BiDiModule.Network,
[ChromiumBidi.BiDiModule.Network],
// Verify that the Request send the message
// To the correct context
MockCdpNetworkEvents.defaultFrameId,
[MockCdpNetworkEvents.defaultFrameId],
null,
);
eventManager.on(EventManagerEvents.Event, ({message, event}) => {
Expand Down
2 changes: 1 addition & 1 deletion src/bidiMapper/modules/script/Realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export abstract class Realm {

#registerEvent(event: ChromiumBidi.Event) {
if (this.associatedBrowsingContexts.length === 0) {
this.#eventManager.registerEvent(event, null);
this.#eventManager.registerGlobalEvent(event);
} else {
for (const browsingContext of this.associatedBrowsingContexts) {
this.#eventManager.registerEvent(event, browsingContext.id);
Expand Down
107 changes: 83 additions & 24 deletions src/bidiMapper/modules/session/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@
import type {BidiPlusChannel} from '../../../protocol/chromium-bidi.js';
import {
ChromiumBidi,
InvalidArgumentException,
type BrowsingContext,
} 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 type {Result} from '../../../utils/result.js';
import {OutgoingMessage} from '../../OutgoingMessage.js';
import type {BrowsingContextStorage} from '../context/BrowsingContextStorage.js';

import {assertSupportedEvent} from './events.js';
import {SubscriptionManager} from './SubscriptionManager.js';
import {
difference,
SubscriptionManager,
unrollEvents,
} from './SubscriptionManager.js';

class EventWrapper {
readonly #idWrapper = new IdWrapper();
Expand Down Expand Up @@ -144,7 +148,7 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {

registerEvent(
event: ChromiumBidi.Event,
contextId: BrowsingContext.BrowsingContext | null,
contextId: BrowsingContext.BrowsingContext,
): void {
this.registerPromiseEvent(
Promise.resolve({
Expand All @@ -156,9 +160,19 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
);
}

registerGlobalEvent(event: ChromiumBidi.Event): void {
this.registerGlobalPromiseEvent(
Promise.resolve({
kind: 'success',
value: event,
}),
event.method,
);
}

registerPromiseEvent(
event: Promise<Result<ChromiumBidi.Event>>,
contextId: BrowsingContext.BrowsingContext | null,
contextId: BrowsingContext.BrowsingContext,
eventName: ChromiumBidi.EventNames,
): void {
const eventWrapper = new EventWrapper(event, contextId);
Expand All @@ -178,11 +192,29 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
}
}

registerGlobalPromiseEvent(
event: Promise<Result<ChromiumBidi.Event>>,
eventName: ChromiumBidi.EventNames,
): void {
const eventWrapper = new EventWrapper(event, null);
const sortedChannels =
this.#subscriptionManager.getChannelsSubscribedToEventGlobally(eventName);
this.#bufferEvent(eventWrapper, eventName);
// Send events to channels in the subscription priority.
for (const channel of sortedChannels) {
this.emit(EventManagerEvents.Event, {
message: OutgoingMessage.createFromPromise(event, channel),
event: eventName,
});
this.#markEventSent(eventWrapper, channel, eventName);
}
}

async subscribe(
eventNames: ChromiumBidi.EventNames[],
contextIds: (BrowsingContext.BrowsingContext | null)[],
contextIds: BrowsingContext.BrowsingContext[],
channel: BidiPlusChannel,
): Promise<void> {
): Promise<string> {
for (const name of eventNames) {
assertSupportedEvent(name);
}
Expand All @@ -195,17 +227,44 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
}
}

// 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: SubscriptionItem[] = [];
const unrolledEventNames = new Set(unrollEvents(eventNames));
const subscribeStepEvents = new Map<ChromiumBidi.EventNames, Set<string>>();
const subscriptionNavigableIds = new Set(
contextIds.length
? contextIds.map((contextId) => {
const id =
this.#browsingContextStorage.findTopLevelContextId(contextId);
if (!id) {
throw new InvalidArgumentException('Invalid context id');
}
return id;
})
: this.#browsingContextStorage.getTopLevelContexts().map((c) => c.id),
);

for (const eventName of eventNames) {
for (const contextId of contextIds) {
addedSubscriptionItems.push(
...this.#subscriptionManager.subscribe(eventName, contextId, channel),
);
for (const eventName of unrolledEventNames) {
const subscribedNavigableIds = new Set(
this.#browsingContextStorage
.getTopLevelContexts()
.map((c) => c.id)
.filter((id) => {
return this.#subscriptionManager.isSubscribedTo(eventName, id);
}),
);
subscribeStepEvents.set(
eventName,
difference(subscriptionNavigableIds, subscribedNavigableIds),
);
}

const subscription = this.#subscriptionManager.subscribe(
eventNames,
contextIds,
channel,
);

for (const eventName of subscription.eventNames) {
for (const contextId of subscriptionNavigableIds) {
for (const eventWrapper of this.#getBufferedEvents(
eventName,
contextId,
Expand All @@ -224,26 +283,26 @@ export class EventManager extends EventEmitter<EventManagerEventsMap> {
}
}

// 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));
});
for (const [eventName, contextIds] of subscribeStepEvents) {
for (const contextId of contextIds) {
this.#subscribeHooks.get(eventName).forEach((hook) => hook(contextId));
}
}

await this.toggleModulesIfNeeded();

return subscription.id;
}

async unsubscribe(
eventNames: ChromiumBidi.EventNames[],
contextIds: (BrowsingContext.BrowsingContext | null)[],
contextIds: BrowsingContext.BrowsingContext[],
channel: BidiPlusChannel,
): Promise<void> {
for (const name of eventNames) {
assertSupportedEvent(name);
}
this.#subscriptionManager.unsubscribeAll(eventNames, contextIds, channel);
this.#subscriptionManager.unsubscribe(eventNames, contextIds, channel);
await this.toggleModulesIfNeeded();
}

Expand Down
12 changes: 7 additions & 5 deletions src/bidiMapper/modules/session/SessionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,15 @@ export class SessionProcessor {
async subscribe(
params: Session.SubscriptionRequest,
channel: BidiPlusChannel = null,
): Promise<EmptyResult> {
await this.#eventManager.subscribe(
): Promise<Session.SubscribeResult> {
const subscription = await this.#eventManager.subscribe(
params.events as ChromiumBidi.EventNames[],
params.contexts ?? [null],
params.contexts ?? [],
channel,
);
return {};
return {
subscription,
};
}

async unsubscribe(
Expand All @@ -160,7 +162,7 @@ export class SessionProcessor {
): Promise<EmptyResult> {
await this.#eventManager.unsubscribe(
params.events as ChromiumBidi.EventNames[],
params.contexts ?? [null],
params.contexts ?? [],
channel,
);
return {};
Expand Down
Loading
Loading