@prismicio/simulator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +98 -0
- package/dist/SimulatorAPI.cjs +50 -0
- package/dist/SimulatorAPI.cjs.map +1 -0
- package/dist/SimulatorAPI.d.ts +12 -0
- package/dist/SimulatorAPI.js +50 -0
- package/dist/SimulatorAPI.js.map +1 -0
- package/dist/SimulatorClient.cjs +54 -0
- package/dist/SimulatorClient.cjs.map +1 -0
- package/dist/SimulatorClient.d.ts +10 -0
- package/dist/SimulatorClient.js +54 -0
- package/dist/SimulatorClient.js.map +1 -0
- package/dist/channel/ChannelEmitter.cjs +146 -0
- package/dist/channel/ChannelEmitter.cjs.map +1 -0
- package/dist/channel/ChannelEmitter.d.ts +38 -0
- package/dist/channel/ChannelEmitter.js +146 -0
- package/dist/channel/ChannelEmitter.js.map +1 -0
- package/dist/channel/ChannelNetwork.cjs +112 -0
- package/dist/channel/ChannelNetwork.cjs.map +1 -0
- package/dist/channel/ChannelNetwork.d.ts +24 -0
- package/dist/channel/ChannelNetwork.js +112 -0
- package/dist/channel/ChannelNetwork.js.map +1 -0
- package/dist/channel/ChannelReceiver.cjs +95 -0
- package/dist/channel/ChannelReceiver.cjs.map +1 -0
- package/dist/channel/ChannelReceiver.d.ts +20 -0
- package/dist/channel/ChannelReceiver.js +95 -0
- package/dist/channel/ChannelReceiver.js.map +1 -0
- package/dist/channel/errors.cjs +54 -0
- package/dist/channel/errors.cjs.map +1 -0
- package/dist/channel/errors.d.ts +25 -0
- package/dist/channel/errors.js +54 -0
- package/dist/channel/errors.js.map +1 -0
- package/dist/channel/index.d.ts +10 -0
- package/dist/channel/messages.cjs +66 -0
- package/dist/channel/messages.cjs.map +1 -0
- package/dist/channel/messages.d.ts +10 -0
- package/dist/channel/messages.js +66 -0
- package/dist/channel/messages.js.map +1 -0
- package/dist/channel/types.cjs +11 -0
- package/dist/channel/types.cjs.map +1 -0
- package/dist/channel/types.d.ts +61 -0
- package/dist/channel/types.js +13 -0
- package/dist/channel/types.js.map +1 -0
- package/dist/index.cjs +33 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/SimulatorManager.cjs +140 -0
- package/dist/kit/SimulatorManager.cjs.map +1 -0
- package/dist/kit/SimulatorManager.d.ts +15 -0
- package/dist/kit/SimulatorManager.js +140 -0
- package/dist/kit/SimulatorManager.js.map +1 -0
- package/dist/kit/State.cjs +82 -0
- package/dist/kit/State.cjs.map +1 -0
- package/dist/kit/State.d.ts +25 -0
- package/dist/kit/State.js +82 -0
- package/dist/kit/State.js.map +1 -0
- package/dist/kit/domHelpers.cjs +53 -0
- package/dist/kit/domHelpers.cjs.map +1 -0
- package/dist/kit/domHelpers.d.ts +9 -0
- package/dist/kit/domHelpers.js +53 -0
- package/dist/kit/domHelpers.js.map +1 -0
- package/dist/kit/eventHandlers.cjs +15 -0
- package/dist/kit/eventHandlers.cjs.map +1 -0
- package/dist/kit/eventHandlers.d.ts +4 -0
- package/dist/kit/eventHandlers.js +15 -0
- package/dist/kit/eventHandlers.js.map +1 -0
- package/dist/kit/getDefault.cjs +16 -0
- package/dist/kit/getDefault.cjs.map +1 -0
- package/dist/kit/getDefault.d.ts +8 -0
- package/dist/kit/getDefault.js +16 -0
- package/dist/kit/getDefault.js.map +1 -0
- package/dist/kit/index.cjs +26 -0
- package/dist/kit/index.cjs.map +1 -0
- package/dist/kit/index.d.ts +8 -0
- package/dist/kit/index.js +23 -0
- package/dist/kit/index.js.map +1 -0
- package/dist/kit/messages.cjs +29 -0
- package/dist/kit/messages.cjs.map +1 -0
- package/dist/kit/messages.d.ts +1 -0
- package/dist/kit/messages.js +29 -0
- package/dist/kit/messages.js.map +1 -0
- package/dist/kit/types.cjs +9 -0
- package/dist/kit/types.cjs.map +1 -0
- package/dist/kit/types.d.ts +23 -0
- package/dist/kit/types.js +10 -0
- package/dist/kit/types.js.map +1 -0
- package/dist/lib/EventEmitter.cjs +27 -0
- package/dist/lib/EventEmitter.cjs.map +1 -0
- package/dist/lib/EventEmitter.d.ts +10 -0
- package/dist/lib/EventEmitter.js +27 -0
- package/dist/lib/EventEmitter.js.map +1 -0
- package/dist/lib/__PRODUCTION__.d.ts +7 -0
- package/dist/lib/throttle.cjs +24 -0
- package/dist/lib/throttle.cjs.map +1 -0
- package/dist/lib/throttle.d.ts +1 -0
- package/dist/lib/throttle.js +24 -0
- package/dist/lib/throttle.js.map +1 -0
- package/dist/types.cjs +14 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +99 -0
- package/src/SimulatorAPI.ts +90 -0
- package/src/SimulatorClient.ts +96 -0
- package/src/channel/ChannelEmitter.ts +278 -0
- package/src/channel/ChannelNetwork.ts +216 -0
- package/src/channel/ChannelReceiver.ts +184 -0
- package/src/channel/errors.ts +53 -0
- package/src/channel/index.ts +75 -0
- package/src/channel/messages.ts +115 -0
- package/src/channel/types.ts +138 -0
- package/src/index.ts +42 -0
- package/src/kit/SimulatorManager.ts +199 -0
- package/src/kit/State.ts +113 -0
- package/src/kit/domHelpers.ts +75 -0
- package/src/kit/eventHandlers.ts +16 -0
- package/src/kit/getDefault.ts +18 -0
- package/src/kit/index.ts +33 -0
- package/src/kit/messages.ts +25 -0
- package/src/kit/types.ts +30 -0
- package/src/lib/EventEmitter.ts +41 -0
- package/src/lib/__PRODUCTION__.ts +12 -0
- package/src/lib/throttle.ts +26 -0
- package/src/types.ts +57 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExtractSuccessResponseMessage,
|
|
3
|
+
InternalEmitterRequestType,
|
|
4
|
+
InternalEmitterTransactions,
|
|
5
|
+
InternalReceiverRequestType,
|
|
6
|
+
SuccessResponseMessage,
|
|
7
|
+
TransactionsHandlers,
|
|
8
|
+
UnknownRequestMessage,
|
|
9
|
+
UnknownResponseMessage,
|
|
10
|
+
UnknownTransaction,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ChannelNotSetError,
|
|
15
|
+
ConnectionTimeoutError,
|
|
16
|
+
NotReadyError,
|
|
17
|
+
} from "./errors";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
ChannelNetwork,
|
|
21
|
+
ChannelNetworkOptions,
|
|
22
|
+
PostRequestOptions,
|
|
23
|
+
} from "./ChannelNetwork";
|
|
24
|
+
import { AllChannelReceiverOptions } from "./ChannelReceiver";
|
|
25
|
+
import {
|
|
26
|
+
createErrorResponseMessage,
|
|
27
|
+
createSuccessResponseMessage,
|
|
28
|
+
isRequestMessage,
|
|
29
|
+
validateMessage,
|
|
30
|
+
} from "./messages";
|
|
31
|
+
|
|
32
|
+
export type ChannelEmitterOptions = {
|
|
33
|
+
connectTimeout: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const channelEmitterDefaultOptions: ChannelEmitterOptions &
|
|
37
|
+
Partial<ChannelNetworkOptions> = {
|
|
38
|
+
connectTimeout: 20000,
|
|
39
|
+
requestIDPrefix: "emitter-",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AllChannelEmitterOptions = ChannelEmitterOptions &
|
|
43
|
+
ChannelNetworkOptions;
|
|
44
|
+
|
|
45
|
+
export abstract class ChannelEmitter<
|
|
46
|
+
TReceiverTransactions extends Record<string, UnknownTransaction> = Record<
|
|
47
|
+
string,
|
|
48
|
+
never
|
|
49
|
+
>,
|
|
50
|
+
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
51
|
+
TReceiverOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
52
|
+
> extends ChannelNetwork<
|
|
53
|
+
TReceiverTransactions,
|
|
54
|
+
ChannelEmitterOptions & TOptions
|
|
55
|
+
> {
|
|
56
|
+
private _target: HTMLIFrameElement;
|
|
57
|
+
private _channel: MessageChannel | null = null;
|
|
58
|
+
protected get channel(): MessageChannel {
|
|
59
|
+
if (!this._channel) {
|
|
60
|
+
throw new ChannelNotSetError();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this._channel;
|
|
64
|
+
}
|
|
65
|
+
protected set channel(channel: MessageChannel | null) {
|
|
66
|
+
this._channel = channel;
|
|
67
|
+
|
|
68
|
+
// Update port automatically
|
|
69
|
+
if (this._channel) {
|
|
70
|
+
this.port = this._channel.port1;
|
|
71
|
+
} else {
|
|
72
|
+
this.port = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
private _receiverReady = "";
|
|
76
|
+
private _receiverReadyCallback: (() => Promise<void>) | null = null;
|
|
77
|
+
private _connected = false;
|
|
78
|
+
public get connected(): boolean {
|
|
79
|
+
return this._connected;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
target: HTMLIFrameElement,
|
|
84
|
+
requestHandlers: TransactionsHandlers<TReceiverTransactions>,
|
|
85
|
+
options: Partial<AllChannelEmitterOptions> & TOptions,
|
|
86
|
+
) {
|
|
87
|
+
super(requestHandlers, { ...channelEmitterDefaultOptions, ...options });
|
|
88
|
+
|
|
89
|
+
this._target = target;
|
|
90
|
+
|
|
91
|
+
window.addEventListener("message", (event) => {
|
|
92
|
+
this._onPublicMessage(event);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initiates connection to receiver
|
|
98
|
+
*
|
|
99
|
+
* @param receiverOptions - Options to configure the receiver with
|
|
100
|
+
* @param newOrigin - Indicates to the emitter that we're connecting to a new
|
|
101
|
+
* origin
|
|
102
|
+
*
|
|
103
|
+
* @returns Success connect message
|
|
104
|
+
*/
|
|
105
|
+
connect(
|
|
106
|
+
receiverOptions: InternalEmitterTransactions<
|
|
107
|
+
AllChannelReceiverOptions & TReceiverOptions
|
|
108
|
+
>["connect"]["request"]["data"] = {},
|
|
109
|
+
newOrigin = false,
|
|
110
|
+
): Promise<SuccessResponseMessage> {
|
|
111
|
+
// Disconnect first
|
|
112
|
+
this.disconnect();
|
|
113
|
+
// If changing origin we'll need to wait for receiver to be ready again
|
|
114
|
+
if (newOrigin) {
|
|
115
|
+
this._receiverReady = "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handshake promise
|
|
119
|
+
return new Promise<SuccessResponseMessage>((resolve, reject) => {
|
|
120
|
+
// Wait for target to be loaded
|
|
121
|
+
this._target.addEventListener(
|
|
122
|
+
"load",
|
|
123
|
+
() => {
|
|
124
|
+
// Throw if target doesn't allow access to content window
|
|
125
|
+
if (!this._target.contentWindow) {
|
|
126
|
+
return reject(new Error("Target window is not available"));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const receiverReadyTimeout = setTimeout(() => {
|
|
130
|
+
reject(new ConnectionTimeoutError());
|
|
131
|
+
}, this.options.connectTimeout);
|
|
132
|
+
|
|
133
|
+
// Connect to target once ready
|
|
134
|
+
const receiverReadyCallback = async (): Promise<void> => {
|
|
135
|
+
// Clear receiver ready timeout
|
|
136
|
+
clearTimeout(receiverReadyTimeout);
|
|
137
|
+
|
|
138
|
+
// Create new message channel (set up port automatically)
|
|
139
|
+
// This is done here to prevent transferable objects neutering
|
|
140
|
+
// when calling `connect()` multiple times
|
|
141
|
+
this.channel = new MessageChannel();
|
|
142
|
+
|
|
143
|
+
// Conclude handshake by sending message channel port to target
|
|
144
|
+
const request = this.createRequestMessage(
|
|
145
|
+
InternalEmitterRequestType.Connect,
|
|
146
|
+
receiverOptions,
|
|
147
|
+
);
|
|
148
|
+
const response = await this.postRequest<
|
|
149
|
+
InternalEmitterTransactions<
|
|
150
|
+
AllChannelReceiverOptions & TReceiverOptions
|
|
151
|
+
>["connect"]["request"],
|
|
152
|
+
InternalEmitterTransactions<
|
|
153
|
+
AllChannelReceiverOptions & TReceiverOptions
|
|
154
|
+
>["connect"]["response"]
|
|
155
|
+
>(request, (request) => {
|
|
156
|
+
// Target content window is checked in previous statement
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
158
|
+
this._target.contentWindow!.postMessage(request, "*", [
|
|
159
|
+
this.channel.port2,
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Finish by aknowledging ready
|
|
164
|
+
this.postResponse(
|
|
165
|
+
createSuccessResponseMessage(this._receiverReady, undefined),
|
|
166
|
+
(response) => {
|
|
167
|
+
// Target content window is checked in previous statement
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
169
|
+
this._target.contentWindow!.postMessage(response, "*");
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// If post request succeed, we're connected
|
|
174
|
+
this._connected = true;
|
|
175
|
+
|
|
176
|
+
resolve(response);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (this._receiverReady) {
|
|
180
|
+
// If receiver is already ready, send port immediately
|
|
181
|
+
receiverReadyCallback();
|
|
182
|
+
} else {
|
|
183
|
+
// Else wait for receiver to be ready
|
|
184
|
+
this._receiverReadyCallback = receiverReadyCallback;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{ once: true },
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Destroys current connection to receiver if any
|
|
194
|
+
*/
|
|
195
|
+
disconnect(): void {
|
|
196
|
+
this._connected = false;
|
|
197
|
+
this.channel = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handles public messages
|
|
202
|
+
*/
|
|
203
|
+
private async _onPublicMessage(event: MessageEvent<unknown>): Promise<void> {
|
|
204
|
+
// Return is event is not from target
|
|
205
|
+
if (event.source !== this._target.contentWindow) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const message = validateMessage(event.data);
|
|
211
|
+
|
|
212
|
+
if (isRequestMessage(message)) {
|
|
213
|
+
if (this.options.debug) {
|
|
214
|
+
// eslint-disable-next-line no-console
|
|
215
|
+
console.debug(event.data);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
switch (message.type) {
|
|
219
|
+
case InternalReceiverRequestType.Ready:
|
|
220
|
+
this._receiverReady = message.requestID;
|
|
221
|
+
|
|
222
|
+
// If emitter is waiting for receiver to be ready
|
|
223
|
+
if (this._receiverReadyCallback) {
|
|
224
|
+
// We don't await the promise directly as we need to clear the callback first
|
|
225
|
+
const receiverReadyCallbackPromise =
|
|
226
|
+
this._receiverReadyCallback();
|
|
227
|
+
|
|
228
|
+
this._receiverReadyCallback = null;
|
|
229
|
+
|
|
230
|
+
await receiverReadyCallbackPromise;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
this.postResponse(
|
|
236
|
+
createErrorResponseMessage(message.requestID, undefined),
|
|
237
|
+
(response) => {
|
|
238
|
+
(event.source as WindowProxy).postMessage(
|
|
239
|
+
response,
|
|
240
|
+
event.origin,
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// No response messages are expected on public channel
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof TypeError) {
|
|
251
|
+
// Ignore unknown messages
|
|
252
|
+
} else {
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
protected postFormattedRequest<
|
|
259
|
+
TRequest extends UnknownRequestMessage,
|
|
260
|
+
TResponse extends UnknownResponseMessage,
|
|
261
|
+
>(
|
|
262
|
+
type: TRequest["type"],
|
|
263
|
+
data?: TRequest["data"],
|
|
264
|
+
options: PostRequestOptions = {},
|
|
265
|
+
): Promise<ExtractSuccessResponseMessage<TResponse>> {
|
|
266
|
+
if (!this._connected) {
|
|
267
|
+
throw new NotReadyError(
|
|
268
|
+
"Emitter is not connected, use `ChannelEmitter.connect()` first",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return this.postRequest(
|
|
273
|
+
this.createRequestMessage(type, data),
|
|
274
|
+
undefined,
|
|
275
|
+
options,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExtractErrorResponseMessage,
|
|
3
|
+
ExtractSuccessResponseMessage,
|
|
4
|
+
RequestMessage,
|
|
5
|
+
TransactionsHandlers,
|
|
6
|
+
UnknownRequestMessage,
|
|
7
|
+
UnknownResponseMessage,
|
|
8
|
+
UnknownTransaction,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
PortNotSetError,
|
|
13
|
+
RequestTimeoutError,
|
|
14
|
+
ResponseError,
|
|
15
|
+
TooManyConcurrentRequestsError,
|
|
16
|
+
} from "./errors";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
createErrorResponseMessage,
|
|
20
|
+
createRequestMessage,
|
|
21
|
+
createSuccessResponseMessage,
|
|
22
|
+
isRequestMessage,
|
|
23
|
+
isSuccessResponseMessage,
|
|
24
|
+
validateMessage,
|
|
25
|
+
} from "./messages";
|
|
26
|
+
|
|
27
|
+
export type PostRequestOptions = {
|
|
28
|
+
timeout?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ChannelNetworkOptions = {
|
|
32
|
+
debug: boolean;
|
|
33
|
+
maximumRequestConcurrency: number;
|
|
34
|
+
defaultTimeout: number;
|
|
35
|
+
requestIDPrefix: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const channelNetworkDefaultOptions: ChannelNetworkOptions = {
|
|
39
|
+
debug: false,
|
|
40
|
+
maximumRequestConcurrency: 20,
|
|
41
|
+
defaultTimeout: 5000,
|
|
42
|
+
requestIDPrefix: "channel-",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export abstract class ChannelNetwork<
|
|
46
|
+
TPartnerTransactions extends Record<string, UnknownTransaction> = Record<
|
|
47
|
+
string,
|
|
48
|
+
never
|
|
49
|
+
>,
|
|
50
|
+
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
51
|
+
> {
|
|
52
|
+
public requestHandlers: TransactionsHandlers<TPartnerTransactions>;
|
|
53
|
+
public options: ChannelNetworkOptions & TOptions;
|
|
54
|
+
|
|
55
|
+
private _port: MessagePort | null = null;
|
|
56
|
+
protected get port(): MessagePort {
|
|
57
|
+
if (!this._port) {
|
|
58
|
+
throw new PortNotSetError();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this._port;
|
|
62
|
+
}
|
|
63
|
+
protected set port(port: MessagePort | null) {
|
|
64
|
+
// Destroy old port
|
|
65
|
+
if (this._port) {
|
|
66
|
+
this._port.onmessage = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this._port = port;
|
|
70
|
+
if (this._port) {
|
|
71
|
+
this._port.onmessage = this.onMessage.bind(this);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
private _pendingRequests: Map<string, (response: any) => void> = new Map();
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
requestHandlers: TransactionsHandlers<TPartnerTransactions>,
|
|
80
|
+
options: Partial<ChannelNetworkOptions> & TOptions,
|
|
81
|
+
) {
|
|
82
|
+
this.requestHandlers = requestHandlers;
|
|
83
|
+
this.options = { ...channelNetworkDefaultOptions, ...options };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public createRequestMessage<TType extends string = string, TData = undefined>(
|
|
87
|
+
type: TType,
|
|
88
|
+
data: TData,
|
|
89
|
+
): RequestMessage<TType, TData> {
|
|
90
|
+
return createRequestMessage(type, data, this.options.requestIDPrefix);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected async onMessage(event: MessageEvent<unknown>): Promise<void> {
|
|
94
|
+
if (this.options.debug) {
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.debug(event.data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const message = validateMessage(event.data);
|
|
101
|
+
|
|
102
|
+
if (isRequestMessage(message)) {
|
|
103
|
+
if (!this.requestHandlers[message.type]) {
|
|
104
|
+
this.postResponse(
|
|
105
|
+
createErrorResponseMessage(message.requestID, undefined, 501),
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
try {
|
|
109
|
+
// TODO: Figure out why type cannot be inferred on its own anymore
|
|
110
|
+
const response = await this.requestHandlers[message.type](message, {
|
|
111
|
+
success: createSuccessResponseMessage.bind(
|
|
112
|
+
this,
|
|
113
|
+
message.requestID,
|
|
114
|
+
) as Parameters<
|
|
115
|
+
(typeof this.requestHandlers)[string]
|
|
116
|
+
>[1]["success"],
|
|
117
|
+
error: createErrorResponseMessage.bind(
|
|
118
|
+
this,
|
|
119
|
+
message.requestID,
|
|
120
|
+
) as Parameters<
|
|
121
|
+
(typeof this.requestHandlers)[string]
|
|
122
|
+
>[1]["error"],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.postResponse(response);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.postResponse(
|
|
128
|
+
createErrorResponseMessage(message.requestID, error, 500),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
if (!this._pendingRequests.has(message.requestID)) {
|
|
134
|
+
console.error(
|
|
135
|
+
`Unknown request ID received in response: ${JSON.stringify(
|
|
136
|
+
message,
|
|
137
|
+
)}`,
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
// Pending requests are checked in previous statement
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
142
|
+
this._pendingRequests.get(message.requestID)!(message);
|
|
143
|
+
this._pendingRequests.delete(message.requestID);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof TypeError) {
|
|
148
|
+
console.warn(error.message);
|
|
149
|
+
} else {
|
|
150
|
+
// Should not be possible, but who knows :shrug:
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected postRequest<
|
|
157
|
+
TRequest extends UnknownRequestMessage,
|
|
158
|
+
TResponse extends UnknownResponseMessage,
|
|
159
|
+
>(
|
|
160
|
+
request: TRequest,
|
|
161
|
+
postMessage = (request: TRequest): void => this.port.postMessage(request),
|
|
162
|
+
options: PostRequestOptions = {},
|
|
163
|
+
): Promise<ExtractSuccessResponseMessage<TResponse>> {
|
|
164
|
+
if (this.options.debug) {
|
|
165
|
+
// eslint-disable-next-line no-console
|
|
166
|
+
console.debug(request);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (this._pendingRequests.size >= this.options.maximumRequestConcurrency) {
|
|
170
|
+
throw new TooManyConcurrentRequestsError(request);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new Promise<ExtractSuccessResponseMessage<TResponse>>(
|
|
174
|
+
(resolve, reject) => {
|
|
175
|
+
const requestTimeout = setTimeout(() => {
|
|
176
|
+
if (this._pendingRequests.has(request.requestID)) {
|
|
177
|
+
this._pendingRequests.delete(request.requestID);
|
|
178
|
+
}
|
|
179
|
+
reject(new RequestTimeoutError(request));
|
|
180
|
+
}, options.timeout || this.options.defaultTimeout);
|
|
181
|
+
|
|
182
|
+
this._pendingRequests.set(
|
|
183
|
+
request.requestID,
|
|
184
|
+
(response: TResponse): void => {
|
|
185
|
+
clearTimeout(requestTimeout);
|
|
186
|
+
|
|
187
|
+
isSuccessResponseMessage(response)
|
|
188
|
+
? resolve(response as ExtractSuccessResponseMessage<TResponse>)
|
|
189
|
+
: reject(
|
|
190
|
+
new ResponseError(
|
|
191
|
+
response as ExtractErrorResponseMessage<TResponse>,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
postMessage(request);
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
protected postResponse<TResponse extends UnknownResponseMessage>(
|
|
203
|
+
response: TResponse,
|
|
204
|
+
postMessage = (response: TResponse): void =>
|
|
205
|
+
this.port.postMessage(response),
|
|
206
|
+
): TResponse {
|
|
207
|
+
if (this.options.debug) {
|
|
208
|
+
// eslint-disable-next-line no-console
|
|
209
|
+
console.debug(response);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
postMessage(response);
|
|
213
|
+
|
|
214
|
+
return response;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExtractSuccessResponseMessage,
|
|
3
|
+
InternalEmitterRequestType,
|
|
4
|
+
InternalEmitterTransactions,
|
|
5
|
+
InternalReceiverRequestType,
|
|
6
|
+
RequestMessage,
|
|
7
|
+
ResponseMessage,
|
|
8
|
+
SuccessResponseMessage,
|
|
9
|
+
TransactionsHandlers,
|
|
10
|
+
UnknownRequestMessage,
|
|
11
|
+
UnknownResponseMessage,
|
|
12
|
+
UnknownTransaction,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
import { NotReadyError } from "./errors";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
ChannelNetwork,
|
|
19
|
+
ChannelNetworkOptions,
|
|
20
|
+
PostRequestOptions,
|
|
21
|
+
} from "./ChannelNetwork";
|
|
22
|
+
import {
|
|
23
|
+
createErrorResponseMessage,
|
|
24
|
+
createSuccessResponseMessage,
|
|
25
|
+
isRequestMessage,
|
|
26
|
+
validateMessage,
|
|
27
|
+
} from "./messages";
|
|
28
|
+
|
|
29
|
+
export type ChannelReceiverOptions = {
|
|
30
|
+
readyTimeout: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const channelReceiverDefaultOptions: ChannelReceiverOptions &
|
|
34
|
+
Partial<ChannelNetworkOptions> = {
|
|
35
|
+
readyTimeout: 20000,
|
|
36
|
+
requestIDPrefix: "receiver-",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AllChannelReceiverOptions = ChannelReceiverOptions &
|
|
40
|
+
ChannelNetworkOptions;
|
|
41
|
+
|
|
42
|
+
export abstract class ChannelReceiver<
|
|
43
|
+
TEmitterTransactions extends Record<string, UnknownTransaction> = Record<
|
|
44
|
+
string,
|
|
45
|
+
never
|
|
46
|
+
>,
|
|
47
|
+
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
48
|
+
> extends ChannelNetwork<
|
|
49
|
+
TEmitterTransactions,
|
|
50
|
+
ChannelReceiverOptions & TOptions
|
|
51
|
+
> {
|
|
52
|
+
private _ready = false;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
requestHandlers: TransactionsHandlers<TEmitterTransactions>,
|
|
56
|
+
options: Partial<AllChannelReceiverOptions> & TOptions,
|
|
57
|
+
) {
|
|
58
|
+
super(requestHandlers, { ...channelReceiverDefaultOptions, ...options });
|
|
59
|
+
|
|
60
|
+
window.addEventListener("message", (event) => {
|
|
61
|
+
this._onPublicMessage(event);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Tells the emitter that receiver is ready
|
|
67
|
+
*/
|
|
68
|
+
public async ready(): Promise<SuccessResponseMessage> {
|
|
69
|
+
if (window.parent === window) {
|
|
70
|
+
throw new Error("Receiver is currently not embedded as an iframe");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._ready = false;
|
|
74
|
+
|
|
75
|
+
const request = this.createRequestMessage(
|
|
76
|
+
InternalReceiverRequestType.Ready,
|
|
77
|
+
undefined,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const response = await this.postRequest<
|
|
81
|
+
RequestMessage<InternalReceiverRequestType.Ready>,
|
|
82
|
+
ResponseMessage
|
|
83
|
+
>(
|
|
84
|
+
request,
|
|
85
|
+
(request) => {
|
|
86
|
+
window.parent.postMessage(request, "*");
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
timeout: this.options.readyTimeout,
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
this._ready = true;
|
|
94
|
+
|
|
95
|
+
return response;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handles public messages
|
|
100
|
+
*/
|
|
101
|
+
private _onPublicMessage(event: MessageEvent<unknown>): void {
|
|
102
|
+
try {
|
|
103
|
+
const message = validateMessage(event.data);
|
|
104
|
+
|
|
105
|
+
if (isRequestMessage(message)) {
|
|
106
|
+
if (this.options.debug) {
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.debug(event.data);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
switch (message.type) {
|
|
112
|
+
case InternalEmitterRequestType.Connect:
|
|
113
|
+
// Set port
|
|
114
|
+
this.port = event.ports[0];
|
|
115
|
+
|
|
116
|
+
// Update options
|
|
117
|
+
const { data } =
|
|
118
|
+
message as InternalEmitterTransactions["connect"]["request"];
|
|
119
|
+
this.options = {
|
|
120
|
+
...this.options,
|
|
121
|
+
...data,
|
|
122
|
+
// Ensure core options remain the same
|
|
123
|
+
debug: this.options.debug,
|
|
124
|
+
requestIDPrefix: this.options.requestIDPrefix,
|
|
125
|
+
readyTimeout: this.options.readyTimeout,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const response = createSuccessResponseMessage(
|
|
129
|
+
message.requestID,
|
|
130
|
+
undefined,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
this.postResponse(response);
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
default:
|
|
137
|
+
this.postResponse(
|
|
138
|
+
createErrorResponseMessage(message.requestID, undefined),
|
|
139
|
+
(response) => {
|
|
140
|
+
(event.source as WindowProxy).postMessage(
|
|
141
|
+
response,
|
|
142
|
+
event.origin,
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// Forward response messages to default message handler if necessary
|
|
150
|
+
if (!this._ready) {
|
|
151
|
+
this.onMessage(event);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof TypeError) {
|
|
156
|
+
// Ignore unknown messages
|
|
157
|
+
} else {
|
|
158
|
+
// Should not be possible, but who knows :shrug:
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected postFormattedRequest<
|
|
165
|
+
TRequest extends UnknownRequestMessage,
|
|
166
|
+
TResponse extends UnknownResponseMessage,
|
|
167
|
+
>(
|
|
168
|
+
type: TRequest["type"],
|
|
169
|
+
data?: TRequest["data"],
|
|
170
|
+
options: PostRequestOptions = {},
|
|
171
|
+
): Promise<ExtractSuccessResponseMessage<TResponse>> {
|
|
172
|
+
if (!this._ready) {
|
|
173
|
+
throw new NotReadyError(
|
|
174
|
+
"Receiver is not ready, use `ChannelReceiver.ready()` first",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return this.postRequest(
|
|
179
|
+
this.createRequestMessage(type, data),
|
|
180
|
+
undefined,
|
|
181
|
+
options,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|