penpal 7.0.3 → 7.0.4

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 (111) hide show
  1. package/dist/penpal.cjs +953 -0
  2. package/dist/penpal.cjs.map +1 -0
  3. package/dist/penpal.d.cts +234 -0
  4. package/dist/penpal.d.ts +234 -0
  5. package/dist/penpal.js +890 -1033
  6. package/dist/penpal.js.map +1 -0
  7. package/dist/penpal.min.js +1 -1
  8. package/dist/penpal.min.js.map +1 -1
  9. package/dist/penpal.mjs +943 -0
  10. package/dist/penpal.mjs.map +1 -0
  11. package/package.json +13 -21
  12. package/cjs/CallOptions.d.ts +0 -10
  13. package/cjs/CallOptions.js +0 -16
  14. package/cjs/ErrorCodeObj.d.ts +0 -9
  15. package/cjs/ErrorCodeObj.js +0 -14
  16. package/cjs/PenpalBugError.d.ts +0 -8
  17. package/cjs/PenpalBugError.js +0 -12
  18. package/cjs/PenpalError.d.ts +0 -6
  19. package/cjs/PenpalError.js +0 -11
  20. package/cjs/Reply.d.ts +0 -9
  21. package/cjs/Reply.js +0 -16
  22. package/cjs/backwardCompatibility.d.ts +0 -56
  23. package/cjs/backwardCompatibility.js +0 -134
  24. package/cjs/connect.d.ts +0 -33
  25. package/cjs/connect.js +0 -78
  26. package/cjs/connectCallHandler.d.ts +0 -8
  27. package/cjs/connectCallHandler.js +0 -90
  28. package/cjs/connectRemoteProxy.d.ts +0 -12
  29. package/cjs/connectRemoteProxy.js +0 -139
  30. package/cjs/debug.d.ts +0 -3
  31. package/cjs/debug.js +0 -8
  32. package/cjs/errorSerialization.d.ts +0 -9
  33. package/cjs/errorSerialization.js +0 -26
  34. package/cjs/generateId.d.ts +0 -8
  35. package/cjs/generateId.js +0 -11
  36. package/cjs/getPromiseWithResolvers.d.ts +0 -6
  37. package/cjs/getPromiseWithResolvers.js +0 -19
  38. package/cjs/guards.d.ts +0 -10
  39. package/cjs/guards.js +0 -40
  40. package/cjs/index.d.ts +0 -12
  41. package/cjs/index.js +0 -21
  42. package/cjs/indexForBundle.d.ts +0 -31
  43. package/cjs/indexForBundle.js +0 -22
  44. package/cjs/messengers/Messenger.d.ts +0 -14
  45. package/cjs/messengers/Messenger.js +0 -2
  46. package/cjs/messengers/PortMessenger.d.ts +0 -21
  47. package/cjs/messengers/PortMessenger.js +0 -47
  48. package/cjs/messengers/WindowMessenger.d.ts +0 -29
  49. package/cjs/messengers/WindowMessenger.js +0 -178
  50. package/cjs/messengers/WorkerMessenger.d.ts +0 -23
  51. package/cjs/messengers/WorkerMessenger.js +0 -86
  52. package/cjs/methodSerialization.d.ts +0 -22
  53. package/cjs/methodSerialization.js +0 -48
  54. package/cjs/namespace.d.ts +0 -2
  55. package/cjs/namespace.js +0 -3
  56. package/cjs/once.d.ts +0 -2
  57. package/cjs/once.js +0 -15
  58. package/cjs/shakeHands.d.ts +0 -76
  59. package/cjs/shakeHands.js +0 -190
  60. package/cjs/types.d.ts +0 -89
  61. package/cjs/types.js +0 -2
  62. package/lib/CallOptions.d.ts +0 -10
  63. package/lib/CallOptions.js +0 -14
  64. package/lib/ErrorCodeObj.d.ts +0 -9
  65. package/lib/ErrorCodeObj.js +0 -12
  66. package/lib/PenpalBugError.d.ts +0 -8
  67. package/lib/PenpalBugError.js +0 -10
  68. package/lib/PenpalError.d.ts +0 -6
  69. package/lib/PenpalError.js +0 -9
  70. package/lib/Reply.d.ts +0 -9
  71. package/lib/Reply.js +0 -14
  72. package/lib/backwardCompatibility.d.ts +0 -56
  73. package/lib/backwardCompatibility.js +0 -128
  74. package/lib/connect.d.ts +0 -33
  75. package/lib/connect.js +0 -76
  76. package/lib/connectCallHandler.d.ts +0 -8
  77. package/lib/connectCallHandler.js +0 -88
  78. package/lib/connectRemoteProxy.d.ts +0 -12
  79. package/lib/connectRemoteProxy.js +0 -137
  80. package/lib/debug.d.ts +0 -3
  81. package/lib/debug.js +0 -6
  82. package/lib/errorSerialization.d.ts +0 -9
  83. package/lib/errorSerialization.js +0 -21
  84. package/lib/generateId.d.ts +0 -8
  85. package/lib/generateId.js +0 -9
  86. package/lib/getPromiseWithResolvers.d.ts +0 -6
  87. package/lib/getPromiseWithResolvers.js +0 -17
  88. package/lib/guards.d.ts +0 -10
  89. package/lib/guards.js +0 -28
  90. package/lib/index.d.ts +0 -12
  91. package/lib/index.js +0 -9
  92. package/lib/indexForBundle.d.ts +0 -31
  93. package/lib/indexForBundle.js +0 -20
  94. package/lib/messengers/Messenger.d.ts +0 -14
  95. package/lib/messengers/Messenger.js +0 -1
  96. package/lib/messengers/PortMessenger.d.ts +0 -21
  97. package/lib/messengers/PortMessenger.js +0 -45
  98. package/lib/messengers/WindowMessenger.d.ts +0 -29
  99. package/lib/messengers/WindowMessenger.js +0 -176
  100. package/lib/messengers/WorkerMessenger.d.ts +0 -23
  101. package/lib/messengers/WorkerMessenger.js +0 -84
  102. package/lib/methodSerialization.d.ts +0 -22
  103. package/lib/methodSerialization.js +0 -42
  104. package/lib/namespace.d.ts +0 -2
  105. package/lib/namespace.js +0 -1
  106. package/lib/once.d.ts +0 -2
  107. package/lib/once.js +0 -13
  108. package/lib/shakeHands.d.ts +0 -76
  109. package/lib/shakeHands.js +0 -188
  110. package/lib/types.d.ts +0 -89
  111. package/lib/types.js +0 -1
