From 224ef9b8de69850705dc8d7e6356614a5974f126 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Mon, 27 Jan 2025 09:34:15 +0100 Subject: [PATCH] feat: implement user context subscriptions --- src/bidiMapper/BidiServer.ts | 8 +- src/bidiMapper/CommandProcessor.ts | 3 + .../modules/browser/BrowserProcessor.ts | 19 +- .../modules/browser/UserContextStorage.ts | 45 ++++ .../modules/network/NetworkStorage.spec.ts | 5 +- .../modules/session/EventManager.ts | 32 ++- .../modules/session/SessionProcessor.ts | 1 + .../session/SubscriptionManager.spec.ts | 210 +++++++++++++++--- .../modules/session/SubscriptionManager.ts | 47 ++-- 9 files changed, 309 insertions(+), 61 deletions(-) create mode 100644 src/bidiMapper/modules/browser/UserContextStorage.ts diff --git a/src/bidiMapper/BidiServer.ts b/src/bidiMapper/BidiServer.ts index 161074393..65d534baa 100644 --- a/src/bidiMapper/BidiServer.ts +++ b/src/bidiMapper/BidiServer.ts @@ -27,6 +27,7 @@ import type {BidiCommandParameterParser} from './BidiParser.js'; import type {BidiTransport} from './BidiTransport.js'; import {CommandProcessor, CommandProcessorEvents} from './CommandProcessor.js'; import {BluetoothProcessor} from './modules/bluetooth/BluetoothProcessor.js'; +import {UserContextStorage} from './modules/browser/UserContextStorage.js'; import {CdpTargetManager} from './modules/cdp/CdpTargetManager.js'; import {BrowsingContextStorage} from './modules/context/BrowsingContextStorage.js'; import {NetworkStorage} from './modules/network/NetworkStorage.js'; @@ -91,7 +92,11 @@ export class BidiServer extends EventEmitter { ); this.#transport = bidiTransport; this.#transport.setOnMessage(this.#handleIncomingMessage); - this.#eventManager = new EventManager(this.#browsingContextStorage); + const userUserContextStorage = new UserContextStorage(browserCdpClient); + this.#eventManager = new EventManager( + this.#browsingContextStorage, + userUserContextStorage, + ); const networkStorage = new NetworkStorage( this.#eventManager, this.#browsingContextStorage, @@ -111,6 +116,7 @@ export class BidiServer extends EventEmitter { this.#preloadScriptStorage, networkStorage, this.#bluetoothProcessor, + userUserContextStorage, parser, async (options: MapperOptions) => { // This is required to ignore certificate errors when service worker is fetched. diff --git a/src/bidiMapper/CommandProcessor.ts b/src/bidiMapper/CommandProcessor.ts index d179357ff..13c245cd5 100644 --- a/src/bidiMapper/CommandProcessor.ts +++ b/src/bidiMapper/CommandProcessor.ts @@ -33,6 +33,7 @@ import type {BidiCommandParameterParser} from './BidiParser.js'; import type {MapperOptions} from './BidiServer.js'; import type {BluetoothProcessor} from './modules/bluetooth/BluetoothProcessor.js'; import {BrowserProcessor} from './modules/browser/BrowserProcessor.js'; +import type {UserContextStorage} from './modules/browser/UserContextStorage.js'; import {CdpProcessor} from './modules/cdp/CdpProcessor.js'; import {BrowsingContextProcessor} from './modules/context/BrowsingContextProcessor.js'; import type {BrowsingContextStorage} from './modules/context/BrowsingContextStorage.js'; @@ -85,6 +86,7 @@ export class CommandProcessor extends EventEmitter { preloadScriptStorage: PreloadScriptStorage, networkStorage: NetworkStorage, bluetoothProcessor: BluetoothProcessor, + userContextStorage: UserContextStorage, parser: BidiCommandParameterParser = new BidiNoOpParser(), initConnection: (options: MapperOptions) => Promise, logger?: LoggerFn, @@ -99,6 +101,7 @@ export class CommandProcessor extends EventEmitter { this.#browserProcessor = new BrowserProcessor( browserCdpClient, browsingContextStorage, + userContextStorage, ); this.#browsingContextProcessor = new BrowsingContextProcessor( browserCdpClient, diff --git a/src/bidiMapper/modules/browser/BrowserProcessor.ts b/src/bidiMapper/modules/browser/BrowserProcessor.ts index b9e086e1f..167ae23b7 100644 --- a/src/bidiMapper/modules/browser/BrowserProcessor.ts +++ b/src/bidiMapper/modules/browser/BrowserProcessor.ts @@ -26,16 +26,21 @@ import { import type {CdpClient} from '../../BidiMapper.js'; import type {BrowsingContextStorage} from '../context/BrowsingContextStorage.js'; +import type {UserContextStorage} from './UserContextStorage.js'; + export class BrowserProcessor { readonly #browserCdpClient: CdpClient; readonly #browsingContextStorage: BrowsingContextStorage; + readonly #userContextStorage: UserContextStorage; constructor( browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, + userContextStorage: UserContextStorage, ) { this.#browserCdpClient = browserCdpClient; this.#browsingContextStorage = browsingContextStorage; + this.#userContextStorage = userContextStorage; } close(): EmptyResult { @@ -90,20 +95,8 @@ export class BrowserProcessor { } async getUserContexts(): Promise { - const result = await this.#browserCdpClient.sendCommand( - 'Target.getBrowserContexts', - ); return { - userContexts: [ - { - userContext: 'default', - }, - ...result.browserContextIds.map((id) => { - return { - userContext: id, - }; - }), - ], + userContexts: await this.#userContextStorage.getUserContexts(), }; } diff --git a/src/bidiMapper/modules/browser/UserContextStorage.ts b/src/bidiMapper/modules/browser/UserContextStorage.ts new file mode 100644 index 000000000..20e077982 --- /dev/null +++ b/src/bidiMapper/modules/browser/UserContextStorage.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2025 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 {Browser} from '../../../protocol/protocol.js'; + +export class UserContextStorage { + #browserClient: CdpClient; + + constructor(browserClient: CdpClient) { + this.#browserClient = browserClient; + } + + async getUserContexts(): Promise< + [Browser.UserContextInfo, ...Browser.UserContextInfo[]] + > { + const result = await this.#browserClient.sendCommand( + 'Target.getBrowserContexts', + ); + return [ + { + userContext: 'default', + }, + ...result.browserContextIds.map((id) => { + return { + userContext: id, + }; + }), + ]; + } +} diff --git a/src/bidiMapper/modules/network/NetworkStorage.spec.ts b/src/bidiMapper/modules/network/NetworkStorage.spec.ts index 167626964..eae17c2ea 100644 --- a/src/bidiMapper/modules/network/NetworkStorage.spec.ts +++ b/src/bidiMapper/modules/network/NetworkStorage.spec.ts @@ -21,6 +21,7 @@ import type {CdpClient} from '../../../cdp/CdpClient.js'; import {ChromiumBidi, Network} from '../../../protocol/protocol.js'; import {ProcessingQueue} from '../../../utils/ProcessingQueue.js'; import type {OutgoingMessage} from '../../OutgoingMessage.js'; +import {UserContextStorage} from '../browser/UserContextStorage.js'; import type {CdpTarget} from '../cdp/CdpTarget.js'; import type {BrowsingContextImpl} from '../context/BrowsingContextImpl.js'; import {BrowsingContextStorage} from '../context/BrowsingContextStorage.js'; @@ -75,9 +76,10 @@ describe('NetworkStorage', () => { id: MockCdpNetworkEvents.defaultFrameId, } as unknown as BrowsingContextImpl; cdpClient = cdpTarget.cdpClient; + const userContextStorage = new UserContextStorage(cdpClient); // We need to add it the storage to emit properly browsingContextStorage.addContext(browsingContext); - eventManager = new EventManager(browsingContextStorage); + eventManager = new EventManager(browsingContextStorage, userContextStorage); processingQueue = new ProcessingQueue( async ({message}) => { if (message.type === 'event') { @@ -93,6 +95,7 @@ describe('NetworkStorage', () => { // Verify that the Request send the message // To the correct context [MockCdpNetworkEvents.defaultFrameId], + [], {}, ); eventManager.on(EventManagerEvents.Event, ({message, event}) => { diff --git a/src/bidiMapper/modules/session/EventManager.ts b/src/bidiMapper/modules/session/EventManager.ts index e15f98795..7bd770f21 100644 --- a/src/bidiMapper/modules/session/EventManager.ts +++ b/src/bidiMapper/modules/session/EventManager.ts @@ -17,8 +17,10 @@ import type {BidiPlusChannel} from '../../../protocol/chromium-bidi.js'; import { + type Browser, ChromiumBidi, InvalidArgumentException, + NoSuchUserContextException, type BrowsingContext, } from '../../../protocol/protocol.js'; import {Buffer} from '../../../utils/Buffer.js'; @@ -27,6 +29,7 @@ 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 {UserContextStorage} from '../browser/UserContextStorage.js'; import type {BrowsingContextStorage} from '../context/BrowsingContextStorage.js'; import {assertSupportedEvent} from './events.js'; @@ -118,9 +121,15 @@ export class EventManager extends EventEmitter { ((contextId: BrowsingContext.BrowsingContext) => void)[] >; - constructor(browsingContextStorage: BrowsingContextStorage) { + #userContextStorage: UserContextStorage; + + constructor( + browsingContextStorage: BrowsingContextStorage, + userContextStorage: UserContextStorage, + ) { super(); this.#browsingContextStorage = browsingContextStorage; + this.#userContextStorage = userContextStorage; this.#subscriptionManager = new SubscriptionManager(browsingContextStorage); this.#subscribeHooks = new DefaultMap(() => []); } @@ -213,12 +222,17 @@ export class EventManager extends EventEmitter { async subscribe( eventNames: ChromiumBidi.EventNames[], contextIds: BrowsingContext.BrowsingContext[], + userContextIds: Browser.UserContext[], channel: BidiPlusChannel, ): Promise { for (const name of eventNames) { assertSupportedEvent(name); } + if (userContextIds.length && contextIds.length) { + throw new InvalidArgumentException("Both userContexts and contexts cannot be specified."); + } + // First check if all the contexts are known. for (const contextId of contextIds) { if (contextId !== null) { @@ -227,6 +241,21 @@ export class EventManager extends EventEmitter { } } + // Validate user contexts. + if (userContextIds.length) { + const userContexts = await this.#userContextStorage.getUserContexts(); + const knownUserContextIds = new Set( + userContexts.map(({userContext}) => userContext), + ); + for (const userContextId of userContextIds) { + if (knownUserContextIds.has(userContextId)) { + throw new NoSuchUserContextException( + `User context ${userContextId} not found.`, + ); + } + } + } + const unrolledEventNames = new Set(unrollEvents(eventNames)); const subscribeStepEvents = new Map>(); const subscriptionNavigableIds = new Set( @@ -260,6 +289,7 @@ export class EventManager extends EventEmitter { const subscription = this.#subscriptionManager.subscribe( eventNames, contextIds, + userContextIds, channel, ); diff --git a/src/bidiMapper/modules/session/SessionProcessor.ts b/src/bidiMapper/modules/session/SessionProcessor.ts index 7f80fe900..9ab55256c 100644 --- a/src/bidiMapper/modules/session/SessionProcessor.ts +++ b/src/bidiMapper/modules/session/SessionProcessor.ts @@ -149,6 +149,7 @@ export class SessionProcessor { const subscription = await this.#eventManager.subscribe( params.events as ChromiumBidi.EventNames[], params.contexts ?? [], + params.userContexts ?? [], channel, ); return { diff --git a/src/bidiMapper/modules/session/SubscriptionManager.spec.ts b/src/bidiMapper/modules/session/SubscriptionManager.spec.ts index dc62a1496..0bf4b0949 100644 --- a/src/bidiMapper/modules/session/SubscriptionManager.spec.ts +++ b/src/bidiMapper/modules/session/SubscriptionManager.spec.ts @@ -37,8 +37,10 @@ const YET_ANOTHER_EVENT = ChromiumBidi.BrowsingContext.EventNames.DomContentLoaded; const SOME_CONTEXT = 'SOME_CONTEXT'; +const SOME_USER_CONTEXT = 'SOME_USER_CONTEXT'; const SOME_NESTED_CONTEXT = 'SOME_NESTED_CONTEXT'; const ANOTHER_CONTEXT = 'ANOTHER_CONTEXT'; +const ANOTHER_USER_CONTEXT = 'ANOTHER_USER_CONTEXT'; const ANOTHER_NESTED_CONTEXT = 'ANOTHER_NESTED_CONTEXT'; const SOME_CHANNEL = {'goog:channel': 'SOME_CHANNEL'}; @@ -68,6 +70,27 @@ describe('SubscriptionManager', () => { return null; }); + browsingContextStorage.findContext = sinon + .stub() + .callsFake((contextId: BrowsingContext.BrowsingContext) => { + if (contextId === SOME_NESTED_CONTEXT) { + return {id: SOME_NESTED_CONTEXT, userContext: SOME_USER_CONTEXT}; + } + if (contextId === SOME_CONTEXT) { + return {id: SOME_CONTEXT, userContext: SOME_USER_CONTEXT}; + } + if (contextId === ANOTHER_NESTED_CONTEXT) { + return { + id: ANOTHER_NESTED_CONTEXT, + userContext: ANOTHER_USER_CONTEXT, + }; + } + if (contextId === ANOTHER_CONTEXT) { + return {id: ANOTHER_CONTEXT, userContext: ANOTHER_USER_CONTEXT}; + } + return undefined; + }); + browsingContextStorage.getTopLevelContexts = sinon.stub().callsFake(() => { return [{id: SOME_CONTEXT}, {id: ANOTHER_CONTEXT}]; }); @@ -77,12 +100,17 @@ describe('SubscriptionManager', () => { describe('getChannelsSubscribedToEvent', () => { it('should maintain channel subscription order', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], ANOTHER_CHANNEL); - subscriptionManager.subscribe([ANOTHER_EVENT], [], ANOTHER_CHANNEL); - subscriptionManager.subscribe([ALL_EVENTS], [], SOME_CHANNEL); - subscriptionManager.subscribe([ALL_EVENTS], [], SOME_CHANNEL); - subscriptionManager.subscribe([YET_ANOTHER_EVENT], [], ANOTHER_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], ANOTHER_CHANNEL); + subscriptionManager.subscribe([ANOTHER_EVENT], [], [], ANOTHER_CHANNEL); + subscriptionManager.subscribe([ALL_EVENTS], [], [], SOME_CHANNEL); + subscriptionManager.subscribe([ALL_EVENTS], [], [], SOME_CHANNEL); + subscriptionManager.subscribe( + [YET_ANOTHER_EVENT], + [], + [], + ANOTHER_CHANNEL, + ); // `SOME_EVENT` was fist subscribed in `SOME_CHANNEL`. expect( @@ -111,10 +139,10 @@ describe('SubscriptionManager', () => { }); it('should re-subscribe in proper order', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], ANOTHER_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], ANOTHER_CHANNEL); subscriptionManager.unsubscribe([SOME_EVENT], [], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); expect( subscriptionManager.getChannelsSubscribedToEvent( SOME_EVENT, @@ -124,9 +152,14 @@ describe('SubscriptionManager', () => { }); it('should subscribe global and specific context in proper order', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], ANOTHER_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], ANOTHER_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); expect( subscriptionManager.getChannelsSubscribedToEvent( SOME_EVENT, @@ -136,9 +169,14 @@ describe('SubscriptionManager', () => { }); it('should subscribe specific context and global in proper order', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], ANOTHER_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); + subscriptionManager.subscribe([SOME_EVENT], [], [], ANOTHER_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); expect( subscriptionManager.getChannelsSubscribedToEvent( SOME_EVENT, @@ -148,10 +186,16 @@ describe('SubscriptionManager', () => { }); it('should subscribe contexts in proper order', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); subscriptionManager.subscribe( [SOME_EVENT], [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], ANOTHER_CHANNEL, ); expect( @@ -163,10 +207,16 @@ describe('SubscriptionManager', () => { }); it('should re-subscribe contexts in proper order', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); subscriptionManager.subscribe( [SOME_EVENT], [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], ANOTHER_CHANNEL, ); subscriptionManager.unsubscribe( @@ -174,7 +224,12 @@ describe('SubscriptionManager', () => { [SOME_CONTEXT], SOME_CHANNEL, ); - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); expect( subscriptionManager.getChannelsSubscribedToEvent( SOME_EVENT, @@ -184,9 +239,54 @@ describe('SubscriptionManager', () => { }); }); + describe('user-context subscription', () => { + it('should subscribe to a user context event', () => { + subscriptionManager.subscribe( + [SOME_EVENT], + [], + [SOME_USER_CONTEXT], + SOME_CHANNEL, + ); + expect( + subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), + ).to.equal(true); + expect( + subscriptionManager.isSubscribedTo(SOME_EVENT, ANOTHER_CONTEXT), + ).to.equal(false); + }); + + it('should not unsubscribe by attributes', () => { + subscriptionManager.subscribe( + [SOME_EVENT], + [], + [SOME_USER_CONTEXT], + SOME_CHANNEL, + ); + expect(() => { + subscriptionManager.unsubscribe([SOME_EVENT], [], SOME_CHANNEL); + }).to.throw('No subscription found'); + expect( + subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), + ).to.equal(true); + }); + + it('should unsubscribe by id', () => { + const {id} = subscriptionManager.subscribe( + [SOME_EVENT], + [], + [SOME_USER_CONTEXT], + SOME_CHANNEL, + ); + subscriptionManager.unsubscribeById([id]); + expect( + subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), + ).to.equal(false); + }); + }); + describe('global subscription', () => { it('should subscribe to a global event', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); expect( subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), ).to.equal(true); @@ -196,7 +296,7 @@ describe('SubscriptionManager', () => { }); it('should unsubscribe', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); subscriptionManager.unsubscribe([SOME_EVENT], [], SOME_CHANNEL); expect( subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), @@ -207,6 +307,7 @@ describe('SubscriptionManager', () => { const {id} = subscriptionManager.subscribe( [SOME_EVENT], [], + [], SOME_CHANNEL, ); subscriptionManager.unsubscribeById([id]); @@ -216,7 +317,7 @@ describe('SubscriptionManager', () => { }); it('should not unsubscribe on error', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); expect(() => subscriptionManager.unsubscribe( [SOME_EVENT, ANOTHER_EVENT], @@ -230,8 +331,8 @@ describe('SubscriptionManager', () => { }); it('should unsubscribe from multiple subscriptions completely', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); - subscriptionManager.subscribe([ANOTHER_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); + subscriptionManager.subscribe([ANOTHER_EVENT], [], [], SOME_CHANNEL); subscriptionManager.unsubscribe( [SOME_EVENT, ANOTHER_EVENT], [], @@ -249,11 +350,13 @@ describe('SubscriptionManager', () => { subscriptionManager.subscribe( [SOME_EVENT, ANOTHER_EVENT], [], + [], SOME_CHANNEL, ); subscriptionManager.subscribe( [SOME_EVENT, YET_ANOTHER_EVENT], [], + [], SOME_CHANNEL, ); subscriptionManager.unsubscribe([SOME_EVENT], [], SOME_CHANNEL); @@ -272,6 +375,7 @@ describe('SubscriptionManager', () => { subscriptionManager.subscribe( [SOME_EVENT, ANOTHER_EVENT], [], + [], SOME_CHANNEL, ); subscriptionManager.unsubscribe([SOME_EVENT], [], SOME_CHANNEL); @@ -291,8 +395,13 @@ describe('SubscriptionManager', () => { }); it('should not unsubscribe context subscriptions', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); subscriptionManager.unsubscribe([SOME_EVENT], [], SOME_CHANNEL); expect( subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), @@ -302,7 +411,12 @@ describe('SubscriptionManager', () => { describe('context subscription', () => { it('should subscribe per context', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); expect( subscriptionManager.isSubscribedTo(SOME_EVENT, SOME_CONTEXT), ).to.equal(true); @@ -315,6 +429,7 @@ describe('SubscriptionManager', () => { const {id} = subscriptionManager.subscribe( [SOME_EVENT], [SOME_CONTEXT], + [], SOME_CHANNEL, ); subscriptionManager.unsubscribeById([id]); @@ -327,6 +442,7 @@ describe('SubscriptionManager', () => { subscriptionManager.subscribe( [SOME_EVENT], [SOME_CONTEXT, ANOTHER_CONTEXT], + [], SOME_CHANNEL, ); subscriptionManager.unsubscribe( @@ -346,6 +462,7 @@ describe('SubscriptionManager', () => { subscriptionManager.subscribe( [SOME_EVENT], [SOME_CONTEXT, ANOTHER_CONTEXT], + [], SOME_CHANNEL, ); subscriptionManager.unsubscribe( @@ -369,7 +486,12 @@ describe('SubscriptionManager', () => { }); it('should completely unsubscribe', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); subscriptionManager.unsubscribe( [SOME_EVENT], [SOME_CONTEXT], @@ -381,7 +503,12 @@ describe('SubscriptionManager', () => { }); it('should unsubscribe a module', () => { - subscriptionManager.subscribe([ALL_EVENTS], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe( + [ALL_EVENTS], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); subscriptionManager.unsubscribe( [ALL_EVENTS], [SOME_CONTEXT], @@ -393,8 +520,13 @@ describe('SubscriptionManager', () => { }); it('should not unsubscribe global subscription', () => { - subscriptionManager.subscribe([SOME_EVENT], [], SOME_CHANNEL); - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe([SOME_EVENT], [], [], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); subscriptionManager.unsubscribe( [SOME_EVENT], [SOME_CONTEXT], @@ -409,6 +541,7 @@ describe('SubscriptionManager', () => { subscriptionManager.subscribe( [SOME_EVENT], [SOME_NESTED_CONTEXT], + [], SOME_CHANNEL, ); expect( @@ -420,6 +553,7 @@ describe('SubscriptionManager', () => { subscriptionManager.subscribe( [SOME_EVENT], [ANOTHER_NESTED_CONTEXT], + [], SOME_CHANNEL, ); expect( @@ -428,7 +562,12 @@ describe('SubscriptionManager', () => { }); it('should unsubscribe from top-level context when unsubscribed from nested context', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); subscriptionManager.unsubscribe( [SOME_EVENT], [SOME_NESTED_CONTEXT], @@ -440,7 +579,12 @@ describe('SubscriptionManager', () => { }); it('should not unsubscribe from top-level context when unsubscribed from nested context in different channel', () => { - subscriptionManager.subscribe([SOME_EVENT], [SOME_CONTEXT], SOME_CHANNEL); + subscriptionManager.subscribe( + [SOME_EVENT], + [SOME_CONTEXT], + [], + SOME_CHANNEL, + ); expect(() => { subscriptionManager.unsubscribe( [SOME_EVENT], @@ -459,6 +603,7 @@ describe('SubscriptionManager', () => { const {id} = subscriptionManager.subscribe( [SOME_EVENT], [], + [], SOME_CHANNEL, ); expect( @@ -482,6 +627,7 @@ describe('SubscriptionManager', () => { const {id} = subscriptionManager.subscribe( [SOME_EVENT], [], + [], SOME_CHANNEL, ); expect( diff --git a/src/bidiMapper/modules/session/SubscriptionManager.ts b/src/bidiMapper/modules/session/SubscriptionManager.ts index 6b5b9d205..028213a28 100644 --- a/src/bidiMapper/modules/session/SubscriptionManager.ts +++ b/src/bidiMapper/modules/session/SubscriptionManager.ts @@ -17,6 +17,7 @@ import type {BidiPlusChannel} from '../../../protocol/chromium-bidi.js'; import { + type Browser, type BrowsingContext, ChromiumBidi, InvalidArgumentException, @@ -78,6 +79,7 @@ export interface Subscription { id: string; // Empty set means a global subscription. topLevelTraversableIds: Set; + userContextIds: Set; // Never empty. eventNames: Set; channel: BidiPlusChannel; @@ -157,23 +159,34 @@ export class SubscriptionManager { return false; } - // global subscription. - if (subscription.topLevelTraversableIds.size === 0) { - return true; - } + // user context subscription. + if (subscription.userContextIds.size !== 0) { + if (!contextId) { + return false; + } - const topLevelContext = contextId - ? this.#browsingContextStorage.findTopLevelContextId(contextId) - : null; + const context = this.#browsingContextStorage.findContext(contextId); + if (!context) { + return false; + } + return subscription.userContextIds.has(context.userContext); + } - if ( - topLevelContext !== null && - subscription.topLevelTraversableIds.has(topLevelContext) - ) { - return true; + // context subscription. + if (subscription.topLevelTraversableIds.size !== 0) { + if (!contextId) { + return false; + } + const topLevelContext = + this.#browsingContextStorage.findTopLevelContextId(contextId); + return ( + topLevelContext !== null && + subscription.topLevelTraversableIds.has(topLevelContext) + ); } - return false; + // global subscription. + return true; } isSubscribedTo( @@ -202,6 +215,7 @@ export class SubscriptionManager { subscribe( eventNames: ChromiumBidi.EventNames[], contextIds: BrowsingContext.BrowsingContext[], + userContextIds: Browser.UserContext[], channel: BidiPlusChannel, ): Subscription { // All the subscriptions are handled on the top-level contexts. @@ -220,6 +234,7 @@ export class SubscriptionManager { return topLevelContext; }), ), + userContextIds: new Set(userContextIds), channel, }; this.#subscriptions.push(subscription); @@ -266,6 +281,11 @@ export class SubscriptionManager { 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); @@ -328,6 +348,7 @@ export class SubscriptionManager { channel: subscription.channel, eventNames: new Set([eventName]), topLevelTraversableIds: remainingContextIds, + userContextIds: new Set(), }; newSubscriptions.push(partialSubscription); }