msw 2.5.1 → 2.6.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 (190) hide show
  1. package/lib/browser/index.d.mts +7 -6
  2. package/lib/browser/index.d.ts +7 -6
  3. package/lib/browser/index.js +29 -1
  4. package/lib/browser/index.js.map +1 -1
  5. package/lib/browser/index.mjs +29 -1
  6. package/lib/browser/index.mjs.map +1 -1
  7. package/lib/core/{GraphQLHandler-ClMB0BOy.d.mts → GraphQLHandler-Doool6Q_.d.mts} +1 -1
  8. package/lib/core/{GraphQLHandler-D6mLMXGZ.d.ts → GraphQLHandler-udzgBRPf.d.ts} +1 -1
  9. package/lib/core/{HttpResponse-vn-Pb4Bi.d.mts → HttpResponse-BLGmJolh.d.mts} +1 -1
  10. package/lib/core/{HttpResponse-DaYkf3ml.d.ts → HttpResponse-Cgbkdkje.d.ts} +1 -1
  11. package/lib/core/HttpResponse.d.mts +1 -1
  12. package/lib/core/HttpResponse.d.ts +1 -1
  13. package/lib/core/SetupApi.d.mts +15 -12
  14. package/lib/core/SetupApi.d.ts +15 -12
  15. package/lib/core/SetupApi.js +3 -1
  16. package/lib/core/SetupApi.js.map +1 -1
  17. package/lib/core/SetupApi.mjs +3 -1
  18. package/lib/core/SetupApi.mjs.map +1 -1
  19. package/lib/core/getResponse.d.mts +1 -1
  20. package/lib/core/getResponse.d.ts +1 -1
  21. package/lib/core/graphql.d.mts +2 -2
  22. package/lib/core/graphql.d.ts +2 -2
  23. package/lib/core/handlers/GraphQLHandler.d.mts +2 -2
  24. package/lib/core/handlers/GraphQLHandler.d.ts +2 -2
  25. package/lib/core/handlers/HttpHandler.d.mts +1 -1
  26. package/lib/core/handlers/HttpHandler.d.ts +1 -1
  27. package/lib/core/handlers/RequestHandler.d.mts +1 -1
  28. package/lib/core/handlers/RequestHandler.d.ts +1 -1
  29. package/lib/core/handlers/WebSocketHandler.d.mts +33 -0
  30. package/lib/core/handlers/WebSocketHandler.d.ts +33 -0
  31. package/lib/core/handlers/WebSocketHandler.js +120 -0
  32. package/lib/core/handlers/WebSocketHandler.js.map +1 -0
  33. package/lib/core/handlers/WebSocketHandler.mjs +102 -0
  34. package/lib/core/handlers/WebSocketHandler.mjs.map +1 -0
  35. package/lib/core/http.d.mts +1 -1
  36. package/lib/core/http.d.ts +1 -1
  37. package/lib/core/index.d.mts +5 -2
  38. package/lib/core/index.d.ts +5 -2
  39. package/lib/core/index.js +5 -1
  40. package/lib/core/index.js.map +1 -1
  41. package/lib/core/index.mjs +7 -1
  42. package/lib/core/index.mjs.map +1 -1
  43. package/lib/core/passthrough.d.mts +1 -1
  44. package/lib/core/passthrough.d.ts +1 -1
  45. package/lib/core/utils/HttpResponse/decorators.d.mts +1 -1
  46. package/lib/core/utils/HttpResponse/decorators.d.ts +1 -1
  47. package/lib/core/utils/executeHandlers.d.mts +1 -1
  48. package/lib/core/utils/executeHandlers.d.ts +1 -1
  49. package/lib/core/utils/executeHandlers.js +4 -0
  50. package/lib/core/utils/executeHandlers.js.map +1 -1
  51. package/lib/core/utils/executeHandlers.mjs +6 -0
  52. package/lib/core/utils/executeHandlers.mjs.map +1 -1
  53. package/lib/core/utils/handleRequest.d.mts +2 -2
  54. package/lib/core/utils/handleRequest.d.ts +2 -2
  55. package/lib/core/utils/handleRequest.js.map +1 -1
  56. package/lib/core/utils/handleRequest.mjs.map +1 -1
  57. package/lib/core/utils/internal/parseGraphQLRequest.d.mts +2 -2
  58. package/lib/core/utils/internal/parseGraphQLRequest.d.ts +2 -2
  59. package/lib/core/utils/internal/parseMultipartData.d.mts +1 -1
  60. package/lib/core/utils/internal/parseMultipartData.d.ts +1 -1
  61. package/lib/core/utils/internal/requestHandlerUtils.d.mts +1 -1
  62. package/lib/core/utils/internal/requestHandlerUtils.d.ts +1 -1
  63. package/lib/core/utils/logging/getTimestamp.d.mts +4 -1
  64. package/lib/core/utils/logging/getTimestamp.d.ts +4 -1
  65. package/lib/core/utils/logging/getTimestamp.js +6 -2
  66. package/lib/core/utils/logging/getTimestamp.js.map +1 -1
  67. package/lib/core/utils/logging/getTimestamp.mjs +6 -2
  68. package/lib/core/utils/logging/getTimestamp.mjs.map +1 -1
  69. package/lib/core/utils/matching/matchRequestUrl.d.mts +2 -1
  70. package/lib/core/utils/matching/matchRequestUrl.d.ts +2 -1
  71. package/lib/core/utils/matching/matchRequestUrl.js +4 -0
  72. package/lib/core/utils/matching/matchRequestUrl.js.map +1 -1
  73. package/lib/core/utils/matching/matchRequestUrl.mjs +4 -0
  74. package/lib/core/utils/matching/matchRequestUrl.mjs.map +1 -1
  75. package/lib/core/ws/WebSocketClientManager.d.mts +63 -0
  76. package/lib/core/ws/WebSocketClientManager.d.ts +63 -0
  77. package/lib/core/ws/WebSocketClientManager.js +149 -0
  78. package/lib/core/ws/WebSocketClientManager.js.map +1 -0
  79. package/lib/core/ws/WebSocketClientManager.mjs +129 -0
  80. package/lib/core/ws/WebSocketClientManager.mjs.map +1 -0
  81. package/lib/core/ws/WebSocketClientStore.d.mts +13 -0
  82. package/lib/core/ws/WebSocketClientStore.d.ts +13 -0
  83. package/lib/core/ws/WebSocketClientStore.js +26 -0
  84. package/lib/core/ws/WebSocketClientStore.js.map +1 -0
  85. package/lib/core/ws/WebSocketClientStore.mjs +6 -0
  86. package/lib/core/ws/WebSocketClientStore.mjs.map +1 -0
  87. package/lib/core/ws/WebSocketIndexedDBClientStore.d.mts +15 -0
  88. package/lib/core/ws/WebSocketIndexedDBClientStore.d.ts +15 -0
  89. package/lib/core/ws/WebSocketIndexedDBClientStore.js +130 -0
  90. package/lib/core/ws/WebSocketIndexedDBClientStore.js.map +1 -0
  91. package/lib/core/ws/WebSocketIndexedDBClientStore.mjs +110 -0
  92. package/lib/core/ws/WebSocketIndexedDBClientStore.mjs.map +1 -0
  93. package/lib/core/ws/WebSocketMemoryClientStore.d.mts +13 -0
  94. package/lib/core/ws/WebSocketMemoryClientStore.d.ts +13 -0
  95. package/lib/core/ws/WebSocketMemoryClientStore.js +41 -0
  96. package/lib/core/ws/WebSocketMemoryClientStore.js.map +1 -0
  97. package/lib/core/ws/WebSocketMemoryClientStore.mjs +21 -0
  98. package/lib/core/ws/WebSocketMemoryClientStore.mjs.map +1 -0
  99. package/lib/core/ws/handleWebSocketEvent.d.mts +19 -0
  100. package/lib/core/ws/handleWebSocketEvent.d.ts +19 -0
  101. package/lib/core/ws/handleWebSocketEvent.js +73 -0
  102. package/lib/core/ws/handleWebSocketEvent.js.map +1 -0
  103. package/lib/core/ws/handleWebSocketEvent.mjs +55 -0
  104. package/lib/core/ws/handleWebSocketEvent.mjs.map +1 -0
  105. package/lib/core/ws/utils/attachWebSocketLogger.d.mts +12 -0
  106. package/lib/core/ws/utils/attachWebSocketLogger.d.ts +12 -0
  107. package/lib/core/ws/utils/attachWebSocketLogger.js +198 -0
  108. package/lib/core/ws/utils/attachWebSocketLogger.js.map +1 -0
  109. package/lib/core/ws/utils/attachWebSocketLogger.mjs +178 -0
  110. package/lib/core/ws/utils/attachWebSocketLogger.mjs.map +1 -0
  111. package/lib/core/ws/utils/getMessageLength.d.mts +11 -0
  112. package/lib/core/ws/utils/getMessageLength.d.ts +11 -0
  113. package/lib/core/ws/utils/getMessageLength.js +33 -0
  114. package/lib/core/ws/utils/getMessageLength.js.map +1 -0
  115. package/lib/core/ws/utils/getMessageLength.mjs +13 -0
  116. package/lib/core/ws/utils/getMessageLength.mjs.map +1 -0
  117. package/lib/core/ws/utils/getPublicData.d.mts +5 -0
  118. package/lib/core/ws/utils/getPublicData.d.ts +5 -0
  119. package/lib/core/ws/utils/getPublicData.js +36 -0
  120. package/lib/core/ws/utils/getPublicData.js.map +1 -0
  121. package/lib/core/ws/utils/getPublicData.mjs +16 -0
  122. package/lib/core/ws/utils/getPublicData.mjs.map +1 -0
  123. package/lib/core/ws/utils/truncateMessage.d.mts +3 -0
  124. package/lib/core/ws/utils/truncateMessage.d.ts +3 -0
  125. package/lib/core/ws/utils/truncateMessage.js +31 -0
  126. package/lib/core/ws/utils/truncateMessage.js.map +1 -0
  127. package/lib/core/ws/utils/truncateMessage.mjs +11 -0
  128. package/lib/core/ws/utils/truncateMessage.mjs.map +1 -0
  129. package/lib/core/ws/webSocketInterceptor.d.mts +5 -0
  130. package/lib/core/ws/webSocketInterceptor.d.ts +5 -0
  131. package/lib/core/ws/webSocketInterceptor.js +26 -0
  132. package/lib/core/ws/webSocketInterceptor.js.map +1 -0
  133. package/lib/core/ws/webSocketInterceptor.mjs +6 -0
  134. package/lib/core/ws/webSocketInterceptor.mjs.map +1 -0
  135. package/lib/core/ws.d.mts +75 -0
  136. package/lib/core/ws.d.ts +75 -0
  137. package/lib/core/ws.js +71 -0
  138. package/lib/core/ws.js.map +1 -0
  139. package/lib/core/ws.mjs +54 -0
  140. package/lib/core/ws.mjs.map +1 -0
  141. package/lib/iife/index.js +1413 -85
  142. package/lib/iife/index.js.map +1 -1
  143. package/lib/mockServiceWorker.js +1 -1
  144. package/lib/native/index.d.mts +6 -5
  145. package/lib/native/index.d.ts +6 -5
  146. package/lib/native/index.js +22 -4
  147. package/lib/native/index.js.map +1 -1
  148. package/lib/native/index.mjs +22 -4
  149. package/lib/native/index.mjs.map +1 -1
  150. package/lib/node/index.d.mts +8 -7
  151. package/lib/node/index.d.ts +8 -7
  152. package/lib/node/index.js +22 -4
  153. package/lib/node/index.js.map +1 -1
  154. package/lib/node/index.mjs +22 -4
  155. package/lib/node/index.mjs.map +1 -1
  156. package/package.json +10 -1
  157. package/src/browser/setupWorker/glossary.ts +10 -10
  158. package/src/browser/setupWorker/setupWorker.ts +32 -3
  159. package/src/browser/setupWorker/start/createRequestListener.ts +7 -1
  160. package/src/browser/setupWorker/start/createStartHandler.ts +5 -0
  161. package/src/browser/setupWorker/stop/createStop.ts +6 -0
  162. package/src/core/SetupApi.ts +28 -20
  163. package/src/core/handlers/WebSocketHandler.ts +142 -0
  164. package/src/core/index.ts +11 -1
  165. package/src/core/utils/executeHandlers.ts +6 -2
  166. package/src/core/utils/handleRequest.ts +1 -1
  167. package/src/core/utils/logging/getTimestamp.test.ts +20 -6
  168. package/src/core/utils/logging/getTimestamp.ts +11 -6
  169. package/src/core/utils/matching/matchRequestUrl.test.ts +44 -0
  170. package/src/core/utils/matching/matchRequestUrl.ts +4 -0
  171. package/src/core/ws/WebSocketClientManager.test.ts +164 -0
  172. package/src/core/ws/WebSocketClientManager.ts +211 -0
  173. package/src/core/ws/WebSocketClientStore.ts +14 -0
  174. package/src/core/ws/WebSocketIndexedDBClientStore.ts +145 -0
  175. package/src/core/ws/WebSocketMemoryClientStore.ts +27 -0
  176. package/src/core/ws/handleWebSocketEvent.ts +82 -0
  177. package/src/core/ws/utils/attachWebSocketLogger.ts +259 -0
  178. package/src/core/ws/utils/getMessageLength.test.ts +16 -0
  179. package/src/core/ws/utils/getMessageLength.ts +19 -0
  180. package/src/core/ws/utils/getPublicData.test.ts +38 -0
  181. package/src/core/ws/utils/getPublicData.ts +17 -0
  182. package/src/core/ws/utils/truncateMessage.test.ts +12 -0
  183. package/src/core/ws/utils/truncateMessage.ts +9 -0
  184. package/src/core/ws/webSocketInterceptor.ts +3 -0
  185. package/src/core/ws.test.ts +23 -0
  186. package/src/core/ws.ts +166 -0
  187. package/src/node/SetupServerApi.ts +8 -7
  188. package/src/node/SetupServerCommonApi.ts +29 -5
  189. package/src/node/glossary.ts +5 -7
  190. package/src/node/setupServer.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.",
