{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/lib/experimental/nats/buildNatsServerUrl.ts","../../../src/lib/experimental/nats/buildSubject.ts","../../../src/lib/experimental/nats/NovaNatsClient.ts"],"sourcesContent":["import { parseNovaInstanceUrl } from \"../../converters.ts\"\n\n/**\n * Builds the WebSocket URL for a NOVA instance's NATS gateway from its\n * instance URL, e.g. `https://foo.instance.wandelbots.io` becomes\n * `wss://foo.instance.wandelbots.io/api/nats`.\n *\n * Pass the result as `servers` in the `NovaNatsClientConfig` passed to\n * `NovaNatsClient`.\n */\nexport function buildNatsServerUrl(instanceUrl: string): string {\n  const url = parseNovaInstanceUrl(instanceUrl)\n  const protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\"\n  return `${protocol}//${url.host}/api/nats`\n}\n","/**\n * Builds a NATS subject from an AsyncAPI-style channel address template\n * (e.g. `\"{instance}.v2.cells.{cell}\"`) by substituting each `{param}`\n * placeholder with the corresponding value from `params`.\n */\n\nfunction isValidSubjectChar(char: string): boolean {\n  const code = char.charCodeAt(0)\n  return (\n    (code >= 48 && code <= 57) || // 0-9\n    (code >= 65 && code <= 90) || // A-Z\n    (code >= 97 && code <= 122) || // a-z\n    char === \"-\" ||\n    char === \"_\"\n  )\n}\n\nfunction isValidSubjectValue(value: string): boolean {\n  // A bare \"*\" is the NATS single-token wildcard, allowed on its own (e.g.\n  // subscribing to all cells with `{ cell: \"*\" }\") but not as part of a\n  // larger value, since it wouldn't act as a wildcard there anyway.\n  if (value === \"*\") return true\n  if (value.length === 0) return false\n  for (const char of value) {\n    if (!isValidSubjectChar(char)) return false\n  }\n  return true\n}\n\nexport function buildSubject(\n  template: string,\n  params: Record<string, string>,\n): string {\n  // Scanned manually (rather than with a regex like /\\{([^}]+)\\}/g) to avoid\n  // a polynomial-time backtracking blowup on pathological input, e.g. a\n  // template consisting of many \"{\" characters with no closing \"}\".\n  let result = \"\"\n  let cursor = 0\n\n  while (cursor < template.length) {\n    const openIndex = template.indexOf(\"{\", cursor)\n    if (openIndex === -1) {\n      result += template.slice(cursor)\n      break\n    }\n\n    const closeIndex = template.indexOf(\"}\", openIndex + 1)\n    if (closeIndex === -1) {\n      result += template.slice(cursor)\n      break\n    }\n\n    result += template.slice(cursor, openIndex)\n    const paramName = template.slice(openIndex + 1, closeIndex)\n    const value = params[paramName]\n    if (value === undefined) {\n      throw new Error(\n        `Missing value for subject parameter \"${paramName}\" in template \"${template}\"`,\n      )\n    }\n    if (!isValidSubjectValue(value)) {\n      throw new Error(\n        `Invalid value for subject parameter \"${paramName}\": \"${value}\" (must be non-empty and contain only letters, digits, \"-\", and \"_\")`,\n      )\n    }\n    result += value\n\n    cursor = closeIndex + 1\n  }\n\n  return result\n}\n","import {\n  type ConnectionOptions,\n  type Msg,\n  type NatsConnection,\n  wsconnect,\n} from \"@nats-io/nats-core\"\nimport type { Nova } from \"../../Nova.ts\"\nimport { buildNatsServerUrl } from \"./buildNatsServerUrl.ts\"\nimport { buildSubject } from \"./buildSubject.ts\"\nimport type {\n  NatsOperationParams,\n  NatsPublishPayloads,\n  NatsPublishSubject,\n  NatsReplyPayloads,\n  NatsRequestPayloads,\n  NatsRequestSubject,\n  NatsSubscribePayloads,\n  NatsSubscribeSubject,\n} from \"./generated/operations.ts\"\n\nexport type NovaNatsClientConfig = ConnectionOptions\n\ntype NatsMessageHandler<K extends NatsSubscribeSubject> = (\n  payload: NatsSubscribePayloads[K],\n  msg: Msg,\n) => void | Promise<void>\n\ntype SubscribeArgs<K extends NatsSubscribeSubject> =\n  keyof NatsOperationParams[K] extends never\n    ? [handler: NatsMessageHandler<K>]\n    : [params: NatsOperationParams[K], handler: NatsMessageHandler<K>]\n\n/**\n * Typed NATS client for the Wandelbots NOVA messaging API, generated from\n * src/asyncapi.yaml (see scripts/generate-nats-client.ts).\n *\n * Connects over WebSocket via `@nats-io/nats-core`'s `wsconnect`.\n */\nexport class NovaNatsClient {\n  readonly config: NovaNatsClientConfig\n  private connectionPromise: Promise<NatsConnection> | null = null\n\n  constructor(nova: Nova, config: NovaNatsClientConfig = {}) {\n    this.config = {\n      servers: buildNatsServerUrl(nova.instanceUrl.href),\n      // Reuse the Nova instance's access token for NATS auth, if it has one\n      // (e.g. from login or a passed-in config.accessToken). Explicit auth\n      // options in `config` (token/user/pass/authenticator) still win.\n      ...(nova.accessToken ? { token: nova.accessToken } : {}),\n      ...config,\n    }\n  }\n\n  /**\n   * Connects to NATS if not already connected or connecting, and returns the\n   * connection. Safe to call concurrently: all callers share the same\n   * in-flight connection attempt instead of each starting their own.\n   */\n  connect(): Promise<NatsConnection> {\n    if (!this.connectionPromise) {\n      this.connectionPromise = wsconnect(this.config).catch((err: unknown) => {\n        // Allow a subsequent connect() call to retry after a failed attempt.\n        this.connectionPromise = null\n        throw err\n      })\n    }\n    return this.connectionPromise\n  }\n\n  /** Closes the underlying NATS connection, if open or connecting. */\n  async close(): Promise<void> {\n    const connectionPromise = this.connectionPromise\n    this.connectionPromise = null\n    if (!connectionPromise) return\n    try {\n      const nc = await connectionPromise\n      await nc.close()\n    } catch {\n      // Connection never succeeded; nothing to close.\n    }\n  }\n\n  /**\n   * Subscribes to a NATS subject published by the server, invoking `handler`\n   * with the JSON-decoded payload of every message received.\n   *\n   * `subject` is the subject template as it appears on the wire, e.g.\n   * `\"nova.v2.cells.{cell}\"`, with `{param}` placeholders filled in from\n   * `params`.\n   *\n   * Errors decoding a message or thrown/rejected by `handler` are caught and\n   * logged per-message, so one bad message doesn't stop later messages on\n   * the same subscription from being handled.\n   *\n   * Returns a function that unsubscribes when called.\n   */\n  async subscribe<K extends NatsSubscribeSubject>(\n    subject: K,\n    ...args: SubscribeArgs<K>\n  ): Promise<() => void> {\n    const [params, handler] =\n      args.length === 1\n        ? ([{}, args[0]] as const)\n        : ([args[0], args[1]] as const)\n\n    const nc = await this.connect()\n    const resolvedSubject = buildSubject(subject, params)\n    const sub = nc.subscribe(resolvedSubject)\n\n    ;(async () => {\n      for await (const msg of sub) {\n        // Handled per-message: a bad payload or a throwing/rejecting handler\n        // should not stop the subscription from processing later messages.\n        try {\n          await handler(msg.json<NatsSubscribePayloads[K]>(), msg)\n        } catch (err) {\n          console.error(\n            `Error handling NATS message on subject \"${resolvedSubject}\"`,\n            err,\n          )\n        }\n      }\n    })().catch((err: unknown) => {\n      console.error(\n        `NATS subscription iterator failed for \"${resolvedSubject}\"`,\n        err,\n      )\n    })\n\n    return () => sub.unsubscribe()\n  }\n\n  /**\n   * Sends a request payload for a NATS subject the server receives, and\n   * waits for the JSON-decoded reply.\n   *\n   * `subject` is the subject template as it appears on the wire, e.g.\n   * `\"nova.v2.cells.{cell}.bus-ios.ios.set\"`, with `{param}` placeholders\n   * filled in from `params`.\n   */\n  async request<K extends NatsRequestSubject>(\n    subject: K,\n    params: NatsOperationParams[K],\n    payload: NatsRequestPayloads[K],\n    opts: { timeout?: number } = {},\n  ): Promise<NatsReplyPayloads[K]> {\n    const nc = await this.connect()\n    const resolvedSubject = buildSubject(subject, params)\n    const msg = await nc.request(resolvedSubject, JSON.stringify(payload), {\n      timeout: opts.timeout ?? 5000,\n    })\n    return msg.json<NatsReplyPayloads[K]>()\n  }\n\n  /**\n   * Publishes a JSON payload to any NATS subject defined in the spec,\n   * without waiting for a reply.\n   *\n   * `subject` is the subject template as it appears on the wire, e.g.\n   * `\"nova.v2.cells.{cell}.bus-ios.ios.set\"`, with `{param}` placeholders\n   * filled in from `params`.\n   */\n  async publish<K extends NatsPublishSubject>(\n    subject: K,\n    params: NatsOperationParams[K],\n    payload: NatsPublishPayloads[K],\n  ): Promise<void> {\n    const nc = await this.connect()\n    const resolvedSubject = buildSubject(subject, params)\n    nc.publish(resolvedSubject, JSON.stringify(payload))\n  }\n}\n"],"mappings":"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"}