msw 2.3.0-ws.rc-5 → 2.3.0-ws.rc-7

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 (77) hide show
  1. package/README.md +3 -9
  2. package/lib/browser/index.js +160 -62
  3. package/lib/browser/index.js.map +1 -1
  4. package/lib/browser/index.mjs +160 -62
  5. package/lib/browser/index.mjs.map +1 -1
  6. package/lib/core/handlers/WebSocketHandler.js +1 -2
  7. package/lib/core/handlers/WebSocketHandler.js.map +1 -1
  8. package/lib/core/handlers/WebSocketHandler.mjs +1 -2
  9. package/lib/core/handlers/WebSocketHandler.mjs.map +1 -1
  10. package/lib/core/utils/internal/Disposable.d.mts +2 -2
  11. package/lib/core/utils/internal/Disposable.d.ts +2 -2
  12. package/lib/core/utils/internal/Disposable.js +5 -2
  13. package/lib/core/utils/internal/Disposable.js.map +1 -1
  14. package/lib/core/utils/internal/Disposable.mjs +5 -2
  15. package/lib/core/utils/internal/Disposable.mjs.map +1 -1
  16. package/lib/core/utils/internal/devUtils.d.mts +10 -1
  17. package/lib/core/utils/internal/devUtils.d.ts +10 -1
  18. package/lib/core/utils/internal/devUtils.js +7 -0
  19. package/lib/core/utils/internal/devUtils.js.map +1 -1
  20. package/lib/core/utils/internal/devUtils.mjs +7 -0
  21. package/lib/core/utils/internal/devUtils.mjs.map +1 -1
  22. package/lib/core/utils/matching/normalizePath.d.mts +1 -0
  23. package/lib/core/utils/matching/normalizePath.d.ts +1 -0
  24. package/lib/core/utils/matching/normalizePath.js.map +1 -1
  25. package/lib/core/utils/matching/normalizePath.mjs.map +1 -1
  26. package/lib/core/utils/request/onUnhandledRequest.js +3 -3
  27. package/lib/core/utils/request/onUnhandledRequest.js.map +1 -1
  28. package/lib/core/utils/request/onUnhandledRequest.mjs +4 -4
  29. package/lib/core/utils/request/onUnhandledRequest.mjs.map +1 -1
  30. package/lib/core/utils/url/cleanUrl.d.mts +2 -1
  31. package/lib/core/utils/url/cleanUrl.d.ts +2 -1
  32. package/lib/core/utils/url/cleanUrl.js +3 -0
  33. package/lib/core/utils/url/cleanUrl.js.map +1 -1
  34. package/lib/core/utils/url/cleanUrl.mjs +3 -0
  35. package/lib/core/utils/url/cleanUrl.mjs.map +1 -1
  36. package/lib/core/ws/WebSocketClientManager.d.mts +9 -15
  37. package/lib/core/ws/WebSocketClientManager.d.ts +9 -15
  38. package/lib/core/ws/WebSocketClientManager.js +73 -34
  39. package/lib/core/ws/WebSocketClientManager.js.map +1 -1
  40. package/lib/core/ws/WebSocketClientManager.mjs +73 -34
  41. package/lib/core/ws/WebSocketClientManager.mjs.map +1 -1
  42. package/lib/core/ws.js +4 -2
  43. package/lib/core/ws.js.map +1 -1
  44. package/lib/core/ws.mjs +4 -2
  45. package/lib/core/ws.mjs.map +1 -1
  46. package/lib/iife/index.js +278 -113
  47. package/lib/iife/index.js.map +1 -1
  48. package/lib/mockServiceWorker.js +1 -1
  49. package/lib/native/index.js +5 -0
  50. package/lib/native/index.js.map +1 -1
  51. package/lib/native/index.mjs +6 -1
  52. package/lib/native/index.mjs.map +1 -1
  53. package/lib/node/index.js +5 -0
  54. package/lib/node/index.js.map +1 -1
  55. package/lib/node/index.mjs +6 -1
  56. package/lib/node/index.mjs.map +1 -1
  57. package/package.json +17 -5
  58. package/src/browser/setupWorker/start/createStartHandler.ts +6 -0
  59. package/src/browser/setupWorker/stop/createStop.ts +4 -0
  60. package/src/core/handlers/WebSocketHandler.ts +1 -2
  61. package/src/core/utils/internal/Disposable.ts +6 -3
  62. package/src/core/utils/internal/devUtils.test.ts +21 -0
  63. package/src/core/utils/internal/devUtils.ts +13 -0
  64. package/src/core/utils/matching/matchRequestUrl.test.ts +11 -0
  65. package/src/core/utils/matching/normalizePath.test.ts +7 -1
  66. package/src/core/utils/matching/normalizePath.ts +1 -0
  67. package/src/core/utils/request/onUnhandledRequest.test.ts +30 -4
  68. package/src/core/utils/request/onUnhandledRequest.ts +4 -4
  69. package/src/core/utils/url/cleanUrl.test.ts +8 -3
  70. package/src/core/utils/url/cleanUrl.ts +9 -1
  71. package/src/core/utils/url/getAbsoluteUrl.node.test.ts +3 -3
  72. package/src/core/utils/url/getAbsoluteUrl.test.ts +5 -5
  73. package/src/core/utils/url/isAbsoluteUrl.test.ts +7 -7
  74. package/src/core/ws/WebSocketClientManager.test.ts +43 -45
  75. package/src/core/ws/WebSocketClientManager.ts +107 -44
  76. package/src/core/ws.ts +4 -2
  77. package/src/node/SetupServerCommonApi.ts +7 -1
