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.
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/penpal.js +773 -0
- package/dist/penpal.min.js +1 -0
- package/dist/penpal.min.js.map +1 -0
- package/es5/child/connectToParent.js +111 -0
- package/es5/child/handleSynAckMessageFactory.js +58 -0
- package/es5/connectCallReceiver.js +101 -0
- package/es5/connectCallSender.js +122 -0
- package/es5/createDestructor.js +29 -0
- package/es5/createLogger.js +19 -0
- package/es5/enums.js +47 -0
- package/es5/errorSerialization.js +34 -0
- package/es5/generateId.js +14 -0
- package/es5/index.js +63 -0
- package/es5/indexForBundle.js +21 -0
- package/es5/methodSerialization.js +94 -0
- package/es5/parent/connectToChild.js +107 -0
- package/es5/parent/getOriginFromSrc.js +53 -0
- package/es5/parent/handleAckMessageFactory.js +66 -0
- package/es5/parent/handleSynMessageFactory.js +29 -0
- package/es5/parent/monitorIframeRemoval.js +34 -0
- package/es5/parent/validateIframeHasSrcOrSrcDoc.js +18 -0
- package/es5/startConnectionTimeout.js +30 -0
- package/es5/types.js +1 -0
- package/lib/child/connectToParent.d.ts +36 -0
- package/lib/child/connectToParent.js +73 -0
- package/lib/child/handleSynAckMessageFactory.d.ts +7 -0
- package/lib/child/handleSynAckMessageFactory.js +41 -0
- package/lib/connectCallReceiver.d.ts +7 -0
- package/lib/connectCallReceiver.js +71 -0
- package/lib/connectCallSender.d.ts +14 -0
- package/lib/connectCallSender.js +93 -0
- package/lib/createDestructor.d.ts +13 -0
- package/lib/createDestructor.js +18 -0
- package/lib/createLogger.d.ts +2 -0
- package/lib/createLogger.js +10 -0
- package/lib/enums.d.ts +22 -0
- package/lib/enums.js +27 -0
- package/lib/errorSerialization.d.ts +14 -0
- package/lib/errorSerialization.js +17 -0
- package/lib/generateId.d.ts +5 -0
- package/lib/generateId.js +5 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +3 -0
- package/lib/indexForBundle.d.ts +21 -0
- package/lib/indexForBundle.js +8 -0
- package/lib/methodSerialization.d.ts +27 -0
- package/lib/methodSerialization.js +67 -0
- package/lib/parent/connectToChild.d.ts +31 -0
- package/lib/parent/connectToChild.js +66 -0
- package/lib/parent/getOriginFromSrc.d.ts +5 -0
- package/lib/parent/getOriginFromSrc.js +42 -0
- package/lib/parent/handleAckMessageFactory.d.ts +7 -0
- package/lib/parent/handleAckMessageFactory.js +47 -0
- package/lib/parent/handleSynMessageFactory.d.ts +6 -0
- package/lib/parent/handleSynMessageFactory.js +18 -0
- package/lib/parent/monitorIframeRemoval.d.ts +12 -0
- package/lib/parent/monitorIframeRemoval.js +22 -0
- package/lib/parent/validateIframeHasSrcOrSrcDoc.d.ts +2 -0
- package/lib/parent/validateIframeHasSrcOrSrcDoc.js +8 -0
- package/lib/startConnectionTimeout.d.ts +6 -0
- package/lib/startConnectionTimeout.js +18 -0
- package/lib/types.d.ts +114 -0
- package/lib/types.js +0 -0
- package/package.json +90 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CallSender, WindowsInfo } from './types';
|
|
2
|
+
declare const _default: (callSender: CallSender, info: WindowsInfo, methodKeyPaths: string[], destroyConnection: Function, log: Function) => () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Augments an object with methods that match those defined by the remote. When these methods are
|
|
5
|
+
* called, a "call" message will be sent to the remote, the remote's corresponding method will be
|
|
6
|
+
* executed, and the method's return value will be returned via a message.
|
|
7
|
+
* @param {Object} callSender Sender object that should be augmented with methods.
|
|
8
|
+
* @param {Object} info Information about the local and remote windows.
|
|
9
|
+
* @param {Array} methodKeyPaths Key paths of methods available to be called on the remote.
|
|
10
|
+
* @param {Promise} destructionPromise A promise resolved when destroy() is called on the penpal
|
|
11
|
+
* connection.
|
|
12
|
+
* @returns {Object} The call sender object with methods that may be called.
|
|
13
|
+
*/
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import generateId from './generateId';
|
|
2
|
+
import { deserializeError } from './errorSerialization';
|
|
3
|
+
import { deserializeMethods } from './methodSerialization';
|
|
4
|
+
import { ErrorCode, MessageType, NativeEventType, Resolution } from './enums';
|
|
5
|
+
/**
|
|
6
|
+
* Augments an object with methods that match those defined by the remote. When these methods are
|
|
7
|
+
* called, a "call" message will be sent to the remote, the remote's corresponding method will be
|
|
8
|
+
* executed, and the method's return value will be returned via a message.
|
|
9
|
+
* @param {Object} callSender Sender object that should be augmented with methods.
|
|
10
|
+
* @param {Object} info Information about the local and remote windows.
|
|
11
|
+
* @param {Array} methodKeyPaths Key paths of methods available to be called on the remote.
|
|
12
|
+
* @param {Promise} destructionPromise A promise resolved when destroy() is called on the penpal
|
|
13
|
+
* connection.
|
|
14
|
+
* @returns {Object} The call sender object with methods that may be called.
|
|
15
|
+
*/
|
|
16
|
+
export default (callSender, info, methodKeyPaths, destroyConnection, log) => {
|
|
17
|
+
const { localName, local, remote, originForSending, originForReceiving, } = info;
|
|
18
|
+
let destroyed = false;
|
|
19
|
+
log(`${localName}: Connecting call sender`);
|
|
20
|
+
const createMethodProxy = (methodName) => {
|
|
21
|
+
return (...args) => {
|
|
22
|
+
log(`${localName}: Sending ${methodName}() call`);
|
|
23
|
+
// This handles the case where the iframe has been removed from the DOM
|
|
24
|
+
// (and therefore its window closed), the consumer has not yet
|
|
25
|
+
// called destroy(), and the user calls a method exposed by
|
|
26
|
+
// the remote. We detect the iframe has been removed and force
|
|
27
|
+
// a destroy() immediately so that the consumer sees the error saying
|
|
28
|
+
// the connection has been destroyed. We wrap this check in a try catch
|
|
29
|
+
// because Edge throws an "Object expected" error when accessing
|
|
30
|
+
// contentWindow.closed on a contentWindow from an iframe that's been
|
|
31
|
+
// removed from the DOM.
|
|
32
|
+
let iframeRemoved;
|
|
33
|
+
try {
|
|
34
|
+
if (remote.closed) {
|
|
35
|
+
iframeRemoved = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
iframeRemoved = true;
|
|
40
|
+
}
|
|
41
|
+
if (iframeRemoved) {
|
|
42
|
+
destroyConnection();
|
|
43
|
+
}
|
|
44
|
+
if (destroyed) {
|
|
45
|
+
const error = new Error(`Unable to send ${methodName}() call due ` + `to destroyed connection`);
|
|
46
|
+
error.code = ErrorCode.ConnectionDestroyed;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const id = generateId();
|
|
51
|
+
const handleMessageEvent = (event) => {
|
|
52
|
+
if (event.source !== remote ||
|
|
53
|
+
event.data.penpal !== MessageType.Reply ||
|
|
54
|
+
event.data.id !== id) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (originForReceiving !== '*' &&
|
|
58
|
+
event.origin !== originForReceiving) {
|
|
59
|
+
log(`${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const replyMessage = event.data;
|
|
63
|
+
log(`${localName}: Received ${methodName}() reply`);
|
|
64
|
+
local.removeEventListener(NativeEventType.Message, handleMessageEvent);
|
|
65
|
+
let returnValue = replyMessage.returnValue;
|
|
66
|
+
if (replyMessage.returnValueIsError) {
|
|
67
|
+
returnValue = deserializeError(returnValue);
|
|
68
|
+
}
|
|
69
|
+
(replyMessage.resolution === Resolution.Fulfilled ? resolve : reject)(returnValue);
|
|
70
|
+
};
|
|
71
|
+
local.addEventListener(NativeEventType.Message, handleMessageEvent);
|
|
72
|
+
const callMessage = {
|
|
73
|
+
penpal: MessageType.Call,
|
|
74
|
+
id,
|
|
75
|
+
methodName,
|
|
76
|
+
args,
|
|
77
|
+
};
|
|
78
|
+
remote.postMessage(callMessage, originForSending);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
// Wrap each method in a proxy which sends it to the corresponding receiver.
|
|
83
|
+
const flattenedMethods = methodKeyPaths.reduce((api, name) => {
|
|
84
|
+
api[name] = createMethodProxy(name);
|
|
85
|
+
return api;
|
|
86
|
+
}, {});
|
|
87
|
+
// Unpack the structure of the provided methods object onto the CallSender, exposing
|
|
88
|
+
// the methods in the same shape they were provided.
|
|
89
|
+
Object.assign(callSender, deserializeMethods(flattenedMethods));
|
|
90
|
+
return () => {
|
|
91
|
+
destroyed = true;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PenpalError } from './types';
|
|
2
|
+
export declare type Destructor = {
|
|
3
|
+
/**
|
|
4
|
+
* Calls all onDestroy callbacks.
|
|
5
|
+
*/
|
|
6
|
+
destroy(error?: PenpalError): void;
|
|
7
|
+
/**
|
|
8
|
+
* Registers a callback to be called when destroy is called.
|
|
9
|
+
*/
|
|
10
|
+
onDestroy(callback: Function): void;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: (localName: "Parent" | "Child", log: Function) => Destructor;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default (localName, log) => {
|
|
2
|
+
const callbacks = [];
|
|
3
|
+
let destroyed = false;
|
|
4
|
+
return {
|
|
5
|
+
destroy(error) {
|
|
6
|
+
if (!destroyed) {
|
|
7
|
+
destroyed = true;
|
|
8
|
+
log(`${localName}: Destroying connection`);
|
|
9
|
+
callbacks.forEach((callback) => {
|
|
10
|
+
callback(error);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
onDestroy(callback) {
|
|
15
|
+
destroyed ? callback() : callbacks.push(callback);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
};
|
package/lib/enums.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare enum MessageType {
|
|
2
|
+
Call = "call",
|
|
3
|
+
Reply = "reply",
|
|
4
|
+
Syn = "syn",
|
|
5
|
+
SynAck = "synAck",
|
|
6
|
+
Ack = "ack"
|
|
7
|
+
}
|
|
8
|
+
export declare enum Resolution {
|
|
9
|
+
Fulfilled = "fulfilled",
|
|
10
|
+
Rejected = "rejected"
|
|
11
|
+
}
|
|
12
|
+
export declare enum ErrorCode {
|
|
13
|
+
ConnectionDestroyed = "ConnectionDestroyed",
|
|
14
|
+
ConnectionTimeout = "ConnectionTimeout",
|
|
15
|
+
NoIframeSrc = "NoIframeSrc"
|
|
16
|
+
}
|
|
17
|
+
export declare enum NativeErrorName {
|
|
18
|
+
DataCloneError = "DataCloneError"
|
|
19
|
+
}
|
|
20
|
+
export declare enum NativeEventType {
|
|
21
|
+
Message = "message"
|
|
22
|
+
}
|
package/lib/enums.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export var MessageType;
|
|
2
|
+
(function (MessageType) {
|
|
3
|
+
MessageType["Call"] = "call";
|
|
4
|
+
MessageType["Reply"] = "reply";
|
|
5
|
+
MessageType["Syn"] = "syn";
|
|
6
|
+
MessageType["SynAck"] = "synAck";
|
|
7
|
+
MessageType["Ack"] = "ack";
|
|
8
|
+
})(MessageType || (MessageType = {}));
|
|
9
|
+
export var Resolution;
|
|
10
|
+
(function (Resolution) {
|
|
11
|
+
Resolution["Fulfilled"] = "fulfilled";
|
|
12
|
+
Resolution["Rejected"] = "rejected";
|
|
13
|
+
})(Resolution || (Resolution = {}));
|
|
14
|
+
export var ErrorCode;
|
|
15
|
+
(function (ErrorCode) {
|
|
16
|
+
ErrorCode["ConnectionDestroyed"] = "ConnectionDestroyed";
|
|
17
|
+
ErrorCode["ConnectionTimeout"] = "ConnectionTimeout";
|
|
18
|
+
ErrorCode["NoIframeSrc"] = "NoIframeSrc";
|
|
19
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
20
|
+
export var NativeErrorName;
|
|
21
|
+
(function (NativeErrorName) {
|
|
22
|
+
NativeErrorName["DataCloneError"] = "DataCloneError";
|
|
23
|
+
})(NativeErrorName || (NativeErrorName = {}));
|
|
24
|
+
export var NativeEventType;
|
|
25
|
+
(function (NativeEventType) {
|
|
26
|
+
NativeEventType["Message"] = "message";
|
|
27
|
+
})(NativeEventType || (NativeEventType = {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare type SerializedError = {
|
|
2
|
+
name: string;
|
|
3
|
+
message: string;
|
|
4
|
+
stack: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Converts an error object into a plain object.
|
|
8
|
+
*/
|
|
9
|
+
export declare const serializeError: ({ name, message, stack, }: Error) => SerializedError;
|
|
10
|
+
/**
|
|
11
|
+
* Converts a plain object into an error object.
|
|
12
|
+
*/
|
|
13
|
+
export declare const deserializeError: (obj: SerializedError) => Error;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an error object into a plain object.
|
|
3
|
+
*/
|
|
4
|
+
export const serializeError = ({ name, message, stack, }) => ({
|
|
5
|
+
name,
|
|
6
|
+
message,
|
|
7
|
+
stack,
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Converts a plain object into an error object.
|
|
11
|
+
*/
|
|
12
|
+
export const deserializeError = (obj) => {
|
|
13
|
+
const deserializedError = new Error();
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
Object.keys(obj).forEach((key) => (deserializedError[key] = obj[key]));
|
|
16
|
+
return deserializedError;
|
|
17
|
+
};
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as connectToChild } from './parent/connectToChild';
|
|
2
|
+
export { default as connectToParent } from './child/connectToParent';
|
|
3
|
+
export { ErrorCode } from './enums';
|
|
4
|
+
export { Connection, AsyncMethodReturns, CallSender, Methods, PenpalError, } from './types';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ErrorCode } from './enums';
|
|
2
|
+
declare const _default: {
|
|
3
|
+
connectToChild: <TCallSender extends object = import("./types").CallSender>(options: {
|
|
4
|
+
iframe: HTMLIFrameElement;
|
|
5
|
+
methods?: import("./types").Methods | undefined;
|
|
6
|
+
childOrigin?: string | undefined;
|
|
7
|
+
timeout?: number | undefined;
|
|
8
|
+
debug?: boolean | undefined;
|
|
9
|
+
}) => import("./types").Connection<TCallSender>;
|
|
10
|
+
connectToParent: <TCallSender_1 extends object = import("./types").CallSender>(options?: {
|
|
11
|
+
parentOrigin?: string | RegExp | undefined;
|
|
12
|
+
methods?: import("./types").Methods | undefined;
|
|
13
|
+
timeout?: number | undefined;
|
|
14
|
+
debug?: boolean | undefined;
|
|
15
|
+
}) => {
|
|
16
|
+
promise: Promise<import("./types").AsyncMethodReturns<TCallSender_1>>;
|
|
17
|
+
destroy: Function;
|
|
18
|
+
};
|
|
19
|
+
ErrorCode: typeof ErrorCode;
|
|
20
|
+
};
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SerializedMethods, Methods } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Given a `keyPath`, set it to be `value` on `subject`, creating any intermediate
|
|
4
|
+
* objects along the way.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} subject The object on which to set value.
|
|
7
|
+
* @param {string} keyPath The key path at which to set value.
|
|
8
|
+
* @param {Object} value The value to store at the given key path.
|
|
9
|
+
* @returns {Object} Updated subject.
|
|
10
|
+
*/
|
|
11
|
+
export declare const setAtKeyPath: (subject: Record<string, any>, keyPath: string, value: any) => Record<string, any>;
|
|
12
|
+
/**
|
|
13
|
+
* Given a dictionary of (nested) keys to function, flatten them to a map
|
|
14
|
+
* from key path to function.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} methods The (potentially nested) object to serialize.
|
|
17
|
+
* @param {string} prefix A string with which to prefix entries. Typically not intended to be used by consumers.
|
|
18
|
+
* @returns {Object} An map from key path in `methods` to functions.
|
|
19
|
+
*/
|
|
20
|
+
export declare const serializeMethods: (methods: Methods, prefix?: string | undefined) => SerializedMethods;
|
|
21
|
+
/**
|
|
22
|
+
* Given a map of key paths to functions, unpack the key paths to an object.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} flattenedMethods A map of key paths to functions to unpack.
|
|
25
|
+
* @returns {Object} A (potentially nested) map of functions.
|
|
26
|
+
*/
|
|
27
|
+
export declare const deserializeMethods: (flattenedMethods: SerializedMethods) => Methods;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const KEY_PATH_DELIMITER = '.';
|
|
2
|
+
const keyPathToSegments = (keyPath) => keyPath ? keyPath.split(KEY_PATH_DELIMITER) : [];
|
|
3
|
+
const segmentsToKeyPath = (segments) => segments.join(KEY_PATH_DELIMITER);
|
|
4
|
+
const createKeyPath = (key, prefix) => {
|
|
5
|
+
const segments = keyPathToSegments(prefix || '');
|
|
6
|
+
segments.push(key);
|
|
7
|
+
return segmentsToKeyPath(segments);
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Given a `keyPath`, set it to be `value` on `subject`, creating any intermediate
|
|
11
|
+
* objects along the way.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} subject The object on which to set value.
|
|
14
|
+
* @param {string} keyPath The key path at which to set value.
|
|
15
|
+
* @param {Object} value The value to store at the given key path.
|
|
16
|
+
* @returns {Object} Updated subject.
|
|
17
|
+
*/
|
|
18
|
+
export const setAtKeyPath = (subject, keyPath, value) => {
|
|
19
|
+
const segments = keyPathToSegments(keyPath);
|
|
20
|
+
segments.reduce((prevSubject, key, idx) => {
|
|
21
|
+
if (typeof prevSubject[key] === 'undefined') {
|
|
22
|
+
prevSubject[key] = {};
|
|
23
|
+
}
|
|
24
|
+
if (idx === segments.length - 1) {
|
|
25
|
+
prevSubject[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return prevSubject[key];
|
|
28
|
+
}, subject);
|
|
29
|
+
return subject;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Given a dictionary of (nested) keys to function, flatten them to a map
|
|
33
|
+
* from key path to function.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} methods The (potentially nested) object to serialize.
|
|
36
|
+
* @param {string} prefix A string with which to prefix entries. Typically not intended to be used by consumers.
|
|
37
|
+
* @returns {Object} An map from key path in `methods` to functions.
|
|
38
|
+
*/
|
|
39
|
+
export const serializeMethods = (methods, prefix) => {
|
|
40
|
+
const flattenedMethods = {};
|
|
41
|
+
Object.keys(methods).forEach((key) => {
|
|
42
|
+
const value = methods[key];
|
|
43
|
+
const keyPath = createKeyPath(key, prefix);
|
|
44
|
+
if (typeof value === 'object') {
|
|
45
|
+
// Recurse into any nested children.
|
|
46
|
+
Object.assign(flattenedMethods, serializeMethods(value, keyPath));
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'function') {
|
|
49
|
+
// If we've found a method, expose it.
|
|
50
|
+
flattenedMethods[keyPath] = value;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return flattenedMethods;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Given a map of key paths to functions, unpack the key paths to an object.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} flattenedMethods A map of key paths to functions to unpack.
|
|
59
|
+
* @returns {Object} A (potentially nested) map of functions.
|
|
60
|
+
*/
|
|
61
|
+
export const deserializeMethods = (flattenedMethods) => {
|
|
62
|
+
const methods = {};
|
|
63
|
+
for (const keyPath in flattenedMethods) {
|
|
64
|
+
setAtKeyPath(methods, keyPath, flattenedMethods[keyPath]);
|
|
65
|
+
}
|
|
66
|
+
return methods;
|
|
67
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CallSender, Connection, Methods } from '../types';
|
|
2
|
+
declare type Options = {
|
|
3
|
+
/**
|
|
4
|
+
* The iframe to which a connection should be made.
|
|
5
|
+
*/
|
|
6
|
+
iframe: HTMLIFrameElement;
|
|
7
|
+
/**
|
|
8
|
+
* Methods that may be called by the iframe.
|
|
9
|
+
*/
|
|
10
|
+
methods?: Methods;
|
|
11
|
+
/**
|
|
12
|
+
* The child origin to use to secure communication. If
|
|
13
|
+
* not provided, the child origin will be derived from the
|
|
14
|
+
* iframe's src or srcdoc value.
|
|
15
|
+
*/
|
|
16
|
+
childOrigin?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The amount of time, in milliseconds, Penpal should wait
|
|
19
|
+
* for the iframe to respond before rejecting the connection promise.
|
|
20
|
+
*/
|
|
21
|
+
timeout?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Whether log messages should be emitted to the console.
|
|
24
|
+
*/
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
};
|
|
27
|
+
declare const _default: <TCallSender extends object = CallSender>(options: Options) => Connection<TCallSender>;
|
|
28
|
+
/**
|
|
29
|
+
* Attempts to establish communication with an iframe.
|
|
30
|
+
*/
|
|
31
|
+
export default _default;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { MessageType, NativeEventType } from '../enums';
|
|
2
|
+
import createDestructor from '../createDestructor';
|
|
3
|
+
import createLogger from '../createLogger';
|
|
4
|
+
import getOriginFromSrc from './getOriginFromSrc';
|
|
5
|
+
import handleAckMessageFactory from './handleAckMessageFactory';
|
|
6
|
+
import handleSynMessageFactory from './handleSynMessageFactory';
|
|
7
|
+
import { serializeMethods } from '../methodSerialization';
|
|
8
|
+
import monitorIframeRemoval from './monitorIframeRemoval';
|
|
9
|
+
import startConnectionTimeout from '../startConnectionTimeout';
|
|
10
|
+
import validateIframeHasSrcOrSrcDoc from './validateIframeHasSrcOrSrcDoc';
|
|
11
|
+
/**
|
|
12
|
+
* Attempts to establish communication with an iframe.
|
|
13
|
+
*/
|
|
14
|
+
export default (options) => {
|
|
15
|
+
let { iframe, methods = {}, childOrigin, timeout, debug = false } = options;
|
|
16
|
+
const log = createLogger(debug);
|
|
17
|
+
const destructor = createDestructor('Parent', log);
|
|
18
|
+
const { onDestroy, destroy } = destructor;
|
|
19
|
+
if (!childOrigin) {
|
|
20
|
+
validateIframeHasSrcOrSrcDoc(iframe);
|
|
21
|
+
childOrigin = getOriginFromSrc(iframe.src);
|
|
22
|
+
}
|
|
23
|
+
// If event.origin is "null", the remote protocol is file: or data: and we
|
|
24
|
+
// must post messages with "*" as targetOrigin when sending messages.
|
|
25
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions
|
|
26
|
+
const originForSending = childOrigin === 'null' ? '*' : childOrigin;
|
|
27
|
+
const serializedMethods = serializeMethods(methods);
|
|
28
|
+
const handleSynMessage = handleSynMessageFactory(log, serializedMethods, childOrigin, originForSending);
|
|
29
|
+
const handleAckMessage = handleAckMessageFactory(serializedMethods, childOrigin, originForSending, destructor, log);
|
|
30
|
+
const promise = new Promise((resolve, reject) => {
|
|
31
|
+
const stopConnectionTimeout = startConnectionTimeout(timeout, destroy);
|
|
32
|
+
const handleMessage = (event) => {
|
|
33
|
+
if (event.source !== iframe.contentWindow || !event.data) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (event.data.penpal === MessageType.Syn) {
|
|
37
|
+
handleSynMessage(event);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (event.data.penpal === MessageType.Ack) {
|
|
41
|
+
const callSender = handleAckMessage(event);
|
|
42
|
+
if (callSender) {
|
|
43
|
+
stopConnectionTimeout();
|
|
44
|
+
resolve(callSender);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
window.addEventListener(NativeEventType.Message, handleMessage);
|
|
50
|
+
log('Parent: Awaiting handshake');
|
|
51
|
+
monitorIframeRemoval(iframe, destructor);
|
|
52
|
+
onDestroy((error) => {
|
|
53
|
+
window.removeEventListener(NativeEventType.Message, handleMessage);
|
|
54
|
+
if (error) {
|
|
55
|
+
reject(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
promise,
|
|
61
|
+
destroy() {
|
|
62
|
+
// Don't allow consumer to pass an error into destroy.
|
|
63
|
+
destroy();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const DEFAULT_PORT_BY_PROTOCOL = {
|
|
2
|
+
'http:': '80',
|
|
3
|
+
'https:': '443',
|
|
4
|
+
};
|
|
5
|
+
const URL_REGEX = /^(https?:)?\/\/([^/:]+)?(:(\d+))?/;
|
|
6
|
+
const opaqueOriginSchemes = ['file:', 'data:'];
|
|
7
|
+
/**
|
|
8
|
+
* Converts a src value into an origin.
|
|
9
|
+
*/
|
|
10
|
+
export default (src) => {
|
|
11
|
+
if (src && opaqueOriginSchemes.find((scheme) => src.startsWith(scheme))) {
|
|
12
|
+
// The origin of the child document is an opaque origin and its
|
|
13
|
+
// serialization is "null"
|
|
14
|
+
// https://html.spec.whatwg.org/multipage/origin.html#origin
|
|
15
|
+
return 'null';
|
|
16
|
+
}
|
|
17
|
+
// Note that if src is undefined, then srcdoc is being used instead of src
|
|
18
|
+
// and we can follow this same logic below to get the origin of the parent,
|
|
19
|
+
// which is the origin that we will need to use.
|
|
20
|
+
const location = document.location;
|
|
21
|
+
const regexResult = URL_REGEX.exec(src);
|
|
22
|
+
let protocol;
|
|
23
|
+
let hostname;
|
|
24
|
+
let port;
|
|
25
|
+
if (regexResult) {
|
|
26
|
+
// It's an absolute URL. Use the parsed info.
|
|
27
|
+
// regexResult[1] will be undefined if the URL starts with //
|
|
28
|
+
protocol = regexResult[1] ? regexResult[1] : location.protocol;
|
|
29
|
+
hostname = regexResult[2];
|
|
30
|
+
port = regexResult[4];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// It's a relative path. Use the current location's info.
|
|
34
|
+
protocol = location.protocol;
|
|
35
|
+
hostname = location.hostname;
|
|
36
|
+
port = location.port;
|
|
37
|
+
}
|
|
38
|
+
// If the port is the default for the protocol, we don't want to add it to the origin string
|
|
39
|
+
// or it won't match the message's event.origin.
|
|
40
|
+
const portSuffix = port && port !== DEFAULT_PORT_BY_PROTOCOL[protocol] ? `:${port}` : '';
|
|
41
|
+
return `${protocol}//${hostname}${portSuffix}`;
|
|
42
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CallSender, SerializedMethods } from '../types';
|
|
2
|
+
import { Destructor } from '../createDestructor';
|
|
3
|
+
declare const _default: (serializedMethods: SerializedMethods, childOrigin: string, originForSending: string, destructor: Destructor, log: Function) => (event: MessageEvent) => CallSender | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Handles an ACK handshake message.
|
|
6
|
+
*/
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import connectCallReceiver from '../connectCallReceiver';
|
|
2
|
+
import connectCallSender from '../connectCallSender';
|
|
3
|
+
/**
|
|
4
|
+
* Handles an ACK handshake message.
|
|
5
|
+
*/
|
|
6
|
+
export default (serializedMethods, childOrigin, originForSending, destructor, log) => {
|
|
7
|
+
const { destroy, onDestroy } = destructor;
|
|
8
|
+
let destroyCallReceiver;
|
|
9
|
+
let receiverMethodNames;
|
|
10
|
+
// We resolve the promise with the call sender. If the child reconnects
|
|
11
|
+
// (for example, after refreshing or navigating to another page that
|
|
12
|
+
// uses Penpal, we'll update the call sender with methods that match the
|
|
13
|
+
// latest provided by the child.
|
|
14
|
+
const callSender = {};
|
|
15
|
+
return (event) => {
|
|
16
|
+
if (childOrigin !== '*' && event.origin !== childOrigin) {
|
|
17
|
+
log(`Parent: Handshake - Received ACK message from origin ${event.origin} which did not match expected origin ${childOrigin}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
log('Parent: Handshake - Received ACK');
|
|
21
|
+
const info = {
|
|
22
|
+
localName: 'Parent',
|
|
23
|
+
local: window,
|
|
24
|
+
remote: event.source,
|
|
25
|
+
originForSending: originForSending,
|
|
26
|
+
originForReceiving: childOrigin,
|
|
27
|
+
};
|
|
28
|
+
// If the child reconnected, we need to destroy the prior call receiver
|
|
29
|
+
// before setting up a new one.
|
|
30
|
+
if (destroyCallReceiver) {
|
|
31
|
+
destroyCallReceiver();
|
|
32
|
+
}
|
|
33
|
+
destroyCallReceiver = connectCallReceiver(info, serializedMethods, log);
|
|
34
|
+
onDestroy(destroyCallReceiver);
|
|
35
|
+
// If the child reconnected, we need to remove the methods from the
|
|
36
|
+
// previous call receiver off the sender.
|
|
37
|
+
if (receiverMethodNames) {
|
|
38
|
+
receiverMethodNames.forEach((receiverMethodName) => {
|
|
39
|
+
delete callSender[receiverMethodName];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
receiverMethodNames = event.data.methodNames;
|
|
43
|
+
const destroyCallSender = connectCallSender(callSender, info, receiverMethodNames, destroy, log);
|
|
44
|
+
onDestroy(destroyCallSender);
|
|
45
|
+
return callSender;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SerializedMethods } from '../types';
|
|
2
|
+
declare const _default: (log: Function, serializedMethods: SerializedMethods, childOrigin: string, originForSending: string) => (event: MessageEvent) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Handles a SYN handshake message.
|
|
5
|
+
*/
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MessageType } from '../enums';
|
|
2
|
+
/**
|
|
3
|
+
* Handles a SYN handshake message.
|
|
4
|
+
*/
|
|
5
|
+
export default (log, serializedMethods, childOrigin, originForSending) => {
|
|
6
|
+
return (event) => {
|
|
7
|
+
if (childOrigin !== '*' && event.origin !== childOrigin) {
|
|
8
|
+
log(`Parent: Handshake - Received SYN message from origin ${event.origin} which did not match expected origin ${childOrigin}`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
log('Parent: Handshake - Received SYN, responding with SYN-ACK');
|
|
12
|
+
const synAckMessage = {
|
|
13
|
+
penpal: MessageType.SynAck,
|
|
14
|
+
methodNames: Object.keys(serializedMethods),
|
|
15
|
+
};
|
|
16
|
+
event.source.postMessage(synAckMessage, originForSending);
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Destructor } from '../createDestructor';
|
|
2
|
+
declare const _default: (iframe: HTMLIFrameElement, destructor: Destructor) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Monitors for iframe removal and destroys connection if iframe
|
|
5
|
+
* is found to have been removed from DOM. This is to prevent memory
|
|
6
|
+
* leaks when the iframe is removed from the document and the consumer
|
|
7
|
+
* hasn't called destroy(). Without this, event listeners attached to
|
|
8
|
+
* the window would stick around and since the event handlers have a
|
|
9
|
+
* reference to the iframe in their closures, the iframe would stick
|
|
10
|
+
* around too.
|
|
11
|
+
*/
|
|
12
|
+
export default _default;
|