@zimic/interceptor 1.5.1 → 1.5.2-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 (42) hide show
  1. package/dist/{chunk-3WCEMV5U.js → chunk-6YD4TBYJ.js} +13 -4
  2. package/dist/chunk-6YD4TBYJ.js.map +1 -0
  3. package/dist/{chunk-YQNDAJTN.mjs → chunk-MCLPYYCY.mjs} +13 -4
  4. package/dist/chunk-MCLPYYCY.mjs.map +1 -0
  5. package/dist/cli.js +16 -16
  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.d.ts +24 -8
  10. package/dist/http.js +368 -289
  11. package/dist/http.js.map +1 -1
  12. package/dist/http.mjs +367 -288
  13. package/dist/http.mjs.map +1 -1
  14. package/dist/server.js +6 -6
  15. package/dist/server.mjs +1 -1
  16. package/package.json +10 -10
  17. package/src/http/errors/HttpTimesCheckError.ts +9 -9
  18. package/src/http/index.ts +6 -1
  19. package/src/http/interceptor/HttpInterceptorImplementation.ts +74 -21
  20. package/src/http/interceptor/HttpInterceptorStore.ts +11 -13
  21. package/src/http/interceptor/LocalHttpInterceptor.ts +1 -10
  22. package/src/http/interceptor/RemoteHttpInterceptor.ts +16 -22
  23. package/src/http/interceptor/errors/UnknownHttpInterceptorPlatformError.ts +3 -1
  24. package/src/http/interceptor/factory.ts +3 -0
  25. package/src/http/interceptor/types/options.ts +1 -1
  26. package/src/http/interceptorWorker/HttpInterceptorWorker.ts +14 -3
  27. package/src/http/interceptorWorker/LocalHttpInterceptorWorker.ts +25 -93
  28. package/src/http/requestHandler/HttpRequestHandlerImplementation.ts +5 -9
  29. package/src/http/requestHandler/LocalHttpRequestHandler.ts +2 -5
  30. package/src/http/requestHandler/RemoteHttpRequestHandler.ts +17 -20
  31. package/src/http/requestHandler/types/public.ts +3 -8
  32. package/src/http/requestHandler/types/requests.ts +31 -3
  33. package/src/http/utils/logging.ts +54 -0
  34. package/src/interceptor/LocalMSWWorkerStore.ts +123 -0
  35. package/src/{http/interceptorWorker → interceptor}/types/msw.ts +1 -2
  36. package/src/server/utils/auth.ts +1 -1
  37. package/src/utils/fetch.ts +2 -2
  38. package/src/utils/logging.ts +0 -54
  39. package/src/utils/webSocket/WebSocketHandler.ts +1 -1
  40. package/dist/chunk-3WCEMV5U.js.map +0 -1
  41. package/dist/chunk-YQNDAJTN.mjs.map +0 -1
  42. /package/src/{http/interceptorWorker → interceptor}/errors/UnregisteredBrowserServiceWorkerError.ts +0 -0
package/dist/server.js CHANGED
@@ -1,29 +1,29 @@
1
1
  'use strict';
2
2
 
3
- var chunk3WCEMV5U_js = require('./chunk-3WCEMV5U.js');
3
+ var chunk6YD4TBYJ_js = require('./chunk-6YD4TBYJ.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 chunk3WCEMV5U_js.DEFAULT_ACCESS_CONTROL_HEADERS; }
10
+ get: function () { return chunk6YD4TBYJ_js.DEFAULT_ACCESS_CONTROL_HEADERS; }
11
11
  });
12
12
  Object.defineProperty(exports, "DEFAULT_PREFLIGHT_STATUS_CODE", {
13
13
  enumerable: true,
14
- get: function () { return chunk3WCEMV5U_js.DEFAULT_PREFLIGHT_STATUS_CODE; }
14
+ get: function () { return chunk6YD4TBYJ_js.DEFAULT_PREFLIGHT_STATUS_CODE; }
15
15
  });
16
16
  Object.defineProperty(exports, "NotRunningInterceptorServerError", {
17
17
  enumerable: true,
18
- get: function () { return chunk3WCEMV5U_js.NotRunningInterceptorServerError_default; }
18
+ get: function () { return chunk6YD4TBYJ_js.NotRunningInterceptorServerError_default; }
19
19
  });
