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
package/dist/penpal.js CHANGED
@@ -1,1101 +1,958 @@
1
- var Penpal = (function () {
1
+ var Penpal = (function (exports) {
2
2
  'use strict';
3
3
 
4
- class PenpalError extends Error {
5
- code;
6
- constructor(code, message) {
7
- super(message);
8
- this.name = 'PenpalError';
9
- this.code = code;
10
- }
11
- }
4
+ // src/PenpalError.ts
5
+ var PenpalError = class extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "PenpalError";
10
+ this.code = code;
11
+ }
12
+ };
13
+ var PenpalError_default = PenpalError;
12
14
 
13
- /**
14
- * Converts an error object into a plain object.
15
- */
16
- const serializeError = (error) => ({
17
- name: error.name,
18
- message: error.message,
19
- stack: error.stack,
20
- penpalCode: error instanceof PenpalError ? error.code : undefined,
15
+ // src/errorSerialization.ts
16
+ var serializeError = (error) => ({
17
+ name: error.name,
18
+ message: error.message,
19
+ stack: error.stack,
20
+ penpalCode: error instanceof PenpalError_default ? error.code : void 0
21
21
  });
22
- /**
23
- * Converts a plain object into an error object.
24
- */
25
- const deserializeError = ({ name, message, stack, penpalCode, }) => {
26
- const deserializedError = penpalCode
27
- ? new PenpalError(penpalCode, message)
28
- : new Error(message);
29
- deserializedError.name = name;
30
- deserializedError.stack = stack;
31
- return deserializedError;
22
+ var deserializeError = ({
23
+ name,
24
+ message,
25
+ stack,
26
+ penpalCode
27
+ }) => {
28
+ const deserializedError = penpalCode ? new PenpalError_default(penpalCode, message) : new Error(message);
29
+ deserializedError.name = name;
30
+ deserializedError.stack = stack;
31
+ return deserializedError;
32
32
  };
33
33
 
34
- const brand$1 = Symbol('Reply');
35
- class Reply {
36
- value;
37
- transferables;
38
- // Allows TypeScript to distinguish between an actual instance of this
39
- // class versus an object that looks structurally similar.
40
- // eslint-disable-next-line no-unused-private-class-members
41
- #brand = brand$1;
42
- constructor(value, options) {
43
- this.value = value;
44
- this.transferables = options?.transferables;
45
- }
46
- }
34
+ // src/Reply.ts
35
+ var brand = Symbol("Reply");
36
+ var Reply = class {
37
+ value;
38
+ transferables;
39
+ // Allows TypeScript to distinguish between an actual instance of this
40
+ // class versus an object that looks structurally similar.
41
+ // eslint-disable-next-line no-unused-private-class-members
42
+ #brand = brand;
43
+ constructor(value, options) {
44
+ this.value = value;
45
+ this.transferables = options?.transferables;
46
+ }
47
+ };
48
+ var Reply_default = Reply;
47
49
 
48
- var namespace = 'penpal';
50
+ // src/namespace.ts
51
+ var namespace_default = "penpal";
49
52
 
50
- const isObject = (value) => {
51
- return typeof value === 'object' && value !== null;
53
+ // src/guards.ts
54
+ var isObject = (value) => {
55
+ return typeof value === "object" && value !== null;
52
56
  };
53
- const isFunction = (value) => {
54
- return typeof value === 'function';
57
+ var isFunction = (value) => {
58
+ return typeof value === "function";
55
59
  };
56
- const isMessage = (data) => {
57
- return isObject(data) && data.namespace === namespace;
60
+ var isMessage = (data) => {
61
+ return isObject(data) && data.namespace === namespace_default;
58
62
  };
59
- const isSynMessage = (message) => {
60
- return message.type === 'SYN';
63
+ var isSynMessage = (message) => {
64
+ return message.type === "SYN";
61
65
  };
62
- const isAck1Message = (message) => {
63
- return message.type === 'ACK1';
66
+ var isAck1Message = (message) => {
67
+ return message.type === "ACK1";
64
68
  };
65
- const isAck2Message = (message) => {
66
- return message.type === 'ACK2';
69
+ var isAck2Message = (message) => {
70
+ return message.type === "ACK2";
67
71
  };
68
- const isCallMessage = (message) => {
69
- return message.type === 'CALL';
72
+ var isCallMessage = (message) => {
73
+ return message.type === "CALL";
70
74
  };
71
- const isReplyMessage = (message) => {
72
- return message.type === 'REPLY';
75
+ var isReplyMessage = (message) => {
76
+ return message.type === "REPLY";
73
77
  };
74
- const isDestroyMessage = (message) => {
75
- return message.type === 'DESTROY';
78
+ var isDestroyMessage = (message) => {
79
+ return message.type === "DESTROY";
76
80
  };
77
81
 
78
- // TODO: Used for backward-compatibility. Remove in next major version.
79
- /**
80
- * Given an object of (nested) keys to functions, extract paths to each function.
81
- *
82
- * @example
83
- * Given this Method object:
84
- * {
85
- * one: {
86
- * two: () => {}
87
- * }
88
- * three: () => {}
89
- * }
90
- *
91
- * the extracted MethodPath[] would be:
92
- * [
93
- * ['one', 'two'],
94
- * ['three']
95
- * ]
96
- */
97
- const extractMethodPathsFromMethods = (methods, currentPath = []) => {
98
- const methodPaths = [];
99
- for (const key of Object.keys(methods)) {
100
- const value = methods[key];
101
- if (isFunction(value)) {
102
- methodPaths.push([...currentPath, key]);
103
- }
104
- else if (isObject(value)) {
105
- methodPaths.push(...extractMethodPathsFromMethods(value, [...currentPath, key]));
106
- }
82
+ // src/methodSerialization.ts
83
+ var extractMethodPathsFromMethods = (methods, currentPath = []) => {
84
+ const methodPaths = [];
85
+ for (const key of Object.keys(methods)) {
86
+ const value = methods[key];
87
+ if (isFunction(value)) {
88
+ methodPaths.push([...currentPath, key]);
89
+ } else if (isObject(value)) {
90
+ methodPaths.push(
91
+ ...extractMethodPathsFromMethods(value, [...currentPath, key])
92
+ );
107
93
  }
108
- return methodPaths;
94
+ }
95
+ return methodPaths;
109
96
  };
110
- const getMethodAtMethodPath = (methodPath, methods) => {
111
- const result = methodPath.reduce((acc, pathSegment) => {
112
- return isObject(acc) ? acc[pathSegment] : undefined;
113
- }, methods);
114
- return isFunction(result) ? result : undefined;
97
+ var getMethodAtMethodPath = (methodPath, methods) => {
98
+ const result = methodPath.reduce(
99
+ (acc, pathSegment) => {
100
+ return isObject(acc) ? acc[pathSegment] : void 0;
101
+ },
102
+ methods
103
+ );
104
+ return isFunction(result) ? result : void 0;
115
105
  };
116
- const formatMethodPath = (methodPath) => {
117
- return methodPath.join('.');
106
+ var formatMethodPath = (methodPath) => {
107
+ return methodPath.join(".");
118
108
  };
119
109
 
120
- const createErrorReplyMessage = (channel, callId, error) => ({
121
- namespace,
122
- channel,
123
- type: 'REPLY',
124
- callId,
125
- isError: true,
126
- ...(error instanceof Error
127
- ? { value: serializeError(error), isSerializedErrorInstance: true }
128
- : { value: error }),
110
+ // src/connectCallHandler.ts
111
+ var createErrorReplyMessage = (channel, callId, error) => ({
112
+ namespace: namespace_default,
113
+ channel,
114
+ type: "REPLY",
115
+ callId,
116
+ isError: true,
117
+ ...error instanceof Error ? { value: serializeError(error), isSerializedErrorInstance: true } : { value: error }
129
118
  });
130
- /**
131
- * Listens for "call" messages from the remote, executes the corresponding method,
132
- * and responds with the return value or error.
133
- */
134
- const connectCallHandler = (messenger, methods, channel, log) => {
135
- let isDestroyed = false;
136
- const handleMessage = async (message) => {
137
- if (isDestroyed) {
138
- // It's possible to throw an error here, but it would only be catchable
139
- // using window.onerror since we're in an asynchronously-called function.
140
- // There is no method call the consumer is making that they could wrap in
141
- // a try-catch. Even if the consumer were to catch the error somehow,
142
- // the value of doing so is questionable.
143
- return;
144
- }
145
- if (!isCallMessage(message)) {
146
- return;
147
- }
148
- log?.(`Received ${formatMethodPath(message.methodPath)}() call`, message);
149
- const { methodPath, args, id: callId } = message;
150
- let replyMessage;
151
- let transferables;
152
- try {
153
- const method = getMethodAtMethodPath(methodPath, methods);
154
- if (!method) {
155
- throw new PenpalError('METHOD_NOT_FOUND', `Method \`${formatMethodPath(methodPath)}\` is not found.`);
156
- }
157
- let value = await method(...args);
158
- if (value instanceof Reply) {
159
- transferables = value.transferables;
160
- value = await value.value;
161
- }
162
- replyMessage = {
163
- namespace,
164
- channel,
165
- type: 'REPLY',
166
- callId,
167
- value,
168
- };
169
- }
170
- catch (error) {
171
- replyMessage = createErrorReplyMessage(channel, callId, error);
172
- }
173
- // Although we checked this at the beginning of the function, we need to
174
- // check it again because we've made async calls, and the connection may
175
- // have been destroyed in the meantime.
176
- if (isDestroyed) {
177
- return;
178
- }
179
- try {
180
- log?.(`Sending ${formatMethodPath(methodPath)}() reply`, replyMessage);
181
- messenger.sendMessage(replyMessage, transferables);
182
- }
183
- catch (error) {
184
- // If a consumer attempts to send an object that's not
185
- // cloneable (e.g., window), we want to ensure the receiver's promise
186
- // gets rejected.
187
- if (error.name === 'DataCloneError') {
188
- replyMessage = createErrorReplyMessage(channel, callId, error);
189
- log?.(`Sending ${formatMethodPath(methodPath)}() reply`, replyMessage);
190
- messenger.sendMessage(replyMessage);
191
- }
192
- throw error;
193
- }
194
- };
195
- messenger.addMessageHandler(handleMessage);
196
- return () => {
197
- isDestroyed = true;
198
- messenger.removeMessageHandler(handleMessage);
199
- };
119
+ var connectCallHandler = (messenger, methods, channel, log) => {
120
+ let isDestroyed = false;
121
+ const handleMessage = async (message) => {
122
+ if (isDestroyed) {
123
+ return;
124
+ }
125
+ if (!isCallMessage(message)) {
126
+ return;
127
+ }
128
+ log?.(`Received ${formatMethodPath(message.methodPath)}() call`, message);
129
+ const { methodPath, args, id: callId } = message;
130
+ let replyMessage;
131
+ let transferables;
132
+ try {
133
+ const method = getMethodAtMethodPath(methodPath, methods);
134
+ if (!method) {
135
+ throw new PenpalError_default(
136
+ "METHOD_NOT_FOUND",
137
+ `Method \`${formatMethodPath(methodPath)}\` is not found.`
138
+ );
139
+ }
140
+ let value = await method(...args);
141
+ if (value instanceof Reply_default) {
142
+ transferables = value.transferables;
143
+ value = await value.value;
144
+ }
145
+ replyMessage = {
146
+ namespace: namespace_default,
147
+ channel,
148
+ type: "REPLY",
149
+ callId,
150
+ value
151
+ };
152
+ } catch (error) {
153
+ replyMessage = createErrorReplyMessage(channel, callId, error);
154
+ }
155
+ if (isDestroyed) {
156
+ return;
157
+ }
158
+ try {
159
+ log?.(`Sending ${formatMethodPath(methodPath)}() reply`, replyMessage);
160
+ messenger.sendMessage(replyMessage, transferables);
161
+ } catch (error) {
162
+ if (error.name === "DataCloneError") {
163
+ replyMessage = createErrorReplyMessage(channel, callId, error);
164
+ log?.(`Sending ${formatMethodPath(methodPath)}() reply`, replyMessage);
165
+ messenger.sendMessage(replyMessage);
166
+ }
167
+ throw error;
168
+ }
169
+ };
170
+ messenger.addMessageHandler(handleMessage);
171
+ return () => {
172
+ isDestroyed = true;
173
+ messenger.removeMessageHandler(handleMessage);
174
+ };
200
175
  };
176
+ var connectCallHandler_default = connectCallHandler;
201
177
 
202
- /**
203
- * @return A unique ID
204
- */
205
- // crypto.randomUUID is not available in insecure contexts.
206
- var generateId = crypto.randomUUID?.bind(crypto) ??
207
- (() => new Array(4)
208
- .fill(0)
209
- .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16))
210
- .join('-'));
178
+ // src/generateId.ts
179
+ var generateId_default = crypto.randomUUID?.bind(crypto) ?? (() => new Array(4).fill(0).map(
180
+ () => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)
181
+ ).join("-"));
211
182
 
212
- const brand = Symbol('CallOptions');
213
- class CallOptions {
214
- transferables;
215
- timeout;
216
- // Allows TypeScript to distinguish between an actual instance of this
217
- // class versus an object that looks structurally similar.
218
- // eslint-disable-next-line no-unused-private-class-members
219
- #brand = brand;
220
- constructor(options) {
221
- this.transferables = options?.transferables;
222
- this.timeout = options?.timeout;
223
- }
224
- }
183
+ // src/CallOptions.ts
184
+ var brand2 = Symbol("CallOptions");
185
+ var CallOptions = class {
186
+ transferables;
187
+ timeout;
188
+ // Allows TypeScript to distinguish between an actual instance of this
189
+ // class versus an object that looks structurally similar.
190
+ // eslint-disable-next-line no-unused-private-class-members
191
+ #brand = brand2;
192
+ constructor(options) {
193
+ this.transferables = options?.transferables;
194
+ this.timeout = options?.timeout;
195
+ }
196
+ };
197
+ var CallOptions_default = CallOptions;
225
198
 
226
- const methodsToTreatAsNative = new Set(['apply', 'call', 'bind']);
227
- const createRemoteProxy = (callback, log, path = []) => {
228
- return new Proxy(path.length
229
- ? () => {
230
- // Intentionally empty
231
- }
232
- : Object.create(null), {
233
- get(target, prop) {
234
- // If a promise is resolved with this proxy object, the JavaScript
235
- // runtime will look for a `then` property on this object to determine
236
- // if it should be treated as a promise (to support promise chaining).
237
- // If we don't return undefined here, the JavaScript runtime will treat
238
- // this object as a promise and attempt to call `then`, which will
239
- // then send a call message to the remote. This is not what we want.
240
- if (prop === 'then') {
241
- return;
242
- }
243
- // Because we're using a proxy and because Penpal supports developers
244
- // exposing nested methods, we have a predicament. If a developer
245
- // calls, for example, remote.auth.apply(), are they
246
- // attempting to call a nested apply() method that a developer has
247
- // explicitly exposed from the remote? Could they instead be attempting
248
- // to call Function.prototype.apply() on the remote.auth() method?
249
- // Without the remote telling the local Penpal which methods the
250
- // developer has exposed, it has no way of knowing (and the main reason
251
- // we use a proxy is so that Penpal doesn't have to communicate which
252
- // methods are exposed). So, we treat certain methods as native methods
253
- // and return the native method rather than a proxy. The downside of
254
- // this is that if a developer has explicitly exposed a nested method
255
- // with the same name as one of these native method names, the developer
256
- // will be unable to call the exposed remote method because they will
257
- // be calling the method on the Function prototype instead.
258
- if (path.length && methodsToTreatAsNative.has(prop)) {
259
- return Reflect.get(target, prop);
260
- }
261
- return createRemoteProxy(callback, log, [...path, prop]);
262
- },
263
- apply(target, _thisArg, args) {
264
- return callback(path, args);
265
- },
266
- });
199
+ // src/connectRemoteProxy.ts
200
+ var methodsToTreatAsNative = /* @__PURE__ */ new Set(["apply", "call", "bind"]);
201
+ var createRemoteProxy = (callback, log, path = []) => {
202
+ return new Proxy(
203
+ path.length ? () => {
204
+ } : /* @__PURE__ */ Object.create(null),
205
+ {
206
+ get(target, prop) {
207
+ if (prop === "then") {
208
+ return;
209
+ }
210
+ if (path.length && methodsToTreatAsNative.has(prop)) {
211
+ return Reflect.get(target, prop);
212
+ }
213
+ return createRemoteProxy(callback, log, [...path, prop]);
214
+ },
215
+ apply(target, _thisArg, args) {
216
+ return callback(path, args);
217
+ }
218
+ }
219
+ );
267
220
  };
268
- const getDestroyedConnectionMethodCallError = (methodPath) => {
269
- return new PenpalError('CONNECTION_DESTROYED', `Method call ${formatMethodPath(methodPath)}() failed due to destroyed connection`);
221
+ var getDestroyedConnectionMethodCallError = (methodPath) => {
222
+ return new PenpalError_default(
223
+ "CONNECTION_DESTROYED",
224
+ `Method call ${formatMethodPath(
225
+ methodPath
226
+ )}() failed due to destroyed connection`
227
+ );
270
228
  };
271
- /**
272
- * Creates a proxy. When methods are called on the proxy, a "call" message will
273
- * be sent to the remote, the remote's corresponding method will be
274
- * executed, and the method's return value will be returned via a message.
275
- */
276
- const connectRemoteProxy = (messenger, channel, log) => {
277
- let isDestroyed = false;
278
- const replyHandlers = new Map();
279
- const handleMessage = (message) => {
280
- if (!isReplyMessage(message)) {
281
- return;
282
- }
283
- const { callId, value, isError, isSerializedErrorInstance } = message;
284
- const replyHandler = replyHandlers.get(callId);
285
- if (!replyHandler) {
286
- return;
287
- }
229
+ var connectRemoteProxy = (messenger, channel, log) => {
230
+ let isDestroyed = false;
231
+ const replyHandlers = /* @__PURE__ */ new Map();
232
+ const handleMessage = (message) => {
233
+ if (!isReplyMessage(message)) {
234
+ return;
235
+ }
236
+ const { callId, value, isError, isSerializedErrorInstance } = message;
237
+ const replyHandler = replyHandlers.get(callId);
238
+ if (!replyHandler) {
239
+ return;
240
+ }
241
+ replyHandlers.delete(callId);
242
+ log?.(
243
+ `Received ${formatMethodPath(replyHandler.methodPath)}() call`,
244
+ message
245
+ );
246
+ if (isError) {
247
+ replyHandler.reject(
248
+ isSerializedErrorInstance ? deserializeError(value) : value
249
+ );
250
+ } else {
251
+ replyHandler.resolve(value);
252
+ }
253
+ };
254
+ messenger.addMessageHandler(handleMessage);
255
+ const remoteProxy = createRemoteProxy((methodPath, args) => {
256
+ if (isDestroyed) {
257
+ throw getDestroyedConnectionMethodCallError(methodPath);
258
+ }
259
+ const callId = generateId_default();
260
+ const lastArg = args[args.length - 1];
261
+ const lastArgIsOptions = lastArg instanceof CallOptions_default;
262
+ const { timeout, transferables } = lastArgIsOptions ? lastArg : {};
263
+ const argsWithoutOptions = lastArgIsOptions ? args.slice(0, -1) : args;
264
+ return new Promise((resolve, reject) => {
265
+ const timeoutId = timeout !== void 0 ? window.setTimeout(() => {
288
266
  replyHandlers.delete(callId);
289
- log?.(`Received ${formatMethodPath(replyHandler.methodPath)}() call`, message);
290
- if (isError) {
291
- replyHandler.reject(isSerializedErrorInstance ? deserializeError(value) : value);
292
- }
293
- else {
294
- replyHandler.resolve(value);
295
- }
296
- };
297
- messenger.addMessageHandler(handleMessage);
298
- const remoteProxy = createRemoteProxy((methodPath, args) => {
299
- if (isDestroyed) {
300
- throw getDestroyedConnectionMethodCallError(methodPath);
301
- }
302
- const callId = generateId();
303
- const lastArg = args[args.length - 1];
304
- const lastArgIsOptions = lastArg instanceof CallOptions;
305
- const { timeout, transferables } = lastArgIsOptions ? lastArg : {};
306
- const argsWithoutOptions = lastArgIsOptions ? args.slice(0, -1) : args;
307
- return new Promise((resolve, reject) => {
308
- // We reference `window.setTimeout` instead of just `setTimeout`
309
- // so that the TypeScript engine doesn't
310
- // get confused when running tests. Something within
311
- // Karma + @rollup/plugin-typescript leaks node types into source
312
- // files when running tests. Node's setTimeout has a return type of
313
- // Timeout rather than number, resulting in a build error when
314
- // running tests if we don't disambiguate the browser setTimeout
315
- // from node's setTimeout. There may be a better way to configure
316
- // Karma + Rollup + Typescript to avoid node type leakage.
317
- const timeoutId = timeout !== undefined
318
- ? window.setTimeout(() => {
319
- replyHandlers.delete(callId);
320
- reject(new PenpalError('METHOD_CALL_TIMEOUT', `Method call ${formatMethodPath(methodPath)}() timed out after ${timeout}ms`));
321
- }, timeout)
322
- : undefined;
323
- replyHandlers.set(callId, { methodPath, resolve, reject, timeoutId });
324
- try {
325
- const callMessage = {
326
- namespace,
327
- channel,
328
- type: 'CALL',
329
- id: callId,
330
- methodPath,
331
- args: argsWithoutOptions,
332
- };
333
- log?.(`Sending ${formatMethodPath(methodPath)}() call`, callMessage);
334
- messenger.sendMessage(callMessage, transferables);
335
- }
336
- catch (error) {
337
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
338
- }
339
- });
340
- }, log);
341
- const destroy = () => {
342
- isDestroyed = true;
343
- messenger.removeMessageHandler(handleMessage);
344
- for (const { methodPath, reject, timeoutId } of replyHandlers.values()) {
345
- clearTimeout(timeoutId);
346
- reject(getDestroyedConnectionMethodCallError(methodPath));
347
- }
348
- replyHandlers.clear();
267
+ reject(
268
+ new PenpalError_default(
269
+ "METHOD_CALL_TIMEOUT",
270
+ `Method call ${formatMethodPath(
271
+ methodPath
272
+ )}() timed out after ${timeout}ms`
273
+ )
274
+ );
275
+ }, timeout) : void 0;
276
+ replyHandlers.set(callId, { methodPath, resolve, reject, timeoutId });
277
+ try {
278
+ const callMessage = {
279
+ namespace: namespace_default,
280
+ channel,
281
+ type: "CALL",
282
+ id: callId,
283
+ methodPath,
284
+ args: argsWithoutOptions
285
+ };
286
+ log?.(`Sending ${formatMethodPath(methodPath)}() call`, callMessage);
287
+ messenger.sendMessage(callMessage, transferables);
288
+ } catch (error) {
289
+ reject(
290
+ new PenpalError_default("TRANSMISSION_FAILED", error.message)
291
+ );
292
+ }
293
+ });
294
+ }, log);
295
+ const destroy = () => {
296
+ isDestroyed = true;
297
+ messenger.removeMessageHandler(handleMessage);
298
+ for (const { methodPath, reject, timeoutId } of replyHandlers.values()) {
299
+ clearTimeout(timeoutId);
300
+ reject(getDestroyedConnectionMethodCallError(methodPath));
301
+ }
302
+ replyHandlers.clear();
303
+ };
304
+ return {
305
+ remoteProxy,
306
+ destroy
307
+ };
308
+ };
309
+ var connectRemoteProxy_default = connectRemoteProxy;
310
+
311
+ // src/getPromiseWithResolvers.ts
312
+ var getPromiseWithResolvers = () => {
313
+ let resolve;
314
+ let reject;
315
+ const promise = new Promise((res, rej) => {
316
+ resolve = res;
317
+ reject = rej;
318
+ });
319
+ return {
320
+ promise,
321
+ resolve,
322
+ reject
323
+ };
324
+ };
325
+ var getPromiseWithResolvers_default = getPromiseWithResolvers;
326
+
327
+ // src/PenpalBugError.ts
328
+ var PenpalBugError = class extends Error {
329
+ constructor(message) {
330
+ super(
331
+ `You've hit a bug in Penpal. Please file an issue with the following information: ${message}`
332
+ );
333
+ }
334
+ };
335
+ var PenpalBugError_default = PenpalBugError;
336
+
337
+ // src/backwardCompatibility.ts
338
+ var DEPRECATED_PENPAL_PARTICIPANT_ID = "deprecated-penpal";
339
+ var isDeprecatedMessage = (data) => {
340
+ return isObject(data) && "penpal" in data;
341
+ };
342
+ var upgradeMethodPath = (methodPath) => methodPath.split(".");
343
+ var downgradeMethodPath = (methodPath) => methodPath.join(".");
344
+ var getUnexpectedMessageError = (message) => {
345
+ return new PenpalBugError_default(
346
+ `Unexpected message to translate: ${JSON.stringify(message)}`
347
+ );
348
+ };
349
+ var upgradeMessage = (message) => {
350
+ if (message.penpal === "syn" /* Syn */) {
351
+ return {
352
+ namespace: namespace_default,
353
+ channel: void 0,
354
+ type: "SYN",
355
+ participantId: DEPRECATED_PENPAL_PARTICIPANT_ID
349
356
  };
357
+ }
358
+ if (message.penpal === "ack" /* Ack */) {
350
359
  return {
351
- remoteProxy,
352
- destroy,
360
+ namespace: namespace_default,
361
+ channel: void 0,
362
+ type: "ACK2"
353
363
  };
364
+ }
365
+ if (message.penpal === "call" /* Call */) {
366
+ return {
367
+ namespace: namespace_default,
368
+ channel: void 0,
369
+ type: "CALL",
370
+ // Actually converting the ID to a string would break communication.
371
+ id: message.id,
372
+ methodPath: upgradeMethodPath(message.methodName),
373
+ args: message.args
374
+ };
375
+ }
376
+ if (message.penpal === "reply" /* Reply */) {
377
+ if (message.resolution === "fulfilled" /* Fulfilled */) {
378
+ return {
379
+ namespace: namespace_default,
380
+ channel: void 0,
381
+ type: "REPLY",
382
+ // Actually converting the ID to a string would break communication.
383
+ callId: message.id,
384
+ value: message.returnValue
385
+ };
386
+ } else {
387
+ return {
388
+ namespace: namespace_default,
389
+ channel: void 0,
390
+ type: "REPLY",
391
+ // Actually converting the ID to a string would break communication.
392
+ callId: message.id,
393
+ isError: true,
394
+ ...message.returnValueIsError ? {
395
+ value: message.returnValue,
396
+ isSerializedErrorInstance: true
397
+ } : {
398
+ value: message.returnValue
399
+ }
400
+ };
401
+ }
402
+ }
403
+ throw getUnexpectedMessageError(message);
354
404
  };
355
-
356
- // Just use the native Promise.withResolvers() once it gains a bit more
357
- // adoption. Safari was the last major browser to support it, which happened
358
- // on March 5, 2024 in Safari 17.4.
359
- const getPromiseWithResolvers = () => {
360
- let resolve;
361
- let reject;
362
- const promise = new Promise((res, rej) => {
363
- resolve = res;
364
- reject = rej;
365
- });
405
+ var downgradeMessage = (message) => {
406
+ if (isAck1Message(message)) {
366
407
  return {
367
- promise,
368
- resolve: resolve,
369
- reject: reject,
408
+ penpal: "synAck" /* SynAck */,
409
+ methodNames: message.methodPaths.map(downgradeMethodPath)
370
410
  };
411
+ }
412
+ if (isCallMessage(message)) {
413
+ return {
414
+ penpal: "call" /* Call */,
415
+ // Actually converting the ID to a number would break communication.
416
+ id: message.id,
417
+ methodName: downgradeMethodPath(message.methodPath),
418
+ args: message.args
419
+ };
420
+ }
421
+ if (isReplyMessage(message)) {
422
+ if (message.isError) {
423
+ return {
424
+ penpal: "reply" /* Reply */,
425
+ // Actually converting the ID to a number would break communication.
426
+ id: message.callId,
427
+ resolution: "rejected" /* Rejected */,
428
+ ...message.isSerializedErrorInstance ? {
429
+ returnValue: message.value,
430
+ returnValueIsError: true
431
+ } : { returnValue: message.value }
432
+ };
433
+ } else {
434
+ return {
435
+ penpal: "reply" /* Reply */,
436
+ // Actually converting the ID to a number would break communication.
437
+ id: message.callId,
438
+ resolution: "fulfilled" /* Fulfilled */,
439
+ returnValue: message.value
440
+ };
441
+ }
442
+ }
443
+ throw getUnexpectedMessageError(message);
371
444
  };
372
445
 
373
- /**
374
- * Error class that is thrown when we've reached a situation that we believe to
375
- * be a bug in Penpal and not anything the consumer has done.
376
- */
377
- class PenpalBugError extends Error {
378
- constructor(message) {
379
- super(`You've hit a bug in Penpal. Please file an issue with the following information: ${message}`);
446
+ // src/shakeHands.ts
447
+ var shakeHands = ({
448
+ messenger,
449
+ methods,
450
+ timeout,
451
+ channel,
452
+ log
453
+ }) => {
454
+ const participantId = generateId_default();
455
+ let remoteParticipantId;
456
+ const destroyHandlers = [];
457
+ let isComplete = false;
458
+ const methodPaths = extractMethodPathsFromMethods(methods);
459
+ const { promise, resolve, reject } = getPromiseWithResolvers_default();
460
+ const timeoutId = timeout !== void 0 ? setTimeout(() => {
461
+ reject(
462
+ new PenpalError_default(
463
+ "CONNECTION_TIMEOUT",
464
+ `Connection timed out after ${timeout}ms`
465
+ )
466
+ );
467
+ }, timeout) : void 0;
468
+ const destroy = () => {
469
+ for (const destroyHandler of destroyHandlers) {
470
+ destroyHandler();
380
471
  }
381
- }
382
-
383
- const DEPRECATED_PENPAL_PARTICIPANT_ID = 'deprecated-penpal';
384
- // TODO: This file is used for backward-compatibility. Remove in next major version.
385
- var DeprecatedMessageType;
386
- (function (DeprecatedMessageType) {
387
- DeprecatedMessageType["Call"] = "call";
388
- DeprecatedMessageType["Reply"] = "reply";
389
- DeprecatedMessageType["Syn"] = "syn";
390
- DeprecatedMessageType["SynAck"] = "synAck";
391
- DeprecatedMessageType["Ack"] = "ack";
392
- })(DeprecatedMessageType || (DeprecatedMessageType = {}));
393
- var DeprecatedResolution;
394
- (function (DeprecatedResolution) {
395
- DeprecatedResolution["Fulfilled"] = "fulfilled";
396
- DeprecatedResolution["Rejected"] = "rejected";
397
- })(DeprecatedResolution || (DeprecatedResolution = {}));
398
- const isDeprecatedMessage = (data) => {
399
- return isObject(data) && 'penpal' in data;
400
- };
401
- const upgradeMethodPath = (methodPath) => methodPath.split('.');
402
- const downgradeMethodPath = (methodPath) => methodPath.join('.');
403
- const getUnexpectedMessageError = (message) => {
404
- return new PenpalBugError(`Unexpected message to translate: ${JSON.stringify(message)}`);
405
- };
406
- const upgradeMessage = (message) => {
407
- if (message.penpal === DeprecatedMessageType.Syn) {
408
- return {
409
- namespace,
410
- channel: undefined,
411
- type: 'SYN',
412
- participantId: DEPRECATED_PENPAL_PARTICIPANT_ID,
413
- };
472
+ };
473
+ const connectCallHandlerAndMethodProxies = () => {
474
+ if (isComplete) {
475
+ return;
414
476
  }
415
- if (message.penpal === DeprecatedMessageType.Ack) {
416
- return {
417
- namespace,
418
- channel: undefined,
419
- type: 'ACK2',
420
- };
477
+ destroyHandlers.push(connectCallHandler_default(messenger, methods, channel, log));
478
+ const { remoteProxy, destroy: destroyMethodProxies } = connectRemoteProxy_default(messenger, channel, log);
479
+ destroyHandlers.push(destroyMethodProxies);
480
+ clearTimeout(timeoutId);
481
+ isComplete = true;
482
+ resolve({
483
+ remoteProxy,
484
+ destroy
485
+ });
486
+ };
487
+ const sendSynMessage = () => {
488
+ const synMessage = {
489
+ namespace: namespace_default,
490
+ type: "SYN",
491
+ channel,
492
+ participantId
493
+ };
494
+ log?.(`Sending handshake SYN`, synMessage);
495
+ try {
496
+ messenger.sendMessage(synMessage);
497
+ } catch (error) {
498
+ reject(new PenpalError_default("TRANSMISSION_FAILED", error.message));
421
499
  }
422
- if (message.penpal === DeprecatedMessageType.Call) {
423
- return {
424
- namespace,
425
- channel: undefined,
426
- type: 'CALL',
427
- // Actually converting the ID to a string would break communication.
428
- id: message.id,
429
- methodPath: upgradeMethodPath(message.methodName),
430
- args: message.args,
431
- };
500
+ };
501
+ const handleSynMessage = (message) => {
502
+ log?.(`Received handshake SYN`, message);
503
+ if (message.participantId === remoteParticipantId && // TODO: Used for backward-compatibility. Remove in next major version.
504
+ remoteParticipantId !== DEPRECATED_PENPAL_PARTICIPANT_ID) {
505
+ return;
432
506
  }
433
- if (message.penpal === DeprecatedMessageType.Reply) {
434
- if (message.resolution === DeprecatedResolution.Fulfilled) {
435
- return {
436
- namespace,
437
- channel: undefined,
438
- type: 'REPLY',
439
- // Actually converting the ID to a string would break communication.
440
- callId: message.id,
441
- value: message.returnValue,
442
- };
443
- }
444
- else {
445
- return {
446
- namespace,
447
- channel: undefined,
448
- type: 'REPLY',
449
- // Actually converting the ID to a string would break communication.
450
- callId: message.id,
451
- isError: true,
452
- ...(message.returnValueIsError
453
- ? {
454
- value: message.returnValue,
455
- isSerializedErrorInstance: true,
456
- }
457
- : {
458
- value: message.returnValue,
459
- }),
460
- };
461
- }
507
+ remoteParticipantId = message.participantId;
508
+ sendSynMessage();
509
+ const isHandshakeLeader = participantId > remoteParticipantId || // TODO: Used for backward-compatibility. Remove in next major version.
510
+ remoteParticipantId === DEPRECATED_PENPAL_PARTICIPANT_ID;
511
+ if (!isHandshakeLeader) {
512
+ return;
462
513
  }
463
- throw getUnexpectedMessageError(message);
464
- };
465
- const downgradeMessage = (message) => {
466
- if (isAck1Message(message)) {
467
- return {
468
- penpal: DeprecatedMessageType.SynAck,
469
- methodNames: message.methodPaths.map(downgradeMethodPath),
470
- };
514
+ const ack1Message = {
515
+ namespace: namespace_default,
516
+ channel,
517
+ type: "ACK1",
518
+ methodPaths
519
+ };
520
+ log?.(`Sending handshake ACK1`, ack1Message);
521
+ try {
522
+ messenger.sendMessage(ack1Message);
523
+ } catch (error) {
524
+ reject(new PenpalError_default("TRANSMISSION_FAILED", error.message));
525
+ return;
471
526
  }
472
- if (isCallMessage(message)) {
473
- return {
474
- penpal: DeprecatedMessageType.Call,
475
- // Actually converting the ID to a number would break communication.
476
- id: message.id,
477
- methodName: downgradeMethodPath(message.methodPath),
478
- args: message.args,
479
- };
527
+ };
528
+ const handleAck1Message = (message) => {
529
+ log?.(`Received handshake ACK1`, message);
530
+ const ack2Message = {
531
+ namespace: namespace_default,
532
+ channel,
533
+ type: "ACK2"
534
+ };
535
+ log?.(`Sending handshake ACK2`, ack2Message);
536
+ try {
537
+ messenger.sendMessage(ack2Message);
538
+ } catch (error) {
539
+ reject(new PenpalError_default("TRANSMISSION_FAILED", error.message));
540
+ return;
480
541
  }
481
- if (isReplyMessage(message)) {
482
- if (message.isError) {
483
- return {
484
- penpal: DeprecatedMessageType.Reply,
485
- // Actually converting the ID to a number would break communication.
486
- id: message.callId,
487
- resolution: DeprecatedResolution.Rejected,
488
- ...(message.isSerializedErrorInstance
489
- ? {
490
- returnValue: message.value,
491
- returnValueIsError: true,
492
- }
493
- : { returnValue: message.value }),
494
- };
495
- }
496
- else {
497
- return {
498
- penpal: DeprecatedMessageType.Reply,
499
- // Actually converting the ID to a number would break communication.
500
- id: message.callId,
501
- resolution: DeprecatedResolution.Fulfilled,
502
- returnValue: message.value,
503
- };
504
- }
542
+ connectCallHandlerAndMethodProxies();
543
+ };
544
+ const handleAck2Message = (message) => {
545
+ log?.(`Received handshake ACK2`, message);
546
+ connectCallHandlerAndMethodProxies();
547
+ };
548
+ const handleMessage = (message) => {
549
+ if (isSynMessage(message)) {
550
+ handleSynMessage(message);
551
+ }
552
+ if (isAck1Message(message)) {
553
+ handleAck1Message(message);
505
554
  }
506
- throw getUnexpectedMessageError(message);
555
+ if (isAck2Message(message)) {
556
+ handleAck2Message(message);
557
+ }
558
+ };
559
+ messenger.addMessageHandler(handleMessage);
560
+ destroyHandlers.push(() => messenger.removeMessageHandler(handleMessage));
561
+ sendSynMessage();
562
+ return promise;
507
563
  };
564
+ var shakeHands_default = shakeHands;
508
565
 
509
- /**
510
- * Attempts to establish communication with the remote via a handshake protocol.
511
- * The handshake protocol fulfills a few requirements:
512
- *
513
- * 1. One participant in the handshake may not be available when the other
514
- * participant starts the handshake. For example, a document inside an iframe
515
- * may not be loaded when the parent window starts a handshake.
516
- * 2. While #1 could be solved by having the consumer of Penpal specify which
517
- * participant should initiate the handshake, we'd rather avoid this
518
- * unnecessary cognitive load.
519
- * 3. While #1 could be solved by having the consumer of Penpal specify which
520
- * participant is the "parent" or "child" and then having Penpal assume
521
- * the child should initiate the handshake, we'd rather avoid parent-child
522
- * terminology since Penpal can support communication between two
523
- * participants where neither would be considered a parent nor child. It may
524
- * also be too presumptive that the child should always initiate the
525
- * handshake.
526
- * 4. For robustness, each participant must know that the other participant is
527
- * receiving its messages for the handshake to be considered complete.
528
- * 5. The handshake should support a participant attempting to
529
- * re-establish the connection. This can occur, for example, if an end user
530
- * were to right-click within an iframe and click reload.
531
- * 6. The handshake should allow a Messenger to easily attach something to
532
- * a handshake message from one participant to the other unidirectionally
533
- * (rather than from both participants to each other).
534
- * This is important when a participant needs to be in charge of, for
535
- * example, creating a MessageChannel and sending one MessagePort from the
536
- * MessagePort pair to the other participant. If both participants attempted
537
- * to do this it could lead to confusion.
538
- * 7. The handshake ideally shouldn't require sending handshake messages on an
539
- * interval (retrying until the other participant is ready to receive them).
540
- * Intervals can increase compute resources if the interval is too short
541
- * or increase latency if the interval is too long. While we could make this
542
- * configurable, it's additional mental load for the consumer. Additionally,
543
- * setInterval and setTimeout are not available within some contexts
544
- * (like AudioWorklet), where a consumer may like to use Penpal.
545
- *
546
- * To accomplish these requirements, the handshake protocol is as follows:
547
- * 1. Each participant generates a random participant ID.
548
- * 2. As soon as possible, each participant sends a SYN message containing its
549
- * participant ID to the other participant.
550
- * 3. When the SYN messages were sent, one of the participants may not have
551
- * been ready to receive the SYN message from the other. At least one
552
- * of the participants was ready, however, and should have received a SYN
553
- * message from the other participant. Each participant that did receive
554
- * a SYN message knows for sure that the other participant is now ready
555
- * to receive a SYN message, so it will send another SYN message in case
556
- * the other participant did not receive the first SYN message. This
557
- * ultimately results in each participant sending two SYN messages.
558
- * 4. Each participant now should have received at least one SYN message from
559
- * the other participant. Each participant compares their own ID with the
560
- * other participant's ID. Whichever participant has the higher ID
561
- * (using a simple string comparison) is considered the handshake leader
562
- * and will send an ACK1 message to the other participant.
563
- * 5. At this point, the handshake leader does not know whether the other
564
- * participant is actually receiving messages. The participant receiving
565
- * the ACK1 message will respond with an ACK2, informing the handshake
566
- * leader that it is indeed receiving messages.
567
- * 6. At this point, both participants know the other is receiving messages
568
- * and the handshake is complete.
569
- */
570
- const shakeHands = ({ messenger, methods, timeout, channel, log, }) => {
571
- const participantId = generateId();
572
- let remoteParticipantId;
573
- const destroyHandlers = [];
574
- let isComplete = false;
575
- const methodPaths = extractMethodPathsFromMethods(methods);
576
- const { promise, resolve, reject } = getPromiseWithResolvers();
577
- const timeoutId = timeout !== undefined
578
- ? setTimeout(() => {
579
- reject(new PenpalError('CONNECTION_TIMEOUT', `Connection timed out after ${timeout}ms`));
580
- }, timeout)
581
- : undefined;
582
- const destroy = () => {
583
- for (const destroyHandler of destroyHandlers) {
584
- destroyHandler();
585
- }
586
- };
587
- const connectCallHandlerAndMethodProxies = () => {
588
- if (isComplete) {
589
- // If we get here, it means the remote is attempting to re-connect. While
590
- // that's supported, we don't need to run the rest of this function again.
591
- return;
592
- }
593
- destroyHandlers.push(connectCallHandler(messenger, methods, channel, log));
594
- const { remoteProxy, destroy: destroyMethodProxies } = connectRemoteProxy(messenger, channel, log);
595
- destroyHandlers.push(destroyMethodProxies);
596
- clearTimeout(timeoutId);
597
- isComplete = true;
598
- resolve({
599
- remoteProxy,
600
- destroy: destroy,
601
- });
602
- };
603
- const sendSynMessage = () => {
604
- const synMessage = {
605
- namespace,
606
- type: 'SYN',
607
- channel,
608
- participantId: participantId,
609
- };
610
- log?.(`Sending handshake SYN`, synMessage);
611
- try {
612
- messenger.sendMessage(synMessage);
613
- }
614
- catch (error) {
615
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
616
- }
617
- };
618
- const handleSynMessage = (message) => {
619
- log?.(`Received handshake SYN`, message);
620
- if (message.participantId === remoteParticipantId &&
621
- // TODO: Used for backward-compatibility. Remove in next major version.
622
- remoteParticipantId !== DEPRECATED_PENPAL_PARTICIPANT_ID) {
623
- return;
624
- }
625
- remoteParticipantId = message.participantId;
626
- // We send another SYN message in case the other participant was not ready
627
- // when we sent the first SYN message.
628
- sendSynMessage();
629
- const isHandshakeLeader = participantId > remoteParticipantId ||
630
- // TODO: Used for backward-compatibility. Remove in next major version.
631
- remoteParticipantId === DEPRECATED_PENPAL_PARTICIPANT_ID;
632
- if (!isHandshakeLeader) {
633
- return;
634
- }
635
- const ack1Message = {
636
- namespace,
637
- channel,
638
- type: 'ACK1',
639
- methodPaths,
640
- };
641
- log?.(`Sending handshake ACK1`, ack1Message);
642
- try {
643
- messenger.sendMessage(ack1Message);
644
- }
645
- catch (error) {
646
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
647
- return;
648
- }
649
- };
650
- const handleAck1Message = (message) => {
651
- log?.(`Received handshake ACK1`, message);
652
- const ack2Message = {
653
- namespace,
654
- channel,
655
- type: 'ACK2',
656
- };
657
- log?.(`Sending handshake ACK2`, ack2Message);
658
- try {
659
- messenger.sendMessage(ack2Message);
660
- }
661
- catch (error) {
662
- reject(new PenpalError('TRANSMISSION_FAILED', error.message));
663
- return;
664
- }
665
- connectCallHandlerAndMethodProxies();
666
- };
667
- const handleAck2Message = (message) => {
668
- log?.(`Received handshake ACK2`, message);
669
- connectCallHandlerAndMethodProxies();
670
- };
671
- const handleMessage = (message) => {
672
- if (isSynMessage(message)) {
673
- handleSynMessage(message);
674
- }
675
- if (isAck1Message(message)) {
676
- handleAck1Message(message);
677
- }
678
- if (isAck2Message(message)) {
679
- handleAck2Message(message);
680
- }
681
- };
682
- messenger.addMessageHandler(handleMessage);
683
- destroyHandlers.push(() => messenger.removeMessageHandler(handleMessage));
684
- sendSynMessage();
685
- return promise;
566
+ // src/once.ts
567
+ var once = (fn) => {
568
+ let isCalled = false;
569
+ let result;
570
+ return (...args) => {
571
+ if (!isCalled) {
572
+ isCalled = true;
573
+ result = fn(...args);
574
+ }
575
+ return result;
576
+ };
686
577
  };
578
+ var once_default = once;
687
579
 
688
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
689
- const once = (fn) => {
690
- let isCalled = false;
691
- let result;
692
- return (...args) => {
693
- if (!isCalled) {
694
- isCalled = true;
695
- result = fn(...args);
696
- }
697
- return result;
698
- };
580
+ // src/connect.ts
581
+ var usedMessengers = /* @__PURE__ */ new WeakSet();
582
+ var connect = ({
583
+ messenger,
584
+ methods = {},
585
+ timeout,
586
+ channel,
587
+ log
588
+ }) => {
589
+ if (!messenger) {
590
+ throw new PenpalError_default("INVALID_ARGUMENT", "messenger must be defined");
591
+ }
592
+ if (usedMessengers.has(messenger)) {
593
+ throw new PenpalError_default(
594
+ "INVALID_ARGUMENT",
595
+ "A messenger can only be used for a single connection"
596
+ );
597
+ }
598
+ usedMessengers.add(messenger);
599
+ const connectionDestroyedHandlers = [messenger.destroy];
600
+ const destroyConnection = once_default((notifyOtherParticipant) => {
601
+ if (notifyOtherParticipant) {
602
+ const destroyMessage = {
603
+ namespace: namespace_default,
604
+ channel,
605
+ type: "DESTROY"
606
+ };
607
+ try {
608
+ messenger.sendMessage(destroyMessage);
609
+ } catch (_) {
610
+ }
611
+ }
612
+ for (const connectionDestroyedHandler of connectionDestroyedHandlers) {
613
+ connectionDestroyedHandler();
614
+ }
615
+ log?.("Connection destroyed");
616
+ });
617
+ const validateReceivedMessage = (data) => {
618
+ return isMessage(data) && data.channel === channel;
619
+ };
620
+ const promise = (async () => {
621
+ try {
622
+ messenger.initialize({ log, validateReceivedMessage });
623
+ messenger.addMessageHandler((message) => {
624
+ if (isDestroyMessage(message)) {
625
+ destroyConnection(false);
626
+ }
627
+ });
628
+ const { remoteProxy, destroy } = await shakeHands_default({
629
+ messenger,
630
+ methods,
631
+ timeout,
632
+ channel,
633
+ log
634
+ });
635
+ connectionDestroyedHandlers.push(destroy);
636
+ return remoteProxy;
637
+ } catch (error) {
638
+ destroyConnection(true);
639
+ throw error;
640
+ }
641
+ })();
642
+ return {
643
+ promise,
644
+ // Why we don't reject the connection promise when consumer calls destroy():
645
+ // https://github.com/Aaronius/penpal/issues/51
646
+ destroy: () => {
647
+ destroyConnection(true);
648
+ }
649
+ };
699
650
  };
651
+ var connect_default = connect;
700
652
 
701
- const usedMessengers = new WeakSet();
702
- /**
703
- * Attempts to establish communication with the remote.
704
- */
705
- const connect = ({ messenger, methods = {}, timeout, channel, log, }) => {
706
- if (!messenger) {
707
- throw new PenpalError('INVALID_ARGUMENT', 'messenger must be defined');
653
+ // src/messengers/WindowMessenger.ts
654
+ var WindowMessenger = class {
655
+ #remoteWindow;
656
+ #allowedOrigins;
657
+ #log;
658
+ #validateReceivedMessage;
659
+ #concreteRemoteOrigin;
660
+ #messageCallbacks = /* @__PURE__ */ new Set();
661
+ #port;
662
+ // TODO: Used for backward-compatibility. Remove in next major version.
663
+ #isChildUsingDeprecatedProtocol = false;
664
+ constructor({ remoteWindow, allowedOrigins }) {
665
+ if (!remoteWindow) {
666
+ throw new PenpalError_default("INVALID_ARGUMENT", "remoteWindow must be defined");
708
667
  }
709
- if (usedMessengers.has(messenger)) {
710
- throw new PenpalError('INVALID_ARGUMENT', 'A messenger can only be used for a single connection');
668
+ this.#remoteWindow = remoteWindow;
669
+ this.#allowedOrigins = allowedOrigins?.length ? allowedOrigins : [window.origin];
670
+ }
671
+ initialize = ({
672
+ log,
673
+ validateReceivedMessage
674
+ }) => {
675
+ this.#log = log;
676
+ this.#validateReceivedMessage = validateReceivedMessage;
677
+ window.addEventListener("message", this.#handleMessageFromRemoteWindow);
678
+ };
679
+ sendMessage = (message, transferables) => {
680
+ if (isSynMessage(message)) {
681
+ const originForSending = this.#getOriginForSendingMessage(message);
682
+ this.#remoteWindow.postMessage(message, {
683
+ targetOrigin: originForSending,
684
+ transfer: transferables
685
+ });
686
+ return;
711
687
  }
712
- usedMessengers.add(messenger);
713
- const connectionDestroyedHandlers = [messenger.destroy];
714
- const destroyConnection = once((notifyOtherParticipant) => {
715
- if (notifyOtherParticipant) {
716
- const destroyMessage = {
717
- namespace,
718
- channel,
719
- type: 'DESTROY',
720
- };
721
- try {
722
- messenger.sendMessage(destroyMessage);
723
- }
724
- catch (_) {
725
- // We do our best to notify the other participant of the connection, but
726
- // if there's an error in doing so (e.g., maybe the handshake hasn't
727
- // completed and a messenger can't send the message), it's probably not
728
- // worth bothering the consumer with an error.
729
- }
730
- }
731
- for (const connectionDestroyedHandler of connectionDestroyedHandlers) {
732
- connectionDestroyedHandler();
733
- }
734
- log?.('Connection destroyed');
735
- });
736
- const validateReceivedMessage = (data) => {
737
- return isMessage(data) && data.channel === channel;
738
- };
739
- const promise = (async () => {
740
- try {
741
- messenger.initialize({ log, validateReceivedMessage });
742
- messenger.addMessageHandler((message) => {
743
- if (isDestroyMessage(message)) {
744
- destroyConnection(false);
745
- }
746
- });
747
- const { remoteProxy, destroy } = await shakeHands({
748
- messenger,
749
- methods,
750
- timeout,
751
- channel,
752
- log,
753
- });
754
- connectionDestroyedHandlers.push(destroy);
755
- return remoteProxy;
756
- }
757
- catch (error) {
758
- destroyConnection(true);
759
- throw error;
760
- }
761
- })();
762
- return {
763
- promise,
764
- // Why we don't reject the connection promise when consumer calls destroy():
765
- // https://github.com/Aaronius/penpal/issues/51
766
- destroy: () => {
767
- destroyConnection(true);
768
- },
769
- };
688
+ if (isAck1Message(message) || // If the child is using a previous version of Penpal, we need to
689
+ // downgrade the message and send it through the window rather than
690
+ // the port because older versions of Penpal don't use MessagePorts.
691
+ this.#isChildUsingDeprecatedProtocol) {
692
+ const payload = this.#isChildUsingDeprecatedProtocol ? downgradeMessage(message) : message;
693
+ const originForSending = this.#getOriginForSendingMessage(message);
694
+ this.#remoteWindow.postMessage(payload, {
695
+ targetOrigin: originForSending,
696
+ transfer: transferables
697
+ });
698
+ return;
699
+ }
700
+ if (isAck2Message(message)) {
701
+ const { port1, port2 } = new MessageChannel();
702
+ this.#port = port1;
703
+ port1.addEventListener("message", this.#handleMessageFromPort);
704
+ port1.start();
705
+ const transferablesToSend = [port2, ...transferables || []];
706
+ const originForSending = this.#getOriginForSendingMessage(message);
707
+ this.#remoteWindow.postMessage(message, {
708
+ targetOrigin: originForSending,
709
+ transfer: transferablesToSend
710
+ });
711
+ return;
712
+ }
713
+ if (this.#port) {
714
+ this.#port.postMessage(message, {
715
+ transfer: transferables
716
+ });
717
+ return;
718
+ }
719
+ throw new PenpalBugError_default("Port is undefined");
720
+ };
721
+ addMessageHandler = (callback) => {
722
+ this.#messageCallbacks.add(callback);
723
+ };
724
+ removeMessageHandler = (callback) => {
725
+ this.#messageCallbacks.delete(callback);
726
+ };
727
+ destroy = () => {
728
+ window.removeEventListener("message", this.#handleMessageFromRemoteWindow);
729
+ this.#destroyPort();
730
+ this.#messageCallbacks.clear();
731
+ };
732
+ #isAllowedOrigin = (origin) => {
733
+ return this.#allowedOrigins.some(
734
+ (allowedOrigin) => allowedOrigin instanceof RegExp ? allowedOrigin.test(origin) : allowedOrigin === origin || allowedOrigin === "*"
735
+ );
736
+ };
737
+ #getOriginForSendingMessage = (message) => {
738
+ if (isSynMessage(message)) {
739
+ return "*";
740
+ }
741
+ if (!this.#concreteRemoteOrigin) {
742
+ throw new PenpalBugError_default("Concrete remote origin not set");
743
+ }
744
+ return this.#concreteRemoteOrigin === "null" && this.#allowedOrigins.includes("*") ? "*" : this.#concreteRemoteOrigin;
745
+ };
746
+ #destroyPort = () => {
747
+ this.#port?.removeEventListener("message", this.#handleMessageFromPort);
748
+ this.#port?.close();
749
+ this.#port = void 0;
750
+ };
751
+ #handleMessageFromRemoteWindow = ({
752
+ source,
753
+ origin,
754
+ ports,
755
+ data
756
+ }) => {
757
+ if (source !== this.#remoteWindow) {
758
+ return;
759
+ }
760
+ if (isDeprecatedMessage(data)) {
761
+ this.#log?.(
762
+ "Please upgrade the child window to the latest version of Penpal."
763
+ );
764
+ this.#isChildUsingDeprecatedProtocol = true;
765
+ data = upgradeMessage(data);
766
+ }
767
+ if (!this.#validateReceivedMessage?.(data)) {
768
+ return;
769
+ }
770
+ if (!this.#isAllowedOrigin(origin)) {
771
+ this.#log?.(
772
+ `Received a message from origin \`${origin}\` which did not match allowed origins \`[${this.#allowedOrigins.join(", ")}]\``
773
+ );
774
+ return;
775
+ }
776
+ if (isSynMessage(data)) {
777
+ this.#destroyPort();
778
+ this.#concreteRemoteOrigin = origin;
779
+ }
780
+ if (isAck2Message(data) && // Previous versions of Penpal don't use MessagePorts and do all
781
+ // communication through the window.
782
+ !this.#isChildUsingDeprecatedProtocol) {
783
+ this.#port = ports[0];
784
+ if (!this.#port) {
785
+ throw new PenpalBugError_default("No port received on ACK2");
786
+ }
787
+ this.#port.addEventListener("message", this.#handleMessageFromPort);
788
+ this.#port.start();
789
+ }
790
+ for (const callback of this.#messageCallbacks) {
791
+ callback(data);
792
+ }
793
+ };
794
+ #handleMessageFromPort = ({ data }) => {
795
+ if (!this.#validateReceivedMessage?.(data)) {
796
+ return;
797
+ }
798
+ for (const callback of this.#messageCallbacks) {
799
+ callback(data);
800
+ }
801
+ };
770
802
  };
803
+ var WindowMessenger_default = WindowMessenger;
771
804
 
772
- /**
773
- * Handles the details of communicating with a child window.
774
- */
775
- class WindowMessenger {
776
- #remoteWindow;
777
- #allowedOrigins;
778
- #log;
779
- #validateReceivedMessage;
780
- #concreteRemoteOrigin;
781
- #messageCallbacks = new Set();
782
- #port;
783
- // TODO: Used for backward-compatibility. Remove in next major version.
784
- #isChildUsingDeprecatedProtocol = false;
785
- constructor({ remoteWindow, allowedOrigins }) {
786
- if (!remoteWindow) {
787
- throw new PenpalError('INVALID_ARGUMENT', 'remoteWindow must be defined');
788
- }
789
- this.#remoteWindow = remoteWindow;
790
- this.#allowedOrigins = allowedOrigins?.length
791
- ? allowedOrigins
792
- : [window.origin];
805
+ // src/messengers/WorkerMessenger.ts
806
+ var WorkerMessenger = class {
807
+ #worker;
808
+ #validateReceivedMessage;
809
+ #messageCallbacks = /* @__PURE__ */ new Set();
810
+ #port;
811
+ constructor({ worker }) {
812
+ if (!worker) {
813
+ throw new PenpalError_default("INVALID_ARGUMENT", "worker must be defined");
793
814
  }
794
- initialize = ({ log, validateReceivedMessage, }) => {
795
- this.#log = log;
796
- this.#validateReceivedMessage = validateReceivedMessage;
797
- window.addEventListener('message', this.#handleMessageFromRemoteWindow);
798
- };
799
- sendMessage = (message, transferables) => {
800
- if (isSynMessage(message)) {
801
- const originForSending = this.#getOriginForSendingMessage(message);
802
- this.#remoteWindow.postMessage(message, {
803
- targetOrigin: originForSending,
804
- transfer: transferables,
805
- });
806
- return;
807
- }
808
- if (isAck1Message(message) ||
809
- // If the child is using a previous version of Penpal, we need to
810
- // downgrade the message and send it through the window rather than
811
- // the port because older versions of Penpal don't use MessagePorts.
812
- this.#isChildUsingDeprecatedProtocol) {
813
- const payload = this.#isChildUsingDeprecatedProtocol
814
- ? downgradeMessage(message)
815
- : message;
816
- const originForSending = this.#getOriginForSendingMessage(message);
817
- this.#remoteWindow.postMessage(payload, {
818
- targetOrigin: originForSending,
819
- transfer: transferables,
820
- });
821
- return;
822
- }
823
- if (isAck2Message(message)) {
824
- const { port1, port2 } = new MessageChannel();
825
- this.#port = port1;
826
- port1.addEventListener('message', this.#handleMessageFromPort);
827
- port1.start();
828
- const transferablesToSend = [port2, ...(transferables || [])];
829
- const originForSending = this.#getOriginForSendingMessage(message);
830
- this.#remoteWindow.postMessage(message, {
831
- targetOrigin: originForSending,
832
- transfer: transferablesToSend,
833
- });
834
- return;
835
- }
836
- if (this.#port) {
837
- this.#port.postMessage(message, {
838
- transfer: transferables,
839
- });
840
- return;
841
- }
842
- throw new PenpalBugError('Port is undefined');
843
- };
844
- addMessageHandler = (callback) => {
845
- this.#messageCallbacks.add(callback);
846
- };
847
- removeMessageHandler = (callback) => {
848
- this.#messageCallbacks.delete(callback);
849
- };
850
- destroy = () => {
851
- window.removeEventListener('message', this.#handleMessageFromRemoteWindow);
852
- this.#destroyPort();
853
- this.#messageCallbacks.clear();
854
- };
855
- #isAllowedOrigin = (origin) => {
856
- return this.#allowedOrigins.some((allowedOrigin) => allowedOrigin instanceof RegExp
857
- ? allowedOrigin.test(origin)
858
- : allowedOrigin === origin || allowedOrigin === '*');
859
- };
860
- #getOriginForSendingMessage = (message) => {
861
- // It's safe to send the SYN message to any origin because it doesn't contain
862
- // anything sensitive. When Penpal receives a SYN message, the origin on
863
- // the message (which we call the concrete origin) is validated against the
864
- // configured allowed origins. All subsequent messages will be sent to the
865
- // concrete origin.
866
- // If you decide to change this, consider https://github.com/Aaronius/penpal/issues/103
867
- if (isSynMessage(message)) {
868
- return '*';
869
- }
870
- if (!this.#concreteRemoteOrigin) {
871
- throw new PenpalBugError('Concrete remote origin not set');
872
- }
873
- // If the concrete remote origin (the origin we received from the remote
874
- // on a prior message) is 'null', it means the remote is within
875
- // an "opaque origin". The only way to post a message to an
876
- // opaque origin is by using '*'. This does carry some security risk,
877
- // so we only do this if the consumer has specifically defined '*' as
878
- // an allowed origin. Opaque origins occur, for example, when
879
- // loading an HTML document directly from the filesystem (not a
880
- // web server) or through a data URI.
881
- return this.#concreteRemoteOrigin === 'null' &&
882
- this.#allowedOrigins.includes('*')
883
- ? '*'
884
- : this.#concreteRemoteOrigin;
885
- };
886
- #destroyPort = () => {
887
- this.#port?.removeEventListener('message', this.#handleMessageFromPort);
888
- this.#port?.close();
889
- this.#port = undefined;
890
- };
891
- #handleMessageFromRemoteWindow = ({ source, origin, ports, data, }) => {
892
- if (source !== this.#remoteWindow) {
893
- return;
894
- }
895
- // TODO: Used for backward-compatibility. Remove in next major version.
896
- if (isDeprecatedMessage(data)) {
897
- this.#log?.('Please upgrade the child window to the latest version of Penpal.');
898
- this.#isChildUsingDeprecatedProtocol = true;
899
- data = upgradeMessage(data);
900
- }
901
- if (!this.#validateReceivedMessage?.(data)) {
902
- return;
903
- }
904
- if (!this.#isAllowedOrigin(origin)) {
905
- this.#log?.(`Received a message from origin \`${origin}\` which did not match ` +
906
- `allowed origins \`[${this.#allowedOrigins.join(', ')}]\``);
907
- return;
908
- }
909
- if (isSynMessage(data)) {
910
- // If we receive a SYN message and already have a port, it means
911
- // the child is re-connecting, in which case we'll receive a new port.
912
- // For this reason, we always make sure we destroy the existing port.
913
- this.#destroyPort();
914
- this.#concreteRemoteOrigin = origin;
915
- }
916
- if (isAck2Message(data) &&
917
- // Previous versions of Penpal don't use MessagePorts and do all
918
- // communication through the window.
919
- !this.#isChildUsingDeprecatedProtocol) {
920
- this.#port = ports[0];
921
- if (!this.#port) {
922
- throw new PenpalBugError('No port received on ACK2');
923
- }
924
- this.#port.addEventListener('message', this.#handleMessageFromPort);
925
- this.#port.start();
926
- }
927
- for (const callback of this.#messageCallbacks) {
928
- callback(data);
929
- }
930
- };
931
- #handleMessageFromPort = ({ data }) => {
932
- // Unlike in _handleMessageFromWindow, we don't need to check if
933
- // the message is from a deprecated version of Penpal because older versions
934
- // of Penpal don't use MessagePorts.
935
- if (!this.#validateReceivedMessage?.(data)) {
936
- return;
937
- }
938
- for (const callback of this.#messageCallbacks) {
939
- callback(data);
940
- }
941
- };
942
- }
943
-
944
- /**
945
- * Handles the details of communicating with a child web worker.
946
- */
947
- class WorkerMessenger {
948
- #worker;
949
- #validateReceivedMessage;
950
- #messageCallbacks = new Set();
951
- #port;
952
- constructor({ worker }) {
953
- if (!worker) {
954
- throw new PenpalError('INVALID_ARGUMENT', 'worker must be defined');
955
- }
956
- this.#worker = worker;
815
+ this.#worker = worker;
816
+ }
817
+ initialize = ({ validateReceivedMessage }) => {
818
+ this.#validateReceivedMessage = validateReceivedMessage;
819
+ this.#worker.addEventListener("message", this.#handleMessage);
820
+ };
821
+ sendMessage = (message, transferables) => {
822
+ if (isSynMessage(message) || isAck1Message(message)) {
823
+ this.#worker.postMessage(message, { transfer: transferables });
824
+ return;
957
825
  }
958
- initialize = ({ validateReceivedMessage }) => {
959
- this.#validateReceivedMessage = validateReceivedMessage;
960
- this.#worker.addEventListener('message', this.#handleMessage);
961
- };
962
- sendMessage = (message, transferables) => {
963
- if (isSynMessage(message) || isAck1Message(message)) {
964
- this.#worker.postMessage(message, { transfer: transferables });
965
- return;
966
- }
967
- if (isAck2Message(message)) {
968
- const { port1, port2 } = new MessageChannel();
969
- this.#port = port1;
970
- port1.addEventListener('message', this.#handleMessage);
971
- port1.start();
972
- this.#worker.postMessage(message, {
973
- transfer: [port2, ...(transferables || [])],
974
- });
975
- return;
976
- }
977
- if (this.#port) {
978
- this.#port.postMessage(message, {
979
- transfer: transferables,
980
- });
981
- return;
982
- }
983
- throw new PenpalBugError('Port is undefined');
984
- };
985
- addMessageHandler = (callback) => {
986
- this.#messageCallbacks.add(callback);
987
- };
988
- removeMessageHandler = (callback) => {
989
- this.#messageCallbacks.delete(callback);
990
- };
991
- destroy = () => {
992
- this.#worker.removeEventListener('message', this.#handleMessage);
993
- this.#destroyPort();
994
- this.#messageCallbacks.clear();
995
- };
996
- #destroyPort = () => {
997
- this.#port?.removeEventListener('message', this.#handleMessage);
998
- this.#port?.close();
999
- this.#port = undefined;
1000
- };
1001
- #handleMessage = ({ ports, data }) => {
1002
- if (!this.#validateReceivedMessage?.(data)) {
1003
- return;
1004
- }
1005
- if (isSynMessage(data)) {
1006
- // If we receive a SYN message and already have a port, it means
1007
- // the child is re-connecting, in which case we'll receive a new port.
1008
- // For this reason, we always make sure we destroy the existing port.
1009
- this.#destroyPort();
1010
- }
1011
- if (isAck2Message(data)) {
1012
- this.#port = ports[0];
1013
- if (!this.#port) {
1014
- throw new PenpalBugError('No port received on ACK2');
1015
- }
1016
- this.#port.addEventListener('message', this.#handleMessage);
1017
- this.#port.start();
1018
- }
1019
- for (const callback of this.#messageCallbacks) {
1020
- callback(data);
1021
- }
1022
- };
1023
- }
1024
-
1025
- /**
1026
- * Handles the details of communicating on a MessagePort.
1027
- */
1028
- class PortMessenger {
1029
- #port;
1030
- #validateReceivedMessage;
1031
- #messageCallbacks = new Set();
1032
- constructor({ port }) {
1033
- if (!port) {
1034
- throw new PenpalError('INVALID_ARGUMENT', 'port must be defined');
1035
- }
1036
- this.#port = port;
826
+ if (isAck2Message(message)) {
827
+ const { port1, port2 } = new MessageChannel();
828
+ this.#port = port1;
829
+ port1.addEventListener("message", this.#handleMessage);
830
+ port1.start();
831
+ this.#worker.postMessage(message, {
832
+ transfer: [port2, ...transferables || []]
833
+ });
834
+ return;
1037
835
  }
1038
- initialize = ({ validateReceivedMessage }) => {
1039
- this.#validateReceivedMessage = validateReceivedMessage;
1040
- this.#port.addEventListener('message', this.#handleMessage);
1041
- this.#port.start();
1042
- };
1043
- sendMessage = (message, transferables) => {
1044
- this.#port?.postMessage(message, {
1045
- transfer: transferables,
1046
- });
1047
- };
1048
- addMessageHandler = (callback) => {
1049
- this.#messageCallbacks.add(callback);
1050
- };
1051
- removeMessageHandler = (callback) => {
1052
- this.#messageCallbacks.delete(callback);
1053
- };
1054
- destroy = () => {
1055
- this.#port.removeEventListener('message', this.#handleMessage);
1056
- this.#port.close();
1057
- this.#messageCallbacks.clear();
1058
- };
1059
- #handleMessage = ({ data }) => {
1060
- if (!this.#validateReceivedMessage?.(data)) {
1061
- return;
1062
- }
1063
- for (const callback of this.#messageCallbacks) {
1064
- callback(data);
1065
- }
1066
- };
1067
- }
836
+ if (this.#port) {
837
+ this.#port.postMessage(message, {
838
+ transfer: transferables
839
+ });
840
+ return;
841
+ }
842
+ throw new PenpalBugError_default("Port is undefined");
843
+ };
844
+ addMessageHandler = (callback) => {
845
+ this.#messageCallbacks.add(callback);
846
+ };
847
+ removeMessageHandler = (callback) => {
848
+ this.#messageCallbacks.delete(callback);
849
+ };
850
+ destroy = () => {
851
+ this.#worker.removeEventListener("message", this.#handleMessage);
852
+ this.#destroyPort();
853
+ this.#messageCallbacks.clear();
854
+ };
855
+ #destroyPort = () => {
856
+ this.#port?.removeEventListener("message", this.#handleMessage);
857
+ this.#port?.close();
858
+ this.#port = void 0;
859
+ };
860
+ #handleMessage = ({ ports, data }) => {
861
+ if (!this.#validateReceivedMessage?.(data)) {
862
+ return;
863
+ }
864
+ if (isSynMessage(data)) {
865
+ this.#destroyPort();
866
+ }
867
+ if (isAck2Message(data)) {
868
+ this.#port = ports[0];
869
+ if (!this.#port) {
870
+ throw new PenpalBugError_default("No port received on ACK2");
871
+ }
872
+ this.#port.addEventListener("message", this.#handleMessage);
873
+ this.#port.start();
874
+ }
875
+ for (const callback of this.#messageCallbacks) {
876
+ callback(data);
877
+ }
878
+ };
879
+ };
880
+ var WorkerMessenger_default = WorkerMessenger;
1068
881
 
1069
- // Not intended to be used internally. Can be useful externally
1070
- // in projects not using TypeScript. It has the `Obj` suffix to disambiguate
1071
- // it from the ErrorCode string union.
1072
- const ErrorCodeObj = {
1073
- ConnectionDestroyed: 'CONNECTION_DESTROYED',
1074
- ConnectionTimeout: 'CONNECTION_TIMEOUT',
1075
- InvalidArgument: 'INVALID_ARGUMENT',
1076
- MethodCallTimeout: 'METHOD_CALL_TIMEOUT',
1077
- MethodNotFound: 'METHOD_NOT_FOUND',
1078
- TransmissionFailed: 'TRANSMISSION_FAILED',
882
+ // src/messengers/PortMessenger.ts
883
+ var PortMessenger = class {
884
+ #port;
885
+ #validateReceivedMessage;
886
+ #messageCallbacks = /* @__PURE__ */ new Set();
887
+ constructor({ port }) {
888
+ if (!port) {
889
+ throw new PenpalError_default("INVALID_ARGUMENT", "port must be defined");
890
+ }
891
+ this.#port = port;
892
+ }
893
+ initialize = ({ validateReceivedMessage }) => {
894
+ this.#validateReceivedMessage = validateReceivedMessage;
895
+ this.#port.addEventListener("message", this.#handleMessage);
896
+ this.#port.start();
897
+ };
898
+ sendMessage = (message, transferables) => {
899
+ this.#port?.postMessage(message, {
900
+ transfer: transferables
901
+ });
902
+ };
903
+ addMessageHandler = (callback) => {
904
+ this.#messageCallbacks.add(callback);
905
+ };
906
+ removeMessageHandler = (callback) => {
907
+ this.#messageCallbacks.delete(callback);
908
+ };
909
+ destroy = () => {
910
+ this.#port.removeEventListener("message", this.#handleMessage);
911
+ this.#port.close();
912
+ this.#messageCallbacks.clear();
913
+ };
914
+ #handleMessage = ({ data }) => {
915
+ if (!this.#validateReceivedMessage?.(data)) {
916
+ return;
917
+ }
918
+ for (const callback of this.#messageCallbacks) {
919
+ callback(data);
920
+ }
921
+ };
1079
922
  };
