@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.
Files changed (127) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -0
  3. package/dist/SimulatorAPI.cjs +50 -0
  4. package/dist/SimulatorAPI.cjs.map +1 -0
  5. package/dist/SimulatorAPI.d.ts +12 -0
  6. package/dist/SimulatorAPI.js +50 -0
  7. package/dist/SimulatorAPI.js.map +1 -0
  8. package/dist/SimulatorClient.cjs +54 -0
  9. package/dist/SimulatorClient.cjs.map +1 -0
  10. package/dist/SimulatorClient.d.ts +10 -0
  11. package/dist/SimulatorClient.js +54 -0
  12. package/dist/SimulatorClient.js.map +1 -0
  13. package/dist/channel/ChannelEmitter.cjs +146 -0
  14. package/dist/channel/ChannelEmitter.cjs.map +1 -0
  15. package/dist/channel/ChannelEmitter.d.ts +38 -0
  16. package/dist/channel/ChannelEmitter.js +146 -0
  17. package/dist/channel/ChannelEmitter.js.map +1 -0
  18. package/dist/channel/ChannelNetwork.cjs +112 -0
  19. package/dist/channel/ChannelNetwork.cjs.map +1 -0
  20. package/dist/channel/ChannelNetwork.d.ts +24 -0
  21. package/dist/channel/ChannelNetwork.js +112 -0
  22. package/dist/channel/ChannelNetwork.js.map +1 -0
  23. package/dist/channel/ChannelReceiver.cjs +95 -0
  24. package/dist/channel/ChannelReceiver.cjs.map +1 -0
  25. package/dist/channel/ChannelReceiver.d.ts +20 -0
  26. package/dist/channel/ChannelReceiver.js +95 -0
  27. package/dist/channel/ChannelReceiver.js.map +1 -0
  28. package/dist/channel/errors.cjs +54 -0
  29. package/dist/channel/errors.cjs.map +1 -0
  30. package/dist/channel/errors.d.ts +25 -0
  31. package/dist/channel/errors.js +54 -0
  32. package/dist/channel/errors.js.map +1 -0
  33. package/dist/channel/index.d.ts +10 -0
  34. package/dist/channel/messages.cjs +66 -0
  35. package/dist/channel/messages.cjs.map +1 -0
  36. package/dist/channel/messages.d.ts +10 -0
  37. package/dist/channel/messages.js +66 -0
  38. package/dist/channel/messages.js.map +1 -0
  39. package/dist/channel/types.cjs +11 -0
  40. package/dist/channel/types.cjs.map +1 -0
  41. package/dist/channel/types.d.ts +61 -0
  42. package/dist/channel/types.js +13 -0
  43. package/dist/channel/types.js.map +1 -0
  44. package/dist/index.cjs +33 -0
  45. package/dist/index.cjs.map +1 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +27 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/kit/SimulatorManager.cjs +140 -0
  50. package/dist/kit/SimulatorManager.cjs.map +1 -0
  51. package/dist/kit/SimulatorManager.d.ts +15 -0
  52. package/dist/kit/SimulatorManager.js +140 -0
  53. package/dist/kit/SimulatorManager.js.map +1 -0
  54. package/dist/kit/State.cjs +82 -0
  55. package/dist/kit/State.cjs.map +1 -0
  56. package/dist/kit/State.d.ts +25 -0
  57. package/dist/kit/State.js +82 -0
  58. package/dist/kit/State.js.map +1 -0
  59. package/dist/kit/domHelpers.cjs +53 -0
  60. package/dist/kit/domHelpers.cjs.map +1 -0
  61. package/dist/kit/domHelpers.d.ts +9 -0
  62. package/dist/kit/domHelpers.js +53 -0
  63. package/dist/kit/domHelpers.js.map +1 -0
  64. package/dist/kit/eventHandlers.cjs +15 -0
  65. package/dist/kit/eventHandlers.cjs.map +1 -0
  66. package/dist/kit/eventHandlers.d.ts +4 -0
  67. package/dist/kit/eventHandlers.js +15 -0
  68. package/dist/kit/eventHandlers.js.map +1 -0
  69. package/dist/kit/getDefault.cjs +16 -0
  70. package/dist/kit/getDefault.cjs.map +1 -0
  71. package/dist/kit/getDefault.d.ts +8 -0
  72. package/dist/kit/getDefault.js +16 -0
  73. package/dist/kit/getDefault.js.map +1 -0
  74. package/dist/kit/index.cjs +26 -0
  75. package/dist/kit/index.cjs.map +1 -0
  76. package/dist/kit/index.d.ts +8 -0
  77. package/dist/kit/index.js +23 -0
  78. package/dist/kit/index.js.map +1 -0
  79. package/dist/kit/messages.cjs +29 -0
  80. package/dist/kit/messages.cjs.map +1 -0
  81. package/dist/kit/messages.d.ts +1 -0
  82. package/dist/kit/messages.js +29 -0
  83. package/dist/kit/messages.js.map +1 -0
  84. package/dist/kit/types.cjs +9 -0
  85. package/dist/kit/types.cjs.map +1 -0
  86. package/dist/kit/types.d.ts +23 -0
  87. package/dist/kit/types.js +10 -0
  88. package/dist/kit/types.js.map +1 -0
  89. package/dist/lib/EventEmitter.cjs +27 -0
  90. package/dist/lib/EventEmitter.cjs.map +1 -0
  91. package/dist/lib/EventEmitter.d.ts +10 -0
  92. package/dist/lib/EventEmitter.js +27 -0
  93. package/dist/lib/EventEmitter.js.map +1 -0
  94. package/dist/lib/__PRODUCTION__.d.ts +7 -0
  95. package/dist/lib/throttle.cjs +24 -0
  96. package/dist/lib/throttle.cjs.map +1 -0
  97. package/dist/lib/throttle.d.ts +1 -0
  98. package/dist/lib/throttle.js +24 -0
  99. package/dist/lib/throttle.js.map +1 -0
  100. package/dist/types.cjs +14 -0
  101. package/dist/types.cjs.map +1 -0
  102. package/dist/types.d.ts +34 -0
  103. package/dist/types.js +16 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +99 -0
  106. package/src/SimulatorAPI.ts +90 -0
  107. package/src/SimulatorClient.ts +96 -0
  108. package/src/channel/ChannelEmitter.ts +278 -0
  109. package/src/channel/ChannelNetwork.ts +216 -0
  110. package/src/channel/ChannelReceiver.ts +184 -0
  111. package/src/channel/errors.ts +53 -0
  112. package/src/channel/index.ts +75 -0
  113. package/src/channel/messages.ts +115 -0
  114. package/src/channel/types.ts +138 -0
  115. package/src/index.ts +42 -0
  116. package/src/kit/SimulatorManager.ts +199 -0
  117. package/src/kit/State.ts +113 -0
  118. package/src/kit/domHelpers.ts +75 -0
  119. package/src/kit/eventHandlers.ts +16 -0
  120. package/src/kit/getDefault.ts +18 -0
  121. package/src/kit/index.ts +33 -0
  122. package/src/kit/messages.ts +25 -0
  123. package/src/kit/types.ts +30 -0
  124. package/src/lib/EventEmitter.ts +41 -0
  125. package/src/lib/__PRODUCTION__.ts +12 -0
  126. package/src/lib/throttle.ts +26 -0
  127. 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
+ }