{"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import WebSocket, {WebSocketServer} from 'ws'\n\ntype JsonPrimitive = null | string | number | boolean\ntype JsonValue = JsonPrimitive | JsonValue[] | {[key: string]: JsonValue}\n\nexport type WsRelayClientMessage =\n  | {type: 'subscribe'; topic: string}\n  | {type: 'unsubscribe'; topic: string}\n  | {type: 'publish'; topic: string; payload: JsonValue}\n\nexport type WsRelayServerMessage = {\n  topic: string\n  payload: JsonValue\n}\n\nexport type WsRelayServerOptions = WebSocket.ServerOptions & {\n  port?: number\n  onError?: (err: Error) => void\n}\n\nexport type WsRelayServer = {\n  wss: WebSocketServer\n  ready: Promise<void>\n  address: () => WebSocket.AddressInfo | string | null\n  close: () => Promise<void>\n  publish: (topic: string, payload: JsonValue) => void\n  getSubscriberCount: (topic?: string) => number\n}\n\nconst defaultPort = 8080\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst isPayload = (value: unknown): value is JsonValue =>\n  value === null ||\n  typeof value === 'string' ||\n  typeof value === 'number' ||\n  typeof value === 'boolean' ||\n  Array.isArray(value) ||\n  isRecord(value)\n\nconst toError = (err: unknown): Error =>\n  err instanceof Error ? err : new Error(String(err))\n\nconst rawDataToString = (data: WebSocket.RawData): string => {\n  if (typeof data === 'string') {\n    return data\n  }\n\n  if (Array.isArray(data)) {\n    return Buffer.concat(data).toString('utf8')\n  }\n\n  if (data instanceof ArrayBuffer) {\n    return Buffer.from(new Uint8Array(data)).toString('utf8')\n  }\n\n  return data.toString('utf8')\n}\n\nconst parseClientMessage = (\n  data: WebSocket.RawData\n): WsRelayClientMessage | null => {\n  const msg = JSON.parse(rawDataToString(data)) as Partial<WsRelayClientMessage>\n\n  if (!isRecord(msg) || typeof msg.topic !== 'string') {\n    return null\n  }\n\n  if (msg.type === 'subscribe' || msg.type === 'unsubscribe') {\n    return {type: msg.type, topic: msg.topic}\n  }\n\n  if (msg.type === 'publish' && isPayload(msg.payload)) {\n    return {type: 'publish', topic: msg.topic, payload: msg.payload}\n  }\n\n  return null\n}\n\nexport const createWsRelayServer = (\n  options: WsRelayServerOptions = {}\n): WsRelayServer => {\n  const {onError, ...wsOptions} = options\n  let resolveReady = (): void => {}\n  let rejectReady = (_err: Error): void => {}\n  let didSettleReady = false\n  const ready = new Promise<void>((resolve, reject) => {\n    resolveReady = resolve\n    rejectReady = reject\n  })\n  const reportError = (err: unknown): void => {\n    const error = toError(err)\n\n    if (!didSettleReady) {\n      didSettleReady = true\n      rejectReady(error)\n    }\n\n    onError?.(error)\n  }\n  const resolveReadyOnce = (): void => {\n    if (!didSettleReady) {\n      didSettleReady = true\n      resolveReady()\n    }\n  }\n  const wss = new WebSocketServer(\n    {\n      port: wsOptions.server ? undefined : (wsOptions.port ?? defaultPort),\n      ...wsOptions\n    },\n    resolveReadyOnce\n  )\n  const topics = new Map<string, Set<WebSocket>>()\n  const socketTopics = new WeakMap<WebSocket, Set<string>>()\n\n  if (wsOptions.server || wsOptions.noServer) {\n    queueMicrotask(resolveReadyOnce)\n  }\n\n  const unsubscribe = (socket: WebSocket, topic: string): void => {\n    socketTopics.get(socket)?.delete(topic)\n\n    const subscribers = topics.get(topic)\n\n    if (!subscribers) {\n      return\n    }\n\n    subscribers.delete(socket)\n\n    if (subscribers.size === 0) {\n      topics.delete(topic)\n    }\n  }\n\n  const subscribe = (socket: WebSocket, topic: string): void => {\n    let subscribers = topics.get(topic)\n\n    if (!subscribers) {\n      subscribers = new Set()\n      topics.set(topic, subscribers)\n    }\n\n    subscribers.add(socket)\n\n    let subscriptions = socketTopics.get(socket)\n\n    if (!subscriptions) {\n      subscriptions = new Set()\n      socketTopics.set(socket, subscriptions)\n    }\n\n    subscriptions.add(topic)\n  }\n\n  const publish = (topic: string, payload: JsonValue): void => {\n    const msg = JSON.stringify({topic, payload} satisfies WsRelayServerMessage)\n\n    topics.get(topic)?.forEach(socket => {\n      if (socket.readyState === WebSocket.OPEN) {\n        socket.send(msg)\n      }\n    })\n  }\n\n  const cleanup = (socket: WebSocket): void => {\n    socketTopics.get(socket)?.forEach(topic => unsubscribe(socket, topic))\n    socketTopics.delete(socket)\n  }\n\n  wss.on('connection', socket => {\n    socket.on('message', data => {\n      try {\n        const msg = parseClientMessage(data)\n\n        if (!msg) {\n          return\n        }\n\n        if (msg.type === 'subscribe') {\n          subscribe(socket, msg.topic)\n        } else if (msg.type === 'unsubscribe') {\n          unsubscribe(socket, msg.topic)\n        } else {\n          publish(msg.topic, msg.payload)\n        }\n      } catch (err) {\n        reportError(err)\n      }\n    })\n    socket.on('close', () => cleanup(socket))\n    socket.on('error', reportError)\n  })\n  wss.on('error', reportError)\n\n  return {\n    wss,\n    ready,\n    address: () => wss.address(),\n    close: () =>\n      new Promise((resolve, reject) => {\n        wss.clients.forEach(socket => socket.close())\n        wss.close(err => (err ? reject(err) : resolve()))\n      }),\n    publish,\n    getSubscriberCount: topic =>\n      topic\n        ? (topics.get(topic)?.size ?? 0)\n        : [...topics.values()].reduce((sum, sockets) => sum + sockets.size, 0)\n  }\n}\n"],"mappings":"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"}