923
+ var PortMessenger_default = PortMessenger;
1080
924
 
1081
- const debug = (prefix) => {
1082
- return (...args) => {
1083
- console.log(`✍️ %c${prefix}%c`, 'font-weight: bold;', '', ...args);
1084
- };
925
+ // src/ErrorCodeObj.ts
926
+ var ErrorCodeObj = {
927
+ ConnectionDestroyed: "CONNECTION_DESTROYED",
928
+ ConnectionTimeout: "CONNECTION_TIMEOUT",
929
+ InvalidArgument: "INVALID_ARGUMENT",
930
+ MethodCallTimeout: "METHOD_CALL_TIMEOUT",
931
+ MethodNotFound: "METHOD_NOT_FOUND",
932
+ TransmissionFailed: "TRANSMISSION_FAILED"
1085
933
  };
934
+ var ErrorCodeObj_default = ErrorCodeObj;
1086
935
 
1087
- var indexForBundle = {
1088
- connect,
1089
- WindowMessenger,
1090
- WorkerMessenger,
1091
- PortMessenger,
1092
- CallOptions,
1093
- Reply,
1094
- PenpalError,
1095
- debug,
1096
- ErrorCode: ErrorCodeObj,
936
+ // src/debug.ts
937
+ var debug = (prefix) => {
938
+ return (...args) => {
939
+ console.log(`\u270D\uFE0F %c${prefix}%c`, "font-weight: bold;", "", ...args);
940
+ };
1097
941
  };
942
+ var debug_default = debug;
943
+
944
+ exports.CallOptions = CallOptions_default;
945
+ exports.ErrorCode = ErrorCodeObj_default;
946
+ exports.PenpalError = PenpalError_default;
947
+ exports.PortMessenger = PortMessenger_default;
948
+ exports.Reply = Reply_default;
949
+ exports.WindowMessenger = WindowMessenger_default;
950
+ exports.WorkerMessenger = WorkerMessenger_default;
951
+ exports.connect = connect_default;
952
+ exports.debug = debug_default;
1098
953
 
1099
- return indexForBundle;
954
+ return exports;
1100
955
 
1101
- })();
956
+ })({});
957
+ //# sourceMappingURL=penpal.js.map
958
+ //# sourceMappingURL=penpal.js.map