@adobe/uix-core 0.6.5 → 0.7.1-nightly.20230114

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.
Files changed (94) hide show
  1. package/dist/__helpers__/jest.messagechannel.d.cts +2 -0
  2. package/dist/__helpers__/jest.messagechannel.d.cts.map +1 -0
  3. package/dist/__mocks__/mock-finalization-registry.d.ts +11 -0
  4. package/dist/__mocks__/mock-finalization-registry.d.ts.map +1 -0
  5. package/dist/__mocks__/mock-weak-ref.d.ts +7 -0
  6. package/dist/__mocks__/mock-weak-ref.d.ts.map +1 -0
  7. package/dist/constants.d.ts +8 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/cross-realm-object.d.ts +44 -0
  10. package/dist/cross-realm-object.d.ts.map +1 -0
  11. package/dist/debuglog.d.ts +11 -0
  12. package/dist/debuglog.d.ts.map +1 -1
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +906 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/message-wrapper.d.ts +9 -0
  18. package/dist/message-wrapper.d.ts.map +1 -0
  19. package/dist/object-simulator.d.ts +28 -0
  20. package/dist/object-simulator.d.ts.map +1 -0
  21. package/dist/object-simulator.test.d.ts +2 -0
  22. package/dist/object-simulator.test.d.ts.map +1 -0
  23. package/dist/object-walker.d.ts +30 -0
  24. package/dist/object-walker.d.ts.map +1 -0
  25. package/dist/promises/index.d.ts +3 -0
  26. package/dist/promises/index.d.ts.map +1 -0
  27. package/dist/promises/promise-wrappers.test.d.ts +2 -0
  28. package/dist/promises/promise-wrappers.test.d.ts.map +1 -0
  29. package/dist/promises/timed.d.ts +15 -0
  30. package/dist/promises/timed.d.ts.map +1 -0
  31. package/dist/promises/wait.d.ts +7 -0
  32. package/dist/promises/wait.d.ts.map +1 -0
  33. package/dist/remote-subject.d.ts +70 -0
  34. package/dist/remote-subject.d.ts.map +1 -0
  35. package/dist/rpc/call-receiver.d.ts +4 -0
  36. package/dist/rpc/call-receiver.d.ts.map +1 -0
  37. package/dist/rpc/call-receiver.test.d.ts +2 -0
  38. package/dist/rpc/call-receiver.test.d.ts.map +1 -0
  39. package/dist/rpc/call-sender.d.ts +4 -0
  40. package/dist/rpc/call-sender.d.ts.map +1 -0
  41. package/dist/rpc/call-sender.test.d.ts +2 -0
  42. package/dist/rpc/call-sender.test.d.ts.map +1 -0
  43. package/dist/rpc/index.d.ts +3 -0
  44. package/dist/rpc/index.d.ts.map +1 -0
  45. package/dist/tickets.d.ts +34 -0
  46. package/dist/tickets.d.ts.map +1 -0
  47. package/dist/tunnel/index.d.ts +2 -0
  48. package/dist/tunnel/index.d.ts.map +1 -0
  49. package/dist/tunnel/tunnel-messenger.d.ts +25 -0
  50. package/dist/tunnel/tunnel-messenger.d.ts.map +1 -0
  51. package/dist/tunnel/tunnel-messenger.test.d.ts +2 -0
  52. package/dist/tunnel/tunnel-messenger.test.d.ts.map +1 -0
  53. package/dist/tunnel/tunnel.d.ts +62 -0
  54. package/dist/tunnel/tunnel.d.ts.map +1 -0
  55. package/dist/tunnel/tunnel.test.d.ts +2 -0
  56. package/dist/tunnel/tunnel.test.d.ts.map +1 -0
  57. package/dist/types.d.ts +1 -4
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/value-assertions.d.ts +13 -0
  60. package/dist/value-assertions.d.ts.map +1 -0
  61. package/package.json +1 -1
  62. package/src/__helpers__/jest.messagechannel.cjs +3 -0
  63. package/src/__mocks__/mock-finalization-registry.ts +13 -0
  64. package/src/__mocks__/mock-weak-ref.ts +10 -0
  65. package/src/constants.ts +10 -0
  66. package/src/cross-realm-object.ts +117 -0
  67. package/src/debuglog.ts +1 -1
  68. package/src/index.ts +4 -1
  69. package/src/message-wrapper.ts +35 -0
  70. package/src/object-simulator.test.ts +328 -0
  71. package/src/object-simulator.ts +145 -0
  72. package/src/object-walker.ts +132 -0
  73. package/src/promises/index.ts +2 -0
  74. package/src/promises/promise-wrappers.test.ts +63 -0
  75. package/src/promises/timed.ts +41 -0
  76. package/src/promises/wait.ts +10 -0
  77. package/src/remote-subject.ts +185 -0
  78. package/src/rpc/call-receiver.test.ts +90 -0
  79. package/src/rpc/call-receiver.ts +29 -0
  80. package/src/rpc/call-sender.test.ts +73 -0
  81. package/src/rpc/call-sender.ts +72 -0
  82. package/src/rpc/index.ts +2 -0
  83. package/src/tickets.ts +71 -0
  84. package/src/tunnel/index.ts +1 -0
  85. package/src/tunnel/tunnel-messenger.test.ts +183 -0
  86. package/src/tunnel/tunnel-messenger.ts +99 -0
  87. package/src/tunnel/tunnel.test.ts +211 -0
  88. package/src/tunnel/tunnel.ts +322 -0
  89. package/src/types.ts +3 -5
  90. package/src/value-assertions.ts +58 -0
  91. package/tsconfig.json +2 -6
  92. package/dist/timeout-promise.d.ts +0 -12
  93. package/dist/timeout-promise.d.ts.map +0 -1
  94. package/src/timeout-promise.ts +0 -36
