Skip to content

Commit

Permalink
feat: implement subscription ids
Browse files Browse the repository at this point in the history
  • Loading branch information
OrKoN committed Jan 8, 2025
1 parent 2a3d277 commit ebe3949
Show file tree
Hide file tree
Showing 15 changed files with 469 additions and 643 deletions.
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
2 changes: 1 addition & 1 deletion src/bidiMapper/modules/network/NetworkRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export class NetworkRequest {
this.#request.info?.frameId ??
this.#request.paused?.frameId ??
this.#request.auth?.frameId ??
null
''
);
}

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

0 comments on commit ebe3949

Please sign in to comment.