20
20
  Object.defineProperty(exports, "RunningInterceptorServerError", {
21
21
  enumerable: true,
22
- get: function () { return chunk3WCEMV5U_js.RunningInterceptorServerError_default; }
22
+ get: function () { return chunk6YD4TBYJ_js.RunningInterceptorServerError_default; }
23
23
  });
24
24
  Object.defineProperty(exports, "createInterceptorServer", {
25
25
  enumerable: true,
26
- get: function () { return chunk3WCEMV5U_js.createInterceptorServer; }
26
+ get: function () { return chunk6YD4TBYJ_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-YQNDAJTN.mjs';
1
+ export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default as NotRunningInterceptorServerError, RunningInterceptorServerError_default as RunningInterceptorServerError, createInterceptorServer } from './chunk-MCLPYYCY.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.5.1",
17
+ "version": "1.5.2-canary.1",
18
18
  "homepage": "https://zimic.dev/docs/interceptor",
19
19
  "repository": {
20
20
  "type": "git",
@@ -100,29 +100,29 @@
100
100
  "isomorphic-ws": "5.0.0",
101
101
  "msw": "2.15.0",
102
102
  "picocolors": "^1.1.1",
103
- "ws": "8.21.1",
103
+ "ws": "8.21.3",
104
104
  "yargs": "18.1.0",
105
- "zod": "^4.4.3"
105
+ "zod": "^4.5.1"
106
106
  },
107
107
  "devDependencies": {
108
- "@types/node": "^26.1.2",
108
+ "@types/node": "^26.4.0",
109
109
  "@types/ws": "^8.18.1",
110
110
  "@types/yargs": "^17.0.35",
111
111
  "@typescript/native": "npm:typescript@^7.0.2",
112
- "@vitest/browser": "^4.1.10",
113
- "@vitest/browser-playwright": "^4.1.10",
114
- "@vitest/coverage-istanbul": "^4.1.10",
112
+ "@vitest/browser": "^4.1.11",
113
+ "@vitest/browser-playwright": "^4.1.11",
114
+ "@vitest/coverage-istanbul": "^4.1.11",
115
115
  "@zimic/eslint-config-node": "workspace:*",
116
116
  "@zimic/lint-staged-config": "workspace:*",
117
117
  "@zimic/tsconfig": "workspace:*",
118
118
  "@zimic/utils": "workspace:*",
119
- "concurrently": "^10.0.4",
119
+ "concurrently": "^10.0.5",
120
120
  "dotenv-cli": "^11.0.0",
121
121
  "eslint": "^9.39.5",
122
- "playwright": "^1.62.0",
122
+ "playwright": "^1.62.1",
123
123
  "tsup": "^8.5.1",
124
124
  "typescript": "npm:@typescript/typescript6@^6.0.2",
125
- "vitest": "^4.1.10"
125
+ "vitest": "^4.1.11"
126
126
  },
127
127
  "peerDependencies": {
128
128
  "@zimic/http": "workspace:^1.2.0",
@@ -3,7 +3,7 @@ import { Range } from '@zimic/utils/types';
3
3
  import color from 'picocolors';
4
4
 
5
5
  import { HttpInterceptorRequestSaving } from '@/http/interceptor/types/public';
6
- import { stringifyValueToLog } from '@/utils/logging';
6
+ import { stringifyHttpValueToLog } from '@/http/utils/logging';
7
7
 
8
8
  import { UnmatchedHttpInterceptorRequestGroup } from '../requestHandler/types/restrictions';
9
9
  import HttpTimesDeclarationPointer from './HttpTimesDeclarationPointer';
@@ -66,8 +66,8 @@ function createMessageDiffs({ requestSaving, unmatchedRequestGroups }: HttpTimes
66
66
  if (diff.computed) {
67
67
  messageParts.push('Computed restriction:');
68
68
 
69
- const stringifiedExpected = stringifyValueToLog(diff.computed.expected);
70
- const stringifiedReceived = stringifyValueToLog(diff.computed.received);
69
+ const stringifiedExpected = stringifyHttpValueToLog(diff.computed.expected);
70
+ const stringifiedReceived = stringifyHttpValueToLog(diff.computed.received);
71
71
 
72
72
  messageParts.push(` ${color.green(`- return ${stringifiedExpected}`)}`);
73
73
  messageParts.push(` ${color.red(`+ return ${stringifiedReceived}`)}`);
@@ -76,8 +76,8 @@ function createMessageDiffs({ requestSaving, unmatchedRequestGroups }: HttpTimes
76
76
  if (diff.headers) {
77
77
  messageParts.push('Headers:');
78
78
 
79
- const stringifiedExpected = stringifyValueToLog(diff.headers.expected);
80
- const stringifiedReceived = stringifyValueToLog(diff.headers.received);
79
+ const stringifiedExpected = stringifyHttpValueToLog(diff.headers.expected);
80
+ const stringifiedReceived = stringifyHttpValueToLog(diff.headers.received);
81
81
 
82
82
  messageParts.push(` ${color.green(`- ${stringifiedExpected}`)}`);
83
83
  messageParts.push(` ${color.red(`+ ${stringifiedReceived}`)}`);
@@ -86,8 +86,8 @@ function createMessageDiffs({ requestSaving, unmatchedRequestGroups }: HttpTimes
86
86
  if (diff.searchParams) {
87
87
  messageParts.push('Search params:');
88
88
 
89
- const stringifiedExpected = stringifyValueToLog(diff.searchParams.expected);
90
- const stringifiedReceived = stringifyValueToLog(diff.searchParams.received);
89
+ const stringifiedExpected = stringifyHttpValueToLog(diff.searchParams.expected);
90
+ const stringifiedReceived = stringifyHttpValueToLog(diff.searchParams.received);
91
91
 
92
92
  messageParts.push(` ${color.green(`- ${stringifiedExpected}`)}`);
93
93
  messageParts.push(` ${color.red(`+ ${stringifiedReceived}`)}`);
@@ -96,10 +96,10 @@ function createMessageDiffs({ requestSaving, unmatchedRequestGroups }: HttpTimes
96
96
  if (diff.body) {
97
97
  messageParts.push('Body:');
98
98
 
99
- const stringifiedExpected = stringifyValueToLog(diff.body.expected, {
99
+ const stringifiedExpected = stringifyHttpValueToLog(diff.body.expected, {
100
100
  includeClassName: { searchParams: true },
101
101
  });
102
- const stringifiedReceived = stringifyValueToLog(diff.body.received, {
102
+ const stringifiedReceived = stringifyHttpValueToLog(diff.body.received, {
103
103
  includeClassName: { searchParams: true },
104
104
  });
105
105
 
package/src/http/index.ts CHANGED
@@ -9,14 +9,18 @@ export { default as UnknownHttpInterceptorPlatformError } from './interceptor/er
9
9
  export { default as UnknownHttpInterceptorTypeError } from './interceptor/errors/UnknownHttpInterceptorTypeError';
10
10
  export { default as RequestSavingSafeLimitExceededError } from './interceptor/errors/RequestSavingSafeLimitExceededError';
11
11
 
12
- export { default as UnregisteredBrowserServiceWorkerError } from './interceptorWorker/errors/UnregisteredBrowserServiceWorkerError';
12
+ export { default as UnregisteredBrowserServiceWorkerError } from '@/interceptor/errors/UnregisteredBrowserServiceWorkerError';
13
13
 
14
14
  export { default as DisabledRequestSavingError } from './requestHandler/errors/DisabledRequestSavingError';
15
15
  // eslint-disable-next-line @typescript-eslint/no-deprecated
16
16
  export { default as HttpTimesCheckError, TimesCheckError } from './errors/HttpTimesCheckError';
17
17
 
18
18
  export type {
19
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
19
20
  HttpRequestHandlerResponseDeclaration,
21
+ HttpRequestHandlerResponseStaticDeclaration,
22
+ HttpRequestHandlerResponseComputedDeclaration,
23
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
20
24
  HttpRequestHandlerResponseDeclarationFactory,
21
25
  HttpRequestHandlerResponseDelayFactory,
22
26
  HttpInterceptorRequest,
@@ -43,6 +47,7 @@ export type {
43
47
  export type {
44
48
  HttpInterceptorType,
45
49
  HttpInterceptorPlatform,
50
+ HttpInterceptorAuthOptions,
46
51
  LocalHttpInterceptorOptions,
47
52
  RemoteHttpInterceptorOptions,
48
53
  HttpInterceptorOptions,
@@ -24,7 +24,7 @@ import { HttpInterceptorRequest } from '../requestHandler/types/requests';
24
24
  import NotRunningHttpInterceptorError from './errors/NotRunningHttpInterceptorError';
25
25
  import RequestSavingSafeLimitExceededError from './errors/RequestSavingSafeLimitExceededError';
26
26
  import RunningHttpInterceptorError from './errors/RunningHttpInterceptorError';
27
- import HttpInterceptorStore from './HttpInterceptorStore';
27
+ import type HttpInterceptorStore from './HttpInterceptorStore';
28
28
  import { UnhandledRequestStrategy } from './types/options';
29
29
  import { HttpInterceptorRequestSaving } from './types/public';
30
30
  import { HttpInterceptorRequestContext } from './types/requests';
@@ -46,6 +46,10 @@ class HttpInterceptorImplementation<
46
46
  private deleteWorker: () => void;
47
47
  private worker?: HttpInterceptorWorker;
48
48
 
49
+ private startPromise?: Promise<void>;
50
+ private numberOfPendingStarts = 0;
51
+ private lifecycleQueue: Promise<void> = Promise.resolve();
52
+
49
53
  requestSaving: HttpInterceptorRequestSaving;
50
54
  private numberOfSavedRequests = 0;
51
55
 
@@ -104,7 +108,7 @@ class HttpInterceptorImplementation<
104
108
  }
105
109
 
106
110
  set baseURL(newBaseURL: URL) {
107
- if (this.isRunning) {
111
+ if (this.isRunning || this.isStarting) {
108
112
  throw new RunningHttpInterceptorError(
109
113
  'Did you forget to call `await interceptor.stop()` before changing the base URL?',
110
114
  );
@@ -134,35 +138,96 @@ class HttpInterceptorImplementation<
134
138
  return this.worker?.platform ?? null;
135
139
  }
136
140
 
137
- async start() {
141
+ start() {
142
+ if (this.startPromise) {
143
+ return this.startPromise;
144
+ }
145
+
146
+ this.numberOfPendingStarts++;
147
+
148
+ const startPromise = this.enqueueLifecycleOperation(() => this.startOnce()).finally(() => {
149
+ this.numberOfPendingStarts--;
150
+
151
+ const isLastStartPromise = this.startPromise === startPromise;
152
+
153
+ if (isLastStartPromise) {
154
+ this.startPromise = undefined;
155
+ }
156
+ });
157
+
158
+ this.startPromise = startPromise;
159
+
160
+ return startPromise;
161
+ }
162
+
163
+ private async startOnce() {
164
+ if (this.isRunning) {
165
+ return;
166
+ }
167
+
138
168
  try {
139
169
  this.worker = this.createWorker();
170
+ this.worker.registerRunningInterceptor(this);
140
171
 
141
172
  await this.worker.start();
142
- this.worker.registerRunningInterceptor(this);
143
173
 
144
174
  this.markAsRunning(true);
145
175
  } catch (error) {
146
- await this.stop();
176
+ await this.stopWorker();
147
177
  throw error;
148
178
  }
149
179
  }
150
180
 
151
- async stop() {
181
+ get isStarting() {
182
+ return this.numberOfPendingStarts > 0;
183
+ }
184
+
185
+ async stop(options: { beforeStop?: () => PossiblePromise<void> }) {
186
+ this.startPromise = undefined;
187
+ await this.enqueueLifecycleOperation(() => this.stopOnce(options));
188
+ }
189
+
190
+ private async stopOnce(options: { beforeStop?: () => PossiblePromise<void> }) {
191
+ if (!this.isRunning) {
192
+ return;
193
+ }
194
+
195
+ await options.beforeStop?.();
196
+
197
+ await this.stopWorker();
198
+ }
199
+
200
+ private async stopWorker() {
152
201
  this.worker?.unregisterRunningInterceptor(this);
153
202
 
154
- // The number of interceptors will be 0 if the first client could not start due to an error.
155
- const isLastRunningInterceptor = this.numberOfRunningInterceptors === 0 || this.numberOfRunningInterceptors === 1;
203
+ const isLastRunningInterceptor = this.worker?.numberOfRunningInterceptors === 0;
156
204
 
157
205
  if (isLastRunningInterceptor) {
158
206
  await this.worker?.stop();
159
- this.deleteWorker();
207
+
208
+ // Stopping is asynchronous, so we need to check again if we are still the last before deleting the worker.
209
+ // Another interceptor might have started in the meantime and the worker should not be deleted if so.
210
+ const isStillLastRunningInterceptor = this.worker?.numberOfRunningInterceptors === 0;
211
+
212
+ if (isStillLastRunningInterceptor) {
213
+ this.deleteWorker();
214
+ }
160
215
  }
161
216
 
162
217
  this.markAsRunning(false);
163
218
  this.worker = undefined;
164
219
  }
165
220
 
221
+ private enqueueLifecycleOperation(operation: () => Promise<void>) {
222
+ const operationPromise = this.lifecycleQueue.then(operation);
223
+
224
+ this.lifecycleQueue = operationPromise
225
+ // Ensure the queue continues even if the operation fails
226
+ .catch(() => undefined);
227
+
228
+ return operationPromise;
229
+ }
230
+
166
231
  private markAsRunning(isRunning: boolean) {
167
232
  if (this.workerOrThrow.type === 'local') {
168
233
  this.store.markLocalInterceptorAsRunning(this, isRunning);
@@ -172,18 +237,6 @@ class HttpInterceptorImplementation<
172
237
  this.isRunning = isRunning;
173
238
  }
174
239
 
175
- get numberOfRunningInterceptors() {
176
- if (!this.isRunning) {
177
- return 0;
178
- }
179
-
180
- if (this.workerOrThrow.type === 'local') {
181
- return this.store.numberOfRunningLocalInterceptors;
182
- } else {
183
- return this.store.numberOfRunningRemoteInterceptors(this.baseURL);
184
- }
185
- }
186
-
187
240
  get(path: HttpSchemaPath<Schema, HttpSchemaMethod<Schema>>) {
188
241
  return this.createHttpRequestHandler('GET' as HttpSchemaMethod<Schema>, path);
189
242
  }
@@ -1,27 +1,25 @@
1
- import { createHttpInterceptorWorker } from '../interceptorWorker/factory';
2
1
  import LocalHttpInterceptorWorker from '../interceptorWorker/LocalHttpInterceptorWorker';
3
2
  import RemoteHttpInterceptorWorker from '../interceptorWorker/RemoteHttpInterceptorWorker';
4
- import {
3
+ import type {
5
4
  LocalHttpInterceptorWorkerOptions,
6
5
  RemoteHttpInterceptorWorkerOptions,
7
6
  } from '../interceptorWorker/types/options';
8
- import { AnyHttpInterceptorImplementation } from './HttpInterceptorImplementation';
7
+ import type { AnyHttpInterceptorImplementation } from './HttpInterceptorImplementation';
9
8
 
10
9
  interface RemoteWorkerKeyOptions {
11
10
  auth: RemoteHttpInterceptorWorkerOptions['auth'];
12
11
  }
13
12
 
14
13
  class HttpInterceptorStore {
15
- private static _localWorker?: LocalHttpInterceptorWorker;
16
- private static runningLocalInterceptors = new Set<AnyHttpInterceptorImplementation>();
17
-
14
+ private static localWorker?: LocalHttpInterceptorWorker;
18
15
  private static remoteWorkers = new Map<string, RemoteHttpInterceptorWorker>();
16
+ private static runningLocalInterceptors = new Set<AnyHttpInterceptorImplementation>();
19
17
  private static runningRemoteInterceptors = new Map<string, Set<AnyHttpInterceptorImplementation>>();
20
18
 
21
19
  private class = HttpInterceptorStore;
22
20
 
23
21
  get localWorker() {
24
- return this.class._localWorker;
22
+ return this.class.localWorker;
25
23
  }
26
24
 
27
25
  private getRemoteWorkerKey(baseURL: URL, options: RemoteWorkerKeyOptions) {
@@ -34,7 +32,7 @@ class HttpInterceptorStore {
34
32
  return key.join(':');
35
33
  }
36
34
 
37
- remoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
35
+ getRemoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
38
36
  const remoteWorkerKey = this.getRemoteWorkerKey(baseURL, options);
39
37
  return this.class.remoteWorkers.get(remoteWorkerKey);
40
38
  }
@@ -74,20 +72,20 @@ class HttpInterceptorStore {
74
72
  }
75
73
 
76
74
  getOrCreateLocalWorker(workerOptions: Omit<LocalHttpInterceptorWorkerOptions, 'type'>) {
77
- const existingWorker = this.class._localWorker;
75
+ const existingWorker = this.class.localWorker;
78
76
 
79
77
  if (existingWorker) {
80
78
  return existingWorker;
81
79
  }
82
80
 
83
- const createdWorker = createHttpInterceptorWorker({ ...workerOptions, type: 'local' });
84
- this.class._localWorker = createdWorker;
81
+ const createdWorker = new LocalHttpInterceptorWorker({ ...workerOptions, type: 'local' });
82
+ this.class.localWorker = createdWorker;
85
83
 
86
84
  return createdWorker;
87
85
  }
88
86
 
89
87
  deleteLocalWorker() {
90
- this.class._localWorker = undefined;
88
+ this.class.localWorker = undefined;
91
89
  }
92
90
 
93
91
  getOrCreateRemoteWorker(workerOptions: Omit<RemoteHttpInterceptorWorkerOptions, 'type'>) {
@@ -98,7 +96,7 @@ class HttpInterceptorStore {
98
96
  return existingWorker;
99
97
  }
100
98
 
101
- const createdWorker = createHttpInterceptorWorker({ ...workerOptions, type: 'remote' });
99
+ const createdWorker = new RemoteHttpInterceptorWorker({ ...workerOptions, type: 'remote' });
102
100
  this.class.remoteWorkers.set(remoteWorkerKey, createdWorker);
103
101
 
104
102
  return createdWorker;
@@ -63,20 +63,11 @@ class LocalHttpInterceptor<Schema extends HttpSchema> implements PublicLocalHttp
63
63
  }
64
64
 
65
65
  async start() {
66
- if (this.isRunning) {
67
- return;
68
- }
69
-
70
66
  await this.implementation.start();
71
67
  }
72
68
 
73
69
  async stop() {
74
- if (!this.isRunning) {
75
- return;
76
- }
77
-
78
- this.clear();
79
- await this.implementation.stop();
70
+ await this.implementation.stop({ beforeStop: () => this.clear() });
80
71
  }
81
72
 
82
73
  get = ((path: HttpSchemaPath<Schema, HttpSchemaMethod<Schema>>) => {
@@ -14,8 +14,6 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
14
14
  #auth?: RemoteHttpInterceptorOptions['auth'];
15
15
 
16
16
  constructor(options: RemoteHttpInterceptorOptions) {
17
- this.#auth = options.auth;
18
-
19
17
  const baseURL = new URL(options.baseURL);
20
18
 
21
19
  this.implementation = new HttpInterceptorImplementation<Schema, typeof RemoteHttpRequestHandler>({
@@ -23,17 +21,19 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
23
21
  baseURL,
24
22
  createWorker: () => {
25
23
  return this.store.getOrCreateRemoteWorker({
26
- serverURL: baseURL,
24
+ serverURL: this.implementation.baseURL,
27
25
  auth: this.#auth,
28
26
  });
29
27
  },
30
28
  deleteWorker: () => {
31
- this.store.deleteRemoteWorker(baseURL, { auth: options.auth });
29
+ this.store.deleteRemoteWorker(this.implementation.baseURL, { auth: this.#auth });
32
30
  },
33
31
  Handler: RemoteHttpRequestHandler,
34
32
  onUnhandledRequest: options.onUnhandledRequest,
35
33
  requestSaving: options.requestSaving,
36
34
  });
35
+
36
+ this.auth = options.auth;
37
37
  }
38
38
 
39
39
  get type() {
@@ -64,7 +64,7 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
64
64
  const cannotChangeAuthWhileRunningMessage =
65
65
  'Did you forget to call `await interceptor.stop()` before changing the authentication parameters?';
66
66
 
67
- if (this.isRunning) {
67
+ if (this.isRunning || this.implementation.isStarting) {
68
68
  throw new RunningHttpInterceptorError(cannotChangeAuthWhileRunningMessage);
69
69
  }
70
70
 
@@ -73,14 +73,17 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
73
73
  return;
74
74
  }
75
75
 
76
- this.#auth = new Proxy(auth, {
77
- set: (target, property, value) => {
78
- if (this.isRunning) {
79
- throw new RunningHttpInterceptorError(cannotChangeAuthWhileRunningMessage);
80
- }
81
- return Reflect.set(target, property, value);
76
+ this.#auth = new Proxy(
77
+ { ...auth }, // Copy object to ensure it's not mutated later without going though the proxy.
78
+ {
79
+ set: (target, property, value) => {
80
+ if (this.isRunning || this.implementation.isStarting) {
81
+ throw new RunningHttpInterceptorError(cannotChangeAuthWhileRunningMessage);
82
+ }
83
+ return Reflect.set(target, property, value);
84
+ },
82
85
  },
83
- });
86
+ );
84
87
  }
85
88
 
86
89
  get onUnhandledRequest() {
@@ -100,20 +103,11 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
100
103
  }
101
104
 
102
105
  async start() {
103
- if (this.isRunning) {
104
- return;
105
- }
106
-
107
106
  await this.implementation.start();
108
107
  }
109
108
 
110
109
  async stop() {
111
- if (!this.isRunning) {
112
- return;
113
- }
114
-
115
- await this.clear();
116
- await this.implementation.stop();
110
+ await this.implementation.stop({ beforeStop: () => this.clear() });
117
111
  }
118
112
 
119
113
  get = ((path: HttpSchemaPath<Schema, HttpSchemaMethod<Schema>>) => {
@@ -1,10 +1,12 @@
1
+ import { ValidationError } from '@zimic/utils/validation';
2
+
1
3
  /**
2
4
  * An error thrown when an unknown interceptor platform is detected. Currently, the platforms `node` and `browser` are
3
5
  * supported.
4
6
  *
5
7
  * @see {@link https://zimic.dev/docs/interceptor/api/http-interceptor#interceptorplatform `interceptor.platform` API reference}
6
8
  */
7
- class UnknownHttpInterceptorPlatformError extends Error {
9
+ class UnknownHttpInterceptorPlatformError extends ValidationError {
8
10
  /* istanbul ignore next -- @preserve
9
11
  * Ignoring because checking unknown platforms is currently not possible in our Vitest setup. */
10
12
  constructor() {
@@ -1,4 +1,5 @@
1
1
  import { HttpSchema } from '@zimic/http';
2
+ import { assertTypeOf } from '@zimic/utils/validation';
2
3
 
3
4
  import UnknownHttpInterceptorTypeError from './errors/UnknownHttpInterceptorTypeError';
4
5
  import LocalHttpInterceptor from './LocalHttpInterceptor';
@@ -30,6 +31,8 @@ export function createHttpInterceptor<Schema extends HttpSchema>(
30
31
  export function createHttpInterceptor<Schema extends HttpSchema>(
31
32
  options: HttpInterceptorOptions,
32
33
  ): PublicLocalHttpInterceptor<Schema> | PublicRemoteHttpInterceptor<Schema> {
34
+ assertTypeOf('options.baseURL', options.baseURL, 'string');
35
+
33
36
  const type = options.type;
34
37
 
35
38
  if (isLocalHttpInterceptorOptions(options)) {
@@ -77,7 +77,7 @@ export interface LocalHttpInterceptorOptions extends SharedHttpInterceptorOption
77
77
  onUnhandledRequest?: UnhandledRequestStrategy.Local;
78
78
  }
79
79
 
80
- interface HttpInterceptorAuthOptions {
80
+ export interface HttpInterceptorAuthOptions {
81
81
  /** @see {@link https://zimic.dev/docs/interceptor/guides/http/remote-interceptors#interceptor-server-authentication Interceptor server authentication} */
82
82
  token: string;
83
83
  }
@@ -52,9 +52,17 @@ abstract class HttpInterceptorWorker {
52
52
 
53
53
  private runningInterceptors: AnyHttpInterceptorImplementation[] = [];
54
54
 
55
+ get numberOfRunningInterceptors() {
56
+ return this.runningInterceptors.length;
57
+ }
58
+
55
59
  abstract start(): Promise<void>;
56
60
 
57
61
  protected async sharedStart(internalStart: () => Promise<void>) {
62
+ if (this.stoppingPromise) {
63
+ await this.stoppingPromise;
64
+ }
65
+
58
66
  if (this.isRunning) {
59
67
  return;
60
68
  }
@@ -99,10 +107,13 @@ abstract class HttpInterceptorWorker {
99
107
  * reliably reproduce in tests. */
100
108
  if (stoppingResult instanceof Promise) {
101
109
  this.stoppingPromise = stoppingResult;
102
- await this.stoppingPromise;
103
- }
104
110
 
105
- this.stoppingPromise = undefined;
111
+ try {
112
+ await this.stoppingPromise;
113
+ } finally {
114
+ this.stoppingPromise = undefined;
115
+ }
116
+ }
106
117
  }
107
118
 
108
119
  abstract use<Schema extends HttpSchema>(