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

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.
@@ -8,7 +8,7 @@
8
8
  * - Please do NOT serve this file on production.
9
9
  */
10
10
 
11
- const PACKAGE_VERSION = '2.3.0-ws.rc-5'
11
+ const PACKAGE_VERSION = '2.3.0-ws.rc-6'
12
12
  const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
13
13
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
14
14
  const activeClientIds = new Set()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw",
3
- "version": "2.3.0-ws.rc-5",
3
+ "version": "2.3.0-ws.rc-6",
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",
@@ -1,4 +1,5 @@
1
1
  import { devUtils } from '~/core/utils/internal/devUtils'
2
+ import { MSW_WEBSOCKET_CLIENTS_KEY } from '~/core/ws/WebSocketClientManager'
2
3
  import { SetupWorkerInternalContext, StopHandler } from '../glossary'
3
4
  import { printStopMessage } from './utils/printStopMessage'
4
5
 
@@ -24,6 +25,9 @@ export const createStop = (
24
25
  context.isMockingEnabled = false
25
26
  window.clearInterval(context.keepAliveInterval)
26
27
 
28
+ // Clear the WebSocket clients from the shared storage.
29
+ localStorage.removeItem(MSW_WEBSOCKET_CLIENTS_KEY)
30
+
27
31
  printStopMessage({ quiet: context.startOptions?.quiet })
28
32
  }
29
33
  }
@@ -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,122 @@ 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
+ if (typeof localStorage !== 'undefined') {
47
+ // When the worker clears the local storage key in "worker.stop()",
48
+ // also clear the in-memory clients map.
49
+ localStorage.removeItem = new Proxy(localStorage.removeItem, {
50
+ apply: (target, thisArg, args) => {
51
+ const [key] = args
52
+
53
+ if (key === MSW_WEBSOCKET_CLIENTS_KEY) {
54
+ this.inMemoryClients.clear()
55
+ }
56
+
57
+ return Reflect.apply(target, thisArg, args)
58
+ },
59
+ })
60
+ }
61
+ }
62
+
38
63
  /**
39
64
  * All active WebSocket client connections.
40
65
  */
41
- public clients: Set<WebSocketClientConnectionProtocol>
66
+ get clients(): Set<WebSocketClientConnectionProtocol> {
67
+ // In the browser, different runtimes use "localStorage"
68
+ // as the shared source of all the clients.
69
+ if (typeof localStorage !== 'undefined') {
70
+ const inMemoryClients = Array.from(this.inMemoryClients)
71
+
72
+ console.log('get clients()', inMemoryClients, this.getSerializedClients())
73
+
74
+ return new Set(
75
+ inMemoryClients.concat(
76
+ this.getSerializedClients()
77
+ // Filter out the serialized clients that are already present
78
+ // in this runtime in-memory. This is crucial because a remote client
79
+ // wrapper CANNOT send a message to the client in THIS runtime
80
+ // (the "message" event on broadcast channel won't trigger).
81
+ .filter((serializedClient) => {
82
+ if (
83
+ inMemoryClients.every(
84
+ (client) => client.id !== serializedClient.clientId,
85
+ )
86
+ ) {
87
+ return serializedClient
88
+ }
89
+ })
90
+ .map((serializedClient) => {
91
+ return new WebSocketRemoteClientConnection(
92
+ serializedClient.clientId,
93
+ new URL(serializedClient.url),
94
+ this.channel,
95
+ )
96
+ }),
97
+ ),
98
+ )
99
+ }
100
+
101
+ // In Node.js, the manager acts as a singleton, and all clients
102
+ // are kept in-memory.
103
+ return this.inMemoryClients
104
+ }
42
105
 
43
- constructor(private channel: BroadcastChannel) {
44
- this.clients = new Set()
106
+ private getSerializedClients(): Array<SerializedClient> {
107
+ invariant(
108
+ typeof localStorage !== 'undefined',
109
+ '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',
110
+ )
45
111
 
46
- this.channel.addEventListener('message', (message) => {
47
- const { type, payload } = message.data as WebSocketBroadcastChannelMessage
112
+ const clientsJson = localStorage.getItem(MSW_WEBSOCKET_CLIENTS_KEY)
48
113
 
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
- }
114
+ if (!clientsJson) {
115
+ return []
116
+ }
117
+
118
+ const allClients = JSON.parse(clientsJson) as Array<SerializedClient>
119
+ const matchingClients = allClients.filter((client) => {
120
+ return matchRequestUrl(new URL(client.url), this.url).matches
57
121
  })
122
+
123
+ return matchingClients
124
+ }
125
+
126
+ private addClient(client: WebSocketClientConnection): void {
127
+ this.inMemoryClients.add(client)
128
+
129
+ if (typeof localStorage !== 'undefined') {
130
+ const serializedClients = this.getSerializedClients()
131
+
132
+ // Serialize the current client for other runtimes to create
133
+ // a remote wrapper over it. This has no effect on the current runtime.
134
+ const nextSerializedClients = serializedClients.concat({
135
+ clientId: client.id,
136
+ url: client.url.href,
137
+ } as SerializedClient)
138
+
139
+ localStorage.setItem(
140
+ MSW_WEBSOCKET_CLIENTS_KEY,
141
+ JSON.stringify(nextSerializedClients),
142
+ )
143
+ }
58
144
  }
59
145
 
60
146
  /**
@@ -64,16 +150,7 @@ export class WebSocketClientManager {
64
150
  * for the opened connections in the same runtime.
65
151
  */
66
152
  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)
153
+ this.addClient(client)
77
154
 
78
155
  // Instruct the current client how to handle events
79
156
  // coming from other runtimes (e.g. when calling `.broadcast()`).
@@ -116,19 +193,6 @@ export class WebSocketClientManager {
116
193
  once: true,
117
194
  })
118
195
  }
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
196
  }
133
197
 
134
198
  /**
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