@@ -0,0 +1,322 @@
1
+ import EventEmitter from "eventemitter3";
2
+ import { isIframe } from "../value-assertions";
3
+ import { TunnelMessenger } from "./tunnel-messenger";
4
+ import { unwrap } from "../message-wrapper";
5
+
6
+ /**
7
+ * Child iframe will send offer messages to parent at this frequency until one
8
+ * is accepted or the attempt times out.
9
+ * TODO: make configurable if ever necessary
10
+ */
11
+ const RETRY_MS = 100;
12
+
13
+ /**
14
+ * Child iframe may unexpectedly close or detach from DOM. It emits no event
15
+ * when this happens, so we must poll it and destroy the tunnel when necessary.
16
+ * TODO: make configurable if ever necessary
17
+ */
18
+ const STATUSCHECK_MS = 5000;
19
+
20
+ /**
21
+ * Semi-unique IDs allow multiple parallel connections to handshake on both parent
22
+ * and child iframe. This generates a semi-random 8-char base 36 string.
23
+ */
24
+ const KEY_BASE = 36;
25
+ const KEY_LENGTH = 8;
26
+ const KEY_EXP = KEY_BASE ** KEY_LENGTH;
27
+ const makeKey = () => Math.round(Math.random() * KEY_EXP).toString(KEY_BASE);
28
+
29
+ /** @alpha */
30
+ export interface TunnelConfig {
31
+ // #region Properties
32
+
33
+ /**
34
+ * To ensure secure communication, target origin must be specified, so the
35
+ * tunnel can't connect to an unauthorized domain. Can be '*' to disable
36
+ * origin checks, but this is discouraged!
37
+ */
38
+ targetOrigin: string;
39
+ /**
40
+ * A Promise for a tunnel will reject if not connected within timeout (ms).
41
+ * @defaultValue 4000
42
+ */
43
+ timeout: number;
44
+ /**
45
+ * Logger instance to use for debugging tunnel connection.
46
+ */
47
+ logger: Console;
48
+
49
+ // #endregion Properties
50
+ }
51
+
52
+ const badTimeout = "\n - timeout value must be a number of milliseconds";
53
+ const badTargetOrigin =
54
+ "\n - targetOrigin must be a valid URL origin or '*' for any origin";
55
+
56
+ function isFromOrigin(
57
+ event: MessageEvent,
58
+ source: WindowProxy,
59
+ targetOrigin: string
60
+ ) {
61
+ try {
62
+ return (
63
+ source === event.source &&
64
+ (targetOrigin === "*" || targetOrigin === new URL(event.origin).origin)
65
+ );
66
+ } catch (_) {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ const { emit: emitOn } = EventEmitter.prototype;
72
+
73
+ /**
74
+ * An EventEmitter across two documents. It emits events on the remote document
75
+ * and takes subscribers from the local document.
76
+ * @alpha
77
+ */
78
+ export class Tunnel extends EventEmitter {
79
+ // #region Properties
80
+
81
+ private _messagePort: MessagePort;
82
+
83
+ config: TunnelConfig;
84
+
85
+ // #endregion Properties
86
+
87
+ // #region Constructors
88
+
89
+ constructor(config: TunnelConfig) {
90
+ super();
91
+ this.config = config;
92
+ }
93
+
94
+ // #endregion Constructors
95
+
96
+ // #region Public Static Methods
97
+
98
+ /**
99
+ * Create a Tunnel that connects to the page running in the provided iframe.
100
+ *
101
+ * @remarks
102
+ * Returns a Tunnel that listens for connection requests from the page in the
103
+ * provided iframe, which it will send periodically until timeout if that page
104
+ * has called {@link Tunnel.toParent}. If it receives one, the Tunnel will accept the
105
+ * connection and send an exclusive MessagePort to the xrobject on the other
106
+ * end. The tunnel may reconnect if the iframe reloads, in which case it will
107
+ * emit another "connected" event.
108
+ *
109
+ * @alpha
110
+ */
111
+ static toIframe(
112
+ target: HTMLIFrameElement,
113
+ options: Partial<TunnelConfig>
114
+ ): Tunnel {
115
+ if (!isIframe(target)) {
116
+ throw new Error(
117
+ `Provided tunnel target is not an iframe! ${Object.prototype.toString.call(
118
+ target
119
+ )}`
120
+ );
121
+ }
122
+
123
+ const source = target.contentWindow;
124
+ const config = Tunnel._normalizeConfig(options);
125
+ const tunnel = new Tunnel(config);
126
+ const messenger = new TunnelMessenger({
127
+ myOrigin: window.location.origin,
128
+ targetOrigin: options.targetOrigin,
129
+ logger: options.logger || console,
130
+ });
131
+ let frameStatusCheck: number;
132
+ let timeout: number;
133
+ const offerListener = (event: MessageEvent) => {
134
+ if (
135
+ isFromOrigin(event, source, config.targetOrigin) &&
136
+ messenger.isHandshakeOffer(event.data)
137
+ ) {
138
+ const accepted = messenger.makeAccepted(unwrap(event.data).offers);
139
+ const channel = new MessageChannel();
140
+ source.postMessage(accepted, config.targetOrigin, [channel.port1]);
141
+ tunnel.connect(channel.port2);
142
+ }
143
+ };
144
+ const cleanup = () => {
145
+ clearTimeout(timeout);
146
+ clearInterval(frameStatusCheck);
147
+ window.removeEventListener("message", offerListener);
148
+ };
149
+ timeout = window.setTimeout(() => {
150
+ tunnel.emitLocal(
151
+ "error",
152
+ new Error(
153
+ `Timed out awaiting initial message from iframe after ${config.timeout}ms`
154
+ )
155
+ );
156
+ tunnel.destroy();
157
+ }, config.timeout);
158
+
159
+ tunnel.on("destroyed", cleanup);
160
+ tunnel.on("connected", () => clearTimeout(timeout));
161
+
162
+ /**
163
+ * Check if the iframe has been unexpectedly removed from the DOM (for
164
+ * example, by React). Unsubscribe event listeners and destroy tunnel.
165
+ */
166
+ frameStatusCheck = window.setInterval(() => {
167
+ if (!target.isConnected) {
168
+ tunnel.destroy();
169
+ }
170
+ }, STATUSCHECK_MS);
171
+
172
+ window.addEventListener("message", offerListener);
173
+
174
+ return tunnel;
175
+ }
176
+
177
+ /**
178
+ * Create a Tunnel that connects to the page running in the parent window.
179
+ *
180
+ * @remarks
181
+ * Returns a Tunnel that starts sending connection requests to the parent
182
+ * window, sending them periodically until the window responds with an accept
183
+ * message or the timeout passes. The parent window will accept the request if
184
+ * it calls {@link Tunnel.toIframe}.
185
+ *
186
+ * @alpha
187
+ */
188
+ static toParent(source: WindowProxy, opts: Partial<TunnelConfig>): Tunnel {
189
+ let retrying: number;
190
+ let timeout: number;
191
+ let timedOut = false;
192
+ const key = makeKey();
193
+ const config = Tunnel._normalizeConfig(opts);
194
+ const tunnel = new Tunnel(config);
195
+ const messenger = new TunnelMessenger({
196
+ myOrigin: window.location.origin,
197
+ targetOrigin: config.targetOrigin,
198
+ logger: config.logger,
199
+ });
200
+ const acceptListener = (event: MessageEvent) => {
201
+ if (
202
+ !timedOut &&
203
+ isFromOrigin(event, source, config.targetOrigin) &&
204
+ messenger.isHandshakeAccepting(event.data, key)
205
+ ) {
206
+ cleanup();
207
+ if (!event.ports || !event.ports.length) {
208
+ const portError = new Error(
209
+ "Received handshake accept message, but it did not include a MessagePort to establish tunnel"
210
+ );
211
+ tunnel.emitLocal("error", portError);
212
+ return;
213
+ }
214
+ tunnel.connect(event.ports[0]);
215
+ }
216
+ };
217
+ const cleanup = () => {
218
+ clearInterval(retrying);
219
+ clearTimeout(timeout);
220
+ window.removeEventListener("message", acceptListener);
221
+ };
222
+
223
+ 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();
231
+ }, config.timeout);
232
+
233
+ window.addEventListener("message", acceptListener);
234
+ tunnel.on("destroyed", cleanup);
235
+ tunnel.on("connected", cleanup);
236
+
237
+ const sendOffer = () =>
238
+ source.postMessage(messenger.makeOffered(key), config.targetOrigin);
239
+ retrying = window.setInterval(sendOffer, RETRY_MS);
240
+ sendOffer();
241
+
242
+ return tunnel;
243
+ }
244
+
245
+ // #endregion Public Static Methods
246
+
247
+ // #region Public Methods
248
+
249
+ connect(remote: MessagePort) {
250
+ if (this._messagePort) {
251
+ this._messagePort.removeEventListener("message", this._emitFromMessage);
252
+ this._messagePort.close();
253
+ }
254
+ this._messagePort = remote;
255
+ remote.addEventListener("message", this._emitFromMessage);
256
+ this.emit("connected");
257
+ this._messagePort.start();
258
+ }
259
+
260
+ destroy(): void {
261
+ if (this._messagePort) {
262
+ this._messagePort.close();
263
+ this._messagePort = null;
264
+ }
265
+ this.emitLocal("destroyed");
266
+ this.emit("destroyed");
267
+ // this.removeAllListeners(); // TODO: maybe necessary for memory leaks
268
+ }
269
+
270
+ emit(type: string | symbol, payload?: unknown): boolean {
271
+ if (!this._messagePort) {
272
+ return false;
273
+ }
274
+ this._messagePort.postMessage({ type, payload });
275
+ return true;
276
+ }
277
+
278
+ emitLocal = (type: string | symbol, payload?: unknown) => {
279
+ return emitOn.call(this, type, payload);
280
+ };
281
+
282
+ // #endregion Public Methods
283
+
284
+ // #region Private Static Methods
285
+
286
+ private static _normalizeConfig(
287
+ options: Partial<TunnelConfig> = {}
288
+ ): TunnelConfig {
289
+ let errorMessage = "";
290
+ const config: Partial<TunnelConfig> = {
291
+ timeout: 4000,
292
+ logger: console,
293
+ ...options,
294
+ };
295
+
296
+ const timeoutMs = Number(config.timeout);
297
+ if (!Number.isSafeInteger(timeoutMs)) {
298
+ errorMessage += badTimeout;
299
+ }
300
+ if (config.targetOrigin !== "*") {
301
+ try {
302
+ new URL(config.targetOrigin);
303
+ } catch (e) {
304
+ errorMessage += badTargetOrigin;
305
+ }
306
+ }
307
+ if (errorMessage) {
308
+ throw new Error(`Invalid tunnel configuration: ${errorMessage}`);
309
+ }
310
+ return config as TunnelConfig;
311
+ }
312
+
313
+ // #endregion Private Static Methods
314
+
315
+ // #region Private Methods
316
+
317
+ private _emitFromMessage = ({ data: { type, payload } }: MessageEvent) => {
318
+ this.emitLocal(type, payload);
319
+ };
320
+
321
+ // #endregion Private Methods
322
+ }
package/src/types.ts CHANGED
@@ -10,6 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag
10
10
  governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import { CrossRealmObject } from "./cross-realm-object";
