{"version":3,"sources":["../../src/core/ws.ts"],"sourcesContent":["import { invariant } from 'outvariant'\nimport { FetchResponse, resolveWebSocketUrl } from '@mswjs/interceptors'\nimport type {\n  WebSocketData,\n  WebSocketClientConnectionProtocol,\n} from '@mswjs/interceptors/WebSocket'\nimport {\n  WebSocketHandler,\n  kEmitter,\n  type WebSocketHandlerEventMap,\n} from './handlers/WebSocketHandler'\nimport { hasRefCounted } from './utils/internal/hasRefCounted'\nimport {\n  type Path,\n  type PathParams,\n  isPath,\n  matchRequestUrl,\n} from './utils/matching/matchRequestUrl'\nimport { WebSocketClientManager } from './ws/WebSocketClientManager'\nimport { http } from './http'\nimport { attachSiblingHandlers } from './utils/internal/attachSiblingHandlers'\n\nconst webSocketChannel = new BroadcastChannel('msw:websocket-client-manager')\n\nif (hasRefCounted(webSocketChannel)) {\n  // Allows the Node.js thread to exit if it is the only active handle in the event system.\n  // https://nodejs.org/api/worker_threads.html#broadcastchannelunref\n  webSocketChannel.unref()\n}\n\nexport type WebSocketEventListener<\n  EventType extends keyof WebSocketHandlerEventMap,\n> = (...args: WebSocketHandlerEventMap[EventType]) => void\n\nexport type WebSocketLink = {\n  /**\n   * A set of all WebSocket clients connected\n   * to this link.\n   *\n   * @see {@link https://mswjs.io/docs/api/ws#clients `clients` API reference}\n   */\n  clients: Set<WebSocketClientConnectionProtocol>\n\n  /**\n   * Adds an event listener to this WebSocket link.\n   *\n   * @example\n   * const chat = ws.link('wss://chat.example.com')\n   * chat.addEventListener('connection', listener)\n   *\n   * @see {@link https://mswjs.io/docs/api/ws#onevent-listener `on()` API reference}\n   */\n  addEventListener: <EventType extends keyof WebSocketHandlerEventMap>(\n    event: EventType,\n    listener: WebSocketEventListener<EventType>,\n  ) => WebSocketHandler\n\n  /**\n   * Broadcasts the given data to all WebSocket clients.\n   *\n   * @example\n   * const service = ws.link('wss://example.com')\n   * service.addEventListener('connection', () => {\n   *   service.broadcast('hello, everyone!')\n   * })\n   *\n   * @see {@link https://mswjs.io/docs/api/ws#broadcastdata `broadcast()` API reference}\n   */\n  broadcast: (data: WebSocketData) => void\n\n  /**\n   * Broadcasts the given data to all WebSocket clients\n   * except the ones provided in the `clients` argument.\n   *\n   * @example\n   * const service = ws.link('wss://example.com')\n   * service.addEventListener('connection', ({ client }) => {\n   *   service.broadcastExcept(client, 'hi, the rest of you!')\n   * })\n   *\n   * @see {@link https://mswjs.io/docs/api/ws#broadcastexceptclients-data `broadcast()` API reference}\n   */\n  broadcastExcept: (\n    clients:\n      | WebSocketClientConnectionProtocol\n      | Array<WebSocketClientConnectionProtocol>,\n    data: WebSocketData,\n  ) => void\n}\n\n/**\n * Intercepts outgoing WebSocket connections to the given URL.\n *\n * @example\n * const chat = ws.link('wss://chat.example.com')\n * chat.addEventListener('connection', ({ client }) => {\n *   client.send('hello from server!')\n * })\n */\nfunction createWebSocketLinkHandler(url: Path): WebSocketLink {\n  invariant(url, 'Expected a WebSocket server URL but got undefined')\n\n  invariant(\n    isPath(url),\n    'Expected a WebSocket server URL to be a valid path but got %s',\n    typeof url,\n  )\n\n  const clientManager = new WebSocketClientManager(webSocketChannel)\n\n  // The same upgrade handler instance is attached as a sibling to every\n  // WebSocketHandler returned by this link. `groupHandlersByKind` dedupes\n  // by reference, so it lands in the `request` bucket exactly once regardless\n  // of which subset of WS handlers the user ends up registering.\n  const upgradeHandler = http.get(({ request }) => {\n    return (\n      request.headers.get('upgrade')?.toLowerCase() === 'websocket' &&\n      matchRequestUrl(new URL(resolveWebSocketUrl(request.url)), url).matches\n    )\n  }, ws.onUpgrade)\n\n  return {\n    get clients() {\n      return clientManager.clients\n    },\n    addEventListener(event, listener) {\n      const webSocketHandler = new WebSocketHandler(url)\n\n      // Add the connection event listener for when the\n      // handler matches and emits a connection event.\n      // When that happens, store that connection in the\n      // set of all connections for reference.\n      webSocketHandler[kEmitter].on('connection', async ({ client }) => {\n        await clientManager.addConnection(client)\n      })\n\n      // The \"handleWebSocketEvent\" function will invoke\n      // the \"run()\" method on the WebSocketHandler.\n      // If the handler matches, it will emit the \"connection\"\n      // event. Attach the user-defined listener to that event.\n      webSocketHandler[kEmitter].on(event, listener)\n\n      return attachSiblingHandlers(webSocketHandler, [upgradeHandler])\n    },\n\n    broadcast(data) {\n      // This will invoke \"send()\" on the immediate clients\n      // in this runtime and post a message to the broadcast channel\n      // to trigger send for the clients in other runtimes.\n      this.broadcastExcept([], data)\n    },\n\n    broadcastExcept(clients, data) {\n      const ignoreClients = Array.prototype\n        .concat(clients)\n        .map((client) => client.id)\n\n      clientManager.clients.forEach((otherClient) => {\n        if (!ignoreClients.includes(otherClient.id)) {\n          otherClient.send(data)\n        }\n      })\n    },\n  }\n}\n\nconst WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'\n\ninterface WebSocketNamespace {\n  link: typeof createWebSocketLinkHandler\n  /**\n   * Request handler for the `upgrade` requests to the WebSocket protocol.\n   * This requires a WebSocket handler to be present to fire.\n   * @note This only affects Node.js as the `upgrade` request header is\n   * forbidden and cannot be read in the browser. Consider using the\n   * `WebSocket` API for establishing WebSocket connections in the browser.\n   */\n  onUpgrade: (info: {\n    requestId: string\n    request: Request\n    params: PathParams\n  }) => Promise<Response | undefined> | Response | undefined\n}\n\n/**\n * A namespace to intercept and mock WebSocket connections.\n *\n * @example\n * const chat = ws.link('wss://chat.example.com')\n *\n * @see {@link https://mswjs.io/docs/api/ws `ws` API reference}\n * @see {@link https://mswjs.io/docs/basics/handling-websocket-events Handling WebSocket events}\n */\nexport const ws: WebSocketNamespace = {\n  link: createWebSocketLinkHandler,\n  async onUpgrade({ request }) {\n    const key = request.headers.get('sec-websocket-key')\n\n    if (!key) {\n      return\n    }\n\n    const keyBytes = new TextEncoder().encode(key + WEBSOCKET_GUID)\n    const digest = await crypto.subtle.digest('SHA-1', keyBytes)\n    const acceptValue = btoa(String.fromCharCode(...new Uint8Array(digest)))\n\n    new WebSocket(resolveWebSocketUrl(request.url))\n\n    return new FetchResponse(null, {\n      status: 101,\n      headers: {\n        upgrade: 'websocket',\n        connection: 'upgrade',\n        'sec-websocket-accept': acceptValue,\n      },\n    })\n  },\n}\n\nexport { type WebSocketData }\n"],"mappings":"AAAA,SAAS,iBAAiB;AAC1B,SAAS,eAAe,2BAA2B;AAKnD;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,qBAAqB;AAC9B;AAAA,EAGE;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AACvC,SAAS,YAAY;AACrB,SAAS,6BAA6B;AAEtC,MAAM,mBAAmB,IAAI,iBAAiB,8BAA8B;AAE5E,IAAI,cAAc,gBAAgB,GAAG;AAGnC,mBAAiB,MAAM;AACzB;AAuEA,SAAS,2BAA2B,KAA0B;AAC5D,YAAU,KAAK,mDAAmD;AAElE;AAAA,IACE,OAAO,GAAG;AAAA,IACV;AAAA,IACA,OAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,IAAI,uBAAuB,gBAAgB;AAMjE,QAAM,iBAAiB,KAAK,IAAI,CAAC,EAAE,QAAQ,MAAM;AAC/C,WACE,QAAQ,QAAQ,IAAI,SAAS,GAAG,YAAY,MAAM,eAClD,gBAAgB,IAAI,IAAI,oBAAoB,QAAQ,GAAG,CAAC,GAAG,GAAG,EAAE;AAAA,EAEpE,GAAG,GAAG,SAAS;AAEf,SAAO;AAAA,IACL,IAAI,UAAU;AACZ,aAAO,cAAc;AAAA,IACvB;AAAA,IACA,iBAAiB,OAAO,UAAU;AAChC,YAAM,mBAAmB,IAAI,iBAAiB,GAAG;AAMjD,uBAAiB,QAAQ,EAAE,GAAG,cAAc,OAAO,EAAE,OAAO,MAAM;AAChE,cAAM,cAAc,cAAc,MAAM;AAAA,MAC1C,CAAC;AAMD,uBAAiB,QAAQ,EAAE,GAAG,OAAO,QAAQ;AAE7C,aAAO,sBAAsB,kBAAkB,CAAC,cAAc,CAAC;AAAA,IACjE;AAAA,IAEA,UAAU,MAAM;AAId,WAAK,gBAAgB,CAAC,GAAG,IAAI;AAAA,IAC/B;AAAA,IAEA,gBAAgB,SAAS,MAAM;AAC7B,YAAM,gBAAgB,MAAM,UACzB,OAAO,OAAO,EACd,IAAI,CAAC,WAAW,OAAO,EAAE;AAE5B,oBAAc,QAAQ,QAAQ,CAAC,gBAAgB;AAC7C,YAAI,CAAC,cAAc,SAAS,YAAY,EAAE,GAAG;AAC3C,sBAAY,KAAK,IAAI;AAAA,QACvB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,MAAM,iBAAiB;AA2BhB,MAAM,KAAyB;AAAA,EACpC,MAAM;AAAA,EACN,MAAM,UAAU,EAAE,QAAQ,GAAG;AAC3B,UAAM,MAAM,QAAQ,QAAQ,IAAI,mBAAmB;AAEnD,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,UAAM,WAAW,IAAI,YAAY,EAAE,OAAO,MAAM,cAAc;AAC9D,UAAM,SAAS,MAAM,OAAO,OAAO,OAAO,SAAS,QAAQ;AAC3D,UAAM,cAAc,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,MAAM,CAAC,CAAC;AAEvE,QAAI,UAAU,oBAAoB,QAAQ,GAAG,CAAC;AAE9C,WAAO,IAAI,cAAc,MAAM;AAAA,MAC7B,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,wBAAwB;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}