@zimic/interceptor 1.2.3-canary.7 → 1.2.4-canary.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/dist/server.js CHANGED
@@ -1,29 +1,29 @@
1
1
  'use strict';
2
2
 
3
- var chunkZU6IGW27_js = require('./chunk-ZU6IGW27.js');
3
+ var chunkIPL73BDI_js = require('./chunk-IPL73BDI.js');
4
4
  require('./chunk-DGUM43GV.js');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "DEFAULT_ACCESS_CONTROL_HEADERS", {
9
9
  enumerable: true,
10
- get: function () { return chunkZU6IGW27_js.DEFAULT_ACCESS_CONTROL_HEADERS; }
10
+ get: function () { return chunkIPL73BDI_js.DEFAULT_ACCESS_CONTROL_HEADERS; }
11
11
  });
12
12
  Object.defineProperty(exports, "DEFAULT_PREFLIGHT_STATUS_CODE", {
13
13
  enumerable: true,
14
- get: function () { return chunkZU6IGW27_js.DEFAULT_PREFLIGHT_STATUS_CODE; }
14
+ get: function () { return chunkIPL73BDI_js.DEFAULT_PREFLIGHT_STATUS_CODE; }
15
15
  });
16
16
  Object.defineProperty(exports, "NotRunningInterceptorServerError", {
17
17
  enumerable: true,
18
- get: function () { return chunkZU6IGW27_js.NotRunningInterceptorServerError_default; }
18
+ get: function () { return chunkIPL73BDI_js.NotRunningInterceptorServerError_default; }
19
19
  });
20
20
  Object.defineProperty(exports, "RunningInterceptorServerError", {
21
21
  enumerable: true,
22
- get: function () { return chunkZU6IGW27_js.RunningInterceptorServerError_default; }
22
+ get: function () { return chunkIPL73BDI_js.RunningInterceptorServerError_default; }
23
23
  });
24
24
  Object.defineProperty(exports, "createInterceptorServer", {
25
25
  enumerable: true,
26
- get: function () { return chunkZU6IGW27_js.createInterceptorServer; }
26
+ get: function () { return chunkIPL73BDI_js.createInterceptorServer; }
27
27
  });
28
28
  //# sourceMappingURL=server.js.map
29
29
  //# sourceMappingURL=server.js.map
package/dist/server.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default as NotRunningInterceptorServerError, RunningInterceptorServerError_default as RunningInterceptorServerError, createInterceptorServer } from './chunk-XCYZ5L2M.mjs';
1
+ export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default as NotRunningInterceptorServerError, RunningInterceptorServerError_default as RunningInterceptorServerError, createInterceptorServer } from './chunk-OTZ5Z633.mjs';
2
2
  import './chunk-BJTO5JO5.mjs';
3
3
  //# sourceMappingURL=server.mjs.map
4
4
  //# sourceMappingURL=server.mjs.map
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "api",
15
15
  "static"
16
16
  ],
17
- "version": "1.2.3-canary.7",
17
+ "version": "1.2.4-canary.0",
18
18
  "homepage": "https://zimic.dev/docs/interceptor",