14
+
13
15
  /* eslint-disable @typescript-eslint/no-explicit-any */
14
16
 
15
17
  /**
@@ -172,11 +174,7 @@ export interface GuestConnection {
172
174
  attachUI(
173
175
  frame: HTMLIFrameElement,
174
176
  privateMethods?: RemoteHostApis
175
- ): {
176
- promise: Promise<unknown>;
177
- // eslint-disable-next-line @typescript-eslint/ban-types
178
- destroy: Function;
179
- };
177
+ ): Promise<unknown>;
180
178
  load(): Promise<unknown>;
181
179
  error?: Error;
182
180
  hasCapabilities(capabilities: unknown): boolean;
@@ -0,0 +1,58 @@
1
+ /** @internal */
2
+ export type Primitive = string | number | boolean;
3
+
4
+ export function isPlainObject<T>(value: unknown): value is T & object {
5
+ if (!value || typeof value !== "object") {
6
+ return false;
7
+ }
8
+ const proto = Reflect.getPrototypeOf(value);
9
+ return proto === null || proto === Object.prototype;
10
+ }
11
+
12
+ export function isPrimitive(value: unknown): value is Primitive {
13
+ if (!value) {
14
+ return true;
15
+ }
16
+ const theType = typeof value;
17
+ return theType === "string" || theType === "number" || theType === "boolean";
18
+ }
19
+
20
+ export function isIterable<T>(value: unknown): value is T[] {
21
+ return Array.isArray(value);
22
+ }
23
+
24
+ export function isFunction(value: unknown): value is CallableFunction {
25
+ return typeof value === "function";
26
+ }
27
+
28
+ export function hasProp(value: unknown, prop: string) {
29
+ return !isPrimitive(value) && Reflect.has(value as object, prop);
30
+ }
31
+
32
+ export function isTunnelSource(
33
+ value: unknown
34
+ ): value is Window | ServiceWorker {
35
+ return (
36
+ value instanceof Window ||
37
+ value instanceof ServiceWorker ||
38
+ hasProp(value, "onmessage")
39
+ );
40
+ }
41
+
42
+ export function isIframe(value: unknown): value is HTMLIFrameElement {
43
+ if (!value || isPrimitive(value)) {
44
+ return false;
45
+ }
46
+ const { nodeName } = value as HTMLIFrameElement;
47
+ return typeof nodeName === "string" && nodeName.toLowerCase() === "iframe";
48
+ }
49
+
50
+ export function isObjectWithPrototype<T>(
51
+ value: unknown
52
+ ): value is T & { [key: string | symbol]: unknown } {
53
+ if (!value || typeof value !== "object") {
54
+ return false;
55
+ }
56
+ const proto = Reflect.getPrototypeOf(value);
57
+ return proto !== Object.prototype;
58
+ }
package/tsconfig.json CHANGED
@@ -5,10 +5,6 @@
5
5
  "outDir": "dist",
