SubscriptionManager.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. "use strict";
  2. /**
  3. * Copyright 2022 Google LLC.
  4. * Copyright (c) Microsoft Corporation.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.SubscriptionManager = exports.unrollEvents = exports.cartesianProduct = void 0;
  20. const protocol_js_1 = require("../../../protocol/protocol.js");
  21. /**
  22. * Returns the cartesian product of the given arrays.
  23. *
  24. * Example:
  25. * cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
  26. */
  27. function cartesianProduct(...a) {
  28. return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
  29. }
  30. exports.cartesianProduct = cartesianProduct;
  31. /** Expands "AllEvents" events into atomic events. */
  32. function unrollEvents(events) {
  33. const allEvents = new Set();
  34. function addEvents(events) {
  35. for (const event of events) {
  36. allEvents.add(event);
  37. }
  38. }
  39. for (const event of events) {
  40. switch (event) {
  41. case protocol_js_1.BrowsingContext.AllEvents:
  42. addEvents(Object.values(protocol_js_1.BrowsingContext.EventNames));
  43. break;
  44. case protocol_js_1.Log.AllEvents:
  45. addEvents(Object.values(protocol_js_1.Log.EventNames));
  46. break;
  47. case protocol_js_1.Network.AllEvents:
  48. addEvents(Object.values(protocol_js_1.Network.EventNames));
  49. break;
  50. case protocol_js_1.Script.AllEvents:
  51. addEvents(Object.values(protocol_js_1.Script.EventNames));
  52. break;
  53. default:
  54. allEvents.add(event);
  55. }
  56. }
  57. return [...allEvents.values()];
  58. }
  59. exports.unrollEvents = unrollEvents;
  60. class SubscriptionManager {
  61. #subscriptionPriority = 0;
  62. // BrowsingContext `null` means the event has subscription across all the
  63. // browsing contexts.
  64. // Channel `null` means no `channel` should be added.
  65. #channelToContextToEventMap = new Map();
  66. #browsingContextStorage;
  67. constructor(browsingContextStorage) {
  68. this.#browsingContextStorage = browsingContextStorage;
  69. }
  70. getChannelsSubscribedToEvent(eventMethod, contextId) {
  71. const prioritiesAndChannels = Array.from(this.#channelToContextToEventMap.keys())
  72. .map((channel) => ({
  73. priority: this.#getEventSubscriptionPriorityForChannel(eventMethod, contextId, channel),
  74. channel,
  75. }))
  76. .filter(({ priority }) => priority !== null);
  77. // Sort channels by priority.
  78. return prioritiesAndChannels
  79. .sort((a, b) => a.priority - b.priority)
  80. .map(({ channel }) => channel);
  81. }
  82. #getEventSubscriptionPriorityForChannel(eventMethod, contextId, channel) {
  83. const contextToEventMap = this.#channelToContextToEventMap.get(channel);
  84. if (contextToEventMap === undefined) {
  85. return null;
  86. }
  87. const maybeTopLevelContextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
  88. // `null` covers global subscription.
  89. const relevantContexts = [...new Set([null, maybeTopLevelContextId])];
  90. // Get all the subscription priorities.
  91. const priorities = relevantContexts
  92. .map((c) => contextToEventMap.get(c)?.get(eventMethod))
  93. .filter((p) => p !== undefined);
  94. if (priorities.length === 0) {
  95. // Not subscribed, return null.
  96. return null;
  97. }
  98. // Return minimal priority.
  99. return Math.min(...priorities);
  100. }
  101. subscribe(event, contextId, channel) {
  102. // All the subscriptions are handled on the top-level contexts.
  103. contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
  104. if (event === protocol_js_1.BrowsingContext.AllEvents) {
  105. Object.values(protocol_js_1.BrowsingContext.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel));
  106. return;
  107. }
  108. if (event === protocol_js_1.Log.AllEvents) {
  109. Object.values(protocol_js_1.Log.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel));
  110. return;
  111. }
  112. if (event === protocol_js_1.Network.AllEvents) {
  113. Object.values(protocol_js_1.Network.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel));
  114. return;
  115. }
  116. if (event === protocol_js_1.Script.AllEvents) {
  117. Object.values(protocol_js_1.Script.EventNames).map((specificEvent) => this.subscribe(specificEvent, contextId, channel));
  118. return;
  119. }
  120. if (!this.#channelToContextToEventMap.has(channel)) {
  121. this.#channelToContextToEventMap.set(channel, new Map());
  122. }
  123. const contextToEventMap = this.#channelToContextToEventMap.get(channel);
  124. if (!contextToEventMap.has(contextId)) {
  125. contextToEventMap.set(contextId, new Map());
  126. }
  127. const eventMap = contextToEventMap.get(contextId);
  128. // Do not re-subscribe to events to keep the priority.
  129. if (eventMap.has(event)) {
  130. return;
  131. }
  132. eventMap.set(event, this.#subscriptionPriority++);
  133. }
  134. /**
  135. * Unsubscribes atomically from all events in the given contexts and channel.
  136. */
  137. unsubscribeAll(events, contextIds, channel) {
  138. // Assert all contexts are known.
  139. for (const contextId of contextIds) {
  140. if (contextId !== null) {
  141. this.#browsingContextStorage.getContext(contextId);
  142. }
  143. }
  144. const eventContextPairs = cartesianProduct(unrollEvents(events), contextIds);
  145. // Assert all unsubscriptions are valid.
  146. // If any of the unsubscriptions are invalid, do not unsubscribe from anything.
  147. eventContextPairs
  148. .map(([event, contextId]) => this.#checkUnsubscribe(event, contextId, channel))
  149. .forEach((unsubscribe) => unsubscribe());
  150. }
  151. /**
  152. * Unsubscribes from the event in the given context and channel.
  153. * Syntactic sugar for "unsubscribeAll".
  154. */
  155. unsubscribe(eventName, contextId, channel) {
  156. this.unsubscribeAll([eventName], [contextId], channel);
  157. }
  158. #checkUnsubscribe(event, contextId, channel) {
  159. // All the subscriptions are handled on the top-level contexts.
  160. contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
  161. if (!this.#channelToContextToEventMap.has(channel)) {
  162. throw new protocol_js_1.Message.InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
  163. }
  164. const contextToEventMap = this.#channelToContextToEventMap.get(channel);
  165. if (!contextToEventMap.has(contextId)) {
  166. throw new protocol_js_1.Message.InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
  167. }
  168. const eventMap = contextToEventMap.get(contextId);
  169. if (!eventMap.has(event)) {
  170. throw new protocol_js_1.Message.InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
  171. }
  172. return () => {
  173. eventMap.delete(event);
  174. // Clean up maps if empty.
  175. if (eventMap.size === 0) {
  176. contextToEventMap.delete(event);
  177. }
  178. if (contextToEventMap.size === 0) {
  179. this.#channelToContextToEventMap.delete(channel);
  180. }
  181. };
  182. }
  183. }
  184. exports.SubscriptionManager = SubscriptionManager;
  185. //# sourceMappingURL=SubscriptionManager.js.map