19
19
  "repository": {
20
20
  "type": "git",
@@ -376,7 +376,7 @@ class HttpInterceptorClient<
376
376
 
377
377
  clear() {
378
378
  const clearPromises: Promise<AnyHttpRequestHandlerClient | void>[] = [
379
- Promise.resolve(this.workerOrThrow.clearInterceptorHandlers(this)),
379
+ Promise.resolve(this.workerOrThrow.clearHandlers({ interceptor: this })),
380
380
  ];
381
381
 
382
382
  for (const method of HTTP_METHODS) {
@@ -25,10 +25,13 @@ class HttpInterceptorStore {
25
25
  }
26
26
 
27
27
  private getRemoteWorkerKey(baseURL: URL, options: RemoteWorkerKeyOptions) {
28
- if (!options.auth) {
29
- return baseURL.origin;
28
+ const key = [`${baseURL.origin}${baseURL.pathname}`];
29
+
30
+ if (options.auth) {
31
+ key.push(options.auth.token);
30
32
  }
31
- return `${baseURL.origin}:${options.auth.token}`;
33
+
34
+ return key.join(':');
32
35
  }
33
36
 
34
37
  remoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
@@ -24,7 +24,7 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
24
24
  baseURL,
25
25
  createWorker: () => {
26
26
  return this.store.getOrCreateRemoteWorker({
27
- serverURL: new URL(baseURL.origin),
27
+ serverURL: baseURL,
28
28
  auth: this._auth,
29
29
  });
30
30
  },
@@ -183,11 +183,9 @@ abstract class HttpInterceptorWorker {
183
183
  return interceptor.onUnhandledRequest;
184
184
  }
185
185
 
186
- abstract clearHandlers(): PossiblePromise<void>;
187
-
188
- abstract clearInterceptorHandlers<Schema extends HttpSchema>(
189
- interceptor: HttpInterceptorClient<Schema>,
190
- ): PossiblePromise<void>;
186
+ abstract clearHandlers<Schema extends HttpSchema>(options?: {
187
+ interceptor?: HttpInterceptorClient<Schema>;
188
+ }): PossiblePromise<void>;
191
189
 
192
190
  abstract get interceptorsWithHandlers(): AnyHttpInterceptorClient[];
193
191
 
@@ -50,6 +50,8 @@ class LocalHttpInterceptorWorker extends HttpInterceptorWorker {
50
50
  }
51
51
 
52
52
  get internalWorkerOrThrow() {
53
+ /* istanbul ignore if -- @preserve
54
+ * Trying to access the internal worker when it does not exist should not happen. */
53
55
  if (!this.internalWorker) {
54
56
  throw new NotRunningHttpInterceptorError();
55
57
  }
@@ -248,22 +250,24 @@ class LocalHttpInterceptorWorker extends HttpInterceptorWorker {
248
250
  }
249
251
  }
250
252
 
251
- clearHandlers() {
252
- this.internalWorkerOrThrow.resetHandlers();
253
-
254
- for (const handlers of Object.values(this.httpHandlersByMethod)) {
255
- handlers.length = 0;
256
- }
257
- }
258
-
259
- clearInterceptorHandlers<Schema extends HttpSchema>(interceptor: HttpInterceptorClient<Schema>) {
253
+ clearHandlers<Schema extends HttpSchema>(
254
+ options: {
255
+ interceptor?: HttpInterceptorClient<Schema>;
256
+ } = {},
257
+ ) {
260
258
  if (!this.isRunning) {
261
259
  throw new NotRunningHttpInterceptorError();
262
260
  }
263
261
 
264
- for (const methodHandlers of Object.values(this.httpHandlersByMethod)) {
265
- const groupToRemoveIndex = methodHandlers.findIndex((group) => group.interceptor === interceptor);
266
- removeArrayIndex(methodHandlers, groupToRemoveIndex);
262
+ if (options.interceptor === undefined) {
263
+ for (const handlers of Object.values(this.httpHandlersByMethod)) {
264
+ handlers.length = 0;
265
+ }
266
+ } else {
267
+ for (const methodHandlers of Object.values(this.httpHandlersByMethod)) {
268
+ const groupToRemoveIndex = methodHandlers.findIndex((group) => group.interceptor === options.interceptor);
269
+ removeArrayIndex(methodHandlers, groupToRemoveIndex);
270
+ }
267
271
  }
268
272
  }
269
273
 
@@ -55,14 +55,14 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
55
55
 
56
56
  async start() {
57
57
  await super.sharedStart(async () => {
58
+ this.webSocketClient.onChannel('event', 'interceptors/responses/create', this.createResponse);
59
+ this.webSocketClient.onChannel('event', 'interceptors/responses/unhandled', this.handleUnhandledServerRequest);
60
+
58
61
  await this.webSocketClient.start({
59
62
  parameters: this.auth ? { token: this.auth.token } : undefined,
60
63
  waitForAuthentication: true,
61
64
  });
62
65
 
63
- this.webSocketClient.onEvent('interceptors/responses/create', this.createResponse);
64
- this.webSocketClient.onEvent('interceptors/responses/unhandled', this.handleUnhandledServerRequest);
65
-
66
66
  this.platform = this.readPlatform();
67
67
  this.isRunning = true;
68
68
  });
@@ -120,10 +120,10 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
120
120
 
121
121
  async stop() {
122
122
  await super.sharedStop(async () => {
123
- await this.clearHandlers();
123
+ this.webSocketClient.offChannel('event', 'interceptors/responses/create', this.createResponse);
124
+ this.webSocketClient.offChannel('event', 'interceptors/responses/unhandled', this.handleUnhandledServerRequest);
124
125
 
125
- this.webSocketClient.offEvent('interceptors/responses/create', this.createResponse);
126
- this.webSocketClient.offEvent('interceptors/responses/unhandled', this.handleUnhandledServerRequest);
126
+ await this.clearHandlers();
127
127
 
128
128
  await this.webSocketClient.stop();
129
129
 
@@ -167,45 +167,22 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
167
167
  });
168
168
  }
169
169
 
170
- async clearHandlers() {
171
- if (!this.isRunning) {
172
- throw new NotRunningHttpInterceptorError();
173
- }
174
-
175
- this.httpHandlers.clear();
176
-
177
- if (!this.webSocketClient.isRunning) {
178
- return;
179
- }
180
-
181
- try {
182
- await this.webSocketClient.request('interceptors/workers/reset', undefined);
183
- } catch (error) {
184
- /* istanbul ignore next -- @preserve
185
- *
186
- * If the socket is closed before receiving a response, the message is aborted with an error. This can happen if
187
- * we send a request message and the interceptor server closes the socket before sending a response. In this case,
188
- * we can safely ignore the error because we know that the server is shutting down and resetting is no longer
189
- * necessary.
190
- *
191
- * Due to the rare nature of this edge case, we can't reliably reproduce it in tests. */
192
- const isMessageAbortError = error instanceof WebSocketMessageAbortError;
193
-
194
- /* istanbul ignore next -- @preserve */
195
- if (!isMessageAbortError) {
196
- throw error;
197
- }
198
- }
199
- }
200
-
201
- async clearInterceptorHandlers<Schema extends HttpSchema>(interceptor: HttpInterceptorClient<Schema>) {
170
+ async clearHandlers<Schema extends HttpSchema>(
171
+ options: {
172
+ interceptor?: HttpInterceptorClient<Schema>;
173
+ } = {},
174
+ ) {
202
175
  if (!this.isRunning) {
203
176
  throw new NotRunningHttpInterceptorError();
204
177
  }
205
178
 
206
- for (const handler of this.httpHandlers.values()) {
207
- if (handler.interceptor === interceptor) {
208
- this.httpHandlers.delete(handler.id);
179
+ if (options.interceptor === undefined) {
180
+ this.httpHandlers.clear();
181
+ } else {
182
+ for (const handler of this.httpHandlers.values()) {
183
+ if (handler.interceptor === options.interceptor) {
184
+ this.httpHandlers.delete(handler.id);
185
+ }
209
186
  }
210
187
  }
211
188
 
@@ -213,7 +190,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
213
190
  return;
214
191
  }
215
192
 
216
- const groupsToRecommit = Array.from<HttpHandler, HttpHandlerCommit>(this.httpHandlers.values(), (handler) => ({
193
+ const handlersToRecommit = Array.from<HttpHandler, HttpHandlerCommit>(this.httpHandlers.values(), (handler) => ({
217
194
  id: handler.id,
218
195
  baseURL: handler.baseURL,
219
196
  method: handler.method,
@@ -221,7 +198,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
221
198
  }));
222
199
 
223
200
  try {
224
- await this.webSocketClient.request('interceptors/workers/reset', groupsToRecommit);
201
+ await this.webSocketClient.request('interceptors/workers/reset', handlersToRecommit);
225
202
  } catch (error) {
226
203
  /* istanbul ignore next -- @preserve
227
204
  *
@@ -164,20 +164,21 @@ class InterceptorServer implements PublicInterceptorServer {
164
164
  }
165
165
 
166
166
  private async startHttpServer() {
167
+ this.httpServerOrThrow.on('request', this.handleHttpRequest);
168
+
167
169
  await startHttpServer(this.httpServerOrThrow, {
168
170
  hostname: this.hostname,
169
171
  port: this.port,
170
172
  });
171
- this.port = getHttpServerPort(this.httpServerOrThrow);
172
173
 
173
- this.httpServerOrThrow.on('request', this.handleHttpRequest);
174
+ this.port = getHttpServerPort(this.httpServerOrThrow);
174
175
  }
175
176
 
176
177
  private startWebSocketServer() {
177
- this.webSocketServerOrThrow.start();
178
+ this.webSocketServerOrThrow.onChannel('event', 'interceptors/workers/commit', this.commitWorker);
179
+ this.webSocketServerOrThrow.onChannel('event', 'interceptors/workers/reset', this.resetWorker);
178
180
 
179
- this.webSocketServerOrThrow.onEvent('interceptors/workers/commit', this.commitWorker);
180
- this.webSocketServerOrThrow.onEvent('interceptors/workers/reset', this.resetWorker);
181
+ this.webSocketServerOrThrow.start();
181
182
  }
182
183
 
183
184
  private commitWorker = (
@@ -193,25 +194,42 @@ class InterceptorServer implements PublicInterceptorServer {
193
194
  };
194
195
 
195
196
  private resetWorker = (
196
- message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/reset'>,
197
+ { data: handlersToRecommit }: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/reset'>,
197
198
  socket: Socket,
198
199
  ) => {
199
- this.removeHttpHandlersBySocket(socket);
200
+ this.registerWorkerSocketIfUnknown(socket);
201
+
202
+ this.webSocketServerOrThrow.emitSocket('abortRequests', socket, {
203
+ shouldAbortRequest: (request) => {
204
+ const isResponseCreationRequest = this.webSocketServerOrThrow.isChannelEvent(
205
+ request,
206
+ 'interceptors/responses/create',
207
+ );
200
208
 
201
- const handlersToResetTo = message.data;
202
- const isWorkerNoLongerCommitted = handlersToResetTo === undefined;
209
+ /* istanbul ignore if -- @preserve
210
+ * While resetting a worker, there could be other types of requests in progress. These are not guaranteed to
211
+ * exist and are not related to handler resets, so we let them continue. */
212
+ if (!isResponseCreationRequest) {
213
+ return false;
214
+ }
203
215
 
204
- if (isWorkerNoLongerCommitted) {
205
- // When a worker is no longer committed, we should abort all requests that were using it.
206
- // This ensures that we only wait for responses from committed worker sockets.
207
- this.webSocketServerOrThrow.abortSocketMessages([socket]);
208
- } else {
209
- for (const handler of handlersToResetTo) {
210
- this.registerHttpHandler(handler, socket);
211
- }
212
- }
216
+ // TODO: create a test with two interceptors, one for each path,, and reset only one of them.
217
+ const isHandlerStillCommitted = handlersToRecommit.some(
218
+ /* istanbul ignore next -- @preserve
219
+ * Ensuring this function is called in tests is difficult because it requires clearing or stopping a worker
220
+ * at the exact moment a request is being handled, in a scenario when there are other handlers still
221
+ * committed. */
222
+ (handler) => request.data.handlerId === handler.id,
223
+ );
224
+ return !isHandlerStillCommitted;
225
+ },
226
+ });
213
227
 
214
- this.registerWorkerSocketIfUnknown(socket);
228
+ this.removeHttpHandlersBySocket(socket);
229
+
230
+ for (const handler of handlersToRecommit) {
231
+ this.registerHttpHandler(handler, socket);
232
+ }
215
233
 
216
234
  return {};
217
235
  };
@@ -263,8 +281,8 @@ class InterceptorServer implements PublicInterceptorServer {
263
281
  }
264
282
 
265
283
  private async stopWebSocketServer() {
266
- this.webSocketServerOrThrow.offEvent('interceptors/workers/commit', this.commitWorker);
267
- this.webSocketServerOrThrow.offEvent('interceptors/workers/reset', this.resetWorker);
284
+ this.webSocketServerOrThrow.offChannel('event', 'interceptors/workers/commit', this.commitWorker);
285
+ this.webSocketServerOrThrow.offChannel('event', 'interceptors/workers/reset', this.resetWorker);
268
286
 
269
287
  await this.webSocketServerOrThrow.stop();
270
288
 
@@ -17,7 +17,7 @@ export type InterceptorServerWebSocketSchema = WebSocketSchema<{
17
17
  };
18
18
 
19
19
  'interceptors/workers/reset': {
20
- event?: HttpHandlerCommit[];
20
+ event: HttpHandlerCommit[];
21
21
  reply: {};
22
22
  };
23
23
 
@@ -49,11 +49,11 @@ class WebSocketClient<Schema extends WebSocketSchema> extends WebSocketHandler<S
49
49
  }
50
50
 
51
51
  async stop() {
52
- super.removeAllChannelListeners();
52
+ super.offAny();
53
53
 
54
- const sockets = this.socket ? [this.socket] : [];
55
- super.abortSocketMessages(sockets);
56
- await super.closeClientSockets(sockets);
54
+ if (this.socket) {
55
+ await super.closeClientSockets([this.socket]);
56
+ }
57
57
 
58
58
  this.socket = undefined;
59
59
  }
@@ -26,6 +26,10 @@ import {
26
26
  WebSocketMessage,
27
27
  } from './types';
28
28
 
29
+ interface WebSocketRequestAbortOptions<Schema extends WebSocketSchema> {
30
+ shouldAbortRequest?: (request: WebSocketEventMessage<Schema>) => boolean;
31
+ }
32
+
29
33
  abstract class WebSocketHandler<Schema extends WebSocketSchema> {
30
34
  private sockets = new Set<ClientSocket>();
31
35
 
@@ -40,7 +44,7 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
40
44
  } = {};
41
45
 
42
46
  private socketListeners = {
43
- messageAbort: new Map<ClientSocket, Set<(error: unknown) => void>>(),
47
+ abortRequests: new Map<ClientSocket, Set<(options: WebSocketRequestAbortOptions<Schema>) => void>>(),
44
48
  };
45
49
 
46
50
  protected constructor(options: { socketTimeout?: number; messageTimeout?: number }) {
@@ -71,11 +75,14 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
71
75
  socket.addEventListener('error', handleSocketError);
72
76
 
73
77
  const handleSocketClose = () => {
78
+ this.sockets.delete(socket);
79
+
80
+ this.emitSocket('abortRequests', socket);
81
+ this.socketListeners.abortRequests.delete(socket);
82
+
74
83
  socket.removeEventListener('message', handleSocketMessage);
75
84
  socket.removeEventListener('close', handleSocketClose);
76
85
  socket.removeEventListener('error', handleSocketError);
77
-
78
- this.removeSocket(socket);
79
86
  };
80
87
 
81
88
  socket.addEventListener('close', handleSocketClose);
@@ -154,6 +161,13 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
154
161
  );
155
162
  }
156
163
 
164
+ isChannelEvent<Channel extends WebSocketChannel<Schema>>(
165
+ event: WebSocketEventMessage<Schema>,
166
+ channel: Channel,
167
+ ): event is WebSocketEventMessage<Schema, Channel> {
168
+ return event.channel === channel;
169
+ }
170
+
157
171
  private async notifyListeners(message: WebSocketMessage<Schema>, socket: ClientSocket) {
158
172
  if (this.isReplyMessage(message)) {
159
173
  await this.notifyReplyListeners(message, socket);
@@ -193,11 +207,6 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
193
207
  await Promise.all(closingPromises);
194
208
  }
195
209
 
196
- private removeSocket(socket: ClientSocket) {
197
- this.abortSocketMessages([socket]);
198
- this.sockets.delete(socket);
199
- }
200
-
201
210
  private async createEventMessage<Channel extends WebSocketChannel<Schema>>(
202
211
  channel: Channel,
203
212
  eventData: WebSocketEventMessage<Schema, Channel>['data'],
@@ -233,43 +242,67 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
233
242
  const request = await this.createEventMessage(channel, requestData);
234
243
  this.sendMessage(request, options.sockets);
235
244
 
236
- const response = await this.waitForReply(channel, request.id, options.sockets);
245
+ const response = await this.waitForReply(channel, request, options.sockets);
237
246
  return response.data;
238
247
  }
239
248
 
240
249
  async waitForReply<Channel extends WebSocketChannelWithReply<Schema>>(
241
250
  channel: Channel,
242
- requestId: WebSocketEventMessage<Schema, Channel>['id'],
251
+ request: WebSocketEventMessage<Schema, Channel>,
243
252
  sockets: Collection<ClientSocket> = this.sockets,
244
253
  ) {
245
254
  return new Promise<WebSocketReplyMessage<Schema, Channel>>((resolve, reject) => {
246
255
  const replyTimeout = setTimeout(() => {
247
- this.offReply(channel, replyListener); // eslint-disable-line @typescript-eslint/no-use-before-define
248
- this.offAbortSocketMessages(sockets, abortListener); // eslint-disable-line @typescript-eslint/no-use-before-define
256
+ this.offChannel('reply', channel, replyListener); // eslint-disable-line @typescript-eslint/no-use-before-define
257
+
258
+ for (const socket of sockets) {
259
+ this.offSocket('abortRequests', socket, abortRequestsHandler); // eslint-disable-line @typescript-eslint/no-use-before-define
260
+ }
249
261
 
250
262
  const timeoutError = new WebSocketMessageTimeoutError(this.messageTimeout);
251
263
  reject(timeoutError);
252
264
  }, this.messageTimeout);
253
265
 
254
- const abortListener = this.onAbortSocketMessages(sockets, (error) => {
266
+ const replyListener = this.onChannel('reply', channel, (message) => {
267
+ if (message.requestId !== request.id) {
268
+ return;
269
+ }
270
+
255
271
  clearTimeout(replyTimeout);
256
272
 
257
- this.offReply(channel, replyListener); // eslint-disable-line @typescript-eslint/no-use-before-define
258
- this.offAbortSocketMessages(sockets, abortListener);
273
+ this.offChannel('reply', channel, replyListener);
274
+
275
+ for (const socket of sockets) {
276
+ this.offSocket('abortRequests', socket, abortRequestsHandler); // eslint-disable-line @typescript-eslint/no-use-before-define
277
+ }
259
278
 
260
- reject(error);
279
+ resolve(message);
261
280
  });
262
281
 
263
- const replyListener = this.onReply(channel, (message) => {
264
- if (message.requestId === requestId) {
265
- clearTimeout(replyTimeout);
282
+ const abortRequestsHandler = (options: WebSocketRequestAbortOptions<Schema>) => {
283
+ const shouldAbortRequest = options.shouldAbortRequest === undefined || options.shouldAbortRequest(request);
284
+
285
+ /* istanbul ignore if -- @preserve
286
+ * Aborting requests is highly non-deterministic because it depends on specific timing of socket events. */
287
+ if (!shouldAbortRequest) {
288
+ return;
289
+ }
290
+
291
+ clearTimeout(replyTimeout);
266
292
 
267
- this.offReply(channel, replyListener);
268
- this.offAbortSocketMessages(sockets, abortListener);
293
+ this.offChannel('reply', channel, replyListener);
269
294
 
270
- resolve(message);
295
+ for (const socket of sockets) {
296
+ this.offSocket('abortRequests', socket, abortRequestsHandler);
271
297
  }
272
- });
298
+
299
+ const abortError = new WebSocketMessageAbortError();
300
+ reject(abortError);
301
+ };
302
+
303
+ for (const socket of sockets) {
304
+ this.onSocket('abortRequests', socket, abortRequestsHandler);
305
+ }
273
306
  });
274
307
  }
275
308
 
@@ -322,12 +355,22 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
322
355
  }
323
356
  }
324
357
 
325
- onEvent<Channel extends WebSocketChannel<Schema>, Listener extends WebSocketEventMessageListener<Schema, Channel>>(
358
+ onChannel<Channel extends WebSocketChannel<Schema>, Listener extends WebSocketEventMessageListener<Schema, Channel>>(
359
+ type: 'event',
326
360
  channel: Channel,
327
- listener: Listener,
328
- ): Listener {
361
+ eventListener: Listener,
362
+ ): Listener;
363
+ onChannel<Channel extends WebSocketChannel<Schema>, Listener extends WebSocketReplyMessageListener<Schema, Channel>>(
364
+ type: 'reply',
365
+ channel: Channel,
366
+ replyListener: Listener,
367
+ ): Listener;
368
+ onChannel<
369
+ Channel extends WebSocketChannel<Schema>,
370
+ Listener extends WebSocketEventMessageListener<Schema, Channel> & WebSocketReplyMessageListener<Schema, Channel>,
371
+ >(type: 'event' | 'reply', channel: Channel, listener: Listener): Listener {
329
372
  const listeners = this.getOrCreateChannelListeners<Channel>(channel);
330
- listeners.event.add(listener);
373
+ listeners[type].add(listener);
331
374
  return listener;
332
375
  }
333
376
 
@@ -344,59 +387,66 @@ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
344
387
  return listeners;
345
388
  }
346
389
 
347
- onReply<
348
- Channel extends WebSocketChannelWithReply<Schema>,
349
- Listener extends WebSocketReplyMessageListener<Schema, Channel>,
350
- >(channel: Channel, listener: Listener): Listener {
351
- const listeners = this.getOrCreateChannelListeners<Channel>(channel);
352
- listeners.reply.add(listener);
353
- return listener;
354
- }
355
-
356
- offEvent<Channel extends WebSocketChannel<Schema>>(
390
+ offChannel<Channel extends WebSocketChannel<Schema>>(
391
+ type: 'event',
357
392
  channel: Channel,
358
- listener: WebSocketEventMessageListener<Schema, Channel>,
359
- ) {
360
- this.channelListeners[channel]?.event.delete(listener);
361
- }
362
-
363
- offReply<Channel extends WebSocketChannelWithReply<Schema>>(
393
+ eventListener: WebSocketEventMessageListener<Schema, Channel>,
394
+ ): void;
395
+ offChannel<Channel extends WebSocketChannel<Schema>>(
396
+ type: 'reply',
397
+ channel: Channel,
398
+ replyListener: WebSocketReplyMessageListener<Schema, Channel>,
399
+ ): void;
400
+ offChannel<Channel extends WebSocketChannel<Schema>>(
401
+ type: 'event' | 'reply',
364
402
  channel: Channel,
365
- listener: WebSocketReplyMessageListener<Schema, Channel>,
403
+ listener: WebSocketEventMessageListener<Schema, Channel> & WebSocketReplyMessageListener<Schema, Channel>,
366
404
  ) {
367
- this.channelListeners[channel]?.reply.delete(listener);
405
+ const listeners = this.channelListeners[channel];
406
+ listeners?.[type].delete(listener);
368
407
  }
369
408
 
370
- removeAllChannelListeners() {
371
- this.channelListeners = {};
409
+ onSocket<Listener extends (options: WebSocketRequestAbortOptions<Schema>) => void>(
410
+ type: 'abortRequests',
411
+ socket: ClientSocket,
412
+ listener: Listener,
413
+ ): Listener {
414
+ const listeners = this.getOrCreateSocketListeners(type, socket);
415
+ listeners.add(listener);
416
+ return listener;
372
417
  }
373
418
 
374
- private onAbortSocketMessages(sockets: Collection<ClientSocket>, listener: (error: unknown) => void) {
375
- for (const socket of sockets) {
376
- let listeners = this.socketListeners.messageAbort.get(socket);
377
- if (!listeners) {
378
- listeners = new Set();
379
- this.socketListeners.messageAbort.set(socket, listeners);
380
- }
381
- listeners.add(listener);
419
+ private getOrCreateSocketListeners(type: 'abortRequests', socket: ClientSocket) {
420
+ const listeners = this.socketListeners[type].get(socket) ?? new Set();
421
+
422
+ if (!this.socketListeners[type].has(socket)) {
423
+ this.socketListeners[type].set(socket, listeners);
382
424
  }
383
425
 
384
- return listener;
426
+ return listeners;
385
427
  }
386
428
 
387
- private offAbortSocketMessages(sockets: Collection<ClientSocket>, listener: (error: unknown) => void) {
388
- for (const socket of sockets) {
389
- this.socketListeners.messageAbort.get(socket)?.delete(listener);
429
+ offSocket<Listener extends (options: WebSocketRequestAbortOptions<Schema>) => void>(
430
+ type: 'abortRequests',
431
+ socket: ClientSocket,
432
+ listener: Listener,
433
+ ) {
434
+ const listeners = this.socketListeners[type].get(socket);
435
+ listeners?.delete(listener);
436
+ }
437
+
438
+ emitSocket(type: 'abortRequests', socket: ClientSocket, options: WebSocketRequestAbortOptions<Schema> = {}) {
439
+ for (const listener of this.socketListeners[type].get(socket) ?? []) {
440
+ listener(options);
390
441
  }
391
442
  }
392
443
 
393
- abortSocketMessages(sockets: Collection<ClientSocket> = this.sockets) {
394
- const abortError = new WebSocketMessageAbortError();
444
+ offAny() {
445
+ this.channelListeners = {};
395
446
 
396
- for (const socket of sockets) {
397
- const listeners = this.socketListeners.messageAbort.get(socket) ?? [];
398
- for (const listener of listeners) {
399
- listener(abortError);
447
+ for (const listenersBySocket of Object.values(this.socketListeners)) {
448
+ for (const listeners of listenersBySocket.values()) {
449
+ listeners.clear();
400
450
  }
401
451
  }
402
452
  }