@@ -1,176 +0,0 @@
1
- import { downgradeMessage, isDeprecatedMessage, upgradeMessage, } from '../backwardCompatibility.js';
2
- import { isAck2Message, isAck1Message, isSynMessage } from '../guards.js';
3
- import PenpalError from '../PenpalError.js';
4
- import PenpalBugError from '../PenpalBugError.js';
5
- /**
6
- * Handles the details of communicating with a child window.
7
- */
8
- class WindowMessenger {
9
- #remoteWindow;
10
- #allowedOrigins;
11
- #log;
12
- #validateReceivedMessage;
13
- #concreteRemoteOrigin;
14
- #messageCallbacks = new Set();
15
- #port;
16
- // TODO: Used for backward-compatibility. Remove in next major version.
17
- #isChildUsingDeprecatedProtocol = false;
18
- constructor({ remoteWindow, allowedOrigins }) {
19
- if (!remoteWindow) {
20
- throw new PenpalError('INVALID_ARGUMENT', 'remoteWindow must be defined');
21
- }
22
- this.#remoteWindow = remoteWindow;
23
- this.#allowedOrigins = allowedOrigins?.length
24
- ? allowedOrigins
25
- : [window.origin];
26
- }
27
- initialize = ({ log, validateReceivedMessage, }) => {
28
- this.#log = log;
29
- this.#validateReceivedMessage = validateReceivedMessage;
30
- window.addEventListener('message', this.#handleMessageFromRemoteWindow);
31
- };
32
- sendMessage = (message, transferables) => {
33
- if (isSynMessage(message)) {
34
- const originForSending = this.#getOriginForSendingMessage(message);
35
- this.#remoteWindow.postMessage(message, {
36
- targetOrigin: originForSending,
37
- transfer: transferables,
38
- });
39
- return;
40
- }
41
- if (isAck1Message(message) ||
42
- // If the child is using a previous version of Penpal, we need to
43
- // downgrade the message and send it through the window rather than
44
- // the port because older versions of Penpal don't use MessagePorts.
45
- this.#isChildUsingDeprecatedProtocol) {
46
- const payload = this.#isChildUsingDeprecatedProtocol
47
- ? downgradeMessage(message)
48
- : message;
49
- const originForSending = this.#getOriginForSendingMessage(message);
50
- this.#remoteWindow.postMessage(payload, {
51
- targetOrigin: originForSending,
52
- transfer: transferables,
53
- });
54
- return;
55
- }
56
- if (isAck2Message(message)) {
57
- const { port1, port2 } = new MessageChannel();
58
- this.#port = port1;
59
- port1.addEventListener('message', this.#handleMessageFromPort);
60
- port1.start();
61
- const transferablesToSend = [port2, ...(transferables || [])];
62
- const originForSending = this.#getOriginForSendingMessage(message);
63
- this.#remoteWindow.postMessage(message, {
64
- targetOrigin: originForSending,
65
- transfer: transferablesToSend,
66
- });
67
- return;
68
- }
69
- if (this.#port) {
70
- this.#port.postMessage(message, {
71
- transfer: transferables,
72
- });
73
- return;
74
- }
75
- throw new PenpalBugError('Port is undefined');
76
- };
77
- addMessageHandler = (callback) => {
78
- this.#messageCallbacks.add(callback);
79
- };
80
- removeMessageHandler = (callback) => {
81
- this.#messageCallbacks.delete(callback);
82
- };
83
- destroy = () => {
84
- window.removeEventListener('message', this.#handleMessageFromRemoteWindow);
85
- this.#destroyPort();
86
- this.#messageCallbacks.clear();
87
- };
88
- #isAllowedOrigin = (origin) => {
89
- return this.#allowedOrigins.some((allowedOrigin) => allowedOrigin instanceof RegExp
90
- ? allowedOrigin.test(origin)
91
- : allowedOrigin === origin || allowedOrigin === '*');
92
- };
93
- #getOriginForSendingMessage = (message) => {
94
- // It's safe to send the SYN message to any origin because it doesn't contain
95
- // anything sensitive. When Penpal receives a SYN message, the origin on
96
- // the message (which we call the concrete origin) is validated against the
97
- // configured allowed origins. All subsequent messages will be sent to the
98
- // concrete origin.
99
- // If you decide to change this, consider https://github.com/Aaronius/penpal/issues/103
100
- if (isSynMessage(message)) {
101
- return '*';
102
- }
103
- if (!this.#concreteRemoteOrigin) {
104
- throw new PenpalBugError('Concrete remote origin not set');
105
- }
106
- // If the concrete remote origin (the origin we received from the remote
107
- // on a prior message) is 'null', it means the remote is within
108
- // an "opaque origin". The only way to post a message to an
109
- // opaque origin is by using '*'. This does carry some security risk,
110
- // so we only do this if the consumer has specifically defined '*' as
111
- // an allowed origin. Opaque origins occur, for example, when
112
- // loading an HTML document directly from the filesystem (not a
113
- // web server) or through a data URI.
114
- return this.#concreteRemoteOrigin === 'null' &&
115
- this.#allowedOrigins.includes('*')
116
- ? '*'
117
- : this.#concreteRemoteOrigin;
118
- };
119
- #destroyPort = () => {
120
- this.#port?.removeEventListener('message', this.#handleMessageFromPort);
121
- this.#port?.close();
122
- this.#port = undefined;
123
- };
124
- #handleMessageFromRemoteWindow = ({ source, origin, ports, data, }) => {
125
- if (source !== this.#remoteWindow) {
126
- return;
127
- }
128
- // TODO: Used for backward-compatibility. Remove in next major version.
129
- if (isDeprecatedMessage(data)) {
130
- this.#log?.('Please upgrade the child window to the latest version of Penpal.');
131
- this.#isChildUsingDeprecatedProtocol = true;
132
- data = upgradeMessage(data);
133
- }
134
- if (!this.#validateReceivedMessage?.(data)) {
135
- return;
136
- }
137
- if (!this.#isAllowedOrigin(origin)) {
138
- this.#log?.(`Received a message from origin \`${origin}\` which did not match ` +
139
- `allowed origins \`[${this.#allowedOrigins.join(', ')}]\``);
140
- return;
141
- }
142
- if (isSynMessage(data)) {
143
- // If we receive a SYN message and already have a port, it means
144
- // the child is re-connecting, in which case we'll receive a new port.
145
- // For this reason, we always make sure we destroy the existing port.
146
- this.#destroyPort();
147
- this.#concreteRemoteOrigin = origin;
148
- }
149
- if (isAck2Message(data) &&
150
- // Previous versions of Penpal don't use MessagePorts and do all
151
- // communication through the window.
152
- !this.#isChildUsingDeprecatedProtocol) {
153
- this.#port = ports[0];
154
- if (!this.#port) {
155
- throw new PenpalBugError('No port received on ACK2');
156
- }
157
- this.#port.addEventListener('message', this.#handleMessageFromPort);
158
- this.#port.start();
159
- }
160
- for (const callback of this.#messageCallbacks) {
161
- callback(data);
162
- }
163
- };
164
- #handleMessageFromPort = ({ data }) => {
165
- // Unlike in _handleMessageFromWindow, we don't need to check if
166
- // the message is from a deprecated version of Penpal because older versions
167
- // of Penpal don't use MessagePorts.
168
- if (!this.#validateReceivedMessage?.(data)) {
169
- return;
170
- }
171
- for (const callback of this.#messageCallbacks) {
172
- callback(data);
173
- }
174
- };
175
- }
176
- export default WindowMessenger;
@@ -1,23 +0,0 @@
1
- import { Message } from '../types.js';
2
- import Messenger, { InitializeMessengerOptions, MessageHandler } from './Messenger.js';
3
- type Options = {
4
- /**
5
- * The web worker receiving/sending communication from/to the parent window.
6
- * If this messenger is being used within the worker, `worker` should
7
- * typically be set to `self`.
8
- */
9
- worker: Worker | DedicatedWorkerGlobalScope;
10
- };
11
- /**
12
- * Handles the details of communicating with a child web worker.
13
- */
14
- declare class WorkerMessenger implements Messenger {
15
- #private;
16
- constructor({ worker }: Options);
17
- initialize: ({ validateReceivedMessage }: InitializeMessengerOptions) => void;
18
- sendMessage: (message: Message, transferables?: Transferable[]) => void;
19
- addMessageHandler: (callback: MessageHandler) => void;
20
- removeMessageHandler: (callback: MessageHandler) => void;
21
- destroy: () => void;
22
- }
23
- export default WorkerMessenger;
@@ -1,84 +0,0 @@
1
- import { isAck2Message, isAck1Message, isSynMessage } from '../guards.js';
2
- import PenpalError from '../PenpalError.js';
3
- import PenpalBugError from '../PenpalBugError.js';
4
- /**
5
- * Handles the details of communicating with a child web worker.
6
- */
7
- class WorkerMessenger {
8
- #worker;
9
- #validateReceivedMessage;
10
- #messageCallbacks = new Set();
11
- #port;
12
- constructor({ worker }) {
13
- if (!worker) {
14
- throw new PenpalError('INVALID_ARGUMENT', 'worker must be defined');
15
- }
16
- this.#worker = worker;
17
- }
18
- initialize = ({ validateReceivedMessage }) => {
19
- this.#validateReceivedMessage = validateReceivedMessage;
20
- this.#worker.addEventListener('message', this.#handleMessage);
21
- };
22
- sendMessage = (message, transferables) => {
23
- if (isSynMessage(message) || isAck1Message(message)) {
24
- this.#worker.postMessage(message, { transfer: transferables });
25
- return;
26
- }
27
- if (isAck2Message(message)) {
28
- const { port1, port2 } = new MessageChannel();
29
- this.#port = port1;
30
- port1.addEventListener('message', this.#handleMessage);
31
- port1.start();
32
- this.#worker.postMessage(message, {
33
- transfer: [port2, ...(transferables || [])],
34
- });
35
- return;
36
- }
37
- if (this.#port) {
38
- this.#port.postMessage(message, {
39
- transfer: transferables,
40
- });
41
- return;
42
- }
43
- throw new PenpalBugError('Port is undefined');
44
- };
45
- addMessageHandler = (callback) => {
46
- this.#messageCallbacks.add(callback);
47
- };
48
- removeMessageHandler = (callback) => {
49
- this.#messageCallbacks.delete(callback);
50
- };
51
- destroy = () => {
52
- this.#worker.removeEventListener('message', this.#handleMessage);
53
- this.#destroyPort();
54
- this.#messageCallbacks.clear();
55
- };
56
- #destroyPort = () => {
57
- this.#port?.removeEventListener('message', this.#handleMessage);
58
- this.#port?.close();
59
- this.#port = undefined;
60
- };
61
- #handleMessage = ({ ports, data }) => {
62
- if (!this.#validateReceivedMessage?.(data)) {
63
- return;
64
- }
65
- if (isSynMessage(data)) {
66
- // If we receive a SYN message and already have a port, it means
67
- // the child is re-connecting, in which case we'll receive a new port.
68
- // For this reason, we always make sure we destroy the existing port.
69
- this.#destroyPort();
70
- }
71
- if (isAck2Message(data)) {
72
- this.#port = ports[0];
73
- if (!this.#port) {
74
- throw new PenpalBugError('No port received on ACK2');
75
- }
76
- this.#port.addEventListener('message', this.#handleMessage);
77
- this.#port.start();
78
- }
79
- for (const callback of this.#messageCallbacks) {
80
- callback(data);
81
- }
82
- };
83
- }
84
- export default WorkerMessenger;
@@ -1,22 +0,0 @@
1
- import { MethodPath, Methods } from './types.js';
2
- /**
3
- * Given an object of (nested) keys to functions, extract paths to each function.
4
- *
5
- * @example
6
- * Given this Method object:
7
- * {
8
- * one: {
9
- * two: () => {}
10
- * }
11
- * three: () => {}
12
- * }
13
- *
14
- * the extracted MethodPath[] would be:
15
- * [
16
- * ['one', 'two'],
17
- * ['three']
18
- * ]
19
- */
20
- export declare const extractMethodPathsFromMethods: (methods: Methods, currentPath?: MethodPath) => MethodPath[];
21
- export declare const getMethodAtMethodPath: (methodPath: MethodPath, methods: Methods) => Function | undefined;
22
- export declare const formatMethodPath: (methodPath: MethodPath) => string;
@@ -1,42 +0,0 @@
1
- import { isFunction, isObject } from './guards.js';
2
- // TODO: Used for backward-compatibility. Remove in next major version.
3
- /**
4
- * Given an object of (nested) keys to functions, extract paths to each function.
5
- *
6
- * @example
7
- * Given this Method object:
8
- * {
9
- * one: {
10
- * two: () => {}
11
- * }
12
- * three: () => {}
13
- * }
14
- *
15
- * the extracted MethodPath[] would be:
16
- * [
17
- * ['one', 'two'],
18
- * ['three']
19
- * ]
20
- */
21
- export const extractMethodPathsFromMethods = (methods, currentPath = []) => {
22
- const methodPaths = [];
23
- for (const key of Object.keys(methods)) {
24
- const value = methods[key];
25
- if (isFunction(value)) {
26
- methodPaths.push([...currentPath, key]);
27
- }
28
- else if (isObject(value)) {
29
- methodPaths.push(...extractMethodPathsFromMethods(value, [...currentPath, key]));
30
- }
31
- }
32
- return methodPaths;
33
- };
34
- export const getMethodAtMethodPath = (methodPath, methods) => {
35
- const result = methodPath.reduce((acc, pathSegment) => {
36
- return isObject(acc) ? acc[pathSegment] : undefined;
37
- }, methods);
38
- return isFunction(result) ? result : undefined;
39
- };
40
- export const formatMethodPath = (methodPath) => {
41
- return methodPath.join('.');
42
- };
@@ -1,2 +0,0 @@
1
- declare const _default: "penpal";
2
- export default _default;
package/lib/namespace.js DELETED
@@ -1 +0,0 @@
1
- export default 'penpal';
package/lib/once.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare const once: <T extends (...args: any[]) => any>(fn: T) => ((...args: Parameters<T>) => ReturnType<T>);
2
- export default once;
package/lib/once.js DELETED
@@ -1,13 +0,0 @@
1
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
- const once = (fn) => {
3
- let isCalled = false;
4
- let result;
5
- return (...args) => {
6
- if (!isCalled) {
7
- isCalled = true;
8
- result = fn(...args);
9
- }
10
- return result;
11
- };
12
- };
13
- export default once;
@@ -1,76 +0,0 @@
1
- import Messenger from './messengers/Messenger.js';
2
- import { Methods, RemoteProxy, Log } from './types.js';
3
- type Options = {
4
- messenger: Messenger;
5
- methods: Methods;
6
- timeout: number | undefined;
7
- channel: string | undefined;
8
- log: Log | undefined;
9
- };
10
- type HandshakeResult<TMethods extends Methods> = {
11
- remoteProxy: RemoteProxy<TMethods>;
12
- destroy: () => void;
13
- };
14
- /**
15
- * Attempts to establish communication with the remote via a handshake protocol.
16
- * The handshake protocol fulfills a few requirements:
17
- *
18
- * 1. One participant in the handshake may not be available when the other
19
- * participant starts the handshake. For example, a document inside an iframe
20
- * may not be loaded when the parent window starts a handshake.
21
- * 2. While #1 could be solved by having the consumer of Penpal specify which
22
- * participant should initiate the handshake, we'd rather avoid this
23
- * unnecessary cognitive load.
24
- * 3. While #1 could be solved by having the consumer of Penpal specify which
25
- * participant is the "parent" or "child" and then having Penpal assume
26
- * the child should initiate the handshake, we'd rather avoid parent-child
27
- * terminology since Penpal can support communication between two
28
- * participants where neither would be considered a parent nor child. It may
29
- * also be too presumptive that the child should always initiate the
30
- * handshake.
31
- * 4. For robustness, each participant must know that the other participant is
32
- * receiving its messages for the handshake to be considered complete.
33
- * 5. The handshake should support a participant attempting to
34
- * re-establish the connection. This can occur, for example, if an end user
35
- * were to right-click within an iframe and click reload.
36
- * 6. The handshake should allow a Messenger to easily attach something to
37
- * a handshake message from one participant to the other unidirectionally
38
- * (rather than from both participants to each other).
39
- * This is important when a participant needs to be in charge of, for
40
- * example, creating a MessageChannel and sending one MessagePort from the
41
- * MessagePort pair to the other participant. If both participants attempted
42
- * to do this it could lead to confusion.
43
- * 7. The handshake ideally shouldn't require sending handshake messages on an
44
- * interval (retrying until the other participant is ready to receive them).
45
- * Intervals can increase compute resources if the interval is too short
46
- * or increase latency if the interval is too long. While we could make this
47
- * configurable, it's additional mental load for the consumer. Additionally,
48
- * setInterval and setTimeout are not available within some contexts
49
- * (like AudioWorklet), where a consumer may like to use Penpal.
50
- *
51
- * To accomplish these requirements, the handshake protocol is as follows:
52
- * 1. Each participant generates a random participant ID.
53
- * 2. As soon as possible, each participant sends a SYN message containing its
54
- * participant ID to the other participant.
55
- * 3. When the SYN messages were sent, one of the participants may not have
56
- * been ready to receive the SYN message from the other. At least one
57
- * of the participants was ready, however, and should have received a SYN
58
- * message from the other participant. Each participant that did receive
59
- * a SYN message knows for sure that the other participant is now ready
60
- * to receive a SYN message, so it will send another SYN message in case
61
- * the other participant did not receive the first SYN message. This
62
- * ultimately results in each participant sending two SYN messages.
63
- * 4. Each participant now should have received at least one SYN message from
64
- * the other participant. Each participant compares their own ID with the
65
- * other participant's ID. Whichever participant has the higher ID
66
- * (using a simple string comparison) is considered the handshake leader
67
- * and will send an ACK1 message to the other participant.
68
- * 5. At this point, the handshake leader does not know whether the other
69
- * participant is actually receiving messages. The participant receiving
70
- * the ACK1 message will respond with an ACK2, informing the handshake
71
- * leader that it is indeed receiving messages.
72
- * 6. At this point, both participants know the other is receiving messages
73
- * and the handshake is complete.
74
- */
75
- declare const shakeHands: <TMethods extends Methods>({ messenger, methods, timeout, channel, log, }: Options) => Promise<HandshakeResult<TMethods>>;
76
- export default shakeHands;
package/lib/shakeHands.js DELETED
@@ -1,188 +0,0 @@
1
- import PenpalError from './PenpalError.js';
2
- import connectCallHandler from './connectCallHandler.js';
3
- import connectRemoteProxy from './connectRemoteProxy.js';
4
- import { isAck2Message, isAck1Message, isSynMessage } from './guards.js';
5
- import getPromiseWithResolvers from './getPromiseWithResolvers.js';
6
- import { extractMethodPathsFromMethods } from './methodSerialization.js';
7
- import generateId from './generateId.js';
8
- import { DEPRECATED_PENPAL_PARTICIPANT_ID } from './backwardCompatibility.js';
9
- import namespace from './namespace.js';
10
- /**
11
- * Attempts to establish communication with the remote via a handshake protocol.
12
- * The handshake protocol fulfills a few requirements:
13
- *
14
- * 1. One participant in the handshake may not be available when the other
15
- * participant starts the handshake. For example, a document inside an iframe
16
- * may not be loaded when the parent window starts a handshake.
17
- * 2. While #1 could be solved by having the consumer of Penpal specify which
18
- * participant should initiate the handshake, we'd rather avoid this
19
- * unnecessary cognitive load.
20
- * 3. While #1 could be solved by having the consumer of Penpal specify which
21
- * participant is the "parent" or "child" and then having Penpal assume
22
- * the child should initiate the handshake, we'd rather avoid parent-child
23
- * terminology since Penpal can support communication between two
24
- * participants where neither would be considered a parent nor child. It may
25
- * also be too presumptive that the child should always initiate the
26
- * handshake.
27
- * 4. For robustness, each participant must know that the other participant is
28
- * receiving its messages for the handshake to be considered complete.
29
- * 5. The handshake should support a participant attempting to
30
- * re-establish the connection. This can occur, for example, if an end user
31
- * were to right-click within an iframe and click reload.
32
- * 6. The handshake should allow a Messenger to easily attach something to
33
- * a handshake message from one participant to the other unidirectionally
34
- * (rather than from both participants to each other).
35
- * This is important when a participant needs to be in charge of, for
36
- * example, creating a MessageChannel and sending one MessagePort from the
37
- * MessagePort pair to the other participant. If both participants attempted
38
- * to do this it could lead to confusion.
39
- * 7. The handshake ideally shouldn't require sending handshake messages on an
40
- * interval (retrying until the other participant is ready to receive them).
41
- * Intervals can increase compute resources if the interval is too short
42
- * or increase latency if the interval is too long. While we could make this
43
- * configurable, it's additional mental load for the consumer. Additionally,
44
- * setInterval and setTimeout are not available within some contexts
45
- * (like AudioWorklet), where a consumer may like to use Penpal.
46
- *
47
- * To accomplish these requirements, the handshake protocol is as follows:
48
- * 1. Each participant generates a random participant ID.
49
- * 2. As soon as possible, each participant sends a SYN message containing its
50
- * participant ID to the other participant.
51
- * 3. When the SYN messages were sent, one of the participants may not have
52
- * been ready to receive the SYN message from the other. At least one
53
- * of the participants was ready, however, and should have received a SYN
54
- * message from the other participant. Each participant that did receive
55
- * a SYN message knows for sure that the other participant is now ready
56
- * to receive a SYN message, so it will send another SYN message in case
57
- * the other participant did not receive the first SYN message. This
58
- * ultimately results in each participant sending two SYN messages.
59
- * 4. Each participant now should have received at least one SYN message from
60
- * the other participant. Each participant compares their own ID with the
61
- * other participant's ID. Whichever participant has the higher ID
62
- * (using a simple string comparison) is considered the handshake leader
63
- * and will send an ACK1 message to the other participant.
64
- * 5. At this point, the handshake leader does not know whether the other
65
- * participant is actually receiving messages. The participant receiving
66
- * the ACK1 message will respond with an ACK2, informing the handshake
67
- * leader that it is indeed receiving messages.
68
- * 6. At this point, both participants know the other is receiving messages
69
- * and the handshake is complete.
70
- */
71
- const shakeHands = ({ messenger, methods, timeout, channel, log, }) => {
72
- const participantId = generateId();
73
- let remoteParticipantId;
74
- const destroyHandlers = [];
75
- let isComplete = false;
76
- const methodPaths = extractMethodPathsFromMethods(methods);
77
- const { promise, resolve, reject } = getPromiseWithResolvers();
78
- const timeoutId = timeout !== undefined
79
- ? setTimeout(() => {
80
- reject(new PenpalError('CONNECTION_TIMEOUT', `Connection timed out after ${timeout}ms`));
81
- }, timeout)
82
- : undefined;
83
- const destroy = () => {
84
- for (const destroyHandler of destroyHandlers) {
85
- destroyHandler();
86
- }
87
- };
88
- const connectCallHandlerAndMethodProxies = () => {
89
- if (isComplete) {
90
- // If we get here, it means the remote is attempting to re-connect. While
91
- // that's supported, we don't need to run the rest of this function again.
92
- return;
93
- }
94
- destroyHandlers.push(connectCallHandler(messenger, methods, channel, log));
95
- const { remoteProxy, destroy: destroyMethodProxies } = connectRemoteProxy(messenger, channel, log);
96
- destroyHandlers.push(destroyMethodProxies);
97
- clearTimeout(timeoutId);
98
- isComplete = true;
99
- resolve({
100
- remoteProxy,
101
- destroy: destroy,
102
- });
103
- };
104
- const sendSynMessage = () => {
105
- const synMessage = {
106
- namespace,
107
- type: 'SYN',
108
- channel,
109
- participantId: participantId,
110
- };
111
- log?.(`Sending handshake SYN`, synMessage);
112
- try {
113
- messenger.sendMessage(synMessage);
114
- }
115
- catch (error) {
116
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
117
- }
118
- };
119
- const handleSynMessage = (message) => {
120
- log?.(`Received handshake SYN`, message);
121
- if (message.participantId === remoteParticipantId &&
122
- // TODO: Used for backward-compatibility. Remove in next major version.
123
- remoteParticipantId !== DEPRECATED_PENPAL_PARTICIPANT_ID) {
124
- return;
125
- }
126
- remoteParticipantId = message.participantId;
127
- // We send another SYN message in case the other participant was not ready
128
- // when we sent the first SYN message.
129
- sendSynMessage();
130
- const isHandshakeLeader = participantId > remoteParticipantId ||
131
- // TODO: Used for backward-compatibility. Remove in next major version.
132
- remoteParticipantId === DEPRECATED_PENPAL_PARTICIPANT_ID;
133
- if (!isHandshakeLeader) {
134
- return;
135
- }
136
- const ack1Message = {
137
- namespace,
138
- channel,
139
- type: 'ACK1',
140
- methodPaths,
141
- };
142
- log?.(`Sending handshake ACK1`, ack1Message);
143
- try {
144
- messenger.sendMessage(ack1Message);
145
- }
146
- catch (error) {
147
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
148
- return;
149
- }
150
- };
151
- const handleAck1Message = (message) => {
152
- log?.(`Received handshake ACK1`, message);
153
- const ack2Message = {
154
- namespace,
155
- channel,
156
- type: 'ACK2',
157
- };
158
- log?.(`Sending handshake ACK2`, ack2Message);
159
- try {
160
- messenger.sendMessage(ack2Message);
161
- }
162
- catch (error) {
163
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
164
- return;
165
- }
166
- connectCallHandlerAndMethodProxies();
167
- };
168
- const handleAck2Message = (message) => {
169
- log?.(`Received handshake ACK2`, message);
170
- connectCallHandlerAndMethodProxies();
171
- };
172
- const handleMessage = (message) => {
173
- if (isSynMessage(message)) {
174
- handleSynMessage(message);
175
- }
176
- if (isAck1Message(message)) {
177
- handleAck1Message(message);
178
- }
179
- if (isAck2Message(message)) {
180
- handleAck2Message(message);
181
- }
182
- };
183
- messenger.addMessageHandler(handleMessage);
184
- destroyHandlers.push(() => messenger.removeMessageHandler(handleMessage));
185
- sendSynMessage();
186
- return promise;
187
- };
188
- export default shakeHands;