6
6
  "rootDir": "src"
7
7
  },
8
- "include": [
9
- "src/**/*"
10
- ],
11
- "exclude": [
12
- "src/**/*.test.tsx?"
13
- ]
8
+ "include": ["src/**/*"],
9
+ "exclude": ["src/**/*.test.tsx?"]
14
10
  }
@@ -1,12 +0,0 @@
1
- /**
2
- * Add a timeout to a Promise. The returned Promise will resolve to the value of
3
- * the original Promise, but if it doesn't resolve within the timeout interval,
4
- * it will reject with a timeout error.
5
- * @internal
6
- *
7
- * @param timeoutMs - Time to wait (ms) before rejecting
8
- * @param promise - Original promise to set a timeout for
9
- * @returns - Promise that rejects after X milliseconds have passed
10
- */
11
- export declare function timeoutPromise<T>(timeoutMs: number, promise: Promise<T>): Promise<unknown>;
12
- //# sourceMappingURL=timeout-promise.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"timeout-promise.d.ts","sourceRoot":"","sources":["../src/timeout-promise.ts"],"names":[],"mappings":"AAYA;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,oBAavE"}
@@ -1,36 +0,0 @@
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
- /**
14
- * Add a timeout to a Promise. The returned Promise will resolve to the value of
15
- * the original Promise, but if it doesn't resolve within the timeout interval,
16
- * it will reject with a timeout error.
17
- * @internal
18
- *
19
- * @param timeoutMs - Time to wait (ms) before rejecting
20
- * @param promise - Original promise to set a timeout for
21
- * @returns - Promise that rejects after X milliseconds have passed
22
- */
23
- export function timeoutPromise<T>(timeoutMs: number, promise: Promise<T>) {
24
- return new Promise((resolve, reject) => {
25
- const timeout = setTimeout(
26
- () => reject(new Error(`Timed out after ${timeoutMs}ms`)),
27
- timeoutMs
28
- );
29
- promise
30
- .then((result) => {
31
- clearTimeout(timeout);
32
- resolve(result);
33
- })
34
- .catch(reject);
35
- });
36
- }