penpal 6.2.0

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/dist/penpal.js +773 -0
  4. package/dist/penpal.min.js +1 -0
  5. package/dist/penpal.min.js.map +1 -0
  6. package/es5/child/connectToParent.js +111 -0
  7. package/es5/child/handleSynAckMessageFactory.js +58 -0
  8. package/es5/connectCallReceiver.js +101 -0
  9. package/es5/connectCallSender.js +122 -0
  10. package/es5/createDestructor.js +29 -0
  11. package/es5/createLogger.js +19 -0
  12. package/es5/enums.js +47 -0
  13. package/es5/errorSerialization.js +34 -0
  14. package/es5/generateId.js +14 -0
  15. package/es5/index.js +63 -0
  16. package/es5/indexForBundle.js +21 -0
  17. package/es5/methodSerialization.js +94 -0
  18. package/es5/parent/connectToChild.js +107 -0
  19. package/es5/parent/getOriginFromSrc.js +53 -0
  20. package/es5/parent/handleAckMessageFactory.js +66 -0
  21. package/es5/parent/handleSynMessageFactory.js +29 -0
  22. package/es5/parent/monitorIframeRemoval.js +34 -0
  23. package/es5/parent/validateIframeHasSrcOrSrcDoc.js +18 -0
  24. package/es5/startConnectionTimeout.js +30 -0
  25. package/es5/types.js +1 -0
  26. package/lib/child/connectToParent.d.ts +36 -0
  27. package/lib/child/connectToParent.js +73 -0
  28. package/lib/child/handleSynAckMessageFactory.d.ts +7 -0
  29. package/lib/child/handleSynAckMessageFactory.js +41 -0
  30. package/lib/connectCallReceiver.d.ts +7 -0
  31. package/lib/connectCallReceiver.js +71 -0
  32. package/lib/connectCallSender.d.ts +14 -0
  33. package/lib/connectCallSender.js +93 -0
  34. package/lib/createDestructor.d.ts +13 -0
  35. package/lib/createDestructor.js +18 -0
  36. package/lib/createLogger.d.ts +2 -0
  37. package/lib/createLogger.js +10 -0
  38. package/lib/enums.d.ts +22 -0
  39. package/lib/enums.js +27 -0
  40. package/lib/errorSerialization.d.ts +14 -0
  41. package/lib/errorSerialization.js +17 -0
  42. package/lib/generateId.d.ts +5 -0
  43. package/lib/generateId.js +5 -0
  44. package/lib/index.d.ts +4 -0
  45. package/lib/index.js +3 -0
  46. package/lib/indexForBundle.d.ts +21 -0
  47. package/lib/indexForBundle.js +8 -0
  48. package/lib/methodSerialization.d.ts +27 -0
  49. package/lib/methodSerialization.js +67 -0
  50. package/lib/parent/connectToChild.d.ts +31 -0
  51. package/lib/parent/connectToChild.js +66 -0
  52. package/lib/parent/getOriginFromSrc.d.ts +5 -0
  53. package/lib/parent/getOriginFromSrc.js +42 -0
  54. package/lib/parent/handleAckMessageFactory.d.ts +7 -0
  55. package/lib/parent/handleAckMessageFactory.js +47 -0
  56. package/lib/parent/handleSynMessageFactory.d.ts +6 -0
  57. package/lib/parent/handleSynMessageFactory.js +18 -0
  58. package/lib/parent/monitorIframeRemoval.d.ts +12 -0
  59. package/lib/parent/monitorIframeRemoval.js +22 -0
  60. package/lib/parent/validateIframeHasSrcOrSrcDoc.d.ts +2 -0
  61. package/lib/parent/validateIframeHasSrcOrSrcDoc.js +8 -0
  62. package/lib/startConnectionTimeout.d.ts +6 -0
  63. package/lib/startConnectionTimeout.js +18 -0
  64. package/lib/types.d.ts +114 -0
  65. package/lib/types.js +0 -0
  66. package/package.json +90 -0
