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.
- package/dist/penpal.cjs +953 -0
- package/dist/penpal.cjs.map +1 -0
- package/dist/penpal.d.cts +234 -0
- package/dist/penpal.d.ts +234 -0
- package/dist/penpal.js +890 -1033
- package/dist/penpal.js.map +1 -0
- package/dist/penpal.min.js +1 -1
- package/dist/penpal.min.js.map +1 -1
- package/dist/penpal.mjs +943 -0
- package/dist/penpal.mjs.map +1 -0
- package/package.json +13 -21
- package/cjs/CallOptions.d.ts +0 -10
- package/cjs/CallOptions.js +0 -16
- package/cjs/ErrorCodeObj.d.ts +0 -9
- package/cjs/ErrorCodeObj.js +0 -14
- package/cjs/PenpalBugError.d.ts +0 -8
- package/cjs/PenpalBugError.js +0 -12
- package/cjs/PenpalError.d.ts +0 -6
- package/cjs/PenpalError.js +0 -11
- package/cjs/Reply.d.ts +0 -9
- package/cjs/Reply.js +0 -16
- package/cjs/backwardCompatibility.d.ts +0 -56
- package/cjs/backwardCompatibility.js +0 -134
- package/cjs/connect.d.ts +0 -33
- package/cjs/connect.js +0 -78
- package/cjs/connectCallHandler.d.ts +0 -8
- package/cjs/connectCallHandler.js +0 -90
- package/cjs/connectRemoteProxy.d.ts +0 -12
- package/cjs/connectRemoteProxy.js +0 -139
- package/cjs/debug.d.ts +0 -3
- package/cjs/debug.js +0 -8
- package/cjs/errorSerialization.d.ts +0 -9
- package/cjs/errorSerialization.js +0 -26
- package/cjs/generateId.d.ts +0 -8
- package/cjs/generateId.js +0 -11
- package/cjs/getPromiseWithResolvers.d.ts +0 -6
- package/cjs/getPromiseWithResolvers.js +0 -19
- package/cjs/guards.d.ts +0 -10
- package/cjs/guards.js +0 -40
- package/cjs/index.d.ts +0 -12
- package/cjs/index.js +0 -21
- package/cjs/indexForBundle.d.ts +0 -31
- package/cjs/indexForBundle.js +0 -22
- package/cjs/messengers/Messenger.d.ts +0 -14
- package/cjs/messengers/Messenger.js +0 -2
- package/cjs/messengers/PortMessenger.d.ts +0 -21
- package/cjs/messengers/PortMessenger.js +0 -47
- package/cjs/messengers/WindowMessenger.d.ts +0 -29
- package/cjs/messengers/WindowMessenger.js +0 -178
- package/cjs/messengers/WorkerMessenger.d.ts +0 -23
- package/cjs/messengers/WorkerMessenger.js +0 -86
- package/cjs/methodSerialization.d.ts +0 -22
- package/cjs/methodSerialization.js +0 -48
- package/cjs/namespace.d.ts +0 -2
- package/cjs/namespace.js +0 -3
- package/cjs/once.d.ts +0 -2
- package/cjs/once.js +0 -15
- package/cjs/shakeHands.d.ts +0 -76
- package/cjs/shakeHands.js +0 -190
- package/cjs/types.d.ts +0 -89
- package/cjs/types.js +0 -2
- package/lib/CallOptions.d.ts +0 -10
- package/lib/CallOptions.js +0 -14
- package/lib/ErrorCodeObj.d.ts +0 -9
- package/lib/ErrorCodeObj.js +0 -12
- package/lib/PenpalBugError.d.ts +0 -8
- package/lib/PenpalBugError.js +0 -10
- package/lib/PenpalError.d.ts +0 -6
- package/lib/PenpalError.js +0 -9
- package/lib/Reply.d.ts +0 -9
- package/lib/Reply.js +0 -14
- package/lib/backwardCompatibility.d.ts +0 -56
- package/lib/backwardCompatibility.js +0 -128
- package/lib/connect.d.ts +0 -33
- package/lib/connect.js +0 -76
- package/lib/connectCallHandler.d.ts +0 -8
- package/lib/connectCallHandler.js +0 -88
- package/lib/connectRemoteProxy.d.ts +0 -12
- package/lib/connectRemoteProxy.js +0 -137
- package/lib/debug.d.ts +0 -3
- package/lib/debug.js +0 -6
- package/lib/errorSerialization.d.ts +0 -9
- package/lib/errorSerialization.js +0 -21
- package/lib/generateId.d.ts +0 -8
- package/lib/generateId.js +0 -9
- package/lib/getPromiseWithResolvers.d.ts +0 -6
- package/lib/getPromiseWithResolvers.js +0 -17
- package/lib/guards.d.ts +0 -10
- package/lib/guards.js +0 -28
- package/lib/index.d.ts +0 -12
- package/lib/index.js +0 -9
- package/lib/indexForBundle.d.ts +0 -31
- package/lib/indexForBundle.js +0 -20
- package/lib/messengers/Messenger.d.ts +0 -14
- package/lib/messengers/Messenger.js +0 -1
- package/lib/messengers/PortMessenger.d.ts +0 -21
- package/lib/messengers/PortMessenger.js +0 -45
- package/lib/messengers/WindowMessenger.d.ts +0 -29
- package/lib/messengers/WindowMessenger.js +0 -176
- package/lib/messengers/WorkerMessenger.d.ts +0 -23
- package/lib/messengers/WorkerMessenger.js +0 -84
- package/lib/methodSerialization.d.ts +0 -22
- package/lib/methodSerialization.js +0 -42
- package/lib/namespace.d.ts +0 -2
- package/lib/namespace.js +0 -1
- package/lib/once.d.ts +0 -2
- package/lib/once.js +0 -13
- package/lib/shakeHands.d.ts +0 -76
- package/lib/shakeHands.js +0 -188
- package/lib/types.d.ts +0 -89
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
+
// src/namespace.ts
|
|
51
|
+
var namespace_default = "penpal";
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// src/guards.ts
|
|
54
|
+
var isObject = (value) => {
|
|
55
|
+
return typeof value === "object" && value !== null;
|
|
52
56
|
};
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
var isFunction = (value) => {
|
|
58
|
+
return typeof value === "function";
|
|
55
59
|
};
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
var isMessage = (data) => {
|
|
61
|
+
return isObject(data) && data.namespace === namespace_default;
|
|
58
62
|
};
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
var isSynMessage = (message) => {
|
|
64
|
+
return message.type === "SYN";
|
|
61
65
|
};
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
var isAck1Message = (message) => {
|
|
67
|
+
return message.type === "ACK1";
|
|
64
68
|
};
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
var isAck2Message = (message) => {
|
|
70
|
+
return message.type === "ACK2";
|
|
67
71
|
};
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
var isCallMessage = (message) => {
|
|
73
|
+
return message.type === "CALL";
|
|
70
74
|
};
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
var isReplyMessage = (message) => {
|
|
76
|
+
return message.type === "REPLY";
|
|
73
77
|
};
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
var isDestroyMessage = (message) => {
|
|
79
|
+
return message.type === "DESTROY";
|
|
76
80
|
};
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
94
|
+
}
|
|
95
|
+
return methodPaths;
|
|
109
96
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
106
|
+
var formatMethodPath = (methodPath) => {
|
|
107
|
+
return methodPath.join(".");
|
|
118
108
|
};
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
//
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
710
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
//
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
|
954
|
+
return exports;
|
|
1100
955
|
|
|
1101
|
-
})();
|
|
956
|
+
})({});
|
|
957
|
+
//# sourceMappingURL=penpal.js.map
|
|
958
|
+
//# sourceMappingURL=penpal.js.map
|