session.sys.mjs (31207B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 11 ContextDescriptorType: 12 "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", 13 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 14 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", 15 getWebDriverSessionById: 16 "chrome://remote/content/shared/webdriver/Session.sys.mjs", 17 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 18 pprint: "chrome://remote/content/shared/Format.sys.mjs", 19 RootMessageHandler: 20 "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs", 21 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 22 UserContextManager: 23 "chrome://remote/content/shared/UserContextManager.sys.mjs", 24 }); 25 class SessionModule extends RootBiDiModule { 26 #knownSubscriptionIds; 27 #subscriptions; 28 29 /** 30 * An object that holds information about the subscription, 31 * if the <var>topLevelTraversableIds</var> and the <var>userContextIds</var> 32 * are both empty, the subscription is considered global. 33 * 34 * @typedef Subscription 35 * 36 * @property {Set} eventNames 37 * A set of event names related to this subscription. 38 * @property {string} subscriptionId 39 * A unique subscription identifier. 40 * @property {Set} topLevelTraversableIds 41 * A set of top level traversable ids related to this subscription. 42 * @property {Set} userContextIds 43 * A set of user context ids related to this subscription. 44 */ 45 46 constructor(messageHandler) { 47 super(messageHandler); 48 49 // Set of subscription ids. 50 this.#knownSubscriptionIds = new Set(); 51 // List of subscription objects type Subscription. 52 this.#subscriptions = []; 53 } 54 55 destroy() { 56 this.#knownSubscriptionIds = null; 57 this.#subscriptions = null; 58 } 59 60 /** 61 * Commands 62 */ 63 64 /** 65 * End the current session. 66 * 67 * Session clean up will happen later in WebDriverBiDiConnection class. 68 */ 69 async end() { 70 const session = lazy.getWebDriverSessionById(this.messageHandler.sessionId); 71 72 if (session.http) { 73 throw new lazy.error.UnsupportedOperationError( 74 "Ending a session started with WebDriver classic is not supported." + 75 ' Use the WebDriver classic "Delete Session" command instead.' 76 ); 77 } 78 } 79 80 /** 81 * An object that holds a unique subscription identifier. 82 * 83 * @typedef SubscribeResult 84 * 85 * @property {string} subscription 86 * A unique subscription identifier. 87 */ 88 89 /** 90 * Enable certain events either globally, or for a list of browsing contexts. 91 * 92 * @param {object=} params 93 * @param {Array<string>} params.events 94 * List of events to subscribe to. 95 * @param {Array<string>=} params.contexts 96 * Optional list of top-level browsing context ids 97 * to subscribe the events for. 98 * @param {Array<string>=} params.userContexts 99 * Optional list of user context ids 100 * to subscribe the events for. 101 * 102 * @returns {SubscribeResult} 103 * A unique subscription identifier. 104 * @throws {InvalidArgumentError} 105 * If <var>events</var> or <var>contexts</var> are not valid types. 106 */ 107 async subscribe(params = {}) { 108 const { events, contexts: contextIds = null, userContexts = null } = params; 109 110 // Check input types until we run schema validation. 111 this.#assertNonEmptyArrayWithStrings(events, "events"); 112 113 if (contextIds !== null) { 114 this.#assertNonEmptyArrayWithStrings(contextIds, "contexts"); 115 } 116 117 if (userContexts !== null) { 118 this.#assertNonEmptyArrayWithStrings(userContexts, "userContexts"); 119 } 120 121 const eventNames = new Set(); 122 events.forEach(name => { 123 this.#obtainEvents(name).forEach(event => eventNames.add(event)); 124 }); 125 126 const inputUserContextIds = new Set(userContexts); 127 const inputContextIds = new Set(contextIds); 128 129 if (inputUserContextIds.size > 0 && inputContextIds.size > 0) { 130 throw new lazy.error.InvalidArgumentError( 131 `Providing both "userContexts" and "contexts" arguments is not supported` 132 ); 133 } 134 135 let subscriptionNavigables = new Set(); 136 const topLevelTraversableContextIds = new Set(); 137 const userContextIds = new Set(); 138 139 if (inputContextIds.size !== 0) { 140 const navigables = this.#getValidNavigablesByIds(inputContextIds); 141 subscriptionNavigables = this.#getTopLevelTraversables(navigables); 142 143 for (const navigable of subscriptionNavigables) { 144 topLevelTraversableContextIds.add( 145 lazy.NavigableManager.getIdForBrowsingContext(navigable) 146 ); 147 } 148 } else if (inputUserContextIds.size !== 0) { 149 for (const userContextId of inputUserContextIds) { 150 const internalId = 151 lazy.UserContextManager.getInternalIdById(userContextId); 152 153 if (internalId === null) { 154 throw new lazy.error.NoSuchUserContextError( 155 `User context with id: ${userContextId} doesn't exist` 156 ); 157 } 158 159 lazy.UserContextManager.getTabsForUserContext(internalId).forEach( 160 item => subscriptionNavigables.add(item) 161 ); 162 163 userContextIds.add(internalId); 164 } 165 } else { 166 for (const tab of lazy.TabManager.allTabs) { 167 subscriptionNavigables.add(tab); 168 } 169 } 170 171 const subscription = { 172 eventNames, 173 subscriptionId: lazy.generateUUID(), 174 topLevelTraversableIds: topLevelTraversableContextIds, 175 userContextIds, 176 }; 177 178 const subscribeStepEvents = new Map(); 179 180 for (const eventName of eventNames) { 181 const existingNavigables = 182 this.#getEnabledTopLevelTraversables(eventName); 183 184 subscribeStepEvents.set( 185 eventName, 186 subscriptionNavigables.difference(existingNavigables) 187 ); 188 } 189 190 this.#subscriptions.push(subscription); 191 this.#knownSubscriptionIds.add(subscription.subscriptionId); 192 193 // TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8). 194 195 const includeGlobal = this.#isSubscriptionGlobal(subscription); 196 197 const listeners = this.#getListenersToSubscribe( 198 eventNames, 199 includeGlobal, 200 subscribeStepEvents, 201 userContextIds 202 ); 203 204 // Subscribe to the relevant engine-internal events. 205 await this.messageHandler.eventsDispatcher.update(listeners); 206 207 return { subscription: subscription.subscriptionId }; 208 } 209 210 /** 211 * Disable certain events either globally, for a list of browsing contexts 212 * or for a list of subscription ids. 213 * 214 * @param {object=} params 215 * @param {Array<string>=} params.events 216 * List of events to unsubscribe from. 217 * @param {Array<string>=} params.contexts 218 * Optional list of top-level browsing context ids 219 * to unsubscribe the events from. 220 * @param {Array<string>=} params.subscriptions 221 * List of subscription identifiers to unsubscribe from. 222 * 223 * @throws {InvalidArgumentError} 224 * If <var>events</var> or <var>contexts</var> are not valid types. 225 */ 226 async unsubscribe(params = {}) { 227 const { events = null, contexts = null, subscriptions = null } = params; 228 229 const listeners = 230 subscriptions === null 231 ? this.#unsubscribeByAttributes(events, contexts) 232 : this.#unsubscribeById(subscriptions); 233 234 // Unsubscribe from the relevant engine-internal events. 235 await this.messageHandler.eventsDispatcher.update(listeners); 236 } 237 238 #assertModuleSupportsEvent(moduleName, event) { 239 const rootModuleClass = this.#getRootModuleClass(moduleName); 240 if (!rootModuleClass?.supportsEvent(event)) { 241 throw new lazy.error.InvalidArgumentError( 242 `${event} is not a valid event name` 243 ); 244 } 245 } 246 247 #assertNonEmptyArrayWithStrings(array, variableName) { 248 lazy.assert.isNonEmptyArray( 249 array, 250 `Expected "${variableName}" to be a non-empty array, ` + 251 lazy.pprint`got ${array}` 252 ); 253 array.forEach(item => { 254 lazy.assert.string( 255 item, 256 `Expected elements of "${variableName}" to be a string, ` + 257 lazy.pprint`got ${item}` 258 ); 259 }); 260 } 261 262 #createListener( 263 enable, 264 { eventName, traversableId = null, userContextId = null } 265 ) { 266 let contextDescriptor; 267 268 if (traversableId === null && userContextId === null) { 269 contextDescriptor = { 270 type: lazy.ContextDescriptorType.All, 271 }; 272 } else if (userContextId !== null) { 273 contextDescriptor = { 274 type: lazy.ContextDescriptorType.UserContext, 275 id: userContextId, 276 }; 277 } else { 278 const traversable = 279 lazy.NavigableManager.getBrowsingContextById(traversableId); 280 281 if (traversable === null) { 282 return null; 283 } 284 285 contextDescriptor = { 286 type: lazy.ContextDescriptorType.TopBrowsingContext, 287 id: traversable.browserId, 288 }; 289 } 290 291 return { 292 event: eventName, 293 contextDescriptor, 294 callback: this.#onMessageHandlerEvent, 295 enable, 296 }; 297 } 298 299 #createListenerToSubscribe(params) { 300 return this.#createListener(true, params); 301 } 302 303 #createListenerToUnsubscribe(params) { 304 return this.#createListener(false, params); 305 } 306 307 /** 308 * Get a set of top-level traversables for which an event is enabled. 309 * 310 * @see https://w3c.github.io/webdriver-bidi/#set-of-top-level-traversables-for-which-an-event-is-enabled 311 * 312 * @param {string} eventName 313 * The name of the event. 314 * 315 * @returns {Array<BrowsingContext>} 316 * The list of top-level traversables. 317 */ 318 #getEnabledTopLevelTraversables(eventName) { 319 let result = new Set(); 320 321 for (const subscription of this.#getSubscriptionsForEvent(eventName)) { 322 const { topLevelTraversableIds } = subscription; 323 324 if (this.#isSubscriptionGlobal(subscription)) { 325 for (const traversable of lazy.TabManager.allTabs) { 326 result.add(traversable); 327 } 328 329 break; 330 } 331 332 result = this.#getNavigablesByIds(topLevelTraversableIds); 333 } 334 335 return result; 336 } 337 338 #getListenersToSubscribe( 339 eventNames, 340 includeGlobal, 341 subscribeStepEvents, 342 userContextIds 343 ) { 344 const listeners = []; 345 346 for (const eventName of eventNames) { 347 if (includeGlobal) { 348 // Since we're going to subscribe to all top-level 349 // traversable ids to not have duplicate subscriptions, 350 // we have to unsubscribe from already subscribed. 351 const alreadyEnabledTraversableIds = 352 this.#obtainEventEnabledTraversableIds(eventName); 353 for (const traversableId of alreadyEnabledTraversableIds) { 354 listeners.push( 355 this.#createListenerToUnsubscribe({ 356 eventName, 357 traversableId, 358 }) 359 ); 360 } 361 362 // Also unsubscribe from already subscribed user contexts. 363 const alreadyEnabledUserContextIds = 364 this.#obtainEventEnabledUserContextIds(eventName); 365 for (const userContextId of alreadyEnabledUserContextIds) { 366 listeners.push( 367 this.#createListenerToUnsubscribe({ 368 eventName, 369 userContextId, 370 }) 371 ); 372 } 373 374 listeners.push(this.#createListenerToSubscribe({ eventName })); 375 } else if (userContextIds.size !== 0) { 376 for (const userContextId of userContextIds) { 377 // Do nothing if the event has already a global subscription. 378 if (this.#hasGlobalEventSubscription(eventName)) { 379 continue; 380 } 381 382 // Since we're going to subscribe to all top-level 383 // traversable ids which belongs to the certain user context 384 // to not have duplicate subscriptions, 385 // we have to unsubscribe from already subscribed. 386 const alreadyEnabledTraversableIds = 387 this.#obtainEventEnabledTraversableIds(eventName, userContextId); 388 for (const traversableId of alreadyEnabledTraversableIds) { 389 listeners.push( 390 this.#createListenerToUnsubscribe({ 391 eventName, 392 traversableId, 393 }) 394 ); 395 } 396 397 listeners.push( 398 this.#createListenerToSubscribe({ eventName, userContextId }) 399 ); 400 } 401 } else { 402 for (const navigable of subscribeStepEvents.get(eventName)) { 403 // Do nothing if the event has already a global subscription 404 // or subscription to the associated user context. 405 if ( 406 this.#hasGlobalEventSubscription(eventName) || 407 this.#hasSubscriptionByAssociatedUserContext(eventName, navigable) 408 ) { 409 continue; 410 } 411 412 const traversableId = 413 lazy.NavigableManager.getIdForBrowsingContext(navigable); 414 listeners.push( 415 this.#createListenerToSubscribe({ 416 eventName, 417 traversableId, 418 }) 419 ); 420 } 421 } 422 } 423 424 return listeners; 425 } 426 427 #getListenersToUnsubscribe(subscription) { 428 const { eventNames, topLevelTraversableIds, userContextIds } = subscription; 429 const listeners = []; 430 431 for (const eventName of eventNames) { 432 // Do nothing if there is a global subscription. 433 if (this.#hasGlobalEventSubscription(eventName)) { 434 continue; 435 } 436 437 if (this.#isSubscriptionGlobal(subscription)) { 438 listeners.push( 439 ...this.#getListenersToUnsubscribeFromGlobalSubscription(eventName) 440 ); 441 } else if (userContextIds.size !== 0) { 442 for (const userContextId of userContextIds) { 443 listeners.push( 444 ...this.#getListenersToUnsubscribeFromUserContext( 445 eventName, 446 userContextId 447 ) 448 ); 449 } 450 } else { 451 for (const traversableId of topLevelTraversableIds) { 452 listeners.push( 453 this.#getListenersToUnsubscribeFromTraversable( 454 eventName, 455 traversableId 456 ) 457 ); 458 } 459 } 460 } 461 462 return listeners; 463 } 464 465 #getListenersToUnsubscribeFromGlobalSubscription(eventName) { 466 // Unsubscribe from the global subscription. 467 const listeners = [this.#createListenerToUnsubscribe({ eventName })]; 468 469 // Subscribe again to user contexts which have a subscription and 470 // to traversables which have individual subscriptions, 471 // but are not associated with subscribed user contexts. 472 for (const item of this.#getSubscriptionsForEvent(eventName)) { 473 for (const userContextId of item.userContextIds) { 474 listeners.push( 475 this.#createListenerToSubscribe({ 476 eventName, 477 userContextId, 478 }) 479 ); 480 } 481 482 for (const traversableId of item.topLevelTraversableIds) { 483 const traversable = 484 lazy.NavigableManager.getBrowsingContextById(traversableId); 485 486 // Do nothing if traversable doesn't exist anymore or 487 // there is already a subscription to the associated user context. 488 if ( 489 traversable === null || 490 this.#hasSubscriptionByAssociatedUserContext(eventName, traversable) 491 ) { 492 continue; 493 } 494 495 listeners.push( 496 this.#createListenerToSubscribe({ 497 eventName, 498 traversableId, 499 }) 500 ); 501 } 502 } 503 504 return listeners; 505 } 506 507 #getListenersToUnsubscribeFromTraversable(eventName, traversableId) { 508 // Do nothing if traversable is already closed or still has another subscription. 509 const traversable = 510 lazy.NavigableManager.getBrowsingContextById(traversableId); 511 512 if ( 513 traversable === null || 514 this.#hasSubscriptionByAssociatedUserContext(eventName, traversable) || 515 this.#hasSubscriptionByTraversableId(eventName, traversableId) 516 ) { 517 return null; 518 } 519 520 return this.#createListenerToUnsubscribe({ 521 eventName, 522 traversableId, 523 }); 524 } 525 526 #getListenersToUnsubscribeFromUserContext(eventName, userContextId) { 527 // Do nothing if there is another subscription for this user context. 528 if (this.#hasSubscriptionByUserContextId(eventName, userContextId)) { 529 return []; 530 } 531 532 // Unsubscribe from the user context. 533 const listeners = [ 534 this.#createListenerToUnsubscribe({ eventName, userContextId }), 535 ]; 536 537 // Resubscribe to traversables which are associated with this user context and 538 // have individual subscriptions. 539 const alreadyEnabledTraversableIds = this.#obtainEventEnabledTraversableIds( 540 eventName, 541 userContextId 542 ); 543 for (const traversableId of alreadyEnabledTraversableIds) { 544 listeners.push( 545 this.#createListenerToSubscribe({ 546 eventName, 547 traversableId, 548 }) 549 ); 550 } 551 552 return listeners; 553 } 554 555 /** 556 * Get a list of navigables by provided ids. 557 * 558 * @see https://w3c.github.io/webdriver-bidi/#get-navigables-by-ids 559 * 560 * @param {Set<string>} navigableIds 561 * The set of the navigable ids. 562 * 563 * @returns {Set<BrowsingContext>} 564 * The set of navigables. 565 */ 566 #getNavigablesByIds(navigableIds) { 567 const result = new Set(); 568 569 for (const navigableId of navigableIds) { 570 const navigable = 571 lazy.NavigableManager.getBrowsingContextById(navigableId); 572 573 if (navigable !== null) { 574 result.add(navigable); 575 } 576 } 577 578 return result; 579 } 580 581 #getRootModuleClass(moduleName) { 582 // Modules which support event subscriptions should have a root module 583 // defining supported events. 584 const rootDestination = { type: lazy.RootMessageHandler.type }; 585 const moduleClasses = this.messageHandler.getAllModuleClasses( 586 moduleName, 587 rootDestination 588 ); 589 590 if (!moduleClasses.length) { 591 throw new lazy.error.InvalidArgumentError( 592 `Module ${moduleName} does not exist` 593 ); 594 } 595 596 return moduleClasses[0]; 597 } 598 599 #getSubscriptionsForEvent(eventName) { 600 return this.#subscriptions.filter(({ eventNames }) => 601 eventNames.has(eventName) 602 ); 603 } 604 605 #getTopLevelTraversableContextIds(contextIds) { 606 const topLevelTraversableContextIds = new Set(); 607 const inputContextIds = new Set(contextIds); 608 609 if (inputContextIds.size !== 0) { 610 const navigables = this.#getValidNavigablesByIds(inputContextIds); 611 const topLevelTraversable = this.#getTopLevelTraversables(navigables); 612 613 for (const navigable of topLevelTraversable) { 614 topLevelTraversableContextIds.add( 615 lazy.NavigableManager.getIdForBrowsingContext(navigable) 616 ); 617 } 618 } 619 620 return topLevelTraversableContextIds; 621 } 622 623 /** 624 * Get a list of top-level traversables for provided navigables. 625 * 626 * @see https://w3c.github.io/webdriver-bidi/#get-top-level-traversables 627 * 628 * @param {Array<BrowsingContext>} navigables 629 * The list of the navigables. 630 * 631 * @returns {Set<BrowsingContext>} 632 * The set of top-level traversables. 633 */ 634 #getTopLevelTraversables(navigables) { 635 const result = new Set(); 636 637 for (const { top } of navigables) { 638 result.add(top); 639 } 640 641 return result; 642 } 643 644 /** 645 * Get a list of valid navigables by provided ids. 646 * 647 * @see https://w3c.github.io/webdriver-bidi/#get-valid-navigables-by-ids 648 * 649 * @param {Set<string>} navigableIds 650 * The set of the navigable ids. 651 * 652 * @returns {Set<BrowsingContext>} 653 * The set of navigables. 654 * @throws {NoSuchFrameError} 655 * If the navigable cannot be found. 656 */ 657 #getValidNavigablesByIds(navigableIds) { 658 const result = new Set(); 659 660 for (const navigableId of navigableIds) { 661 result.add(this._getNavigable(navigableId)); 662 } 663 664 return result; 665 } 666 667 #hasGlobalEventSubscription(eventName) { 668 for (const subscription of this.#getSubscriptionsForEvent(eventName)) { 669 if (this.#isSubscriptionGlobal(subscription)) { 670 return true; 671 } 672 } 673 674 return false; 675 } 676 677 // Check if for a given event name and traversable there is 678 // a subscription for a user context associated with this traversable. 679 #hasSubscriptionByAssociatedUserContext(eventName, traversable) { 680 if (traversable === null) { 681 return false; 682 } 683 684 return this.#hasSubscriptionByUserContextId( 685 eventName, 686 traversable.originAttributes.userContextId 687 ); 688 } 689 690 #hasSubscriptionByTraversableId(eventName, traversableId) { 691 for (const subscription of this.#getSubscriptionsForEvent(eventName)) { 692 const { topLevelTraversableIds } = subscription; 693 694 for (const topLevelTraversableId of topLevelTraversableIds) { 695 if (topLevelTraversableId === traversableId) { 696 return true; 697 } 698 } 699 } 700 701 return false; 702 } 703 704 #hasSubscriptionByUserContextId(eventName, userContextId) { 705 for (const subscription of this.#getSubscriptionsForEvent(eventName)) { 706 const { userContextIds } = subscription; 707 708 if (userContextIds.has(userContextId)) { 709 return true; 710 } 711 } 712 713 return false; 714 } 715 716 /** 717 * Identify if a provided subscription is global. 718 * 719 * @see https://w3c.github.io/webdriver-bidi/#subscription-global 720 * 721 * @param {Subscription} subscription 722 * A subscription object. 723 * 724 * @returns {boolean} 725 * Return true if the subscription is global, false otherwise. 726 */ 727 #isSubscriptionGlobal(subscription) { 728 return ( 729 subscription.topLevelTraversableIds.size === 0 && 730 subscription.userContextIds.size === 0 731 ); 732 } 733 734 /** 735 * Obtain a list of event enabled traversable ids. 736 * 737 * @param {string} eventName 738 * The name of the event. 739 * @param {string=} userContextId 740 * The user context id. 741 * 742 * @returns {Set<string>} 743 * The set of traversable ids. 744 */ 745 #obtainEventEnabledTraversableIds(eventName, userContextId = null) { 746 let traversableIds = new Set(); 747 748 for (const { topLevelTraversableIds } of this.#getSubscriptionsForEvent( 749 eventName 750 )) { 751 if (topLevelTraversableIds.size === 0) { 752 continue; 753 } 754 755 if (userContextId === null) { 756 traversableIds = traversableIds.union(topLevelTraversableIds); 757 continue; 758 } 759 760 for (const traversableId of topLevelTraversableIds) { 761 const traversable = 762 lazy.NavigableManager.getBrowsingContextById(traversableId); 763 764 if (traversable === null) { 765 continue; 766 } 767 768 if (traversable.originAttributes.userContextId === userContextId) { 769 traversableIds.add(traversableId); 770 } 771 } 772 } 773 774 return traversableIds; 775 } 776 777 #obtainEventEnabledUserContextIds(eventName) { 778 let enabledUserContextIds = new Set(); 779 780 for (const { userContextIds } of this.#getSubscriptionsForEvent( 781 eventName 782 )) { 783 enabledUserContextIds = enabledUserContextIds.union(userContextIds); 784 } 785 786 return enabledUserContextIds; 787 } 788 789 /** 790 * Obtain a set of events based on the given event name. 791 * 792 * Could contain a period for a specific event, 793 * or just the module name for all events. 794 * 795 * @param {string} event 796 * Name of the event to process. 797 * 798 * @returns {Set<string>} 799 * A Set with the expanded events in the form of `<module>.<event>`. 800 * 801 * @throws {InvalidArgumentError} 802 * If <var>event</var> does not reference a valid event. 803 */ 804 #obtainEvents(event) { 805 const events = new Set(); 806 807 // Check if a period is present that splits the event name into the module, 808 // and the actual event. Hereby only care about the first found instance. 809 const index = event.indexOf("."); 810 if (index >= 0) { 811 const [moduleName] = event.split("."); 812 this.#assertModuleSupportsEvent(moduleName, event); 813 events.add(event); 814 } else { 815 // Interpret the name as module, and register all its available events 816 const rootModuleClass = this.#getRootModuleClass(event); 817 const supportedEvents = rootModuleClass?.supportedEvents; 818 819 for (const eventName of supportedEvents) { 820 events.add(eventName); 821 } 822 } 823 824 return events; 825 } 826 827 #onMessageHandlerEvent = (name, event) => { 828 this.messageHandler.emitProtocolEvent(name, event); 829 }; 830 831 #unsubscribeByAttributes(events, contextIds) { 832 const listeners = []; 833 834 // Check input types until we run schema validation. 835 this.#assertNonEmptyArrayWithStrings(events, "events"); 836 if (contextIds !== null) { 837 this.#assertNonEmptyArrayWithStrings(contextIds, "contexts"); 838 } 839 840 const eventNames = new Set(); 841 events.forEach(name => { 842 this.#obtainEvents(name).forEach(event => eventNames.add(event)); 843 }); 844 845 const topLevelTraversableContextIds = 846 this.#getTopLevelTraversableContextIds(contextIds); 847 848 const newSubscriptions = []; 849 const matchedEvents = new Set(); 850 const matchedContexts = new Set(); 851 852 for (const subscription of this.#subscriptions) { 853 // Keep subscription if it doesn't contain any target events. 854 if (subscription.eventNames.intersection(eventNames).size === 0) { 855 newSubscriptions.push(subscription); 856 continue; 857 } 858 859 // Unsubscribe globally. 860 if (topLevelTraversableContextIds.size === 0) { 861 // Keep subscription if verified subscription is not global. 862 if (!this.#isSubscriptionGlobal(subscription)) { 863 newSubscriptions.push(subscription); 864 continue; 865 } 866 867 // Delete event names from the subscription. 868 const subscriptionEventNames = new Set(subscription.eventNames); 869 for (const eventName of eventNames) { 870 if (subscriptionEventNames.has(eventName)) { 871 matchedEvents.add(eventName); 872 subscriptionEventNames.delete(eventName); 873 874 listeners.push(this.#createListenerToUnsubscribe({ eventName })); 875 } 876 } 877 878 // If the subscription still contains some event, 879 // save a new partial subscription. 880 if (subscriptionEventNames.size !== 0) { 881 const clonedSubscription = { 882 subscriptionId: subscription.subscriptionId, 883 eventNames: new Set(subscriptionEventNames), 884 topLevelTraversableIds: new Set(), 885 userContextIds: new Set(subscription.userContextIds), 886 }; 887 newSubscriptions.push(clonedSubscription); 888 } 889 } 890 // Keep the subscription if it's global but we want to unsubscribe only from some contexts. 891 else if (this.#isSubscriptionGlobal(subscription)) { 892 newSubscriptions.push(subscription); 893 } else { 894 // Map with an event name as a key and the set of subscribed traversable ids as a value. 895 const eventMap = new Map(); 896 897 // Populate the map. 898 for (const eventName of subscription.eventNames) { 899 eventMap.set(eventName, new Set(subscription.topLevelTraversableIds)); 900 } 901 902 for (const eventName of eventNames) { 903 // Skip if there is no subscription related to this event. 904 if (!eventMap.has(eventName)) { 905 continue; 906 } 907 908 for (const topLevelTraversableId of topLevelTraversableContextIds) { 909 // Skip if there is no subscription related to this event and this traversable id. 910 if (!eventMap.get(eventName).has(topLevelTraversableId)) { 911 continue; 912 } 913 914 matchedContexts.add(topLevelTraversableId); 915 matchedEvents.add(eventName); 916 eventMap.get(eventName).delete(topLevelTraversableId); 917 918 listeners.push( 919 this.#createListenerToUnsubscribe({ 920 eventName, 921 traversableId: topLevelTraversableId, 922 }) 923 ); 924 } 925 926 if (eventMap.get(eventName).size === 0) { 927 eventMap.delete(eventName); 928 } 929 } 930 931 // Build new partial subscriptions based on the remaining data in eventMap. 932 for (const [ 933 eventName, 934 remainingTopLevelTraversableIds, 935 ] of eventMap.entries()) { 936 const partialSubscription = { 937 subscriptionId: subscription.subscriptionId, 938 eventNames: new Set([eventName]), 939 topLevelTraversableIds: remainingTopLevelTraversableIds, 940 userContextIds: new Set(subscription.userContextIds), 941 }; 942 943 newSubscriptions.push(partialSubscription); 944 945 const traversableIdsToUnsubscribe = 946 subscription.topLevelTraversableIds.difference( 947 remainingTopLevelTraversableIds 948 ); 949 950 for (const traversableId of traversableIdsToUnsubscribe) { 951 listeners.push( 952 this.#createListenerToUnsubscribe({ eventName, traversableId }) 953 ); 954 } 955 } 956 } 957 } 958 959 if (matchedEvents.symmetricDifference(eventNames).size > 0) { 960 throw new lazy.error.InvalidArgumentError( 961 `Failed to unsubscribe from events: ${Array.from(eventNames).join(", ")}` 962 ); 963 } 964 if ( 965 topLevelTraversableContextIds.size > 0 && 966 matchedContexts.symmetricDifference(topLevelTraversableContextIds).size > 967 0 968 ) { 969 throw new lazy.error.InvalidArgumentError( 970 `Failed to unsubscribe from events: ${Array.from(eventNames).join(", ")} for context ids: ${Array.from(topLevelTraversableContextIds).join(", ")}` 971 ); 972 } 973 974 this.#subscriptions = newSubscriptions; 975 976 return listeners; 977 } 978 979 #unsubscribeById(subscriptionIds) { 980 this.#assertNonEmptyArrayWithStrings(subscriptionIds, "subscriptions"); 981 982 const subscriptions = new Set(subscriptionIds); 983 const unknownSubscriptionIds = subscriptions.difference( 984 this.#knownSubscriptionIds 985 ); 986 987 if (unknownSubscriptionIds.size !== 0) { 988 throw new lazy.error.InvalidArgumentError( 989 `Failed to unsubscribe from subscriptions with ids: ${Array.from(subscriptionIds).join(", ")} ` + 990 `(unknown ids: ${Array.from(unknownSubscriptionIds).join(", ")})` 991 ); 992 } 993 994 const listeners = []; 995 const subscriptionIdsToRemove = new Set(); 996 const subscriptionsToRemove = new Set(); 997 998 for (const subscription of this.#subscriptions) { 999 const { subscriptionId } = subscription; 1000 1001 if (!subscriptions.has(subscriptionId)) { 1002 continue; 1003 } 1004 1005 subscriptionIdsToRemove.add(subscriptionId); 1006 subscriptionsToRemove.add(subscription); 1007 } 1008 1009 this.#knownSubscriptionIds = 1010 this.#knownSubscriptionIds.difference(subscriptions); 1011 this.#subscriptions = this.#subscriptions.filter( 1012 ({ subscriptionId }) => !subscriptionIdsToRemove.has(subscriptionId) 1013 ); 1014 1015 for (const subscription of subscriptionsToRemove) { 1016 listeners.push(...this.#getListenersToUnsubscribe(subscription)); 1017 } 1018 1019 return listeners; 1020 } 1021 } 1022 1023 // To export the class as lower-case 1024 export const session = SessionModule;