channelProxy.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. "use strict";
  2. /*
  3. * Copyright 2023 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. */
  19. Object.defineProperty(exports, "__esModule", { value: true });
  20. exports.ChannelProxy = void 0;
  21. const protocol_js_1 = require("../../../protocol/protocol.js");
  22. const uuid_1 = require("../../../utils/uuid");
  23. /**
  24. * Used to send messages from realm to BiDi user.
  25. */
  26. class ChannelProxy {
  27. #properties;
  28. #id = (0, uuid_1.uuidv4)();
  29. constructor(channel) {
  30. if (![0, null, undefined].includes(channel.serializationOptions?.maxDomDepth)) {
  31. throw new Error('serializationOptions.maxDomDepth other than 0 or null is not supported');
  32. }
  33. if (![undefined, 'none'].includes(channel.serializationOptions?.includeShadowTree)) {
  34. throw new Error('serializationOptions.includeShadowTree other than "none" is not supported');
  35. }
  36. this.#properties = channel;
  37. }
  38. /**
  39. * Creates a channel proxy in the given realm, initialises listener and
  40. * returns a handle to `sendMessage` delegate.
  41. */
  42. async init(realm, eventManager) {
  43. const channelHandle = await ChannelProxy.#createAndGetHandleInRealm(realm);
  44. const sendMessageHandle = await ChannelProxy.#createSendMessageHandle(realm, channelHandle);
  45. void this.#startListener(realm, channelHandle, eventManager);
  46. return sendMessageHandle;
  47. }
  48. /** Gets a ChannelProxy from window and returns its handle. */
  49. async startListenerFromWindow(realm, eventManager) {
  50. const channelHandle = await this.#getHandleFromWindow(realm);
  51. void this.#startListener(realm, channelHandle, eventManager);
  52. }
  53. /**
  54. * Evaluation string which creates a ChannelProxy object on the client side.
  55. */
  56. static #createChannelProxyEvalStr() {
  57. const functionStr = String(() => {
  58. const queue = [];
  59. let queueNonEmptyResolver = null;
  60. return {
  61. /**
  62. * Gets a promise, which is resolved as soon as a message occurs
  63. * in the queue.
  64. */
  65. async getMessage() {
  66. const onMessage = queue.length > 0
  67. ? Promise.resolve()
  68. : new Promise((resolve) => {
  69. queueNonEmptyResolver = resolve;
  70. });
  71. await onMessage;
  72. return queue.shift();
  73. },
  74. /**
  75. * Adds a message to the queue.
  76. * Resolves the pending promise if needed.
  77. */
  78. sendMessage(message) {
  79. queue.push(message);
  80. if (queueNonEmptyResolver !== null) {
  81. queueNonEmptyResolver();
  82. queueNonEmptyResolver = null;
  83. }
  84. },
  85. };
  86. });
  87. return `(${functionStr})()`;
  88. }
  89. /** Creates a ChannelProxy in the given realm. */
  90. static async #createAndGetHandleInRealm(realm) {
  91. const createChannelHandleResult = await realm.cdpClient.sendCommand('Runtime.evaluate', {
  92. expression: this.#createChannelProxyEvalStr(),
  93. contextId: realm.executionContextId,
  94. serializationOptions: {
  95. serialization: 'idOnly',
  96. },
  97. });
  98. if (createChannelHandleResult.exceptionDetails ||
  99. createChannelHandleResult.result.objectId === undefined) {
  100. throw new Error(`Cannot create channel`);
  101. }
  102. return createChannelHandleResult.result.objectId;
  103. }
  104. /** Gets a handle to `sendMessage` delegate from the ChannelProxy handle. */
  105. static async #createSendMessageHandle(realm, channelHandle) {
  106. const sendMessageArgResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  107. functionDeclaration: String((channelHandle) => {
  108. return channelHandle.sendMessage;
  109. }),
  110. arguments: [{ objectId: channelHandle }],
  111. executionContextId: realm.executionContextId,
  112. serializationOptions: {
  113. serialization: 'idOnly',
  114. },
  115. });
  116. // TODO: check for exceptionDetails.
  117. return sendMessageArgResult.result.objectId;
  118. }
  119. /** Starts listening for the channel events of the provided ChannelProxy. */
  120. async #startListener(realm, channelHandle, eventManager) {
  121. // TODO(#294): Remove this loop after the realm is destroyed.
  122. // Rely on the CDP throwing exception in such a case.
  123. // noinspection InfiniteLoopJS
  124. for (;;) {
  125. const message = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  126. functionDeclaration: String(async (channelHandle) => channelHandle.getMessage()),
  127. arguments: [
  128. {
  129. objectId: channelHandle,
  130. },
  131. ],
  132. awaitPromise: true,
  133. executionContextId: realm.executionContextId,
  134. serializationOptions: {
  135. serialization: 'deep',
  136. ...(this.#properties.serializationOptions?.maxObjectDepth ===
  137. undefined ||
  138. this.#properties.serializationOptions.maxObjectDepth === null
  139. ? {}
  140. : {
  141. maxDepth: this.#properties.serializationOptions.maxObjectDepth,
  142. }),
  143. },
  144. });
  145. if (message.exceptionDetails) {
  146. // TODO: add logging.
  147. // TODO: check if a error should be thrown.
  148. return;
  149. }
  150. eventManager.registerEvent({
  151. method: protocol_js_1.Script.EventNames.MessageEvent,
  152. params: {
  153. channel: this.#properties.channel,
  154. data: realm.cdpToBidiValue(message, this.#properties.ownership ?? 'none'),
  155. source: {
  156. realm: realm.realmId,
  157. context: realm.browsingContextId,
  158. },
  159. },
  160. }, realm.browsingContextId);
  161. }
  162. }
  163. /**
  164. * Returns a handle of ChannelProxy from window's property which was set there
  165. * by `getEvalInWindowStr`. If window property is not set yet, sets a promise
  166. * resolver to the window property, so that `getEvalInWindowStr` can resolve
  167. * the promise later on with the channel.
  168. * This is needed because `getEvalInWindowStr` can be called before or
  169. * after this method.
  170. */
  171. async #getHandleFromWindow(realm) {
  172. const channelHandleResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  173. functionDeclaration: String((id) => {
  174. const w = window;
  175. if (w[id] === undefined) {
  176. // The channelProxy is not created yet. Create a promise, put the
  177. // resolver to window property and return the promise.
  178. // `getEvalInWindowStr` will resolve the promise later.
  179. return new Promise((resolve) => (w[id] = resolve));
  180. }
  181. // The channelProxy is already created by `getEvalInWindowStr` and
  182. // is set into window property. Return it.
  183. const channelProxy = w[id];
  184. delete w[id];
  185. return channelProxy;
  186. }),
  187. arguments: [{ value: this.#id }],
  188. executionContextId: realm.executionContextId,
  189. awaitPromise: true,
  190. serializationOptions: {
  191. serialization: 'idOnly',
  192. },
  193. });
  194. if (channelHandleResult.exceptionDetails !== undefined ||
  195. channelHandleResult.result.objectId === undefined) {
  196. throw new Error(`ChannelHandle not found in window["${this.#id}"]`);
  197. }
  198. return channelHandleResult.result.objectId;
  199. }
  200. /**
  201. * String to be evaluated to create a ProxyChannel and put it to window.
  202. * Returns the delegate `sendMessage`. Used to provide an argument for preload
  203. * script. Does the following:
  204. * 1. Creates a ChannelProxy.
  205. * 2. Puts the ChannelProxy to window['${this.#id}'] or resolves the promise
  206. * by calling delegate stored in window['${this.#id}'].
  207. * This is needed because `#getHandleFromWindow` can be called before or
  208. * after this method.
  209. * 3. Returns the delegate `sendMessage` of the created ChannelProxy.
  210. */
  211. getEvalInWindowStr() {
  212. const delegate = String((id, channelProxy) => {
  213. const w = window;
  214. if (w[id] === undefined) {
  215. // `#getHandleFromWindow` is not initialized yet, and will get the
  216. // channelProxy later.
  217. w[id] = channelProxy;
  218. }
  219. else {
  220. // `#getHandleFromWindow` is already set a delegate to window property
  221. // and is waiting for it to be called with the channelProxy.
  222. w[id](channelProxy);
  223. delete w[id];
  224. }
  225. return channelProxy.sendMessage;
  226. });
  227. const channelProxyEval = ChannelProxy.#createChannelProxyEvalStr();
  228. return `(${delegate})('${this.#id}',${channelProxyEval})`;
  229. }
  230. }
  231. exports.ChannelProxy = ChannelProxy;
  232. //# sourceMappingURL=channelProxy.js.map