package/dist/penpal.js ADDED
@@ -0,0 +1,773 @@
1
+ var Penpal = (function () {
2
+ 'use strict';
3
+
4
+ var MessageType;
5
+
6
+ (function (MessageType) {
7
+ MessageType["Call"] = "call";
8
+ MessageType["Reply"] = "reply";
9
+ MessageType["Syn"] = "syn";
10
+ MessageType["SynAck"] = "synAck";
11
+ MessageType["Ack"] = "ack";
12
+ })(MessageType || (MessageType = {}));
13
+
14
+ var Resolution;
15
+
16
+ (function (Resolution) {
17
+ Resolution["Fulfilled"] = "fulfilled";
18
+ Resolution["Rejected"] = "rejected";
19
+ })(Resolution || (Resolution = {}));
20
+
21
+ var ErrorCode;
22
+
23
+ (function (ErrorCode) {
24
+ ErrorCode["ConnectionDestroyed"] = "ConnectionDestroyed";
25
+ ErrorCode["ConnectionTimeout"] = "ConnectionTimeout";
26
+ ErrorCode["NoIframeSrc"] = "NoIframeSrc";
27
+ })(ErrorCode || (ErrorCode = {}));
28
+
29
+ var NativeErrorName;
30
+
31
+ (function (NativeErrorName) {
32
+ NativeErrorName["DataCloneError"] = "DataCloneError";
33
+ })(NativeErrorName || (NativeErrorName = {}));
34
+
35
+ var NativeEventType;
36
+
37
+ (function (NativeEventType) {
38
+ NativeEventType["Message"] = "message";
39
+ })(NativeEventType || (NativeEventType = {}));
40
+
41
+ var createDestructor = ((localName, log) => {
42
+ const callbacks = [];
43
+ let destroyed = false;
44
+ return {
45
+ destroy(error) {
46
+ if (!destroyed) {
47
+ destroyed = true;
48
+ log("".concat(localName, ": Destroying connection"));
49
+ callbacks.forEach(callback => {
50
+ callback(error);
51
+ });
52
+ }
53
+ },
54
+
55
+ onDestroy(callback) {
56
+ destroyed ? callback() : callbacks.push(callback);
57
+ }
58
+
59
+ };
60
+ });
61
+
62
+ var createLogger = (debug => {
63
+ /**
64
+ * Logs a message if debug is enabled.
65
+ */
66
+ return (...args) => {
67
+ if (debug) {
68
+ console.log('[Penpal]', ...args); // eslint-disable-line no-console
69
+ }
70
+ };
71
+ });
72
+
73
+ const DEFAULT_PORT_BY_PROTOCOL = {
74
+ 'http:': '80',
75
+ 'https:': '443'
76
+ };
77
+ const URL_REGEX = /^(https?:)?\/\/([^/:]+)?(:(\d+))?/;
78
+ const opaqueOriginSchemes = ['file:', 'data:'];
79
+ /**
80
+ * Converts a src value into an origin.
81
+ */
82
+
83
+ var getOriginFromSrc = (src => {
84
+ if (src && opaqueOriginSchemes.find(scheme => src.startsWith(scheme))) {
85
+ // The origin of the child document is an opaque origin and its
86
+ // serialization is "null"
87
+ // https://html.spec.whatwg.org/multipage/origin.html#origin
88
+ return 'null';
89
+ } // Note that if src is undefined, then srcdoc is being used instead of src
90
+ // and we can follow this same logic below to get the origin of the parent,
91
+ // which is the origin that we will need to use.
92
+
93
+
94
+ const location = document.location;
95
+ const regexResult = URL_REGEX.exec(src);
96
+ let protocol;
97
+ let hostname;
98
+ let port;
99
+
100
+ if (regexResult) {
101
+ // It's an absolute URL. Use the parsed info.
102
+ // regexResult[1] will be undefined if the URL starts with //
103
+ protocol = regexResult[1] ? regexResult[1] : location.protocol;
104
+ hostname = regexResult[2];
105
+ port = regexResult[4];
106
+ } else {
107
+ // It's a relative path. Use the current location's info.
108
+ protocol = location.protocol;
109
+ hostname = location.hostname;
110
+ port = location.port;
111
+ } // If the port is the default for the protocol, we don't want to add it to the origin string
112
+ // or it won't match the message's event.origin.
113
+
114
+
115
+ const portSuffix = port && port !== DEFAULT_PORT_BY_PROTOCOL[protocol] ? ":".concat(port) : '';
116
+ return "".concat(protocol, "//").concat(hostname).concat(portSuffix);
117
+ });
118
+
119
+ /**
120
+ * Converts an error object into a plain object.
121
+ */
122
+ const serializeError = ({
123
+ name,
124
+ message,
125
+ stack
126
+ }) => ({
127
+ name,
128
+ message,
129
+ stack
130
+ });
131
+ /**
132
+ * Converts a plain object into an error object.
133
+ */
134
+
135
+ const deserializeError = obj => {
136
+ const deserializedError = new Error(); // @ts-ignore
137
+
138
+ Object.keys(obj).forEach(key => deserializedError[key] = obj[key]);
139
+ return deserializedError;
140
+ };
141
+
142
+ /**
143
+ * Listens for "call" messages coming from the remote, executes the corresponding method, and
144
+ * responds with the return value.
145
+ */
146
+
147
+ var connectCallReceiver = ((info, serializedMethods, log) => {
148
+ const {
149
+ localName,
150
+ local,
151
+ remote,
152
+ originForSending,
153
+ originForReceiving
154
+ } = info;
155
+ let destroyed = false;
156
+
157
+ const handleMessageEvent = event => {
158
+ if (event.source !== remote || event.data.penpal !== MessageType.Call) {
159
+ return;
160
+ }
161
+
162
+ if (originForReceiving !== '*' && event.origin !== originForReceiving) {
163
+ log("".concat(localName, " received message from origin ").concat(event.origin, " which did not match expected origin ").concat(originForReceiving));
164
+ return;
165
+ }
166
+
167
+ const callMessage = event.data;
168
+ const {
169
+ methodName,
170
+ args,
171
+ id
172
+ } = callMessage;
173
+ log("".concat(localName, ": Received ").concat(methodName, "() call"));
174
+
175
+ const createPromiseHandler = resolution => {
176
+ return returnValue => {
177
+ log("".concat(localName, ": Sending ").concat(methodName, "() reply"));
178
+
179
+ if (destroyed) {
180
+ // It's possible to throw an error here, but it would need to be thrown asynchronously
181
+ // and would only be catchable using window.onerror. This is because the consumer
182
+ // is merely returning a value from their method and not calling any function
183
+ // that they could wrap in a try-catch. Even if the consumer were to catch the error,
184
+ // the value of doing so is questionable. Instead, we'll just log a message.
185
+ log("".concat(localName, ": Unable to send ").concat(methodName, "() reply due to destroyed connection"));
186
+ return;
187
+ }
188
+
189
+ const message = {
190
+ penpal: MessageType.Reply,
191
+ id,
192
+ resolution,
193
+ returnValue
194
+ };
195
+
196
+ if (resolution === Resolution.Rejected && returnValue instanceof Error) {
197
+ message.returnValue = serializeError(returnValue);
198
+ message.returnValueIsError = true;
199
+ }
200
+
201
+ try {
202
+ remote.postMessage(message, originForSending);
203
+ } catch (err) {
204
+ // If a consumer attempts to send an object that's not cloneable (e.g., window),
205
+ // we want to ensure the receiver's promise gets rejected.
206
+ if (err.name === NativeErrorName.DataCloneError) {
207
+ const errorReplyMessage = {
208
+ penpal: MessageType.Reply,
209
+ id,
210
+ resolution: Resolution.Rejected,
211
+ returnValue: serializeError(err),
212
+ returnValueIsError: true
213
+ };
214
+ remote.postMessage(errorReplyMessage, originForSending);
215
+ }
216
+
217
+ throw err;
218
+ }
219
+ };
220
+ };
221
+
222
+ new Promise(resolve => resolve(serializedMethods[methodName].apply(serializedMethods, args))).then(createPromiseHandler(Resolution.Fulfilled), createPromiseHandler(Resolution.Rejected));
223
+ };
224
+
225
+ local.addEventListener(NativeEventType.Message, handleMessageEvent);
226
+ return () => {
227
+ destroyed = true;
228
+ local.removeEventListener(NativeEventType.Message, handleMessageEvent);
229
+ };
230
+ });
231
+
232
+ let id = 0;
233
+ /**
234
+ * @return {number} A unique ID (not universally unique)
235
+ */
236
+
237
+ var generateId = (() => ++id);
238
+
239
+ const KEY_PATH_DELIMITER = '.';
240
+
241
+ const keyPathToSegments = keyPath => keyPath ? keyPath.split(KEY_PATH_DELIMITER) : [];
242
+
243
+ const segmentsToKeyPath = segments => segments.join(KEY_PATH_DELIMITER);
244
+
245
+ const createKeyPath = (key, prefix) => {
246
+ const segments = keyPathToSegments(prefix || '');
247
+ segments.push(key);
248
+ return segmentsToKeyPath(segments);
249
+ };
250
+ /**
251
+ * Given a `keyPath`, set it to be `value` on `subject`, creating any intermediate
252
+ * objects along the way.
253
+ *
254
+ * @param {Object} subject The object on which to set value.
255
+ * @param {string} keyPath The key path at which to set value.
256
+ * @param {Object} value The value to store at the given key path.
257
+ * @returns {Object} Updated subject.
258
+ */
259
+
260
+
261
+ const setAtKeyPath = (subject, keyPath, value) => {
262
+ const segments = keyPathToSegments(keyPath);
263
+ segments.reduce((prevSubject, key, idx) => {
264
+ if (typeof prevSubject[key] === 'undefined') {
265
+ prevSubject[key] = {};
266
+ }
267
+
268
+ if (idx === segments.length - 1) {
269
+ prevSubject[key] = value;
270
+ }
271
+
272
+ return prevSubject[key];
273
+ }, subject);
274
+ return subject;
275
+ };
276
+ /**
277
+ * Given a dictionary of (nested) keys to function, flatten them to a map
278
+ * from key path to function.
279
+ *
280
+ * @param {Object} methods The (potentially nested) object to serialize.
281
+ * @param {string} prefix A string with which to prefix entries. Typically not intended to be used by consumers.
282
+ * @returns {Object} An map from key path in `methods` to functions.
283
+ */
284
+
285
+ const serializeMethods = (methods, prefix) => {
286
+ const flattenedMethods = {};
287
+ Object.keys(methods).forEach(key => {
288
+ const value = methods[key];
289
+ const keyPath = createKeyPath(key, prefix);
290
+
291
+ if (typeof value === 'object') {
292
+ // Recurse into any nested children.
293
+ Object.assign(flattenedMethods, serializeMethods(value, keyPath));
294
+ }
295
+
296
+ if (typeof value === 'function') {
297
+ // If we've found a method, expose it.
298
+ flattenedMethods[keyPath] = value;
299
+ }
300
+ });
301
+ return flattenedMethods;
302
+ };
303
+ /**
304
+ * Given a map of key paths to functions, unpack the key paths to an object.
305
+ *
306
+ * @param {Object} flattenedMethods A map of key paths to functions to unpack.
307
+ * @returns {Object} A (potentially nested) map of functions.
308
+ */
309
+
310
+ const deserializeMethods = flattenedMethods => {
311
+ const methods = {};
312
+
313
+ for (const keyPath in flattenedMethods) {
314
+ setAtKeyPath(methods, keyPath, flattenedMethods[keyPath]);
315
+ }
316
+
317
+ return methods;
318
+ };
319
+
320
+ /**
321
+ * Augments an object with methods that match those defined by the remote. When these methods are
322
+ * called, a "call" message will be sent to the remote, the remote's corresponding method will be
323
+ * executed, and the method's return value will be returned via a message.
324
+ * @param {Object} callSender Sender object that should be augmented with methods.
325
+ * @param {Object} info Information about the local and remote windows.
326
+ * @param {Array} methodKeyPaths Key paths of methods available to be called on the remote.
327
+ * @param {Promise} destructionPromise A promise resolved when destroy() is called on the penpal
328
+ * connection.
329
+ * @returns {Object} The call sender object with methods that may be called.
330
+ */
331
+
332
+ var connectCallSender = ((callSender, info, methodKeyPaths, destroyConnection, log) => {
333
+ const {
334
+ localName,
335
+ local,
336
+ remote,
337
+ originForSending,
338
+ originForReceiving
339
+ } = info;
340
+ let destroyed = false;
341
+ log("".concat(localName, ": Connecting call sender"));
342
+
343
+ const createMethodProxy = methodName => {
344
+ return (...args) => {
345
+ log("".concat(localName, ": Sending ").concat(methodName, "() call")); // This handles the case where the iframe has been removed from the DOM
346
+ // (and therefore its window closed), the consumer has not yet
347
+ // called destroy(), and the user calls a method exposed by
348
+ // the remote. We detect the iframe has been removed and force
349
+ // a destroy() immediately so that the consumer sees the error saying
350
+ // the connection has been destroyed. We wrap this check in a try catch
351
+ // because Edge throws an "Object expected" error when accessing
352
+ // contentWindow.closed on a contentWindow from an iframe that's been
353
+ // removed from the DOM.
354
+
355
+ let iframeRemoved;
356
+
357
+ try {
358
+ if (remote.closed) {
359
+ iframeRemoved = true;
360
+ }
361
+ } catch (e) {
362
+ iframeRemoved = true;
363
+ }
364
+
365
+ if (iframeRemoved) {
366
+ destroyConnection();
367
+ }
368
+
369
+ if (destroyed) {
370
+ const error = new Error("Unable to send ".concat(methodName, "() call due ") + "to destroyed connection");
371
+ error.code = ErrorCode.ConnectionDestroyed;
372
+ throw error;
373
+ }
374
+
375
+ return new Promise((resolve, reject) => {
376
+ const id = generateId();
377
+
378
+ const handleMessageEvent = event => {
379
+ if (event.source !== remote || event.data.penpal !== MessageType.Reply || event.data.id !== id) {
380
+ return;
381
+ }
382
+
383
+ if (originForReceiving !== '*' && event.origin !== originForReceiving) {
384
+ log("".concat(localName, " received message from origin ").concat(event.origin, " which did not match expected origin ").concat(originForReceiving));
385
+ return;
386
+ }
387
+
388
+ const replyMessage = event.data;
389
+ log("".concat(localName, ": Received ").concat(methodName, "() reply"));
390
+ local.removeEventListener(NativeEventType.Message, handleMessageEvent);
391
+ let returnValue = replyMessage.returnValue;
392
+
393
+ if (replyMessage.returnValueIsError) {
394
+ returnValue = deserializeError(returnValue);
395
+ }
396
+
397
+ (replyMessage.resolution === Resolution.Fulfilled ? resolve : reject)(returnValue);
398
+ };
399
+
400
+ local.addEventListener(NativeEventType.Message, handleMessageEvent);
401
+ const callMessage = {
402
+ penpal: MessageType.Call,
403
+ id,
404
+ methodName,
405
+ args
406
+ };
407
+ remote.postMessage(callMessage, originForSending);
408
+ });
409
+ };
410
+ }; // Wrap each method in a proxy which sends it to the corresponding receiver.
411
+
412
+
413
+ const flattenedMethods = methodKeyPaths.reduce((api, name) => {
414
+ api[name] = createMethodProxy(name);
415
+ return api;
416
+ }, {}); // Unpack the structure of the provided methods object onto the CallSender, exposing
417
+ // the methods in the same shape they were provided.
418
+
419
+ Object.assign(callSender, deserializeMethods(flattenedMethods));
420
+ return () => {
421
+ destroyed = true;
422
+ };
423
+ });
424
+
425
+ /**
426
+ * Handles an ACK handshake message.
427
+ */
428
+
429
+ var handleAckMessageFactory = ((serializedMethods, childOrigin, originForSending, destructor, log) => {
430
+ const {
431
+ destroy,
432
+ onDestroy
433
+ } = destructor;
434
+ let destroyCallReceiver;
435
+ let receiverMethodNames; // We resolve the promise with the call sender. If the child reconnects
436
+ // (for example, after refreshing or navigating to another page that
437
+ // uses Penpal, we'll update the call sender with methods that match the
438
+ // latest provided by the child.
439
+
440
+ const callSender = {};
441
+ return event => {
442
+ if (childOrigin !== '*' && event.origin !== childOrigin) {
443
+ log("Parent: Handshake - Received ACK message from origin ".concat(event.origin, " which did not match expected origin ").concat(childOrigin));
444
+ return;
445
+ }
446
+
447
+ log('Parent: Handshake - Received ACK');
448
+ const info = {
449
+ localName: 'Parent',
450
+ local: window,
451
+ remote: event.source,
452
+ originForSending: originForSending,
453
+ originForReceiving: childOrigin
454
+ }; // If the child reconnected, we need to destroy the prior call receiver
455
+ // before setting up a new one.
456
+
457
+ if (destroyCallReceiver) {
458
+ destroyCallReceiver();
459
+ }
460
+
461
+ destroyCallReceiver = connectCallReceiver(info, serializedMethods, log);
462
+ onDestroy(destroyCallReceiver); // If the child reconnected, we need to remove the methods from the
463
+ // previous call receiver off the sender.
464
+
465
+ if (receiverMethodNames) {
466
+ receiverMethodNames.forEach(receiverMethodName => {
467
+ delete callSender[receiverMethodName];
468
+ });
469
+ }
470
+
471
+ receiverMethodNames = event.data.methodNames;
472
+ const destroyCallSender = connectCallSender(callSender, info, receiverMethodNames, destroy, log);
473
+ onDestroy(destroyCallSender);
474
+ return callSender;
475
+ };
476
+ });
477
+
478
+ /**
479
+ * Handles a SYN handshake message.
480
+ */
481
+
482
+ var handleSynMessageFactory = ((log, serializedMethods, childOrigin, originForSending) => {
483
+ return event => {
484
+ if (childOrigin !== '*' && event.origin !== childOrigin) {
485
+ log("Parent: Handshake - Received SYN message from origin ".concat(event.origin, " which did not match expected origin ").concat(childOrigin));
486
+ return;
487
+ }
488
+
489
+ log('Parent: Handshake - Received SYN, responding with SYN-ACK');
490
+ const synAckMessage = {
491
+ penpal: MessageType.SynAck,
492
+ methodNames: Object.keys(serializedMethods)
493
+ };
494
+ event.source.postMessage(synAckMessage, originForSending);
495
+ };
496
+ });
497
+
498
+ const CHECK_IFRAME_IN_DOC_INTERVAL = 60000;
499
+ /**
500
+ * Monitors for iframe removal and destroys connection if iframe
501
+ * is found to have been removed from DOM. This is to prevent memory
502
+ * leaks when the iframe is removed from the document and the consumer
503
+ * hasn't called destroy(). Without this, event listeners attached to
504
+ * the window would stick around and since the event handlers have a
505
+ * reference to the iframe in their closures, the iframe would stick
506
+ * around too.
507
+ */
508
+
509
+ var monitorIframeRemoval = ((iframe, destructor) => {
510
+ const {
511
+ destroy,
512
+ onDestroy
513
+ } = destructor;
514
+ const checkIframeInDocIntervalId = setInterval(() => {
515
+ if (!iframe.isConnected) {
516
+ clearInterval(checkIframeInDocIntervalId);
517
+ destroy();
518
+ }
519
+ }, CHECK_IFRAME_IN_DOC_INTERVAL);
520
+ onDestroy(() => {
521
+ clearInterval(checkIframeInDocIntervalId);
522
+ });
523
+ });
524
+
525
+ /**
526
+ * Starts a timeout and calls the callback with an error
527
+ * if the timeout completes before the stop function is called.
528
+ */
529
+
530
+ var startConnectionTimeout = ((timeout, callback) => {
531
+ let timeoutId;
532
+
533
+ if (timeout !== undefined) {
534
+ timeoutId = window.setTimeout(() => {
535
+ const error = new Error("Connection timed out after ".concat(timeout, "ms"));
536
+ error.code = ErrorCode.ConnectionTimeout;
537
+ callback(error);
538
+ }, timeout);
539
+ }
540
+
541
+ return () => {
542
+ clearTimeout(timeoutId);
543
+ };
544
+ });
545
+
546
+ var validateIframeHasSrcOrSrcDoc = (iframe => {
547
+ if (!iframe.src && !iframe.srcdoc) {
548
+ const error = new Error('Iframe must have src or srcdoc property defined.');
549
+ error.code = ErrorCode.NoIframeSrc;
550
+ throw error;
551
+ }
552
+ });
553
+
554
+ /**
555
+ * Attempts to establish communication with an iframe.
556
+ */
557
+
558
+ var connectToChild = (options => {
559
+ let {
560
+ iframe,
561
+ methods = {},
562
+ childOrigin,
563
+ timeout,
564
+ debug = false
565
+ } = options;
566
+ const log = createLogger(debug);
567
+ const destructor = createDestructor('Parent', log);
568
+ const {
569
+ onDestroy,
570
+ destroy
571
+ } = destructor;
572
+
573
+ if (!childOrigin) {
574
+ validateIframeHasSrcOrSrcDoc(iframe);
575
+ childOrigin = getOriginFromSrc(iframe.src);
576
+ } // If event.origin is "null", the remote protocol is file: or data: and we
577
+ // must post messages with "*" as targetOrigin when sending messages.
578
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions
579
+
580
+
581
+ const originForSending = childOrigin === 'null' ? '*' : childOrigin;
582
+ const serializedMethods = serializeMethods(methods);
583
+ const handleSynMessage = handleSynMessageFactory(log, serializedMethods, childOrigin, originForSending);
584
+ const handleAckMessage = handleAckMessageFactory(serializedMethods, childOrigin, originForSending, destructor, log);
585
+ const promise = new Promise((resolve, reject) => {
586
+ const stopConnectionTimeout = startConnectionTimeout(timeout, destroy);
587
+
588
+ const handleMessage = event => {
589
+ if (event.source !== iframe.contentWindow || !event.data) {
590
+ return;
591
+ }
592
+
593
+ if (event.data.penpal === MessageType.Syn) {
594
+ handleSynMessage(event);
595
+ return;
596
+ }
597
+
598
+ if (event.data.penpal === MessageType.Ack) {
599
+ const callSender = handleAckMessage(event);
600
+
601
+ if (callSender) {
602
+ stopConnectionTimeout();
603
+ resolve(callSender);
604
+ }
605
+
606
+ return;
607
+ }
608
+ };
609
+
610
+ window.addEventListener(NativeEventType.Message, handleMessage);
611
+ log('Parent: Awaiting handshake');
612
+ monitorIframeRemoval(iframe, destructor);
613
+ onDestroy(error => {
614
+ window.removeEventListener(NativeEventType.Message, handleMessage);
615
+
616
+ if (error) {
617
+ reject(error);
618
+ }
619
+ });
620
+ });
621
+ return {
622
+ promise,
623
+
624
+ destroy() {
625
+ // Don't allow consumer to pass an error into destroy.
626
+ destroy();
627
+ }
628
+
629
+ };
630
+ });
631
+
632
+ /**
633
+ * Handles a SYN-ACK handshake message.
634
+ */
635
+
636
+ var handleSynAckMessageFactory = ((parentOrigin, serializedMethods, destructor, log) => {
637
+ const {
638
+ destroy,
639
+ onDestroy
640
+ } = destructor;
641
+ return event => {
642
+ let originQualifies = parentOrigin instanceof RegExp ? parentOrigin.test(event.origin) : parentOrigin === '*' || parentOrigin === event.origin;
643
+
644
+ if (!originQualifies) {
645
+ log("Child: Handshake - Received SYN-ACK from origin ".concat(event.origin, " which did not match expected origin ").concat(parentOrigin));
646
+ return;
647
+ }
648
+
649
+ log('Child: Handshake - Received SYN-ACK, responding with ACK'); // If event.origin is "null", the remote protocol is file: or data: and we
650
+ // must post messages with "*" as targetOrigin when sending messages.
651
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions
652
+
653
+ const originForSending = event.origin === 'null' ? '*' : event.origin;
654
+ const ackMessage = {
655
+ penpal: MessageType.Ack,
656
+ methodNames: Object.keys(serializedMethods)
657
+ };
658
+ window.parent.postMessage(ackMessage, originForSending);
659
+ const info = {
660
+ localName: 'Child',
661
+ local: window,
662
+ remote: window.parent,
663
+ originForSending,
664
+ originForReceiving: event.origin
665
+ };
666
+ const destroyCallReceiver = connectCallReceiver(info, serializedMethods, log);
667
+ onDestroy(destroyCallReceiver);
668
+ const callSender = {};
669
+ const destroyCallSender = connectCallSender(callSender, info, event.data.methodNames, destroy, log);
670
+ onDestroy(destroyCallSender);
671
+ return callSender;
672
+ };
673
+ });
674
+
675
+ const areGlobalsAccessible = () => {
676
+ try {
677
+ clearTimeout();
678
+ } catch (e) {
679
+ return false;
680
+ }
681
+
682
+ return true;
683
+ };
684
+ /**
685
+ * Attempts to establish communication with the parent window.
686
+ */
687
+
688
+
689
+ var connectToParent = ((options = {}) => {
690
+ const {
691
+ parentOrigin = '*',
692
+ methods = {},
693
+ timeout,
694
+ debug = false
695
+ } = options;
696
+ const log = createLogger(debug);
697
+ const destructor = createDestructor('Child', log);
698
+ const {
699
+ destroy,
700
+ onDestroy
701
+ } = destructor;
702
+ const serializedMethods = serializeMethods(methods);
703
+ const handleSynAckMessage = handleSynAckMessageFactory(parentOrigin, serializedMethods, destructor, log);
704
+
705
+ const sendSynMessage = () => {
706
+ log('Child: Handshake - Sending SYN');
707
+ const synMessage = {
708
+ penpal: MessageType.Syn
709
+ };
710
+ const parentOriginForSyn = parentOrigin instanceof RegExp ? '*' : parentOrigin;
711
+ window.parent.postMessage(synMessage, parentOriginForSyn);
712
+ };
713
+
714
+ const promise = new Promise((resolve, reject) => {
715
+ const stopConnectionTimeout = startConnectionTimeout(timeout, destroy);
716
+
717
+ const handleMessage = event => {
718
+ // Under niche scenarios, we get into this function after
719
+ // the iframe has been removed from the DOM. In Edge, this
720
+ // results in "Object expected" errors being thrown when we
721
+ // try to access properties on window (global properties).
722
+ // For this reason, we try to access a global up front (clearTimeout)
723
+ // and if it fails we can assume the iframe has been removed
724
+ // and we ignore the message event.
725
+ if (!areGlobalsAccessible()) {
726
+ return;
727
+ }
728
+
729
+ if (event.source !== parent || !event.data) {
730
+ return;
731
+ }
732
+
733
+ if (event.data.penpal === MessageType.SynAck) {
734
+ const callSender = handleSynAckMessage(event);
735
+
736
+ if (callSender) {
737
+ window.removeEventListener(NativeEventType.Message, handleMessage);
738
+ stopConnectionTimeout();
739
+ resolve(callSender);
740
+ }
741
+ }
742
+ };
743
+
744
+ window.addEventListener(NativeEventType.Message, handleMessage);
745
+ sendSynMessage();
746
+ onDestroy(error => {
747
+ window.removeEventListener(NativeEventType.Message, handleMessage);
748
+
749
+ if (error) {
750
+ reject(error);
751
+ }
752
+ });
753
+ });
754
+ return {
755
+ promise,
756
+
757
+ destroy() {
758
+ // Don't allow consumer to pass an error into destroy.
759
+ destroy();
760
+ }
761
+
762
+ };
763
+ });
764
+
765
+ var indexForBundle = {
766
+ connectToChild,
767
+ connectToParent,
768
+ ErrorCode
769
+ };
770
+
771
+ return indexForBundle;
772
+
773
+ }());