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
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.deserializeMethods = exports.serializeMethods = exports.setAtKeyPath = void 0;
7
+ const KEY_PATH_DELIMITER = '.';
8
+
9
+ const keyPathToSegments = keyPath => keyPath ? keyPath.split(KEY_PATH_DELIMITER) : [];
10
+
11
+ const segmentsToKeyPath = segments => segments.join(KEY_PATH_DELIMITER);
12
+
13
+ const createKeyPath = (key, prefix) => {
14
+ const segments = keyPathToSegments(prefix || '');
15
+ segments.push(key);
16
+ return segmentsToKeyPath(segments);
17
+ };
18
+ /**
19
+ * Given a `keyPath`, set it to be `value` on `subject`, creating any intermediate
20
+ * objects along the way.
21
+ *
22
+ * @param {Object} subject The object on which to set value.
23
+ * @param {string} keyPath The key path at which to set value.
24
+ * @param {Object} value The value to store at the given key path.
25
+ * @returns {Object} Updated subject.
26
+ */
27
+
28
+
29
+ const setAtKeyPath = (subject, keyPath, value) => {
30
+ const segments = keyPathToSegments(keyPath);
31
+ segments.reduce((prevSubject, key, idx) => {
32
+ if (typeof prevSubject[key] === 'undefined') {
33
+ prevSubject[key] = {};
34
+ }
35
+
36
+ if (idx === segments.length - 1) {
37
+ prevSubject[key] = value;
38
+ }
39
+
40
+ return prevSubject[key];
41
+ }, subject);
42
+ return subject;
43
+ };
44
+ /**
45
+ * Given a dictionary of (nested) keys to function, flatten them to a map
46
+ * from key path to function.
47
+ *
48
+ * @param {Object} methods The (potentially nested) object to serialize.
49
+ * @param {string} prefix A string with which to prefix entries. Typically not intended to be used by consumers.
50
+ * @returns {Object} An map from key path in `methods` to functions.
51
+ */
52
+
53
+
54
+ exports.setAtKeyPath = setAtKeyPath;
55
+
56
+ const serializeMethods = (methods, prefix) => {
57
+ const flattenedMethods = {};
58
+ Object.keys(methods).forEach(key => {
59
+ const value = methods[key];
60
+ const keyPath = createKeyPath(key, prefix);
61
+
62
+ if (typeof value === 'object') {
63
+ // Recurse into any nested children.
64
+ Object.assign(flattenedMethods, serializeMethods(value, keyPath));
65
+ }
66
+
67
+ if (typeof value === 'function') {
68
+ // If we've found a method, expose it.
69
+ flattenedMethods[keyPath] = value;
70
+ }
71
+ });
72
+ return flattenedMethods;
73
+ };
74
+ /**
75
+ * Given a map of key paths to functions, unpack the key paths to an object.
76
+ *
77
+ * @param {Object} flattenedMethods A map of key paths to functions to unpack.
78
+ * @returns {Object} A (potentially nested) map of functions.
79
+ */
80
+
81
+
82
+ exports.serializeMethods = serializeMethods;
83
+
84
+ const deserializeMethods = flattenedMethods => {
85
+ const methods = {};
86
+
87
+ for (const keyPath in flattenedMethods) {
88
+ setAtKeyPath(methods, keyPath, flattenedMethods[keyPath]);
89
+ }
90
+
91
+ return methods;
92
+ };
93
+
94
+ exports.deserializeMethods = deserializeMethods;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _enums = require("../enums");
9
+
10
+ var _createDestructor = _interopRequireDefault(require("../createDestructor"));
11
+
12
+ var _createLogger = _interopRequireDefault(require("../createLogger"));
13
+
14
+ var _getOriginFromSrc = _interopRequireDefault(require("./getOriginFromSrc"));
15
+
16
+ var _handleAckMessageFactory = _interopRequireDefault(require("./handleAckMessageFactory"));
17
+
18
+ var _handleSynMessageFactory = _interopRequireDefault(require("./handleSynMessageFactory"));
19
+
20
+ var _methodSerialization = require("../methodSerialization");
21
+
22
+ var _monitorIframeRemoval = _interopRequireDefault(require("./monitorIframeRemoval"));
23
+
24
+ var _startConnectionTimeout = _interopRequireDefault(require("../startConnectionTimeout"));
25
+
26
+ var _validateIframeHasSrcOrSrcDoc = _interopRequireDefault(require("./validateIframeHasSrcOrSrcDoc"));
27
+
28
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29
+
30
+ /**
31
+ * Attempts to establish communication with an iframe.
32
+ */
33
+ var _default = options => {
34
+ let {
35
+ iframe,
36
+ methods = {},
37
+ childOrigin,
38
+ timeout,
39
+ debug = false
40
+ } = options;
41
+ const log = (0, _createLogger.default)(debug);
42
+ const destructor = (0, _createDestructor.default)('Parent', log);
43
+ const {
44
+ onDestroy,
45
+ destroy
46
+ } = destructor;
47
+
48
+ if (!childOrigin) {
49
+ (0, _validateIframeHasSrcOrSrcDoc.default)(iframe);
50
+ childOrigin = (0, _getOriginFromSrc.default)(iframe.src);
51
+ } // If event.origin is "null", the remote protocol is file: or data: and we
52
+ // must post messages with "*" as targetOrigin when sending messages.
53
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions
54
+
55
+
56
+ const originForSending = childOrigin === 'null' ? '*' : childOrigin;
57
+ const serializedMethods = (0, _methodSerialization.serializeMethods)(methods);
58
+ const handleSynMessage = (0, _handleSynMessageFactory.default)(log, serializedMethods, childOrigin, originForSending);
59
+ const handleAckMessage = (0, _handleAckMessageFactory.default)(serializedMethods, childOrigin, originForSending, destructor, log);
60
+ const promise = new Promise((resolve, reject) => {
61
+ const stopConnectionTimeout = (0, _startConnectionTimeout.default)(timeout, destroy);
62
+
63
+ const handleMessage = event => {
64
+ if (event.source !== iframe.contentWindow || !event.data) {
65
+ return;
66
+ }
67
+
68
+ if (event.data.penpal === _enums.MessageType.Syn) {
69
+ handleSynMessage(event);
70
+ return;
71
+ }
72
+
73
+ if (event.data.penpal === _enums.MessageType.Ack) {
74
+ const callSender = handleAckMessage(event);
75
+
76
+ if (callSender) {
77
+ stopConnectionTimeout();
78
+ resolve(callSender);
79
+ }
80
+
81
+ return;
82
+ }
83
+ };
84
+
85
+ window.addEventListener(_enums.NativeEventType.Message, handleMessage);
86
+ log('Parent: Awaiting handshake');
87
+ (0, _monitorIframeRemoval.default)(iframe, destructor);
88
+ onDestroy(error => {
89
+ window.removeEventListener(_enums.NativeEventType.Message, handleMessage);
90
+
91
+ if (error) {
92
+ reject(error);
93
+ }
94
+ });
95
+ });
96
+ return {
97
+ promise,
98
+
99
+ destroy() {
100
+ // Don't allow consumer to pass an error into destroy.
101
+ destroy();
102
+ }
103
+
104
+ };
105
+ };
106
+
107
+ exports.default = _default;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ const DEFAULT_PORT_BY_PROTOCOL = {
8
+ 'http:': '80',
9
+ 'https:': '443'
10
+ };
11
+ const URL_REGEX = /^(https?:)?\/\/([^/:]+)?(:(\d+))?/;
12
+ const opaqueOriginSchemes = ['file:', 'data:'];
13
+ /**
14
+ * Converts a src value into an origin.
15
+ */
16
+
17
+ var _default = src => {
18
+ if (src && opaqueOriginSchemes.find(scheme => src.startsWith(scheme))) {
19
+ // The origin of the child document is an opaque origin and its
20
+ // serialization is "null"
21
+ // https://html.spec.whatwg.org/multipage/origin.html#origin
22
+ return 'null';
23
+ } // Note that if src is undefined, then srcdoc is being used instead of src
24
+ // and we can follow this same logic below to get the origin of the parent,
25
+ // which is the origin that we will need to use.
26
+
27
+
28
+ const location = document.location;
29
+ const regexResult = URL_REGEX.exec(src);
30
+ let protocol;
31
+ let hostname;
32
+ let port;
33
+
34
+ if (regexResult) {
35
+ // It's an absolute URL. Use the parsed info.
36
+ // regexResult[1] will be undefined if the URL starts with //
37
+ protocol = regexResult[1] ? regexResult[1] : location.protocol;
38
+ hostname = regexResult[2];
39
+ port = regexResult[4];
40
+ } else {
41
+ // It's a relative path. Use the current location's info.
42
+ protocol = location.protocol;
43
+ hostname = location.hostname;
44
+ port = location.port;
45
+ } // If the port is the default for the protocol, we don't want to add it to the origin string
46
+ // or it won't match the message's event.origin.
47
+
48
+
49
+ const portSuffix = port && port !== DEFAULT_PORT_BY_PROTOCOL[protocol] ? ":".concat(port) : '';
50
+ return "".concat(protocol, "//").concat(hostname).concat(portSuffix);
51
+ };
52
+
53
+ exports.default = _default;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _connectCallReceiver = _interopRequireDefault(require("../connectCallReceiver"));
9
+
10
+ var _connectCallSender = _interopRequireDefault(require("../connectCallSender"));
11
+
12
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
+
14
+ /**
15
+ * Handles an ACK handshake message.
16
+ */
17
+ var _default = (serializedMethods, childOrigin, originForSending, destructor, log) => {
18
+ const {
19
+ destroy,
20
+ onDestroy
21
+ } = destructor;
22
+ let destroyCallReceiver;
23
+ let receiverMethodNames; // We resolve the promise with the call sender. If the child reconnects
24
+ // (for example, after refreshing or navigating to another page that
25
+ // uses Penpal, we'll update the call sender with methods that match the
26
+ // latest provided by the child.
27
+
28
+ const callSender = {};
29
+ return event => {
30
+ if (childOrigin !== '*' && event.origin !== childOrigin) {
31
+ log("Parent: Handshake - Received ACK message from origin ".concat(event.origin, " which did not match expected origin ").concat(childOrigin));
32
+ return;
33
+ }
34
+
35
+ log('Parent: Handshake - Received ACK');
36
+ const info = {
37
+ localName: 'Parent',
38
+ local: window,
39
+ remote: event.source,
40
+ originForSending: originForSending,
41
+ originForReceiving: childOrigin
42
+ }; // If the child reconnected, we need to destroy the prior call receiver
43
+ // before setting up a new one.
44
+
45
+ if (destroyCallReceiver) {
46
+ destroyCallReceiver();
47
+ }
48
+
49
+ destroyCallReceiver = (0, _connectCallReceiver.default)(info, serializedMethods, log);
50
+ onDestroy(destroyCallReceiver); // If the child reconnected, we need to remove the methods from the
51
+ // previous call receiver off the sender.
52
+
53
+ if (receiverMethodNames) {
54
+ receiverMethodNames.forEach(receiverMethodName => {
55
+ delete callSender[receiverMethodName];
56
+ });
57
+ }
58
+
59
+ receiverMethodNames = event.data.methodNames;
60
+ const destroyCallSender = (0, _connectCallSender.default)(callSender, info, receiverMethodNames, destroy, log);
61
+ onDestroy(destroyCallSender);
62
+ return callSender;
63
+ };
64
+ };
65
+
66
+ exports.default = _default;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _enums = require("../enums");
9
+
10
+ /**
11
+ * Handles a SYN handshake message.
12
+ */
13
+ var _default = (log, serializedMethods, childOrigin, originForSending) => {
14
+ return event => {
15
+ if (childOrigin !== '*' && event.origin !== childOrigin) {
16
+ log("Parent: Handshake - Received SYN message from origin ".concat(event.origin, " which did not match expected origin ").concat(childOrigin));
17
+ return;
18
+ }
19
+
20
+ log('Parent: Handshake - Received SYN, responding with SYN-ACK');
21
+ const synAckMessage = {
22
+ penpal: _enums.MessageType.SynAck,
23
+ methodNames: Object.keys(serializedMethods)
24
+ };
25
+ event.source.postMessage(synAckMessage, originForSending);
26
+ };
27
+ };
28
+
29
+ exports.default = _default;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ const CHECK_IFRAME_IN_DOC_INTERVAL = 60000;
8
+ /**
9
+ * Monitors for iframe removal and destroys connection if iframe
10
+ * is found to have been removed from DOM. This is to prevent memory
11
+ * leaks when the iframe is removed from the document and the consumer
12
+ * hasn't called destroy(). Without this, event listeners attached to
13
+ * the window would stick around and since the event handlers have a
14
+ * reference to the iframe in their closures, the iframe would stick
15
+ * around too.
16
+ */
17
+
18
+ var _default = (iframe, destructor) => {
19
+ const {
20
+ destroy,
21
+ onDestroy
22
+ } = destructor;
23
+ const checkIframeInDocIntervalId = setInterval(() => {
24
+ if (!iframe.isConnected) {
25
+ clearInterval(checkIframeInDocIntervalId);
26
+ destroy();
27
+ }
28
+ }, CHECK_IFRAME_IN_DOC_INTERVAL);
29
+ onDestroy(() => {
30
+ clearInterval(checkIframeInDocIntervalId);
31
+ });
32
+ };
33
+
34
+ exports.default = _default;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _enums = require("../enums");
9
+
10
+ var _default = iframe => {
11
+ if (!iframe.src && !iframe.srcdoc) {
12
+ const error = new Error('Iframe must have src or srcdoc property defined.');
13
+ error.code = _enums.ErrorCode.NoIframeSrc;
14
+ throw error;
15
+ }
16
+ };
17
+
18
+ exports.default = _default;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _enums = require("./enums");
9
+
10
+ /**
11
+ * Starts a timeout and calls the callback with an error
12
+ * if the timeout completes before the stop function is called.
13
+ */
14
+ var _default = (timeout, callback) => {
15
+ let timeoutId;
16
+
17
+ if (timeout !== undefined) {
18
+ timeoutId = window.setTimeout(() => {
19
+ const error = new Error("Connection timed out after ".concat(timeout, "ms"));
20
+ error.code = _enums.ErrorCode.ConnectionTimeout;
21
+ callback(error);
22
+ }, timeout);
23
+ }
24
+
25
+ return () => {
26
+ clearTimeout(timeoutId);
27
+ };
28
+ };
29
+
30
+ exports.default = _default;
package/es5/types.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,36 @@
1
+ import { Methods, CallSender, AsyncMethodReturns } from '../types';
2
+ declare type Options = {
3
+ /**
4
+ * Valid parent origin used to restrict communication.
5
+ */
6
+ parentOrigin?: string | RegExp;
7
+ /**
8
+ * Methods that may be called by the parent window.
9
+ */
10
+ methods?: Methods;
11
+ /**
12
+ * The amount of time, in milliseconds, Penpal should wait
13
+ * for the parent to respond before rejecting the connection promise.
14
+ */
15
+ timeout?: number;
16
+ /**
17
+ * Whether log messages should be emitted to the console.
18
+ */
19
+ debug?: boolean;
20
+ };
21
+ declare type Connection<TCallSender extends object = CallSender> = {
22
+ /**
23
+ * A promise which will be resolved once a connection has been established.
24
+ */
25
+ promise: Promise<AsyncMethodReturns<TCallSender>>;
26
+ /**
27
+ * A method that, when called, will disconnect any messaging channels.
28
+ * You may call this even before a connection has been established.
29
+ */
30
+ destroy: Function;
31
+ };
32
+ declare const _default: <TCallSender extends object = CallSender>(options?: Options) => Connection<TCallSender>;
33
+ /**
34
+ * Attempts to establish communication with the parent window.
35
+ */
36
+ export default _default;
@@ -0,0 +1,73 @@
1
+ import createDestructor from '../createDestructor';
2
+ import createLogger from '../createLogger';
3
+ import { MessageType, NativeEventType } from '../enums';
4
+ import handleSynAckMessageFactory from './handleSynAckMessageFactory';
5
+ import { serializeMethods } from '../methodSerialization';
6
+ import startConnectionTimeout from '../startConnectionTimeout';
7
+ const areGlobalsAccessible = () => {
8
+ try {
9
+ clearTimeout();
10
+ }
11
+ catch (e) {
12
+ return false;
13
+ }
14
+ return true;
15
+ };
16
+ /**
17
+ * Attempts to establish communication with the parent window.
18
+ */
19
+ export default (options = {}) => {
20
+ const { parentOrigin = '*', methods = {}, timeout, debug = false } = options;
21
+ const log = createLogger(debug);
22
+ const destructor = createDestructor('Child', log);
23
+ const { destroy, onDestroy } = destructor;
24
+ const serializedMethods = serializeMethods(methods);
25
+ const handleSynAckMessage = handleSynAckMessageFactory(parentOrigin, serializedMethods, destructor, log);
26
+ const sendSynMessage = () => {
27
+ log('Child: Handshake - Sending SYN');
28
+ const synMessage = { penpal: MessageType.Syn };
29
+ const parentOriginForSyn = parentOrigin instanceof RegExp ? '*' : parentOrigin;
30
+ window.parent.postMessage(synMessage, parentOriginForSyn);
31
+ };
32
+ const promise = new Promise((resolve, reject) => {
33
+ const stopConnectionTimeout = startConnectionTimeout(timeout, destroy);
34
+ const handleMessage = (event) => {
35
+ // Under niche scenarios, we get into this function after
36
+ // the iframe has been removed from the DOM. In Edge, this
37
+ // results in "Object expected" errors being thrown when we
38
+ // try to access properties on window (global properties).
39
+ // For this reason, we try to access a global up front (clearTimeout)
40
+ // and if it fails we can assume the iframe has been removed
41
+ // and we ignore the message event.
42
+ if (!areGlobalsAccessible()) {
43
+ return;
44
+ }
45
+ if (event.source !== parent || !event.data) {
46
+ return;
47
+ }
48
+ if (event.data.penpal === MessageType.SynAck) {
49
+ const callSender = handleSynAckMessage(event);
50
+ if (callSender) {
51
+ window.removeEventListener(NativeEventType.Message, handleMessage);
52
+ stopConnectionTimeout();
53
+ resolve(callSender);
54
+ }
55
+ }
56
+ };
57
+ window.addEventListener(NativeEventType.Message, handleMessage);
58
+ sendSynMessage();
59
+ onDestroy((error) => {
60
+ window.removeEventListener(NativeEventType.Message, handleMessage);
61
+ if (error) {
62
+ reject(error);
63
+ }
64
+ });
65
+ });
66
+ return {
67
+ promise,
68
+ destroy() {
69
+ // Don't allow consumer to pass an error into destroy.
70
+ destroy();
71
+ },
72
+ };
73
+ };
@@ -0,0 +1,7 @@
1
+ import { CallSender, SerializedMethods } from '../types';
2
+ import { Destructor } from '../createDestructor';
3
+ declare const _default: (parentOrigin: string | RegExp, serializedMethods: SerializedMethods, destructor: Destructor, log: Function) => (event: MessageEvent) => CallSender | undefined;
4
+ /**
5
+ * Handles a SYN-ACK handshake message.
6
+ */
7
+ export default _default;
@@ -0,0 +1,41 @@
1
+ import { MessageType } from '../enums';
2
+ import connectCallReceiver from '../connectCallReceiver';
3
+ import connectCallSender from '../connectCallSender';
4
+ /**
5
+ * Handles a SYN-ACK handshake message.
6
+ */
7
+ export default (parentOrigin, serializedMethods, destructor, log) => {
8
+ const { destroy, onDestroy } = destructor;
9
+ return (event) => {
10
+ let originQualifies = parentOrigin instanceof RegExp
11
+ ? parentOrigin.test(event.origin)
12
+ : parentOrigin === '*' || parentOrigin === event.origin;
13
+ if (!originQualifies) {
14
+ log(`Child: Handshake - Received SYN-ACK from origin ${event.origin} which did not match expected origin ${parentOrigin}`);
15
+ return;
16
+ }
17
+ log('Child: Handshake - Received SYN-ACK, responding with ACK');
18
+ // If event.origin is "null", the remote protocol is file: or data: and we
19
+ // must post messages with "*" as targetOrigin when sending messages.
20
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions
21
+ const originForSending = event.origin === 'null' ? '*' : event.origin;
22
+ const ackMessage = {
23
+ penpal: MessageType.Ack,
24
+ methodNames: Object.keys(serializedMethods),
25
+ };
26
+ window.parent.postMessage(ackMessage, originForSending);
27
+ const info = {
28
+ localName: 'Child',
29
+ local: window,
30
+ remote: window.parent,
31
+ originForSending,
32
+ originForReceiving: event.origin,
33
+ };
34
+ const destroyCallReceiver = connectCallReceiver(info, serializedMethods, log);
35
+ onDestroy(destroyCallReceiver);
36
+ const callSender = {};
37
+ const destroyCallSender = connectCallSender(callSender, info, event.data.methodNames, destroy, log);
38
+ onDestroy(destroyCallSender);
39
+ return callSender;
40
+ };
41
+ };
@@ -0,0 +1,7 @@
1
+ import { SerializedMethods, WindowsInfo } from './types';
2
+ declare const _default: (info: WindowsInfo, serializedMethods: SerializedMethods, log: Function) => () => void;
3
+ /**
4
+ * Listens for "call" messages coming from the remote, executes the corresponding method, and
5
+ * responds with the return value.
6
+ */
7
+ export default _default;
@@ -0,0 +1,71 @@
1
+ import { serializeError } from './errorSerialization';
2
+ import { MessageType, NativeEventType, NativeErrorName, Resolution, } from './enums';
3
+ /**
4
+ * Listens for "call" messages coming from the remote, executes the corresponding method, and
5
+ * responds with the return value.
6
+ */
7
+ export default (info, serializedMethods, log) => {
8
+ const { localName, local, remote, originForSending, originForReceiving, } = info;
9
+ let destroyed = false;
10
+ const handleMessageEvent = (event) => {
11
+ if (event.source !== remote || event.data.penpal !== MessageType.Call) {
12
+ return;
13
+ }
14
+ if (originForReceiving !== '*' && event.origin !== originForReceiving) {
15
+ log(`${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}`);
16
+ return;
17
+ }
18
+ const callMessage = event.data;
19
+ const { methodName, args, id } = callMessage;
20
+ log(`${localName}: Received ${methodName}() call`);
21
+ const createPromiseHandler = (resolution) => {
22
+ return (returnValue) => {
23
+ log(`${localName}: Sending ${methodName}() reply`);
24
+ if (destroyed) {
25
+ // It's possible to throw an error here, but it would need to be thrown asynchronously
26
+ // and would only be catchable using window.onerror. This is because the consumer
27
+ // is merely returning a value from their method and not calling any function
28
+ // that they could wrap in a try-catch. Even if the consumer were to catch the error,
29
+ // the value of doing so is questionable. Instead, we'll just log a message.
30
+ log(`${localName}: Unable to send ${methodName}() reply due to destroyed connection`);
31
+ return;
32
+ }
33
+ const message = {
34
+ penpal: MessageType.Reply,
35
+ id,
36
+ resolution,
37
+ returnValue,
38
+ };
39
+ if (resolution === Resolution.Rejected &&
40
+ returnValue instanceof Error) {
41
+ message.returnValue = serializeError(returnValue);
42
+ message.returnValueIsError = true;
43
+ }
44
+ try {
45
+ remote.postMessage(message, originForSending);
46
+ }
47
+ catch (err) {
48
+ // If a consumer attempts to send an object that's not cloneable (e.g., window),
49
+ // we want to ensure the receiver's promise gets rejected.
50
+ if (err.name === NativeErrorName.DataCloneError) {
51
+ const errorReplyMessage = {
52
+ penpal: MessageType.Reply,
53
+ id,
54
+ resolution: Resolution.Rejected,
55
+ returnValue: serializeError(err),
56
+ returnValueIsError: true,
57
+ };
58
+ remote.postMessage(errorReplyMessage, originForSending);
59
+ }
60
+ throw err;
61
+ }
62
+ };
63
+ };
64
+ new Promise((resolve) => resolve(serializedMethods[methodName].apply(serializedMethods, args))).then(createPromiseHandler(Resolution.Fulfilled), createPromiseHandler(Resolution.Rejected));
65
+ };
66
+ local.addEventListener(NativeEventType.Message, handleMessageEvent);
67
+ return () => {
68
+ destroyed = true;
69
+ local.removeEventListener(NativeEventType.Message, handleMessageEvent);
70
+ };
71
+ };