@zimic/interceptor 1.2.3 → 1.2.4-canary.1

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 (34) hide show
  1. package/dist/{chunk-XCYZ5L2M.mjs → chunk-EIYQEPK2.mjs} +111 -85
  2. package/dist/chunk-EIYQEPK2.mjs.map +1 -0
  3. package/dist/{chunk-ZU6IGW27.js → chunk-OKZEX5DQ.js} +111 -85
  4. package/dist/chunk-OKZEX5DQ.js.map +1 -0
  5. package/dist/cli.js +18 -18
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cli.mjs +2 -2
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/http.js +112 -110
  10. package/dist/http.js.map +1 -1
  11. package/dist/http.mjs +112 -110
  12. package/dist/http.mjs.map +1 -1
  13. package/dist/server.js +6 -6
  14. package/dist/server.mjs +1 -1
  15. package/package.json +1 -1
  16. package/src/http/interceptor/HttpInterceptorClient.ts +1 -1
  17. package/src/http/interceptor/HttpInterceptorStore.ts +6 -3
  18. package/src/http/interceptor/RemoteHttpInterceptor.ts +1 -1
  19. package/src/http/interceptor/errors/UnknownHttpInterceptorPlatformError.ts +1 -1
  20. package/src/http/interceptorWorker/HttpInterceptorWorker.ts +3 -5
  21. package/src/http/interceptorWorker/LocalHttpInterceptorWorker.ts +16 -12
  22. package/src/http/interceptorWorker/RemoteHttpInterceptorWorker.ts +20 -43
  23. package/src/server/InterceptorServer.ts +39 -21
  24. package/src/server/types/schema.ts +1 -1
  25. package/src/utils/http.ts +2 -2
  26. package/src/utils/webSocket.ts +3 -3
  27. package/src/webSocket/WebSocketClient.ts +4 -4
  28. package/src/webSocket/WebSocketHandler.ts +119 -69
  29. package/src/webSocket/WebSocketServer.ts +2 -3
  30. package/src/webSocket/errors/InvalidWebSocketMessageError.ts +8 -0
  31. package/src/webSocket/types.ts +9 -14
  32. package/dist/chunk-XCYZ5L2M.mjs.map +0 -1
  33. package/dist/chunk-ZU6IGW27.js.map +0 -1
  34. package/src/webSocket/errors/InvalidWebSocketMessage.ts +0 -8
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 chunkOKZEX5DQ_js = require('./chunk-OKZEX5DQ.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 chunkOKZEX5DQ_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 chunkOKZEX5DQ_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 chunkOKZEX5DQ_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 chunkOKZEX5DQ_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 chunkOKZEX5DQ_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-EIYQEPK2.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",
17
+ "version": "1.2.4-canary.1",
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
  },
@@ -9,7 +9,7 @@ class UnknownHttpInterceptorPlatformError extends Error {
9
9
  * Ignoring because checking unknown platforms is currently not possible in our Vitest setup. */
10
10
  constructor() {
11
11
  super('Unknown interceptor platform.');
12
- this.name = 'UnknownHttpInterceptorPlatform';
12
+ this.name = 'UnknownHttpInterceptorPlatformError';
13
13
  }
14
14
  }
15
15
 
@@ -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
 
package/src/utils/http.ts CHANGED
@@ -6,14 +6,14 @@ class HttpServerTimeoutError extends Error {}
6
6
  export class HttpServerStartTimeoutError extends HttpServerTimeoutError {
7
7
  constructor(reachedTimeout: number) {
8
8
  super(`HTTP server start timed out after ${reachedTimeout}ms.`);
9
- this.name = 'HttpServerStartTimeout';
9
+ this.name = 'HttpServerStartTimeoutError';
10
10
  }
11
11
  }
12
12
 
13
13
  export class HttpServerStopTimeoutError extends HttpServerTimeoutError {
14
14
  constructor(reachedTimeout: number) {
15
15
  super(`HTTP server stop timed out after ${reachedTimeout}ms.`);
16
- this.name = 'HttpServerStopTimeout';
16
+ this.name = 'HttpServerStopTimeoutError';
17
17
  }
18
18
  }
19
19
 
@@ -8,14 +8,14 @@ class WebSocketTimeoutError extends Error {}
8
8
  export class WebSocketOpenTimeoutError extends WebSocketTimeoutError {
9
9
  constructor(reachedTimeout: number) {
10
10
  super(`Web socket open timed out after ${reachedTimeout}ms.`);
11
- this.name = 'WebSocketOpenTimeout';
11
+ this.name = 'WebSocketOpenTimeoutError';
12
12
  }
13
13
  }
14
14
 
15
15
  export class WebSocketMessageTimeoutError extends WebSocketTimeoutError {
16
16
  constructor(reachedTimeout: number) {
17
17
  super(`Web socket message timed out after ${reachedTimeout}ms.`);
18
- this.name = 'WebSocketMessageTimeout';
18
+ this.name = 'WebSocketMessageTimeoutError';
19
19
  }
20
20
  }
21
21
 
@@ -29,7 +29,7 @@ export class WebSocketMessageAbortError extends WebSocketTimeoutError {
29
29
  export class WebSocketCloseTimeoutError extends WebSocketTimeoutError {
30
30
  constructor(reachedTimeout: number) {
31
31
  super(`Web socket close timed out after ${reachedTimeout}ms.`);
32
- this.name = 'WebSocketCloseTimeout';
32
+ this.name = 'WebSocketCloseTimeoutError';
33
33
  }
34
34
  }
35
35
 
@@ -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
  }