{"version":3,"sources":["../../../src/core/ws/WebSocketClientManager.ts"],"sourcesContent":["import type {\n  WebSocketData,\n  WebSocketClientConnectionProtocol,\n  WebSocketClientEventMap,\n} from '@mswjs/interceptors/WebSocket'\nimport { WebSocketClientStore } from './WebSocketClientStore'\nimport { WebSocketMemoryClientStore } from './WebSocketMemoryClientStore'\nimport { WebSocketIndexedDBClientStore } from './WebSocketIndexedDBClientStore'\n\nexport type WebSocketBroadcastChannelMessage =\n  | {\n      type: 'extraneous:send'\n      payload: {\n        clientId: string\n        data: WebSocketData\n      }\n    }\n  | {\n      type: 'extraneous:close'\n      payload: {\n        clientId: string\n        code?: number\n        reason?: string\n      }\n    }\n\n/**\n * A manager responsible for accumulating WebSocket client\n * connections across different browser runtimes.\n */\nexport class WebSocketClientManager {\n  private store: WebSocketClientStore\n  private runtimeClients: Map<string, WebSocketClientConnectionProtocol>\n  private allClients: Set<WebSocketClientConnectionProtocol>\n\n  constructor(private channel: BroadcastChannel) {\n    // Store the clients in the IndexedDB in the browser,\n    // otherwise, store the clients in memory.\n    this.store =\n      typeof indexedDB !== 'undefined'\n        ? new WebSocketIndexedDBClientStore()\n        : new WebSocketMemoryClientStore()\n\n    this.runtimeClients = new Map()\n    this.allClients = new Set()\n\n    this.channel.addEventListener('message', (message) => {\n      if (message.data?.type === 'db:update') {\n        this.flushDatabaseToMemory()\n      }\n    })\n\n    if (typeof window !== 'undefined') {\n      window.addEventListener('message', async (message) => {\n        if (message.data?.type === 'msw/worker:stop') {\n          await this.removeRuntimeClients()\n        }\n      })\n    }\n  }\n\n  private async flushDatabaseToMemory() {\n    const storedClients = await this.store.getAll()\n\n    this.allClients = new Set(\n      storedClients.map((client) => {\n        const runtimeClient = this.runtimeClients.get(client.id)\n\n        /**\n         * @note For clients originating in this runtime, use their\n         * direct references. No need to wrap them in a remote connection.\n         */\n        if (runtimeClient) {\n          return runtimeClient\n        }\n\n        return new WebSocketRemoteClientConnection(\n          client.id,\n          new URL(client.url),\n          this.channel,\n        )\n      }),\n    )\n  }\n\n  private async removeRuntimeClients(): Promise<void> {\n    await this.store.deleteMany(Array.from(this.runtimeClients.keys()))\n    this.runtimeClients.clear()\n    await this.flushDatabaseToMemory()\n    this.notifyOthersAboutDatabaseUpdate()\n  }\n\n  /**\n   * All active WebSocket client connections.\n   */\n  get clients(): Set<WebSocketClientConnectionProtocol> {\n    return this.allClients\n  }\n\n  /**\n   * Notify other runtimes about the database update\n   * using the shared `BroadcastChannel` instance.\n   */\n  private notifyOthersAboutDatabaseUpdate(): void {\n    this.channel.postMessage({ type: 'db:update' })\n  }\n\n  private async addClient(\n    client: WebSocketClientConnectionProtocol,\n  ): Promise<void> {\n    await this.store.add(client)\n    // Sync the in-memory clients in this runtime with the\n    // updated database. This pulls in all the stored clients.\n    await this.flushDatabaseToMemory()\n    this.notifyOthersAboutDatabaseUpdate()\n  }\n\n  /**\n   * Adds the given `WebSocket` client connection to the set\n   * of all connections. The given connection is always the complete\n   * connection object because `addConnection()` is called only\n   * for the opened connections in the same runtime.\n   */\n  public async addConnection(\n    client: WebSocketClientConnectionProtocol,\n  ): Promise<void> {\n    // Store this client in the map of clients created in this runtime.\n    // This way, the manager can distinguish between this runtime clients\n    // and extraneous runtime clients when synchronizing clients storage.\n    this.runtimeClients.set(client.id, client)\n\n    // Add the new client to the storage.\n    await this.addClient(client)\n\n    // Handle the incoming BroadcastChannel messages from other runtimes\n    // that attempt to control this runtime (via a remote connection wrapper).\n    // E.g. another runtime calling `client.send()` for the client in this runtime.\n    const handleExtraneousMessage = (\n      message: MessageEvent<WebSocketBroadcastChannelMessage>,\n    ) => {\n      const { type, payload } = message.data\n\n      // Ignore broadcasted messages for other clients.\n      if (\n        typeof payload === 'object' &&\n        'clientId' in payload &&\n        payload.clientId !== client.id\n      ) {\n        return\n      }\n\n      switch (type) {\n        case 'extraneous:send': {\n          client.send(payload.data)\n          break\n        }\n\n        case 'extraneous:close': {\n          client.close(payload.code, payload.reason)\n          break\n        }\n      }\n    }\n\n    const abortController = new AbortController()\n\n    this.channel.addEventListener('message', handleExtraneousMessage, {\n      signal: abortController.signal,\n    })\n\n    // Once closed, this connection cannot be operated on.\n    // This must include the extraneous runtimes as well.\n    client.addEventListener('close', () => abortController.abort(), {\n      once: true,\n    })\n  }\n}\n\n/**\n * A wrapper class to operate with WebSocket client connections\n * from other runtimes. This class maintains 1-1 public API\n * compatibility to the `WebSocketClientConnection` but relies\n * on the given `BroadcastChannel` to communicate instructions\n * with the client connections from other runtimes.\n */\nexport class WebSocketRemoteClientConnection\n  implements WebSocketClientConnectionProtocol\n{\n  constructor(\n    public readonly id: string,\n    public readonly url: URL,\n    private channel: BroadcastChannel,\n  ) {}\n\n  send(data: WebSocketData): void {\n    this.channel.postMessage({\n      type: 'extraneous:send',\n      payload: {\n        clientId: this.id,\n        data,\n      },\n    } as WebSocketBroadcastChannelMessage)\n  }\n\n  close(code?: number | undefined, reason?: string | undefined): void {\n    this.channel.postMessage({\n      type: 'extraneous:close',\n      payload: {\n        clientId: this.id,\n        code,\n        reason,\n      },\n    } as WebSocketBroadcastChannelMessage)\n  }\n\n  addEventListener<EventType extends keyof WebSocketClientEventMap>(\n    _type: EventType,\n    _listener: (\n      this: WebSocket,\n      event: WebSocketClientEventMap[EventType],\n    ) => void,\n    _options?: AddEventListenerOptions | boolean,\n  ): void {\n    throw new Error(\n      'WebSocketRemoteClientConnection.addEventListener is not supported',\n    )\n  }\n\n  removeEventListener<EventType extends keyof WebSocketClientEventMap>(\n    _event: EventType,\n    _listener: (\n      this: WebSocket,\n      event: WebSocketClientEventMap[EventType],\n    ) => void,\n    _options?: EventListenerOptions | boolean,\n  ): void {\n    throw new Error(\n      'WebSocketRemoteClientConnection.removeEventListener is not supported',\n    )\n  }\n}\n"],"mappings":"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","names":[]}