{"version":3,"file":"WebSocketService.cjs","sourceRoot":"","sources":["../../src/websocket/WebSocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Messenger } from '@metamask/messenger';\nimport { rpcErrors } from '@metamask/rpc-errors';\nimport type { SnapId, WebSocketEvent } from '@metamask/snaps-sdk';\nimport { HandlerType, isEqual, logError } from '@metamask/snaps-utils';\nimport { assert, createDeferredPromise } from '@metamask/utils';\nimport { nanoid } from 'nanoid';\n\nimport type { WebSocketServiceMethodActions } from './WebSocketService-method-action-types';\nimport type {\n  SnapControllerHandleRequestAction,\n  SnapControllerSnapInstalledEvent,\n  SnapControllerSnapUninstalledEvent,\n  SnapControllerSnapUpdatedEvent,\n} from '../snaps';\nimport { METAMASK_ORIGIN } from '../snaps';\n\nconst serviceName = 'WebSocketService';\n\nconst MESSENGER_EXPOSED_METHODS = [\n  'open',\n  'close',\n  'sendMessage',\n  'getAll',\n] as const;\n\nexport type WebSocketServiceActions = WebSocketServiceMethodActions;\n\ntype AllowedActions = SnapControllerHandleRequestAction;\n\ntype AllowedEvents =\n  | SnapControllerSnapUninstalledEvent\n  | SnapControllerSnapUpdatedEvent\n  | SnapControllerSnapInstalledEvent;\n\nexport type WebSocketServiceMessenger = Messenger<\n  'WebSocketService',\n  WebSocketServiceActions | AllowedActions,\n  AllowedEvents\n>;\n\ntype WebSocketServiceArgs = {\n  messenger: WebSocketServiceMessenger;\n};\n\ntype InternalSocket = {\n  id: string;\n  snapId: SnapId;\n  url: string;\n  protocols: string[];\n  openPromise: Promise<void>;\n  // eslint-disable-next-line no-restricted-globals\n  socket: WebSocket;\n};\n\nexport class WebSocketService {\n  name: typeof serviceName = serviceName;\n\n  state = null;\n\n  readonly #messenger: WebSocketServiceMessenger;\n\n  readonly #sockets: Map<string, InternalSocket>;\n\n  constructor({ messenger }: WebSocketServiceArgs) {\n    this.#messenger = messenger;\n    this.#sockets = new Map();\n\n    this.#messenger.registerMethodActionHandlers(\n      this,\n      MESSENGER_EXPOSED_METHODS,\n    );\n\n    this.#messenger.subscribe('SnapController:snapUpdated', (snap) => {\n      this.#closeAll(snap.id);\n    });\n\n    this.#messenger.subscribe('SnapController:snapUninstalled', (snap) => {\n      this.#closeAll(snap.id);\n    });\n\n    // Due to local Snaps not currently emitting snapUninstalled we also have to\n    // listen to snapInstalled.\n    this.#messenger.subscribe('SnapController:snapInstalled', (snap) => {\n      this.#closeAll(snap.id);\n    });\n  }\n\n  /**\n   * Get information about a given WebSocket connection with an ID.\n   *\n   * @param snapId - The Snap ID.\n   * @param id - The identifier for the WebSocket connection.\n   * @returns Information about the WebSocket connection.\n   * @throws If the WebSocket connection cannot be found.\n   */\n  #get(snapId: SnapId, id: string) {\n    const socket = this.#sockets.get(id);\n\n    assert(\n      socket && socket.snapId === snapId,\n      `Socket with ID \"${id}\" not found.`,\n    );\n\n    return socket;\n  }\n\n  /**\n   * Check whether a given Snap ID already has an open connection for a URL and protocol.\n   *\n   * @param snapId - The Snap ID.\n   * @param url - The URL.\n   * @param protocols - A protocols parameter.\n   * @returns True if a matching connection already exists, otherwise false.\n   */\n  #exists(snapId: SnapId, url: string, protocols: string[]) {\n    return this.getAll(snapId).some(\n      (socket) => socket.url === url && isEqual(socket.protocols, protocols),\n    );\n  }\n\n  /**\n   * Handle sending a specific WebSocketEvent to a Snap.\n   *\n   * @param snapId - The Snap ID.\n   * @param event - The WebSocketEvent.\n   */\n  #handleEvent(snapId: SnapId, event: WebSocketEvent) {\n    this.#messenger\n      .call('SnapController:handleRequest', {\n        origin: METAMASK_ORIGIN,\n        snapId,\n        handler: HandlerType.OnWebSocketEvent,\n        request: { method: '', params: { event } },\n      })\n      .catch((error) => {\n        logError(\n          `An error occurred while handling a WebSocket message for Snap \"${snapId}\":`,\n          error,\n        );\n      });\n  }\n\n  /**\n   * Open a WebSocket connection.\n   *\n   * @param snapId - The Snap ID.\n   * @param url - The URL for the WebSocket connection.\n   * @param protocols - An optional parameter for protocols.\n   * @returns The identifier for the opened connection.\n   * @throws If the connection fails.\n   */\n  async open(snapId: SnapId, url: string, protocols: string[] = []) {\n    assert(\n      !this.#exists(snapId, url, protocols),\n      `An open WebSocket connection to ${url} already exists.`,\n    );\n\n    const parsedUrl = new URL(url);\n    const { origin } = parsedUrl;\n\n    const id = nanoid();\n\n    // eslint-disable-next-line no-restricted-globals\n    const socket = new WebSocket(url, protocols);\n    socket.binaryType = 'arraybuffer';\n\n    const { promise, resolve, reject } = createDeferredPromise();\n\n    socket.addEventListener('open', () => {\n      resolve();\n      this.#handleEvent(snapId, {\n        type: 'open',\n        id,\n        origin,\n      });\n    });\n\n    socket.addEventListener('close', (event) => {\n      this.#sockets.delete(id);\n\n      this.#handleEvent(snapId, {\n        type: 'close',\n        id,\n        origin,\n        code: event.code,\n        reason: event.reason,\n        // wasClean is not available on mobile.\n        wasClean: event.wasClean ?? null,\n      });\n    });\n\n    const errorListener = () => {\n      reject(\n        rpcErrors.resourceUnavailable(\n          'An error occurred while opening the WebSocket.',\n        ),\n      );\n    };\n\n    socket.addEventListener('error', errorListener);\n\n    socket.addEventListener('message', (event) => {\n      const isText = typeof event.data === 'string';\n      const data = isText\n        ? { type: 'text' as const, message: event.data }\n        : {\n            type: 'binary' as const,\n            // We request that the WebSocket gives us an array buffer.\n            message: Array.from(new Uint8Array(event.data)),\n          };\n\n      this.#handleEvent(snapId, {\n        type: 'message',\n        id,\n        origin,\n        data,\n      });\n    });\n\n    this.#sockets.set(id, {\n      id,\n      snapId,\n      url,\n      protocols,\n      socket,\n      openPromise: promise,\n    });\n\n    await promise;\n\n    socket.removeEventListener('error', errorListener);\n\n    return id;\n  }\n\n  /**\n   * Close a given WebSocket connection.\n   *\n   * @param snapId - The Snap ID.\n   * @param id - The identifier for the WebSocket connection.\n   */\n  close(snapId: SnapId, id: string) {\n    const { socket } = this.#get(snapId, id);\n\n    socket.close();\n  }\n\n  /**\n   * Close all open connections for a given Snap ID.\n   *\n   * @param snapId - The Snap ID.\n   */\n  #closeAll(snapId: SnapId) {\n    for (const socket of this.getAll(snapId)) {\n      this.close(snapId, socket.id);\n    }\n  }\n\n  /**\n   * Send a message from a given Snap ID to a WebSocket connection.\n   *\n   * @param snapId - The Snap ID.\n   * @param id - The identifier for the WebSocket connection.\n   * @param data - The message to send.\n   */\n  async sendMessage(snapId: SnapId, id: string, data: string | number[]) {\n    const { socket, openPromise } = this.#get(snapId, id);\n\n    await openPromise;\n\n    const wrappedData = Array.isArray(data) ? new Uint8Array(data) : data;\n\n    socket.send(wrappedData);\n  }\n\n  /**\n   * Get a list of all open WebSocket connections for a Snap ID.\n   *\n   * @param snapId - The Snap ID.\n   * @returns A list of WebSocket connections.\n   */\n  getAll(snapId: SnapId) {\n    return [...this.#sockets.values()]\n      .filter((socket) => socket.snapId === snapId)\n      .map((socket) => ({\n        id: socket.id,\n        url: socket.url,\n        protocols: socket.protocols,\n      }));\n  }\n}\n"]}