@@ -3,27 +3,27 @@
3
3
  */
4
4
  import { getAbsoluteUrl } from './getAbsoluteUrl'
5
5
 
6
- test('rebases a relative URL against the current "baseURI" (default)', () => {
6
+ it('rebases a relative URL against the current "baseURI" (default)', () => {
7
7
  expect(getAbsoluteUrl('/reviews')).toEqual('http://localhost/reviews')
8
8
  })
9
9
 
10
- test('rebases a relative URL against a custom base URL', () => {
10
+ it('rebases a relative URL against a custom base URL', () => {
11
11
  expect(getAbsoluteUrl('/user', 'https://api.github.com')).toEqual(
12
12
  'https://api.github.com/user',
13
13
  )
14
14
  })
15
15
 
16
- test('returns a given absolute URL as-is', () => {
16
+ it('returns a given absolute URL as-is', () => {
17
17
  expect(getAbsoluteUrl('https://api.mswjs.io/users')).toEqual(
18
18
  'https://api.mswjs.io/users',
19
19
  )
20
20
  })
21
21
 
22
- test('returns an absolute URL given a relative path without a leading slash', () => {
22
+ it('returns an absolute URL given a relative path without a leading slash', () => {
23
23
  expect(getAbsoluteUrl('users')).toEqual('http://localhost/users')
24
24
  })
25
25
 
26
- test('returns a path with a pattern as-is', () => {
26
+ it('returns a path with a pattern as-is', () => {
27
27
  expect(getAbsoluteUrl(':api/user')).toEqual('http://localhost/:api/user')
28
28
  expect(getAbsoluteUrl('*/resource/*')).toEqual('*/resource/*')
29
29
  })
@@ -3,30 +3,30 @@
3
3
  */
4
4
  import { isAbsoluteUrl } from './isAbsoluteUrl'
5
5
 
6
- test('returns true for the "http" scheme', () => {
6
+ it('returns true for the "http" scheme', () => {
7
7
  expect(isAbsoluteUrl('http://www.domain.com')).toEqual(true)
8
8
  })
9
9
 
10
- test('returns true for the "https" scheme', () => {
10
+ it('returns true for the "https" scheme', () => {
11
11
  expect(isAbsoluteUrl('https://www.domain.com')).toEqual(true)
12
12
  })
13
13
 
14
- test('returns true for the "ws" scheme', () => {
14
+ it('returns true for the "ws" scheme', () => {
15
15
  expect(isAbsoluteUrl('ws://www.domain.com')).toEqual(true)
16
16
  })
17
17
 
18
- test('returns true for the "ftp" scheme', () => {
18
+ it('returns true for the "ftp" scheme', () => {
19
19
  expect(isAbsoluteUrl('ftp://www.domain.com')).toEqual(true)
20
20
  })
21
21
 
22
- test('returns true for the custom scheme', () => {
22
+ it('returns true for the custom scheme', () => {
23
23
  expect(isAbsoluteUrl('web+my://www.example.com')).toEqual(true)
24
24
  })
25
25
 
26
- test('returns false for the relative URL', () => {
26
+ it('returns false for the relative URL', () => {
27
27
  expect(isAbsoluteUrl('/test')).toEqual(false)
28
28
  })
29
29
 
30
- test('returns false for the relative URL without a leading slash', () => {
30
+ it('returns false for the relative URL without a leading slash', () => {
31
31
  expect(isAbsoluteUrl('test')).toEqual(false)
32
32
  })
@@ -1,79 +1,73 @@
1
1
  /**
2
2
  * @vitest-environment node-websocket
3
3
  */
4
- import { randomUUID } from 'node:crypto'
5
4
  import {
6
5
  WebSocketClientConnection,
6
+ WebSocketData,
7
7
  WebSocketTransport,
8
8
  } from '@mswjs/interceptors/WebSocket'
9
9
  import {
10
10
  WebSocketClientManager,
11
11
  WebSocketBroadcastChannelMessage,
12
- WebSocketRemoteClientConnection,
13
12
  } from './WebSocketClientManager'
14
13
 
15
14
  const channel = new BroadcastChannel('test:channel')
16
15
  vi.spyOn(channel, 'postMessage')
17
16
 
18
17
  const socket = new WebSocket('ws://localhost')
19
- const transport = {
20
- onOutgoing: vi.fn(),
21
- onIncoming: vi.fn(),
22
- onClose: vi.fn(),
23
- send: vi.fn(),
24
- close: vi.fn(),
25
- } satisfies WebSocketTransport
18
+
19
+ class TestWebSocketTransport extends EventTarget implements WebSocketTransport {
20
+ send(_data: WebSocketData): void {}
21
+ close(_code?: number | undefined, _reason?: string | undefined): void {}
22
+ }
26
23
 
27
24
  afterEach(() => {
28
25
  vi.resetAllMocks()
29
26
  })
30
27
 
31
28
  it('adds a client from this runtime to the list of clients', () => {
32
- const manager = new WebSocketClientManager(channel)
33
- const connection = new WebSocketClientConnection(socket, transport)
29
+ const manager = new WebSocketClientManager(channel, '*')
30
+ const connection = new WebSocketClientConnection(
31
+ socket,
32
+ new TestWebSocketTransport(),
33
+ )
34
34
 
35
35
  manager.addConnection(connection)
36
36
 
37
37
  // Must add the client to the list of clients.
38
38
  expect(Array.from(manager.clients.values())).toEqual([connection])
39
-
40
- // Must emit the connection open event to notify other runtimes.
41
- expect(channel.postMessage).toHaveBeenCalledWith({
42
- type: 'connection:open',
43
- payload: {
44
- clientId: connection.id,
45
- url: socket.url,
46
- },
47
- } satisfies WebSocketBroadcastChannelMessage)
48
39
  })
49
40
 
50
- it('adds a client from another runtime to the list of clients', async () => {
51
- const clientId = randomUUID()
52
- const url = new URL('ws://localhost')
53
- const manager = new WebSocketClientManager(channel)
41
+ it('adds multiple clients from this runtime to the list of clients', () => {
42
+ const manager = new WebSocketClientManager(channel, '*')
43
+ const connectionOne = new WebSocketClientConnection(
44
+ socket,
45
+ new TestWebSocketTransport(),
46
+ )
47
+ manager.addConnection(connectionOne)
54
48
 
55
- channel.dispatchEvent(
56
- new MessageEvent<WebSocketBroadcastChannelMessage>('message', {
57
- data: {
58
- type: 'connection:open',
59
- payload: {
60
- clientId,
61
- url: url.href,
62
- },
63
- },
64
- }),
49
+ // Must add the client to the list of clients.
50
+ expect(Array.from(manager.clients.values())).toEqual([connectionOne])
51
+
52
+ const connectionTwo = new WebSocketClientConnection(
53
+ socket,
54
+ new TestWebSocketTransport(),
65
55
  )
56
+ manager.addConnection(connectionTwo)
66
57
 
67
- await vi.waitFor(() => {
68
- expect(Array.from(manager.clients.values())).toEqual([
69
- new WebSocketRemoteClientConnection(clientId, url, channel),
70
- ])
71
- })
58
+ // Must add the new cilent to the list as well.
59
+ expect(Array.from(manager.clients.values())).toEqual([
60
+ connectionOne,
61
+ connectionTwo,
62
+ ])
72
63
  })
73
64
 
74
65
  it('replays a "send" event coming from another runtime', async () => {
75
- const manager = new WebSocketClientManager(channel)
76
- const connection = new WebSocketClientConnection(socket, transport)
66
+ const manager = new WebSocketClientManager(channel, '*')
67
+ const connection = new WebSocketClientConnection(
68
+ socket,
69
+ new TestWebSocketTransport(),
70
+ )
77
71
  manager.addConnection(connection)
78
72
  vi.spyOn(connection, 'send')
79
73
 
@@ -98,8 +92,11 @@ it('replays a "send" event coming from another runtime', async () => {
98
92
  })
99
93
 
100
94
  it('replays a "close" event coming from another runtime', async () => {
101
- const manager = new WebSocketClientManager(channel)
102
- const connection = new WebSocketClientConnection(socket, transport)
95
+ const manager = new WebSocketClientManager(channel, '*')
96
+ const connection = new WebSocketClientConnection(
97
+ socket,
98
+ new TestWebSocketTransport(),
99
+ )
103
100
  manager.addConnection(connection)
104
101
  vi.spyOn(connection, 'close')
105
102
 
@@ -125,7 +122,8 @@ it('replays a "close" event coming from another runtime', async () => {
125
122
  })
126
123
 
127
124
  it('removes the extraneous message listener when the connection closes', async () => {
128
- const manager = new WebSocketClientManager(channel)
125
+ const manager = new WebSocketClientManager(channel, '*')
126
+ const transport = new TestWebSocketTransport()
129
127
  const connection = new WebSocketClientConnection(socket, transport)
130
128
  vi.spyOn(connection, 'close').mockImplementationOnce(() => {
131
129
  /**
@@ -135,7 +133,7 @@ it('removes the extraneous message listener when the connection closes', async (
135
133
  * All we care here is that closing the connection triggers
136
134
  * the transport closure, which it always does.
137
135
  */
138
- connection['transport'].onClose()
136
+ transport.dispatchEvent(new Event('close'))
139
137
  })
140
138
  vi.spyOn(connection, 'send')
141
139
 
@@ -1,17 +1,14 @@
1
+ import { invariant } from 'outvariant'
1
2
  import type {
2
3
  WebSocketData,
3
4
  WebSocketClientConnection,
4
5
  WebSocketClientConnectionProtocol,
5
6
  } from '@mswjs/interceptors/WebSocket'
7
+ import { matchRequestUrl, type Path } from '../utils/matching/matchRequestUrl'
8
+
9
+ export const MSW_WEBSOCKET_CLIENTS_KEY = 'msw:ws:clients'
6
10
 
7
11
  export type WebSocketBroadcastChannelMessage =
8
- | {
9
- type: 'connection:open'
10
- payload: {
11
- clientId: string
12
- url: string
13
- }
14
- }
15
12
  | {
16
13
  type: 'extraneous:send'
17
14
  payload: {
@@ -28,33 +25,121 @@ export type WebSocketBroadcastChannelMessage =
28
25
  }
29
26
  }
30
27
 
31
- export const kAddByClientId = Symbol('kAddByClientId')
28
+ type SerializedClient = {
29
+ clientId: string
30
+ url: string
31
+ }
32
32
 
33
33
  /**
34
34
  * A manager responsible for accumulating WebSocket client
35
35
  * connections across different browser runtimes.
36
36
  */
37
37
  export class WebSocketClientManager {
38
+ private inMemoryClients: Set<WebSocketClientConnectionProtocol>
39
+
40
+ constructor(
41
+ private channel: BroadcastChannel,
42
+ private url: Path,
43
+ ) {
44
+ this.inMemoryClients = new Set()
45
+
46
+ // Purge in-memory clients when the worker stops.
47
+ if (typeof localStorage !== 'undefined') {
48
+ localStorage.removeItem = new Proxy(localStorage.removeItem, {
49
+ apply: (target, thisArg, args) => {
50
+ const [key] = args
51
+
52
+ if (key === MSW_WEBSOCKET_CLIENTS_KEY) {
53
+ this.inMemoryClients.clear()
54
+ }
55
+
56
+ return Reflect.apply(target, thisArg, args)
57
+ },
58
+ })
59
+ }
60
+ }
61
+
38
62
  /**
39
63
  * All active WebSocket client connections.
40
64
  */
41
- public clients: Set<WebSocketClientConnectionProtocol>
65
+ get clients(): Set<WebSocketClientConnectionProtocol> {
66
+ // In the browser, different runtimes use "localStorage"
67
+ // as the shared source of all the clients.
68
+ if (typeof localStorage !== 'undefined') {
69
+ const inMemoryClients = Array.from(this.inMemoryClients)
70
+
71
+ console.log('get clients()', inMemoryClients, this.getSerializedClients())
72
+
73
+ return new Set(
74
+ inMemoryClients.concat(
75
+ this.getSerializedClients()
76
+ // Filter out the serialized clients that are already present
77
+ // in this runtime in-memory. This is crucial because a remote client
78
+ // wrapper CANNOT send a message to the client in THIS runtime
79
+ // (the "message" event on broadcast channel won't trigger).
80
+ .filter((serializedClient) => {
81
+ if (
82
+ inMemoryClients.every(
83
+ (client) => client.id !== serializedClient.clientId,
84
+ )
85
+ ) {
86
+ return serializedClient
87
+ }
88
+ })
89
+ .map((serializedClient) => {
90
+ return new WebSocketRemoteClientConnection(
91
+ serializedClient.clientId,
92
+ new URL(serializedClient.url),
93
+ this.channel,
94
+ )
95
+ }),
96
+ ),
97
+ )
98
+ }
99
+
100
+ // In Node.js, the manager acts as a singleton, and all clients
101
+ // are kept in-memory.
102
+ return this.inMemoryClients
103
+ }
42
104
 
43
- constructor(private channel: BroadcastChannel) {
44
- this.clients = new Set()
105
+ private getSerializedClients(): Array<SerializedClient> {
106
+ invariant(
107
+ typeof localStorage !== 'undefined',
108
+ 'Failed to call WebSocketClientManager#getSerializedClients() in a non-browser environment. This is likely a bug in MSW. Please, report it on GitHub: https://github.com/mswjs/msw',
109
+ )
45
110
 
46
- this.channel.addEventListener('message', (message) => {
47
- const { type, payload } = message.data as WebSocketBroadcastChannelMessage
111
+ const clientsJson = localStorage.getItem(MSW_WEBSOCKET_CLIENTS_KEY)
48
112
 
49
- switch (type) {
50
- case 'connection:open': {
51
- // When another runtime notifies about a new connection,
52
- // create a connection wrapper class and add it to the set.
53
- this.onRemoteConnection(payload.clientId, new URL(payload.url))
54
- break
55
- }
56
- }
113
+ if (!clientsJson) {
114
+ return []
115
+ }
116
+
117
+ const allClients = JSON.parse(clientsJson) as Array<SerializedClient>
118
+ const matchingClients = allClients.filter((client) => {
119
+ return matchRequestUrl(new URL(client.url), this.url).matches
57
120
  })
121
+
122
+ return matchingClients
123
+ }
124
+
125
+ private addClient(client: WebSocketClientConnection): void {
126
+ this.inMemoryClients.add(client)
127
+
128
+ if (typeof localStorage !== 'undefined') {
129
+ const serializedClients = this.getSerializedClients()
130
+
131
+ // Serialize the current client for other runtimes to create
132
+ // a remote wrapper over it. This has no effect on the current runtime.
133
+ const nextSerializedClients = serializedClients.concat({
134
+ clientId: client.id,
135
+ url: client.url.href,
136
+ } as SerializedClient)
137
+
138
+ localStorage.setItem(
139
+ MSW_WEBSOCKET_CLIENTS_KEY,
140
+ JSON.stringify(nextSerializedClients),
141
+ )
142
+ }
58
143
  }
59
144
 
60
145
  /**
@@ -64,16 +149,7 @@ export class WebSocketClientManager {
64
149
  * for the opened connections in the same runtime.
65
150
  */
66
151
  public addConnection(client: WebSocketClientConnection): void {
67
- this.clients.add(client)
68
-
69
- // Signal to other runtimes about this connection.
70
- this.channel.postMessage({
71
- type: 'connection:open',
72
- payload: {
73
- clientId: client.id,
74
- url: client.url.toString(),
75
- },
76
- } as WebSocketBroadcastChannelMessage)
152
+ this.addClient(client)
77
153
 
78
154
  // Instruct the current client how to handle events
79
155
  // coming from other runtimes (e.g. when calling `.broadcast()`).
@@ -116,19 +192,6 @@ export class WebSocketClientManager {
116
192
  once: true,
117
193
  })
118
194
  }
119
-
120
- /**
121
- * Adds a client connection wrapper to operate with
122
- * WebSocket client connections in other runtimes.
123
- */
124
- private onRemoteConnection(id: string, url: URL): void {
125
- this.clients.add(
126
- // Create a connection-compatible instance that can
127
- // operate with this client from a different runtime
128
- // using the BroadcastChannel messages.
129
- new WebSocketRemoteClientConnection(id, url, this.channel),
130
- )
131
- }
132
195
  }
133
196
 
134
197
  /**
package/src/core/ws.ts CHANGED
@@ -72,10 +72,12 @@ function createWebSocketLinkHandler(url: Path): WebSocketLink {
72
72
  typeof url,
73
73
  )
74
74
 
75
- const clientManager = new WebSocketClientManager(wsBroadcastChannel)
75
+ const clientManager = new WebSocketClientManager(wsBroadcastChannel, url)
76
76
 
77
77
  return {
78
- clients: clientManager.clients,
78
+ get clients() {
79
+ return clientManager.clients
80
+ },
79
81
  on(event, listener) {
80
82
  const handler = new WebSocketHandler(url)
81
83
 
@@ -16,7 +16,7 @@ import { handleRequest } from '~/core/utils/handleRequest'
16
16
  import type { RequestHandler } from '~/core/handlers/RequestHandler'
17
17
  import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
18
18
  import { mergeRight } from '~/core/utils/internal/mergeRight'
19
- import { devUtils } from '~/core/utils/internal/devUtils'
19
+ import { InternalError, devUtils } from '~/core/utils/internal/devUtils'
20
20
  import type { SetupServerCommon } from './glossary'
21
21
  import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent'
22
22
  import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor'
@@ -71,6 +71,12 @@ export class SetupServerCommonApi
71
71
  return
72
72
  })
73
73
 
74
+ this.interceptor.on('unhandledException', ({ error }) => {
75
+ if (error instanceof InternalError) {
76
+ throw error
77
+ }
78
+ })
79
+
74
80
  this.interceptor.on(
75
81
  'response',
76
82
  ({ response, isMockedResponse, request, requestId }) => {