5
5
  "main": "./lib/core/index.js",
6
6
  "module": "./lib/core/index.mjs",
@@ -58,6 +58,12 @@
58
58
  "import": "./lib/core/graphql.mjs",
59
59
  "default": "./lib/core/graphql.js"
60
60
  },
61
+ "./core/ws": {
62
+ "types": "./lib/core/ws.d.ts",
63
+ "require": "./lib/core/ws.js",
64
+ "import": "./lib/core/ws.mjs",
65
+ "default": "./lib/core/ws.js"
66
+ },
61
67
  "./mockServiceWorker.js": "./lib/mockServiceWorker.js",
62
68
  "./package.json": "./package.json"
63
69
  },
@@ -118,6 +124,7 @@
118
124
  "@bundled-es-modules/tough-cookie": "^0.1.6",
119
125
  "@inquirer/confirm": "^5.0.0",
120
126
  "@mswjs/interceptors": "^0.36.5",
127
+ "@open-draft/deferred-promise": "^2.2.0",
121
128
  "@open-draft/until": "^2.1.0",
122
129
  "@types/cookie": "^0.6.0",
123
130
  "@types/statuses": "^2.0.4",
@@ -134,6 +141,7 @@
134
141
  "devDependencies": {
135
142
  "@commitlint/cli": "^18.4.4",
136
143
  "@commitlint/config-conventional": "^18.4.4",
144
+ "@fastify/websocket": "^8.3.1",
137
145
  "@open-draft/test-server": "^0.4.2",
138
146
  "@ossjs/release": "^0.8.1",
139
147
  "@playwright/test": "^1.48.0",
@@ -157,6 +165,7 @@
157
165
  "eslint-config-prettier": "^9.1.0",
158
166
  "eslint-plugin-prettier": "^5.2.1",
159
167
  "express": "^5.0.0",
168
+ "fastify": "^4.26.0",
160
169
  "fs-extra": "^11.2.0",
161
170
  "fs-teardown": "^0.3.0",
162
171
  "glob": "^11.0.0",
@@ -5,13 +5,11 @@ import {
5
5
  SharedOptions,
6
6
  } from '~/core/sharedOptions'
7
7
  import { ServiceWorkerMessage } from './start/utils/createMessageChannel'
8
- import {
9
- RequestHandler,
10
- RequestHandlerDefaultInfo,
11
- } from '~/core/handlers/RequestHandler'
8
+ import { RequestHandler } from '~/core/handlers/RequestHandler'
12
9
  import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors'
13
- import { Path } from '~/core/utils/matching/matchRequestUrl'
14
- import { RequiredDeep } from '~/core/typeUtils'
10
+ import type { Path } from '~/core/utils/matching/matchRequestUrl'
11
+ import type { RequiredDeep } from '~/core/typeUtils'
12
+ import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
15
13
 
16
14
  export type ResolvedPath = Path | URL
17
15
 
@@ -92,7 +90,7 @@ export interface SetupWorkerInternalContext {
92
90
  startOptions: RequiredDeep<StartOptions>
93
91
  worker: ServiceWorker | null
94
92
  registration: ServiceWorkerRegistration | null
95
- getRequestHandlers(): Array<RequestHandler>
93
+ getRequestHandlers(): Array<RequestHandler | WebSocketHandler>
96
94
  requests: Map<string, Request>
97
95
  emitter: Emitter<LifeCycleEventsMap>
98
96
  keepAliveInterval?: number
@@ -216,7 +214,7 @@ export interface SetupWorker {
216
214
  *
217
215
  * @see {@link https://mswjs.io/docs/api/setup-worker/use `worker.use()` API reference}
218
216
  */
219
- use: (...handlers: RequestHandler[]) => void
217
+ use: (...handlers: Array<RequestHandler | WebSocketHandler>) => void
220
218
 
221
219
  /**
222
220
  * Marks all request handlers that respond using `res.once()` as unused.
@@ -231,14 +229,16 @@ export interface SetupWorker {
231
229
  *
232
230
  * @see {@link https://mswjs.io/docs/api/setup-worker/reset-handlers `worker.resetHandlers()` API reference}
233
231
  */
234
- resetHandlers: (...nextHandlers: RequestHandler[]) => void
232
+ resetHandlers: (
233
+ ...nextHandlers: Array<RequestHandler | WebSocketHandler>
234
+ ) => void
235
235
 
236
236
  /**
237
237
  * Returns a readonly list of currently active request handlers.
238
238
  *
239
239
  * @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()` API reference}
240
240
  */
241
- listHandlers(): ReadonlyArray<RequestHandler<RequestHandlerDefaultInfo, any>>
241
+ listHandlers(): ReadonlyArray<RequestHandler | WebSocketHandler>
242
242
 
243
243
  /**
244
244
  * Life-cycle events.
@@ -18,9 +18,13 @@ import { createFallbackStop } from './stop/createFallbackStop'
18
18
  import { devUtils } from '~/core/utils/internal/devUtils'
19
19
  import { SetupApi } from '~/core/SetupApi'
20
20
  import { mergeRight } from '~/core/utils/internal/mergeRight'
21
- import { LifeCycleEventsMap } from '~/core/sharedOptions'
21
+ import type { LifeCycleEventsMap } from '~/core/sharedOptions'
22
+ import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
22
23
  import { SetupWorker } from './glossary'
23
24
  import { supportsReadableStreamTransfer } from '../utils/supportsReadableStreamTransfer'
25
+ import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor'
26
+ import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent'
27
+ import { attachWebSocketLogger } from '~/core/ws/utils/attachWebSocketLogger'
24
28
 
25
29
  interface Listener {
26
30
  target: EventTarget
@@ -37,7 +41,7 @@ export class SetupWorkerApi
37
41
  private stopHandler: StopHandler = null as any
38
42
  private listeners: Array<Listener>
39
43
 
40
- constructor(...handlers: Array<RequestHandler>) {
44
+ constructor(...handlers: Array<RequestHandler | WebSocketHandler>) {
41
45
  super(...handlers)
42
46
 
43
47
  invariant(
@@ -176,6 +180,29 @@ export class SetupWorkerApi
176
180
  options,
177
181
  ) as SetupWorkerInternalContext['startOptions']
178
182
 
183
+ // Enable the WebSocket interception.
184
+ handleWebSocketEvent({
185
+ getUnhandledRequestStrategy: () => {
186
+ return this.context.startOptions.onUnhandledRequest
187
+ },
188
+ getHandlers: () => {
189
+ return this.handlersController.currentHandlers()
190
+ },
191
+ onMockedConnection: (connection) => {
192
+ if (!this.context.startOptions.quiet) {
193
+ // Attach the logger for mocked connections since
194
+ // those won't be visible in the browser's devtools.
195
+ attachWebSocketLogger(connection)
196
+ }
197
+ },
198
+ onPassthroughConnection() {},
199
+ })
200
+ webSocketInterceptor.apply()
201
+
202
+ this.subscriptions.push(() => {
203
+ webSocketInterceptor.dispose()
204
+ })
205
+
179
206
  return await this.startHandler(this.context.startOptions, options)
180
207
  }
181
208
 
@@ -193,6 +220,8 @@ export class SetupWorkerApi
193
220
  *
194
221
  * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker()` API reference}
195
222
  */
196
- export function setupWorker(...handlers: Array<RequestHandler>): SetupWorker {
223
+ export function setupWorker(
224
+ ...handlers: Array<RequestHandler | WebSocketHandler>
225
+ ): SetupWorker {
197
226
  return new SetupWorkerApi(...handlers)
198
227
  }
@@ -9,6 +9,8 @@ import {
9
9
  } from './utils/createMessageChannel'
10
10
  import { parseWorkerRequest } from '../../utils/parseWorkerRequest'
11
11
  import { RequestHandler } from '~/core/handlers/RequestHandler'
12
+ import { HttpHandler } from '~/core/handlers/HttpHandler'
13
+ import { GraphQLHandler } from '~/core/handlers/GraphQLHandler'
12
14
  import { handleRequest } from '~/core/utils/handleRequest'
13
15
  import { RequiredDeep } from '~/core/typeUtils'
14
16
  import { devUtils } from '~/core/utils/internal/devUtils'
@@ -43,7 +45,11 @@ export const createRequestListener = (
43
45
  await handleRequest(
44
46
  request,
45
47
  requestId,
46
- context.getRequestHandlers(),
48
+ context.getRequestHandlers().filter((handler) => {
49
+ return (
50
+ handler instanceof HttpHandler || handler instanceof GraphQLHandler
51
+ )
52
+ }),
47
53
  options,
48
54
  context.emitter,
49
55
  {
@@ -71,6 +71,11 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
71
71
  // Make sure we're always clearing the interval - there are reports that not doing this can
72
72
  // cause memory leaks in headless browser environments.
73
73
  window.clearInterval(context.keepAliveInterval)
74
+
75
+ // Notify others about this client disconnecting.
76
+ // E.g. this will purge the in-memory WebSocket clients since
77
+ // starting the worker again will assign them new IDs.
78
+ window.postMessage({ type: 'msw/worker:stop' })
74
79
  })
75
80
 
76
81
  // Check if the active Service Worker has been generated
@@ -24,6 +24,12 @@ export const createStop = (
24
24
  context.isMockingEnabled = false
25
25
  window.clearInterval(context.keepAliveInterval)
26
26
 
27
+ // Post the internal stop message on the window
28
+ // to let any logic know when the worker has stopped.
29
+ // E.g. the WebSocket client manager needs this to know
30
+ // when to clear its in-memory clients list.
31
+ window.postMessage({ type: 'msw/worker:stop' })
32
+
27
33
  printStopMessage({ quiet: context.startOptions?.quiet })
28
34
  }
29
35
  }
@@ -1,38 +1,42 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { EventMap, Emitter } from 'strict-event-emitter'
3
- import {
4
- RequestHandler,
5
- RequestHandlerDefaultInfo,
6
- } from './handlers/RequestHandler'
3
+ import { RequestHandler } from './handlers/RequestHandler'
7
4
  import { LifeCycleEventEmitter } from './sharedOptions'
8
5
  import { devUtils } from './utils/internal/devUtils'
9
6
  import { pipeEvents } from './utils/internal/pipeEvents'
10
7
  import { toReadonlyArray } from './utils/internal/toReadonlyArray'
11
8
  import { Disposable } from './utils/internal/Disposable'
9
+ import type { WebSocketHandler } from './handlers/WebSocketHandler'
12
10
 
13
11
  export abstract class HandlersController {
14
- abstract prepend(runtimeHandlers: Array<RequestHandler>): void
15
- abstract reset(nextHandles: Array<RequestHandler>): void
16
- abstract currentHandlers(): Array<RequestHandler>
12
+ abstract prepend(
13
+ runtimeHandlers: Array<RequestHandler | WebSocketHandler>,
14
+ ): void
15
+ abstract reset(nextHandles: Array<RequestHandler | WebSocketHandler>): void
16
+ abstract currentHandlers(): Array<RequestHandler | WebSocketHandler>
17
17
  }
18
18
 
19
19
  export class InMemoryHandlersController implements HandlersController {
20
- private handlers: Array<RequestHandler>
20
+ private handlers: Array<RequestHandler | WebSocketHandler>
21
21
 
22
- constructor(private initialHandlers: Array<RequestHandler>) {
22
+ constructor(
23
+ private initialHandlers: Array<RequestHandler | WebSocketHandler>,
24
+ ) {
23
25
  this.handlers = [...initialHandlers]
24
26
  }
25
27
 
26
- public prepend(runtimeHandles: Array<RequestHandler>): void {
28
+ public prepend(
29
+ runtimeHandles: Array<RequestHandler | WebSocketHandler>,
30
+ ): void {
27
31
  this.handlers.unshift(...runtimeHandles)
28
32
  }
29
33
 
30
- public reset(nextHandlers: Array<RequestHandler>): void {
34
+ public reset(nextHandlers: Array<RequestHandler | WebSocketHandler>): void {
31
35
  this.handlers =
32
36
  nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers]
33
37
  }
34
38
 
35
- public currentHandlers(): Array<RequestHandler> {
39
+ public currentHandlers(): Array<RequestHandler | WebSocketHandler> {
36
40
  return this.handlers
37
41
  }
38
42
  }
@@ -47,7 +51,7 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
47
51
 
48
52
  public readonly events: LifeCycleEventEmitter<EventsMap>
49
53
 
50
- constructor(...initialHandlers: Array<RequestHandler>) {
54
+ constructor(...initialHandlers: Array<RequestHandler | WebSocketHandler>) {
51
55
  super()
52
56
 
53
57
  invariant(
@@ -71,12 +75,14 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
71
75
  })
72
76
  }
73
77
 
74
- private validateHandlers(handlers: ReadonlyArray<RequestHandler>): boolean {
78
+ private validateHandlers(handlers: ReadonlyArray<unknown>): boolean {
75
79
  // Guard against incorrect call signature of the setup API.
76
80
  return handlers.every((handler) => !Array.isArray(handler))
77
81
  }
78
82
 
79
- public use(...runtimeHandlers: Array<RequestHandler>): void {
83
+ public use(
84
+ ...runtimeHandlers: Array<RequestHandler | WebSocketHandler>
85
+ ): void {
80
86
  invariant(
81
87
  this.validateHandlers(runtimeHandlers),
82
88
  devUtils.formatMessage(
@@ -89,17 +95,19 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
89
95
 
90
96
  public restoreHandlers(): void {
91
97
  this.handlersController.currentHandlers().forEach((handler) => {
92
- handler.isUsed = false
98
+ if ('isUsed' in handler) {
99
+ handler.isUsed = false
100
+ }
93
101
  })
94
102
  }
95
103
 
96
- public resetHandlers(...nextHandlers: Array<RequestHandler>): void {
104
+ public resetHandlers(
105
+ ...nextHandlers: Array<RequestHandler | WebSocketHandler>
106
+ ): void {
97
107
  this.handlersController.reset(nextHandlers)
98
108
  }
99
109
 
100
- public listHandlers(): ReadonlyArray<
101
- RequestHandler<RequestHandlerDefaultInfo, any, any>
102
- > {
110
+ public listHandlers(): ReadonlyArray<RequestHandler | WebSocketHandler> {
103
111
  return toReadonlyArray(this.handlersController.currentHandlers())
104
112
  }
105
113
 
@@ -0,0 +1,142 @@
1
+ import { Emitter } from 'strict-event-emitter'
2
+ import { createRequestId } from '@mswjs/interceptors'
3
+ import type { WebSocketConnectionData } from '@mswjs/interceptors/WebSocket'
4
+ import {
5
+ type Match,
6
+ type Path,
7
+ type PathParams,
8
+ matchRequestUrl,
9
+ } from '../utils/matching/matchRequestUrl'
10
+ import { getCallFrame } from '../utils/internal/getCallFrame'
11
+
12
+ type WebSocketHandlerParsedResult = {
13
+ match: Match
14
+ }
15
+
16
+ export type WebSocketHandlerEventMap = {
17
+ connection: [args: WebSocketHandlerConnection]
18
+ }
19
+
20
+ export interface WebSocketHandlerConnection extends WebSocketConnectionData {
21
+ params: PathParams
22
+ }
23
+
24
+ export const kEmitter = Symbol('kEmitter')
25
+ export const kDispatchEvent = Symbol('kDispatchEvent')
26
+ export const kSender = Symbol('kSender')
27
+ const kStopPropagationPatched = Symbol('kStopPropagationPatched')
28
+ const KOnStopPropagation = Symbol('KOnStopPropagation')
29
+
30
+ export class WebSocketHandler {
31
+ public id: string
32
+ public callFrame?: string
33
+
34
+ protected [kEmitter]: Emitter<WebSocketHandlerEventMap>
35
+
36
+ constructor(private readonly url: Path) {
37
+ this.id = createRequestId()
38
+
39
+ this[kEmitter] = new Emitter()
40
+ this.callFrame = getCallFrame(new Error())
41
+ }
42
+
43
+ public parse(args: {
44
+ event: MessageEvent<WebSocketConnectionData>
45
+ }): WebSocketHandlerParsedResult {
46
+ const connection = args.event.data
47
+ const match = matchRequestUrl(connection.client.url, this.url)
48
+
49
+ return {
50
+ match,
51
+ }
52
+ }
53
+
54
+ public predicate(args: {
55
+ event: MessageEvent<WebSocketConnectionData>
56
+ parsedResult: WebSocketHandlerParsedResult
57
+ }): boolean {
58
+ return args.parsedResult.match.matches
59
+ }
60
+
61
+ async [kDispatchEvent](
62
+ event: MessageEvent<WebSocketConnectionData>,
63
+ ): Promise<void> {
64
+ const parsedResult = this.parse({ event })
65
+ const connection = event.data
66
+
67
+ const resolvedConnection: WebSocketHandlerConnection = {
68
+ ...connection,
69
+ params: parsedResult.match.params || {},
70
+ }
71
+
72
+ // Support `event.stopPropagation()` for various client/server events.
73
+ connection.client.addEventListener(
74
+ 'message',
75
+ createStopPropagationListener(this),
76
+ )
77
+ connection.client.addEventListener(
78
+ 'close',
79
+ createStopPropagationListener(this),
80
+ )
81
+
82
+ connection.server.addEventListener(
83
+ 'open',
84
+ createStopPropagationListener(this),
85
+ )
86
+ connection.server.addEventListener(
87
+ 'message',
88
+ createStopPropagationListener(this),
89
+ )
90
+ connection.server.addEventListener(
91
+ 'error',
92
+ createStopPropagationListener(this),
93
+ )
94
+ connection.server.addEventListener(
95
+ 'close',
96
+ createStopPropagationListener(this),
97
+ )
98
+
99
+ // Emit the connection event on the handler.
100
+ // This is what the developer adds listeners for.
101
+ this[kEmitter].emit('connection', resolvedConnection)
102
+ }
103
+ }
104
+
105
+ function createStopPropagationListener(handler: WebSocketHandler) {
106
+ return function stopPropagationListener(event: Event) {
107
+ const propagationStoppedAt = Reflect.get(event, 'kPropagationStoppedAt') as
108
+ | string
109
+ | undefined
110
+
111
+ if (propagationStoppedAt && handler.id !== propagationStoppedAt) {
112
+ event.stopImmediatePropagation()
113
+ return
114
+ }
115
+
116
+ Object.defineProperty(event, KOnStopPropagation, {
117
+ value(this: WebSocketHandler) {
118
+ Object.defineProperty(event, 'kPropagationStoppedAt', {
119
+ value: handler.id,
120
+ })
121
+ },
122
+ configurable: true,
123
+ })
124
+
125
+ // Since the same event instance is shared between all client/server objects,
126
+ // make sure to patch its `stopPropagation` method only once.
127
+ if (!Reflect.get(event, kStopPropagationPatched)) {
128
+ event.stopPropagation = new Proxy(event.stopPropagation, {
129
+ apply: (target, thisArg, args) => {
130
+ Reflect.get(event, KOnStopPropagation)?.call(handler)
131
+ return Reflect.apply(target, thisArg, args)
132
+ },
133
+ })
134
+
135
+ Object.defineProperty(event, kStopPropagationPatched, {
136
+ value: true,
137
+ // If something else attempts to redefine this, throw.
138
+ configurable: false,
139
+ })
140
+ }
141
+ }
142
+ }
package/src/core/index.ts CHANGED
@@ -2,13 +2,21 @@ import { checkGlobals } from './utils/internal/checkGlobals'
2
2
 
3
3
  export { SetupApi } from './SetupApi'
4
4
 
5
- /* Request handlers */
5
+ /* HTTP handlers */
6
6
  export { RequestHandler } from './handlers/RequestHandler'
7
7
  export { http } from './http'
8
8
  export { HttpHandler, HttpMethods } from './handlers/HttpHandler'
9
9
  export { graphql } from './graphql'
10
10
  export { GraphQLHandler } from './handlers/GraphQLHandler'
11
11
 
12
+ /* WebSocket handler */
13
+ export { ws, type WebSocketLink } from './ws'
14
+ export {
15
+ WebSocketHandler,
16
+ type WebSocketHandlerEventMap,
17
+ type WebSocketHandlerConnection,
18
+ } from './handlers/WebSocketHandler'
19
+
12
20
  /* Utils */
13
21
  export { matchRequestUrl } from './utils/matching/matchRequestUrl'
14
22
  export * from './utils/handleRequest'
@@ -45,6 +53,8 @@ export type {
45
53
  } from './handlers/GraphQLHandler'
46
54
  export type { GraphQLRequestHandler, GraphQLResponseResolver } from './graphql'
47
55
 
56
+ export type { WebSocketData, WebSocketEventListener } from './ws'
57
+
48
58
  export type { Path, PathParams, Match } from './utils/matching/matchRequestUrl'
49
59
  export type { ParsedGraphQLRequest } from './utils/internal/parseGraphQLRequest'
50
60
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RequestHandler,
3
- RequestHandlerExecutionResult,
3
+ type RequestHandlerExecutionResult,
4
4
  } from '../handlers/RequestHandler'
5
5
 
6
6
  export interface HandlersExecutionResult {
@@ -18,7 +18,7 @@ export interface ResponseResolutionContext {
18
18
  * Returns the execution result object containing any matching request
19
19
  * handler and any mocked response it returned.
20
20
  */
21
- export const executeHandlers = async <Handlers extends Array<RequestHandler>>({
21
+ export const executeHandlers = async <Handlers extends Array<unknown>>({
22
22
  request,
23
23
  requestId,
24
24
  handlers,
@@ -33,6 +33,10 @@ export const executeHandlers = async <Handlers extends Array<RequestHandler>>({
33
33
  let result: RequestHandlerExecutionResult<any> | null = null
34
34
 
35
35
  for (const handler of handlers) {
36
+ if (!(handler instanceof RequestHandler)) {
37
+ continue
38
+ }
39
+
36
40
  result = await handler.run({ request, requestId, resolutionContext })
37
41
 
38
42
  // If the handler produces some result for this request,
@@ -1,8 +1,8 @@
1
1
  import { until } from '@open-draft/until'
2
2
  import { Emitter } from 'strict-event-emitter'
3
- import { RequestHandler } from '../handlers/RequestHandler'
4
3
  import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions'
5
4
  import { RequiredDeep } from '../typeUtils'
5
+ import type { RequestHandler } from '../handlers/RequestHandler'
6
6
  import { HandlersExecutionResult, executeHandlers } from './executeHandlers'
7
7
  import { onUnhandledRequest } from './request/onUnhandledRequest'
8
8
  import { storeResponseCookies } from './request/storeResponseCookies'
@@ -1,18 +1,32 @@
1
1
  import { getTimestamp } from './getTimestamp'
2
2
 
3
3
  beforeAll(() => {
4
- // Stub native `Date` prototype methods used in the tested module,
5
- // to always produce a predictable value for testing purposes.
6
- vi.spyOn(global.Date.prototype, 'getHours').mockImplementation(() => 12)
7
- vi.spyOn(global.Date.prototype, 'getMinutes').mockImplementation(() => 4)
8
- vi.spyOn(global.Date.prototype, 'getSeconds').mockImplementation(() => 8)
4
+ vi.useFakeTimers()
9
5
  })
10
6
 
11
7
  afterAll(() => {
12
- vi.restoreAllMocks()
8
+ vi.useRealTimers()
13
9
  })
14
10
 
15
11
  test('returns a timestamp string of the invocation time', () => {
12
+ vi.setSystemTime(new Date('2024-01-01 12:4:8'))
16
13
  const timestamp = getTimestamp()
17
14
  expect(timestamp).toBe('12:04:08')
18
15
  })
16
+
17
+ test('returns a timestamp with milliseconds', () => {
18
+ vi.setSystemTime(new Date('2024-01-01 12:4:8'))
19
+ expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.000')
20
+
21
+ vi.setSystemTime(new Date('2024-01-01 12:4:8.000'))
22
+ expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.000')
23
+
24
+ vi.setSystemTime(new Date('2024-01-01 12:4:8.4'))
25
+ expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.400')
26
+
27
+ vi.setSystemTime(new Date('2024-01-01 12:4:8.123'))
28
+ expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.123')
29
+
30
+ vi.setSystemTime(new Date('2024-01-01 12:00:00'))
31
+ expect(getTimestamp({ milliseconds: true })).toBe('12:00:00.000')
32
+ })
@@ -1,12 +1,17 @@
1
+ interface GetTimestampOptions {
2
+ milliseconds?: boolean
3
+ }
4
+
1
5
  /**
2
6
  * Returns a timestamp string in a "HH:MM:SS" format.
3
7
  */
4
- export function getTimestamp(): string {
8
+ export function getTimestamp(options?: GetTimestampOptions): string {
5
9
  const now = new Date()
10
+ const timestamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
11
+
12
+ if (options?.milliseconds) {
13
+ return `${timestamp}.${now.getMilliseconds().toString().padStart(3, '0')}`
14
+ }
6
15
 
7
- return [now.getHours(), now.getMinutes(), now.getSeconds()]
8
- .map(String)
9
- .map((chunk) => chunk.slice(0, 2))
10
- .map((chunk) => chunk.padStart(2, '0'))
11
- .join(':')
16
+ return timestamp
12
17
  }
@@ -72,6 +72,50 @@ describe('matchRequestUrl', () => {
72
72
  userId: undefined,
73
73
  })
74
74
  })
75
+
76
+ test('returns true for matching WebSocket URL', () => {
77
+ expect(
78
+ matchRequestUrl(new URL('ws://test.mswjs.io'), 'ws://test.mswjs.io'),
79
+ ).toEqual({
80
+ matches: true,
81
+ params: {},
82
+ })
83
+ expect(
84
+ matchRequestUrl(new URL('wss://test.mswjs.io'), 'wss://test.mswjs.io'),
85
+ ).toEqual({
86
+ matches: true,
87
+ params: {},
88
+ })
89
+ })
90
+
91
+ test('returns false for non-matching WebSocket URL', () => {
92
+ expect(
93
+ matchRequestUrl(new URL('ws://test.mswjs.io'), 'ws://foo.mswjs.io'),
94
+ ).toEqual({
95
+ matches: false,
96
+ params: {},
97
+ })
98
+ expect(
99
+ matchRequestUrl(new URL('wss://test.mswjs.io'), 'wss://completely.diff'),
100
+ ).toEqual({
101
+ matches: false,
102
+ params: {},
103
+ })
104
+ })
105
+
106
+ test('returns path parameters when matched a WebSocket URL', () => {
107
+ expect(
108
+ matchRequestUrl(
109
+ new URL('wss://test.mswjs.io'),
110
+ 'wss://:service.mswjs.io',
111
+ ),
112
+ ).toEqual({
113
+ matches: true,
114
+ params: {
115
+ service: 'test',
116
+ },
117
+ })
118
+ })
75
119
  })
76
120
 
77
121
  describe('coercePath', () => {
@@ -71,3 +71,7 @@ export function matchRequestUrl(url: URL, path: Path, baseUrl?: string): Match {
71
71
  params,
72
72
  }
73
73
  }
74
+
75
+ export function isPath(value: unknown): value is Path {
76
+ return typeof value === 'string' || value instanceof RegExp
77
+ }