1+ import { invariant } from 'outvariant'
12import type {
23 WebSocketData ,
34 WebSocketClientConnection ,
45 WebSocketClientConnectionProtocol ,
56} from '@mswjs/interceptors/WebSocket'
7+ import { matchRequestUrl , type Path } from '../utils/matching/matchRequestUrl'
8+
9+ export const MSW_WEBSOCKET_CLIENTS_KEY = 'msw:ws:clients'
610
711export type WebSocketBroadcastChannelMessage =
8- | {
9- type : 'connection:open'
10- payload : {
11- clientId : string
12- url : string
13- }
14- }
1512 | {
1613 type : 'extraneous:send'
1714 payload : {
@@ -28,33 +25,122 @@ export type WebSocketBroadcastChannelMessage =
2825 }
2926 }
3027
31- export const kAddByClientId = Symbol ( 'kAddByClientId' )
28+ type SerializedClient = {
29+ clientId : string
30+ url : string
31+ }
3232
3333/**
3434 * A manager responsible for accumulating WebSocket client
3535 * connections across different browser runtimes.
3636 */
3737export 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+
3863 /**
3964 * All active WebSocket client connections.
4065 */
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+ }
42105
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+ )
45111
46- this . channel . addEventListener ( 'message' , ( message ) => {
47- const { type, payload } = message . data as WebSocketBroadcastChannelMessage
112+ const clientsJson = localStorage . getItem ( MSW_WEBSOCKET_CLIENTS_KEY )
48113
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
57121 } )
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+ }
58144 }
59145
60146 /**
@@ -64,16 +150,7 @@ export class WebSocketClientManager {
64150 * for the opened connections in the same runtime.
65151 */
66152 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 )
77154
78155 // Instruct the current client how to handle events
79156 // coming from other runtimes (e.g. when calling `.broadcast()`).
@@ -116,19 +193,6 @@ export class WebSocketClientManager {
116193 once : true ,
117194 } )
118195 }
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- }
132196}
133197
134198/**
0 commit comments