@adobe/uix-core 0.7.1-nightly.20230226 → 0.7.1-nightly.20230228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ /**
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import type { HostMethodAddress } from "./types";
14
+ import { isFunction, isIterable, isPrimitive } from "./value-assertions";
15
+
16
+ /**
17
+ * Try and format any type of value for logging.
18
+ *
19
+ * @privateRemarks
20
+ * **WARNING**: This is an expensive operation due to the JSON.stringify, and
21
+ * should only be done when debugging or in error conditions.
22
+ * @internal
23
+ */
24
+ export function formatHostMethodArgument(argument: unknown): string {
25
+ try {
26
+ return JSON.stringify(argument, null, 2);
27
+ } catch (e) {
28
+ if (isIterable(argument)) {
29
+ return `Iterable<${argument.length}>`;
30
+ }
31
+ if (isPrimitive(argument) || isFunction(argument)) {
32
+ return `${argument}`;
33
+ }
34
+ return Object.prototype.toString.call(argument);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Try and format a remote method call as it would appear during debugging.
40
+ *
41
+ * @privateRemarks
42
+ * **WARNING**: This is an expensive operation due to the JSON.stringify, and
43
+ * should only be done when debugging or in error conditions. This Functions
44
+ * like {@link @adobe/uix-core/#timedPromise} which take logging strings also
45
+ * take callbacks for lazy evaluation of debugging messages. Use this only in
46
+ * such callbacks.
47
+ * @internal
48
+ */
49
+ export function formatHostMethodAddress(address: HostMethodAddress) {
50
+ const path =
51
+ address.path?.length < 1
52
+ ? "<Missing method path!>"
53
+ : address.path.join(".");
54
+ const name = address.name || "<Missing method name!>";
55
+ const args = address.args?.map(formatHostMethodArgument).join(",");
56
+ return `host.${path}.${name}(${args})`;
57
+ }
@@ -3,7 +3,7 @@
3
3
  * the original Promise, but if it doesn't resolve within the timeout interval,
4
4
  * it will reject with a timeout error.
5
5
  *
6
- * @param description - Job description to be used in the timeout error
6
+ * @param describe - Job description to be used in the timeout error
7
7
  * @param promise - Original promise to set a timeout for
8
8
  * @param timeoutMs - Time to wait (ms) before rejecting
9
9
  * @param onReject - Run when promise times out to clean up handles
@@ -12,21 +12,29 @@
12
12
  * @internal
13
13
  */
14
14
  export function timeoutPromise<T>(
15
- description: string,
15
+ describe: string | (() => string),
16
16
  promise: Promise<T>,
17
17
  ms: number,
18
- onReject: (e: Error) => void
18
+ onReject?: (e: Error) => void
19
19
  ): Promise<T> {
20
20
  return new Promise((resolve, reject) => {
21
21
  const cleanupAndReject = async (e: Error) => {
22
22
  try {
23
- await onReject(e);
23
+ if (onReject) {
24
+ await onReject(e);
25
+ }
24
26
  } finally {
25
27
  reject(e);
26
28
  }
27
29
  };
28
30
  const timeout = setTimeout(() => {
29
- cleanupAndReject(new Error(`${description} timed out after ${ms}ms`));
31
+ cleanupAndReject(
32
+ new Error(
33
+ `${
34
+ typeof describe === "function" ? describe() : describe
35
+ } timed out after ${ms}ms`
36
+ )
37
+ );
30
38
  }, ms);
31
39
  promise
32
40
  .then((result) => {
@@ -3,6 +3,22 @@ import { wait } from "../promises/wait";
3
3
  import { Tunnel } from "./tunnel";
4
4
  import { TunnelMessenger } from "./tunnel-messenger";
5
5
 
6
+ class FakeIframe {
7
+ contentWindow: MessagePort;
8
+ isConnected: boolean;
9
+ src: string;
10
+ private channel = new MessageChannel();
11
+ nodeName = "IFRAME";
12
+ constructor() {
13
+ this.contentWindow = new MessageChannel().port1;
14
+ }
15
+ destroy() {
16
+ this.channel.port1.close();
17
+ this.channel.port2.close();
18
+ this.channel = null;
19
+ }
20
+ }
21
+
6
22
  const fakeConsole = {
7
23
  error: jest.fn(),
8
24
  warn: jest.fn(),
@@ -20,7 +36,6 @@ function tunnelHarness(
20
36
  config = defaultTunnelConfig
21
37
  ): TunnelHarness {
22
38
  const tunnel = new Tunnel(config);
23
- tunnel.connect(port);
24
39
  openPorts.push(port);
25
40
  return {
26
41
  tunnel,
@@ -52,6 +67,10 @@ describe("an EventEmitter dispatching and receiving from a MessagePort", () => {
52
67
  local = tunnelHarness(channel.port1);
53
68
  remote = tunnelHarness(channel.port2);
54
69
  });
70
+ const connectTunnels = () => {
71
+ local.tunnel.connect(local.port);
72
+ remote.tunnel.connect(remote.port);
73
+ };
55
74
  afterEach(() => {
56
75
  while (openPorts.length > 0) {
57
76
  openPorts.pop().close();
@@ -60,6 +79,7 @@ describe("an EventEmitter dispatching and receiving from a MessagePort", () => {
60
79
  it("receives MessageEvents and emits local events to listeners", async () => {
61
80
  const test1Handler = jest.fn();
62
81
  local.tunnel.on("test1", test1Handler);
82
+ connectTunnels();
63
83
  remote.port.postMessage({
64
84
  type: "test1",
65
85
  payload: {
@@ -75,6 +95,7 @@ describe("an EventEmitter dispatching and receiving from a MessagePort", () => {
75
95
  const remoteConnectHandler = jest.fn();
76
96
  local.tunnel.on("connected", localConnectHandler);
77
97
  remote.tunnel.on("connected", remoteConnectHandler);
98
+ connectTunnels();
78
99
  await wait(100);
79
100
  expect(localConnectHandler).toHaveBeenCalledTimes(1);
80
101
  expect(remoteConnectHandler).toHaveBeenCalledTimes(1);
@@ -82,22 +103,19 @@ describe("an EventEmitter dispatching and receiving from a MessagePort", () => {
82
103
  it("#emitRemote() sends remote events after connect", async () => {
83
104
  const messageListener = jest.fn();
84
105
  remote.port.addEventListener("message", messageListener);
106
+ connectTunnels();
85
107
  local.tunnel.emit("test2", { test2Payload: true });
86
108
  local.tunnel.emit("test3", { test3Payload: true });
87
109
  await wait(10);
88
- expect(messageListener).toHaveBeenCalledTimes(3);
89
- const connectMessageEvent = messageListener.mock.calls[0][0];
90
- expect(connectMessageEvent).toHaveProperty("data", {
91
- type: "connected",
92
- });
93
- const test2MessageEvent = messageListener.mock.calls[1][0];
110
+ expect(messageListener).toHaveBeenCalledTimes(2);
111
+ const test2MessageEvent = messageListener.mock.calls[0][0];
94
112
  expect(test2MessageEvent).toHaveProperty("data", {
95
113
  type: "test2",
96
114
  payload: {
97
115
  test2Payload: true,
98
116
  },
99
117
  });
100
- const test3MessageEvent = messageListener.mock.calls[2][0];
118
+ const test3MessageEvent = messageListener.mock.calls[1][0];
101
119
  expect(test3MessageEvent).toHaveProperty("data", {
102
120
  type: "test3",
103
121
  payload: {
@@ -106,16 +124,17 @@ describe("an EventEmitter dispatching and receiving from a MessagePort", () => {
106
124
  });
107
125
  });
108
126
  it("exchanges events between two emitters sharing ports", async () => {
127
+ connectTunnels();
109
128
  await testEventExchange(local.tunnel, remote.tunnel);
110
129
  });
111
130
  it("#connect(port) accepts a new messageport", async () => {
112
131
  const connectHandler = jest.fn();
113
132
  local.tunnel.on("connected", connectHandler);
114
- remote.tunnel.on("reconnect", connectHandler);
115
133
  const confirmHandler = jest.fn();
116
134
  local.tunnel.on("confirm", confirmHandler);
117
135
  const dispelHandler = jest.fn();
118
136
  remote.tunnel.on("dispel", dispelHandler);
137
+ connectTunnels();
119
138
  local.tunnel.emit("dispel", { dispelled: 1 });
120
139
  remote.tunnel.emit("confirm", { confirmed: 1 });
121
140
  await wait(10);
@@ -165,14 +184,13 @@ describe("static Tunnel.toIframe(iframe, options)", () => {
165
184
  * https://github.com/jsdom/jsdom/blob/22f7c3c51829a6f14387f7a99e5cdf087f72e685/lib/jsdom/living/post-message.js#L31-L37
166
185
  */
167
186
  describe.skip("creates a Tunnel connected to an iframe", () => {
168
- it.only("listens for handshakes from the frame window", async () => {
187
+ it("listens for handshakes from the frame window", async () => {
169
188
  let remoteTunnel: Tunnel;
170
189
  const connectMessageHandler = jest.fn();
171
190
  const acceptListener = jest.fn();
172
191
  const targetOrigin = "https://example.com:4001";
173
- const loadedFrame = document.createElement("iframe");
192
+ const loadedFrame = new FakeIframe() as unknown as HTMLIFrameElement;
174
193
  loadedFrame.src = targetOrigin;
175
- document.body.appendChild(loadedFrame);
176
194
  loadedFrame.contentWindow.addEventListener("message", acceptListener);
177
195
  const localTunnel = Tunnel.toIframe(loadedFrame, {
178
196
  targetOrigin,
@@ -185,14 +203,18 @@ describe("static Tunnel.toIframe(iframe, options)", () => {
185
203
  });
186
204
  localTunnel.on("connected", connectMessageHandler);
187
205
  await wait(100);
188
- fireEvent(
189
- window,
190
- new MessageEvent("message", {
191
- data: messenger.makeOffered("iframe-test-1"),
192
- origin: loadedFrame.src,
193
- source: loadedFrame.contentWindow,
194
- })
206
+ window.postMessage(
207
+ messenger.makeOffered("iframe-test-1"),
208
+ loadedFrame.src
195
209
  );
210
+ // fireEvent(
211
+ // window,
212
+ // new MessageEvent("message", {
213
+ // data: messenger.makeOffered("iframe-test-1"),
214
+ // origin: loadedFrame.src,
215
+ // source: loadedFrame.contentWindow,
216
+ // })
217
+ // );
196
218
  await wait(100);
197
219
  expect(acceptListener).toHaveBeenCalled();
198
220
  const acceptEvent = acceptListener.mock.lastCall[0];
@@ -2,6 +2,7 @@ import EventEmitter from "eventemitter3";
2
2
  import { isIframe } from "../value-assertions";
3
3
  import { TunnelMessenger } from "./tunnel-messenger";
4
4
  import { unwrap } from "../message-wrapper";
5
+ import { quietConsole } from "../debuglog";
5
6
 
6
7
  /**
7
8
  * Child iframe will send offer messages to parent at this frequency until one
@@ -81,6 +82,7 @@ export class Tunnel extends EventEmitter {
81
82
  private _messagePort: MessagePort;
82
83
 
83
84
  config: TunnelConfig;
85
+ isConnected: boolean;
84
86
 
85
87
  // #endregion Properties
86
88
 
@@ -120,24 +122,48 @@ export class Tunnel extends EventEmitter {
120
122
  );
121
123
  }
122
124
 
123
- const source = target.contentWindow;
124
125
  const config = Tunnel._normalizeConfig(options);
125
126
  const tunnel = new Tunnel(config);
126
127
  const messenger = new TunnelMessenger({
127
128
  myOrigin: window.location.origin,
128
- targetOrigin: options.targetOrigin,
129
- logger: options.logger || console,
129
+ targetOrigin: config.targetOrigin,
130
+ logger: config.logger,
130
131
  });
132
+ tunnel.on("destroyed", () =>
133
+ config.logger.log(
134
+ `Tunnel to iframe at ${config.targetOrigin} destroyed!`,
135
+ tunnel,
136
+ target
137
+ )
138
+ );
139
+ tunnel.on("connected", () =>
140
+ config.logger.log(
141
+ `Tunnel to iframe at ${config.targetOrigin} connected!`,
142
+ tunnel,
143
+ target
144
+ )
145
+ );
146
+ tunnel.on("error", (e) =>
147
+ config.logger.log(
148
+ `Tunnel to iframe at ${config.targetOrigin} error!`,
149
+ tunnel,
150
+ target,
151
+ e
152
+ )
153
+ );
131
154
  let frameStatusCheck: number;
132
155
  let timeout: number;
133
156
  const offerListener = (event: MessageEvent) => {
134
157
  if (
135
- isFromOrigin(event, source, config.targetOrigin) &&
158
+ !tunnel.isConnected &&
159
+ isFromOrigin(event, target.contentWindow, config.targetOrigin) &&
136
160
  messenger.isHandshakeOffer(event.data)
137
161
  ) {
138
162
  const accepted = messenger.makeAccepted(unwrap(event.data).offers);
139
163
  const channel = new MessageChannel();
140
- source.postMessage(accepted, config.targetOrigin, [channel.port1]);
164
+ target.contentWindow.postMessage(accepted, config.targetOrigin, [
165
+ channel.port1,
166
+ ]);
141
167
  tunnel.connect(channel.port2);
142
168
  }
143
169
  };
@@ -147,13 +173,11 @@ export class Tunnel extends EventEmitter {
147
173
  window.removeEventListener("message", offerListener);
148
174
  };
149
175
  timeout = window.setTimeout(() => {
150
- tunnel.emitLocal(
151
- "error",
176
+ tunnel.abort(
152
177
  new Error(
153
- `Timed out awaiting initial message from iframe after ${config.timeout}ms`
178
+ `Timed out awaiting initial message from target iframe after ${config.timeout}ms`
154
179
  )
155
180
  );
156
- tunnel.destroy();
157
181
  }, config.timeout);
158
182
 
159
183
  tunnel.on("destroyed", cleanup);
@@ -165,7 +189,16 @@ export class Tunnel extends EventEmitter {
165
189
  */
166
190
  frameStatusCheck = window.setInterval(() => {
167
191
  if (!target.isConnected) {
168
- tunnel.destroy();
192
+ cleanup();
193
+ if (tunnel.isConnected) {
194
+ const frameDisconnectError = new Error(
195
+ `Tunnel target iframe at ${tunnel.config.targetOrigin} was disconnected from the document!`
196
+ );
197
+ Object.assign(frameDisconnectError, { target });
198
+ tunnel.abort(frameDisconnectError);
199
+ } else {
200
+ tunnel.destroy();
201
+ }
169
202
  }
170
203
  }, STATUSCHECK_MS);
171
204
 
@@ -192,6 +225,15 @@ export class Tunnel extends EventEmitter {
192
225
  const key = makeKey();
193
226
  const config = Tunnel._normalizeConfig(opts);
194
227
  const tunnel = new Tunnel(config);
228
+ tunnel.on("destroyed", () =>
229
+ config.logger.log(`Tunnel ${key} to parent window destroyed!`, tunnel)
230
+ );
231
+ tunnel.on("connected", () =>
232
+ config.logger.log(`Tunnel ${key} to parent window connected!`, tunnel)
233
+ );
234
+ tunnel.on("error", (e) =>
235
+ config.logger.log(`Tunnel ${key} to parent window error!`, tunnel, e)
236
+ );
195
237
  const messenger = new TunnelMessenger({
196
238
  myOrigin: window.location.origin,
197
239
  targetOrigin: config.targetOrigin,
@@ -221,21 +263,31 @@ export class Tunnel extends EventEmitter {
221
263
  };
222
264
 
223
265
  timeout = window.setTimeout(() => {
224
- tunnel.emitLocal(
225
- "error",
226
- new Error(
227
- `Timed out waiting for initial response from parent after ${config.timeout}ms`
228
- )
229
- );
230
- tunnel.destroy();
266
+ if (!timedOut) {
267
+ timedOut = true;
268
+ tunnel.abort(
269
+ new Error(
270
+ `Timed out waiting for initial response from parent after ${config.timeout}ms`
271
+ )
272
+ );
273
+ }
231
274
  }, config.timeout);
232
275
 
233
276
  window.addEventListener("message", acceptListener);
234
- tunnel.on("destroyed", cleanup);
235
- tunnel.on("connected", cleanup);
277
+ tunnel.on("destroyed", () => {
278
+ cleanup();
279
+ });
280
+ tunnel.on("connected", () => {
281
+ cleanup();
282
+ });
236
283
 
237
- const sendOffer = () =>
238
- source.postMessage(messenger.makeOffered(key), config.targetOrigin);
284
+ const sendOffer = () => {
285
+ if (tunnel.isConnected) {
286
+ clearInterval(retrying);
287
+ } else {
288
+ source.postMessage(messenger.makeOffered(key), config.targetOrigin);
289
+ }
290
+ };
239
291
  retrying = window.setInterval(sendOffer, RETRY_MS);
240
292
  sendOffer();
241
293
 
@@ -253,17 +305,26 @@ export class Tunnel extends EventEmitter {
253
305
  }
254
306
  this._messagePort = remote;
255
307
  remote.addEventListener("message", this._emitFromMessage);
256
- this.emit("connected");
308
+ this.emitLocal("connected");
257
309
  this._messagePort.start();
310
+ this.isConnected = true;
311
+ }
312
+
313
+ abort(error: Error): void {
314
+ this.emitLocal("error", error);
315
+ this.destroy(error);
258
316
  }
259
317
 
260
- destroy(): void {
318
+ destroy(e?: Error): void {
261
319
  if (this._messagePort) {
262
320
  this._messagePort.close();
263
321
  this._messagePort = null;
322
+ this.isConnected = false;
264
323
  }
265
- this.emitLocal("destroyed");
266
- this.emit("destroyed");
324
+ // don't add the argument to the logging if it doesn't exist; otherwise, on
325
+ // a normal destroy, it logs a confusing "undefined"
326
+ const context = e ? [e] : [];
327
+ this.emitLocal("destroyed", ...context);
267
328
  // this.removeAllListeners(); // TODO: maybe necessary for memory leaks
268
329
  }
269
330
 
@@ -289,8 +350,8 @@ export class Tunnel extends EventEmitter {
289
350
  let errorMessage = "";
290
351
  const config: Partial<TunnelConfig> = {
291
352
  timeout: 4000,
292
- logger: console,
293
353
  ...options,
354
+ logger: options.logger || quietConsole,
294
355
  };
295
356
 
296
357
  const timeoutMs = Number(config.timeout);