{"version":3,"sources":["../src/TwirpRPC.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { JsonValue } from '@bufbuild/protobuf';\nimport { randomUUID } from './crypto/uuid.js';\nimport {\n  FAILOVER_BACKOFF_BASE_MS,\n  failoverAttempts,\n  hostKey,\n  pickNext,\n  regionOrigins,\n  sleep,\n} from './failover.js';\nimport { SDK_VERSION } from './version.js';\n\n// Identifies the SDK and version to the server on every request. Browsers forbid\n// setting User-Agent via fetch and silently drop it; Node honors it.\nconst USER_AGENT = `livekit-server-sdk-node/${SDK_VERSION}`;\n\n// Carries a per-request idempotency key. The SDK's auto-retries (see failover)\n// keep the same key across attempts, so the server can identify and deduplicate\n// repeated requests.\nexport const REQUEST_ID_HEADER = 'X-Livekit-Request-Id';\n\n// twirp RPC adapter for client implementation\n\ntype Options = {\n  /** Prefix for the RPC requests */\n  prefix?: string;\n  /** Timeout for fetch requests, in seconds. Must be within the valid range for abort signal timeouts. */\n  requestTimeout?: number;\n  /** Whether region failover is enabled (LiveKit Cloud hosts only). Defaults to true. */\n  failover?: boolean;\n  /** @internal test-only: force failover regardless of host. */\n  failoverForce?: boolean;\n  /** @internal test-only: base retry backoff in ms. */\n  failoverBackoffMs?: number;\n};\n\nconst defaultPrefix = '/twirp';\nconst defaultTimeoutSeconds = 10;\n\nexport const livekitPackage = 'livekit';\nexport interface Rpc {\n  request(\n    service: string,\n    method: string,\n    data: JsonValue,\n    headers: any, // eslint-disable-line @typescript-eslint/no-explicit-any\n    timeout?: number,\n  ): Promise<string>;\n}\n\nexport class ServerError extends Error {\n  status: number;\n  code?: string;\n  metadata?: Record<string, string>;\n\n  constructor(\n    name: string,\n    message: string,\n    status: number,\n    code?: string,\n    metadata?: Record<string, string>,\n  ) {\n    super(message);\n    this.name = name;\n    this.status = status;\n    this.code = code;\n    this.metadata = metadata;\n  }\n}\n\n/** @deprecated use {@link ServerError} */\nexport const TwirpError = ServerError;\n/** @deprecated use {@link ServerError} */\nexport type TwirpError = ServerError;\n\n/**\n * A {@link ServerError} from a SIP dialing call (`createSipParticipant` /\n * `transferSipParticipant`) that failed with a SIP response status. The SIP code\n * and reason are exposed as getters; any other error metadata remains available\n * via {@link ServerError.metadata}.\n */\nexport class SipCallError extends ServerError {\n  constructor(\n    name: string,\n    message: string,\n    status: number,\n    code?: string,\n    metadata?: Record<string, string>,\n  ) {\n    super(name, SipCallError.describe(message, code, metadata), status, code, metadata);\n    this.name = 'SipCallError';\n  }\n\n  /** The SIP response code of the failed call, e.g. 486 (Busy Here). */\n  get sipStatusCode(): number | undefined {\n    const raw = this.metadata?.sip_status_code;\n    return raw !== undefined ? Number(raw) : undefined;\n  }\n\n  /** The SIP reason phrase of the failed call, e.g. \"Busy Here\". */\n  get sipStatus(): string | undefined {\n    return this.metadata?.sip_status;\n  }\n\n  /** Builds a SipCallError from a ServerError, preserving its code and metadata. */\n  static fromServerError(err: ServerError): SipCallError {\n    return new SipCallError(err.name, err.message, err.status, err.code, err.metadata);\n  }\n\n  // describe renders a clear message: the SIP status, the error code, and any\n  // other metadata the server attached. Falls back to the raw message when the\n  // error carries no SIP status.\n  private static describe(fallback: string, code?: string, metadata?: Record<string, string>) {\n    const sipCode = metadata?.sip_status_code;\n    if (!sipCode) {\n      return fallback;\n    }\n    const reason = metadata?.sip_status;\n    let msg = `SIP call failed: ${sipCode}${reason ? ` ${reason}` : ''}`;\n    if (code) {\n      msg += ` (${code})`;\n    }\n    const extra = Object.entries(metadata ?? {})\n      .filter(([k]) => k !== 'sip_status_code' && k !== 'sip_status' && k !== 'error_details')\n      .map(([k, v]) => `${k}=${v}`);\n    if (extra.length) {\n      msg += ` [${extra.join(', ')}]`;\n    }\n    return msg;\n  }\n}\n\n/**\n * JSON based Twirp V7 RPC\n */\nexport class TwirpRpc {\n  host: string;\n\n  pkg: string;\n\n  prefix: string;\n\n  requestTimeout: number;\n\n  failover: boolean;\n\n  private failoverForce: boolean;\n\n  private failoverBackoffMs: number;\n\n  constructor(host: string, pkg: string, options?: Options) {\n    if (host.startsWith('ws')) {\n      host = host.replace('ws', 'http');\n    }\n    this.host = host;\n    this.pkg = pkg;\n    this.requestTimeout = options?.requestTimeout ?? defaultTimeoutSeconds;\n    this.prefix = options?.prefix || defaultPrefix;\n    this.failover = options?.failover ?? true;\n    this.failoverForce = options?.failoverForce ?? false;\n    this.failoverBackoffMs = options?.failoverBackoffMs ?? FAILOVER_BACKOFF_BASE_MS;\n  }\n\n  /**\n   * Issues a Twirp request, failing over to alternative regions on retryable\n   * errors. On any transport error or HTTP 5xx it discovers regions via\n   * /settings/regions and replays the request — body and headers intact —\n   * against the next untried region, with exponential backoff. A 4xx is\n   * returned immediately.\n   */\n  async request(\n    service: string,\n    method: string,\n    data: any, // eslint-disable-line @typescript-eslint/no-explicit-any\n    headers: any, // eslint-disable-line @typescript-eslint/no-explicit-any\n    timeout = this.requestTimeout,\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  ): Promise<any> {\n    const path = `${this.prefix}/${this.pkg}.${service}/${method}`;\n    const body = JSON.stringify(data);\n    const requestHeaders: Record<string, string> = {\n      'Content-Type': 'application/json;charset=UTF-8',\n      'User-Agent': USER_AGENT,\n      ...headers,\n    };\n    requestHeaders[REQUEST_ID_HEADER] = await randomUUID();\n\n    const origin = new URL(this.host);\n    const maxAttempts = failoverAttempts(\n      this.failover,\n      origin.hostname,\n      this.failoverForce,\n      timeout,\n    );\n    const attempted = new Set([hostKey(origin)]);\n    let regions: string[] | undefined;\n    let current = this.host;\n\n    for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n      const isLast = attempt + 1 >= maxAttempts;\n      const init: RequestInit = { method: 'POST', headers: requestHeaders, body };\n      if (timeout) {\n        init.signal = AbortSignal.timeout(timeout * 1000);\n      }\n\n      let response: Response | undefined;\n      let transportError: unknown;\n      try {\n        response = await fetch(new URL(path, current), init);\n      } catch (e) {\n        transportError = e;\n      }\n\n      if (response?.ok) {\n        // Return the raw JSON. Every caller parses it with protobuf-es\n        // fromJson(), which per the proto3 JSON spec accepts both the proto\n        // field names (snake_case) and their json_name (camelCase), so no key\n        // conversion is needed. Converting keys would also corrupt map<string,…>\n        // entries (e.g. participant attributes), whose keys are user data.\n        return (await response.json()) as Record<string, unknown>;\n      }\n\n      // Only retryable failures (a transport error or HTTP 5xx) continue;\n      // a 4xx is terminal.\n      const retryable = transportError !== undefined || (!!response && response.status >= 500);\n      let next: string | undefined;\n      if (retryable && !isLast) {\n        if (!regions) {\n          regions = await regionOrigins(origin, headers);\n        }\n        next = pickNext(regions, attempted);\n      }\n\n      if (!retryable || next === undefined) {\n        if (response) {\n          throw await toTwirpError(response);\n        }\n        throw transportError;\n      }\n\n      const reason = response ? `status ${response.status}` : transportError;\n      console.warn(\n        `livekit API request to ${new URL(current).host} failed (${reason}), retrying with fallback url ${next}`,\n      );\n      await sleep(this.failoverBackoffMs * 2 ** attempt);\n      attempted.add(hostKey(new URL(next)));\n      current = next;\n    }\n\n    throw new Error('failover loop exited without returning'); // unreachable\n  }\n}\n\n/** Builds a TwirpError from a non-2xx response, mirroring Twirp's JSON error shape. */\nasync function toTwirpError(response: Response): Promise<TwirpError> {\n  const isJson = response.headers.get('content-type') === 'application/json';\n  let errorMessage = 'Unknown internal error';\n  let errorCode: string | undefined = undefined;\n  let metadata: Record<string, string> | undefined = undefined;\n  try {\n    if (isJson) {\n      const parsedError = (await response.json()) as Record<string, unknown>;\n      if ('msg' in parsedError) {\n        errorMessage = <string>parsedError.msg;\n      }\n      if ('code' in parsedError) {\n        errorCode = <string>parsedError.code;\n      }\n      if ('meta' in parsedError) {\n        metadata = <Record<string, string>>parsedError.meta;\n      }\n    } else {\n      errorMessage = await response.text();\n    }\n  } catch (e) {\n    // parsing went wrong, no op and we keep default error message\n    console.debug(`Error when trying to parse error message, using defaults`, e);\n  }\n  return new TwirpError(response.statusText, errorMessage, response.status, errorCode, metadata);\n}\n"],"mappings":"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","names":[]}