livekit-server-sdk 2.16.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +82 -15
  2. package/dist/AgentDispatchClient.cjs +5 -3
  3. package/dist/AgentDispatchClient.cjs.map +1 -1
  4. package/dist/AgentDispatchClient.js +5 -3
  5. package/dist/AgentDispatchClient.js.map +1 -1
  6. package/dist/ClientOptions.cjs.map +1 -1
  7. package/dist/ClientOptions.d.cts +12 -0
  8. package/dist/ClientOptions.d.ts +12 -0
  9. package/dist/ClientOptions.d.ts.map +1 -1
  10. package/dist/ConnectorClient.cjs +9 -5
  11. package/dist/ConnectorClient.cjs.map +1 -1
  12. package/dist/ConnectorClient.d.cts +6 -3
  13. package/dist/ConnectorClient.d.ts +6 -3
  14. package/dist/ConnectorClient.d.ts.map +1 -1
  15. package/dist/ConnectorClient.js +9 -5
  16. package/dist/ConnectorClient.js.map +1 -1
  17. package/dist/EgressClient.cjs +19 -3
  18. package/dist/EgressClient.cjs.map +1 -1
  19. package/dist/EgressClient.d.cts +7 -1
  20. package/dist/EgressClient.d.ts +7 -1
  21. package/dist/EgressClient.d.ts.map +1 -1
  22. package/dist/EgressClient.js +20 -3
  23. package/dist/EgressClient.js.map +1 -1
  24. package/dist/IngressClient.cjs +5 -3
  25. package/dist/IngressClient.cjs.map +1 -1
  26. package/dist/IngressClient.js +5 -3
  27. package/dist/IngressClient.js.map +1 -1
  28. package/dist/LiveKitAPI.cjs +80 -0
  29. package/dist/LiveKitAPI.cjs.map +1 -0
  30. package/dist/LiveKitAPI.d.cts +77 -0
  31. package/dist/LiveKitAPI.d.ts +77 -0
  32. package/dist/LiveKitAPI.d.ts.map +1 -0
  33. package/dist/LiveKitAPI.js +56 -0
  34. package/dist/LiveKitAPI.js.map +1 -0
  35. package/dist/RoomServiceClient.cjs +5 -3
  36. package/dist/RoomServiceClient.cjs.map +1 -1
  37. package/dist/RoomServiceClient.js +5 -3
  38. package/dist/RoomServiceClient.js.map +1 -1
  39. package/dist/ServiceBase.cjs +9 -9
  40. package/dist/ServiceBase.cjs.map +1 -1
  41. package/dist/ServiceBase.d.cts +20 -1
  42. package/dist/ServiceBase.d.ts +20 -1
  43. package/dist/ServiceBase.d.ts.map +1 -1
  44. package/dist/ServiceBase.js +9 -9
  45. package/dist/ServiceBase.js.map +1 -1
  46. package/dist/SipClient.cjs +40 -19
  47. package/dist/SipClient.cjs.map +1 -1
  48. package/dist/SipClient.d.cts +4 -2
  49. package/dist/SipClient.d.ts +4 -2
  50. package/dist/SipClient.d.ts.map +1 -1
  51. package/dist/SipClient.js +41 -20
  52. package/dist/SipClient.js.map +1 -1
  53. package/dist/TwirpRPC.cjs +135 -48
  54. package/dist/TwirpRPC.cjs.map +1 -1
  55. package/dist/TwirpRPC.d.cts +38 -2
  56. package/dist/TwirpRPC.d.ts +38 -2
  57. package/dist/TwirpRPC.d.ts.map +1 -1
  58. package/dist/TwirpRPC.js +140 -38
  59. package/dist/TwirpRPC.js.map +1 -1
  60. package/dist/dialTimeout.cjs +39 -0
  61. package/dist/dialTimeout.cjs.map +1 -0
  62. package/dist/dialTimeout.d.cts +21 -0
  63. package/dist/dialTimeout.d.ts +21 -0
  64. package/dist/dialTimeout.d.ts.map +1 -0
  65. package/dist/dialTimeout.js +13 -0
  66. package/dist/dialTimeout.js.map +1 -0
  67. package/dist/failover.cjs +139 -0
  68. package/dist/failover.cjs.map +1 -0
  69. package/dist/failover.d.cts +30 -0
  70. package/dist/failover.d.ts +30 -0
  71. package/dist/failover.d.ts.map +1 -0
  72. package/dist/failover.js +107 -0
  73. package/dist/failover.js.map +1 -0
  74. package/dist/index.cjs +22 -0
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.cts +4 -3
  77. package/dist/index.d.ts +4 -3
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +20 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/version.cjs +29 -0
  82. package/dist/version.cjs.map +1 -0
  83. package/dist/version.d.cts +3 -0
  84. package/dist/version.d.ts +3 -0
  85. package/dist/version.d.ts.map +1 -0
  86. package/dist/version.js +5 -0
  87. package/dist/version.js.map +1 -0
  88. package/package.json +3 -3
  89. package/src/AgentDispatchClient.ts +5 -5
  90. package/src/ClientOptions.ts +12 -0
  91. package/src/ConnectorClient.ts +20 -11
  92. package/src/EgressClient.ts +21 -5
  93. package/src/IngressClient.ts +5 -5
  94. package/src/LiveKitAPI.ts +126 -0
  95. package/src/RoomServiceClient.ts +5 -5
  96. package/src/ServiceBase.ts +35 -4
  97. package/src/SipClient.ts +55 -23
  98. package/src/TwirpRPC.test.ts +42 -0
  99. package/src/TwirpRPC.ts +187 -39
  100. package/src/dialTimeout.test.ts +41 -0
  101. package/src/dialTimeout.ts +37 -0
  102. package/src/failover.test.ts +188 -0
  103. package/src/failover.ts +170 -0
  104. package/src/index.ts +11 -1
  105. package/src/version.ts +1 -0
package/dist/TwirpRPC.js CHANGED
@@ -1,7 +1,17 @@
1
+ import {
2
+ FAILOVER_BACKOFF_BASE_MS,
3
+ failoverAttempts,
4
+ hostKey,
5
+ pickNext,
6
+ regionOrigins,
7
+ sleep
8
+ } from "./failover.js";
9
+ import { SDK_VERSION } from "./version.js";
10
+ const USER_AGENT = `livekit-server-sdk-node/${SDK_VERSION}`;
1
11
  const defaultPrefix = "/twirp";
2
- const defaultTimeoutSeconds = 60;
12
+ const defaultTimeoutSeconds = 10;
3
13
  const livekitPackage = "livekit";
4
- class TwirpError extends Error {
14
+ class ServerError extends Error {
5
15
  constructor(name, message, status, code, metadata) {
6
16
  super(message);
7
17
  this.name = name;
@@ -10,6 +20,47 @@ class TwirpError extends Error {
10
20
  this.metadata = metadata;
11
21
  }
12
22
  }
23
+ const TwirpError = ServerError;
24
+ class SipCallError extends ServerError {
25
+ constructor(name, message, status, code, metadata) {
26
+ super(name, SipCallError.describe(message, code, metadata), status, code, metadata);
27
+ this.name = "SipCallError";
28
+ }
29
+ /** The SIP response code of the failed call, e.g. 486 (Busy Here). */
30
+ get sipStatusCode() {
31
+ var _a;
32
+ const raw = (_a = this.metadata) == null ? void 0 : _a.sip_status_code;
33
+ return raw !== void 0 ? Number(raw) : void 0;
34
+ }
35
+ /** The SIP reason phrase of the failed call, e.g. "Busy Here". */
36
+ get sipStatus() {
37
+ var _a;
38
+ return (_a = this.metadata) == null ? void 0 : _a.sip_status;
39
+ }
40
+ /** Builds a SipCallError from a ServerError, preserving its code and metadata. */
41
+ static fromServerError(err) {
42
+ return new SipCallError(err.name, err.message, err.status, err.code, err.metadata);
43
+ }
44
+ // describe renders a clear message: the SIP status, the error code, and any
45
+ // other metadata the server attached. Falls back to the raw message when the
46
+ // error carries no SIP status.
47
+ static describe(fallback, code, metadata) {
48
+ const sipCode = metadata == null ? void 0 : metadata.sip_status_code;
49
+ if (!sipCode) {
50
+ return fallback;
51
+ }
52
+ const reason = metadata == null ? void 0 : metadata.sip_status;
53
+ let msg = `SIP call failed: ${sipCode}${reason ? ` ${reason}` : ""}`;
54
+ if (code) {
55
+ msg += ` (${code})`;
56
+ }
57
+ const extra = Object.entries(metadata ?? {}).filter(([k]) => k !== "sip_status_code" && k !== "sip_status" && k !== "error_details").map(([k, v]) => `${k}=${v}`);
58
+ if (extra.length) {
59
+ msg += ` [${extra.join(", ")}]`;
60
+ }
61
+ return msg;
62
+ }
63
+ }
13
64
  class TwirpRpc {
14
65
  constructor(host, pkg, options) {
15
66
  if (host.startsWith("ws")) {
@@ -19,53 +70,104 @@ class TwirpRpc {
19
70
  this.pkg = pkg;
20
71
  this.requestTimeout = (options == null ? void 0 : options.requestTimeout) ?? defaultTimeoutSeconds;
21
72
  this.prefix = (options == null ? void 0 : options.prefix) || defaultPrefix;
73
+ this.failover = (options == null ? void 0 : options.failover) ?? true;
74
+ this.failoverForce = (options == null ? void 0 : options.failoverForce) ?? false;
75
+ this.failoverBackoffMs = (options == null ? void 0 : options.failoverBackoffMs) ?? FAILOVER_BACKOFF_BASE_MS;
22
76
  }
77
+ /**
78
+ * Issues a Twirp request, failing over to alternative regions on retryable
79
+ * errors. On any transport error or HTTP 5xx it discovers regions via
80
+ * /settings/regions and replays the request — body and headers intact —
81
+ * against the next untried region, with exponential backoff. A 4xx is
82
+ * returned immediately.
83
+ */
23
84
  async request(service, method, data, headers, timeout = this.requestTimeout) {
24
85
  const path = `${this.prefix}/${this.pkg}.${service}/${method}`;
25
- const url = new URL(path, this.host);
26
- const init = {
27
- method: "POST",
28
- headers: {
29
- "Content-Type": "application/json;charset=UTF-8",
30
- ...headers
31
- },
32
- body: JSON.stringify(data)
86
+ const body = JSON.stringify(data);
87
+ const requestHeaders = {
88
+ "Content-Type": "application/json;charset=UTF-8",
89
+ "User-Agent": USER_AGENT,
90
+ ...headers
33
91
  };
34
- if (timeout) {
35
- init.signal = AbortSignal.timeout(timeout * 1e3);
36
- }
37
- const response = await fetch(url, init);
38
- if (!response.ok) {
39
- const isJson = response.headers.get("content-type") === "application/json";
40
- let errorMessage = "Unknown internal error";
41
- let errorCode = void 0;
42
- let metadata = void 0;
92
+ const origin = new URL(this.host);
93
+ const maxAttempts = failoverAttempts(
94
+ this.failover,
95
+ origin.hostname,
96
+ this.failoverForce,
97
+ timeout
98
+ );
99
+ const attempted = /* @__PURE__ */ new Set([hostKey(origin)]);
100
+ let regions;
101
+ let current = this.host;
102
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
103
+ const isLast = attempt + 1 >= maxAttempts;
104
+ const init = { method: "POST", headers: requestHeaders, body };
105
+ if (timeout) {
106
+ init.signal = AbortSignal.timeout(timeout * 1e3);
107
+ }
108
+ let response;
109
+ let transportError;
43
110
  try {
44
- if (isJson) {
45
- const parsedError = await response.json();
46
- if ("msg" in parsedError) {
47
- errorMessage = parsedError.msg;
48
- }
49
- if ("code" in parsedError) {
50
- errorCode = parsedError.code;
51
- }
52
- if ("meta" in parsedError) {
53
- metadata = parsedError.meta;
54
- }
55
- } else {
56
- errorMessage = await response.text();
57
- }
111
+ response = await fetch(new URL(path, current), init);
58
112
  } catch (e) {
59
- console.debug(`Error when trying to parse error message, using defaults`, e);
113
+ transportError = e;
114
+ }
115
+ if (response == null ? void 0 : response.ok) {
116
+ return await response.json();
117
+ }
118
+ const retryable = transportError !== void 0 || !!response && response.status >= 500;
119
+ let next;
120
+ if (retryable && !isLast) {
121
+ if (!regions) {
122
+ regions = await regionOrigins(origin, headers);
123
+ }
124
+ next = pickNext(regions, attempted);
125
+ }
126
+ if (!retryable || next === void 0) {
127
+ if (response) {
128
+ throw await toTwirpError(response);
129
+ }
130
+ throw transportError;
131
+ }
132
+ const reason = response ? `status ${response.status}` : transportError;
133
+ console.warn(
134
+ `livekit API request to ${new URL(current).host} failed (${reason}), retrying with fallback url ${next}`
135
+ );
136
+ await sleep(this.failoverBackoffMs * 2 ** attempt);
137
+ attempted.add(hostKey(new URL(next)));
138
+ current = next;
139
+ }
140
+ throw new Error("failover loop exited without returning");
141
+ }
142
+ }
143
+ async function toTwirpError(response) {
144
+ const isJson = response.headers.get("content-type") === "application/json";
145
+ let errorMessage = "Unknown internal error";
146
+ let errorCode = void 0;
147
+ let metadata = void 0;
148
+ try {
149
+ if (isJson) {
150
+ const parsedError = await response.json();
151
+ if ("msg" in parsedError) {
152
+ errorMessage = parsedError.msg;
153
+ }
154
+ if ("code" in parsedError) {
155
+ errorCode = parsedError.code;
156
+ }
157
+ if ("meta" in parsedError) {
158
+ metadata = parsedError.meta;
60
159
  }
61
- throw new TwirpError(response.statusText, errorMessage, response.status, errorCode, metadata);
160
+ } else {
161
+ errorMessage = await response.text();
62
162
  }
63
- const parsedResp = await response.json();
64
- const camelcaseKeys = await import("camelcase-keys").then((mod) => mod.default);
65
- return camelcaseKeys(parsedResp, { deep: true });
163
+ } catch (e) {
164
+ console.debug(`Error when trying to parse error message, using defaults`, e);
66
165
  }
166
+ return new TwirpError(response.statusText, errorMessage, response.status, errorCode, metadata);
67
167
  }
68
168
  export {
169
+ ServerError,
170
+ SipCallError,
69
171
  TwirpError,
70
172
  TwirpRpc,
71
173
  livekitPackage
@@ -1 +1 @@
1
- {"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';\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};\n\nconst defaultPrefix = '/twirp';\nconst defaultTimeoutSeconds = 60;\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 TwirpError 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/**\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 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 }\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 url = new URL(path, this.host);\n const init: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json;charset=UTF-8',\n ...headers,\n },\n body: JSON.stringify(data),\n };\n\n if (timeout) {\n init.signal = AbortSignal.timeout(timeout * 1000);\n }\n\n const response = await fetch(url, init);\n\n if (!response.ok) {\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\n throw new TwirpError(response.statusText, errorMessage, response.status, errorCode, metadata);\n }\n const parsedResp = (await response.json()) as Record<string, unknown>;\n\n const camelcaseKeys = await import('camelcase-keys').then((mod) => mod.default);\n return camelcaseKeys(parsedResp, { deep: true });\n }\n}\n"],"mappings":"AAcA,MAAM,gBAAgB;AACtB,MAAM,wBAAwB;AAEvB,MAAM,iBAAiB;AAWvB,MAAM,mBAAmB,MAAM;AAAA,EAKpC,YACE,MACA,SACA,QACA,MACA,UACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EAClB;AACF;AAKO,MAAM,SAAS;AAAA,EASpB,YAAY,MAAc,KAAa,SAAmB;AACxD,QAAI,KAAK,WAAW,IAAI,GAAG;AACzB,aAAO,KAAK,QAAQ,MAAM,MAAM;AAAA,IAClC;AACA,SAAK,OAAO;AACZ,SAAK,MAAM;AACX,SAAK,kBAAiB,mCAAS,mBAAkB;AACjD,SAAK,UAAS,mCAAS,WAAU;AAAA,EACnC;AAAA,EAEA,MAAM,QACJ,SACA,QACA,MACA,SACA,UAAU,KAAK,gBAED;AACd,UAAM,OAAO,GAAG,KAAK,MAAM,IAAI,KAAK,GAAG,IAAI,OAAO,IAAI,MAAM;AAC5D,UAAM,MAAM,IAAI,IAAI,MAAM,KAAK,IAAI;AACnC,UAAM,OAAoB;AAAA,MACxB,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG;AAAA,MACL;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B;AAEA,QAAI,SAAS;AACX,WAAK,SAAS,YAAY,QAAQ,UAAU,GAAI;AAAA,IAClD;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK,IAAI;AAEtC,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,SAAS,SAAS,QAAQ,IAAI,cAAc,MAAM;AACxD,UAAI,eAAe;AACnB,UAAI,YAAgC;AACpC,UAAI,WAA+C;AACnD,UAAI;AACF,YAAI,QAAQ;AACV,gBAAM,cAAe,MAAM,SAAS,KAAK;AACzC,cAAI,SAAS,aAAa;AACxB,2BAAuB,YAAY;AAAA,UACrC;AACA,cAAI,UAAU,aAAa;AACzB,wBAAoB,YAAY;AAAA,UAClC;AACA,cAAI,UAAU,aAAa;AACzB,uBAAmC,YAAY;AAAA,UACjD;AAAA,QACF,OAAO;AACL,yBAAe,MAAM,SAAS,KAAK;AAAA,QACrC;AAAA,MACF,SAAS,GAAG;AAEV,gBAAQ,MAAM,4DAA4D,CAAC;AAAA,MAC7E;AAEA,YAAM,IAAI,WAAW,SAAS,YAAY,cAAc,SAAS,QAAQ,WAAW,QAAQ;AAAA,IAC9F;AACA,UAAM,aAAc,MAAM,SAAS,KAAK;AAExC,UAAM,gBAAgB,MAAM,OAAO,gBAAgB,EAAE,KAAK,CAAC,QAAQ,IAAI,OAAO;AAC9E,WAAO,cAAc,YAAY,EAAE,MAAM,KAAK,CAAC;AAAA,EACjD;AACF;","names":[]}
1
+ {"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 {\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// 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 = {\n 'Content-Type': 'application/json;charset=UTF-8',\n 'User-Agent': USER_AGENT,\n ...headers,\n };\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":[]}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var dialTimeout_exports = {};
20
+ __export(dialTimeout_exports, {
21
+ DEFAULT_RINGING_TIMEOUT_SECONDS: () => DEFAULT_RINGING_TIMEOUT_SECONDS,
22
+ RINGING_TIMEOUT_MARGIN_SECONDS: () => RINGING_TIMEOUT_MARGIN_SECONDS,
23
+ dialRequestTimeout: () => dialRequestTimeout
24
+ });
25
+ module.exports = __toCommonJS(dialTimeout_exports);
26
+ const DEFAULT_RINGING_TIMEOUT_SECONDS = 30;
27
+ const RINGING_TIMEOUT_MARGIN_SECONDS = 2;
28
+ function dialRequestTimeout(timeout, ringingTimeout) {
29
+ const ring = ringingTimeout ?? DEFAULT_RINGING_TIMEOUT_SECONDS;
30
+ const floor = ring + RINGING_TIMEOUT_MARGIN_SECONDS;
31
+ return Math.max(timeout ?? floor, floor);
32
+ }
33
+ // Annotate the CommonJS export names for ESM import in node:
34
+ 0 && (module.exports = {
35
+ DEFAULT_RINGING_TIMEOUT_SECONDS,
36
+ RINGING_TIMEOUT_MARGIN_SECONDS,
37
+ dialRequestTimeout
38
+ });
39
+ //# sourceMappingURL=dialTimeout.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dialTimeout.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n// Shared request-timeout handling for calls that may block until a call is\n// answered (SIP CreateSIPParticipant/TransferSIPParticipant, WhatsApp\n// AcceptWhatsAppCall). These take longer than a normal API call, and the request\n// must outlast the wait or it would abort before the call is answered.\n\n/**\n * Ring window (seconds) assumed when a request doesn't set a ringing timeout;\n * matches the server default. A dialing request must outlast it.\n */\nexport const DEFAULT_RINGING_TIMEOUT_SECONDS = 30;\n\n/**\n * When a call waits on ringing, the request must outlast the ringing window or\n * it would abort before the call can be answered. We keep at least this margin\n * (seconds) of request timeout above the ringing timeout.\n */\nexport const RINGING_TIMEOUT_MARGIN_SECONDS = 2;\n\n/**\n * Resolves the request timeout (seconds) for a phone-dialing call: the ring\n * window plus a margin, so the request doesn't abort before the call can be\n * answered. The ring window is the request's `ringingTimeout` when set, else\n * {@link DEFAULT_RINGING_TIMEOUT_SECONDS}. A longer user-supplied `timeout` is\n * honored; a shorter one is raised to the floor.\n */\nexport function dialRequestTimeout(\n timeout: number | undefined,\n ringingTimeout: number | undefined,\n): number {\n const ring = ringingTimeout ?? DEFAULT_RINGING_TIMEOUT_SECONDS;\n const floor = ring + RINGING_TIMEOUT_MARGIN_SECONDS;\n return Math.max(timeout ?? floor, floor);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaO,MAAM,kCAAkC;AAOxC,MAAM,iCAAiC;AASvC,SAAS,mBACd,SACA,gBACQ;AACR,QAAM,OAAO,kBAAkB;AAC/B,QAAM,QAAQ,OAAO;AACrB,SAAO,KAAK,IAAI,WAAW,OAAO,KAAK;AACzC;","names":[]}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Ring window (seconds) assumed when a request doesn't set a ringing timeout;
3
+ * matches the server default. A dialing request must outlast it.
4
+ */
5
+ declare const DEFAULT_RINGING_TIMEOUT_SECONDS = 30;
6
+ /**
7
+ * When a call waits on ringing, the request must outlast the ringing window or
8
+ * it would abort before the call can be answered. We keep at least this margin
9
+ * (seconds) of request timeout above the ringing timeout.
10
+ */
11
+ declare const RINGING_TIMEOUT_MARGIN_SECONDS = 2;
12
+ /**
13
+ * Resolves the request timeout (seconds) for a phone-dialing call: the ring
14
+ * window plus a margin, so the request doesn't abort before the call can be
15
+ * answered. The ring window is the request's `ringingTimeout` when set, else
16
+ * {@link DEFAULT_RINGING_TIMEOUT_SECONDS}. A longer user-supplied `timeout` is
17
+ * honored; a shorter one is raised to the floor.
18
+ */
19
+ declare function dialRequestTimeout(timeout: number | undefined, ringingTimeout: number | undefined): number;
20
+
21
+ export { DEFAULT_RINGING_TIMEOUT_SECONDS, RINGING_TIMEOUT_MARGIN_SECONDS, dialRequestTimeout };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Ring window (seconds) assumed when a request doesn't set a ringing timeout;
3
+ * matches the server default. A dialing request must outlast it.
4
+ */
5
+ declare const DEFAULT_RINGING_TIMEOUT_SECONDS = 30;
6
+ /**
7
+ * When a call waits on ringing, the request must outlast the ringing window or
8
+ * it would abort before the call can be answered. We keep at least this margin
9
+ * (seconds) of request timeout above the ringing timeout.
10
+ */
11
+ declare const RINGING_TIMEOUT_MARGIN_SECONDS = 2;
12
+ /**
13
+ * Resolves the request timeout (seconds) for a phone-dialing call: the ring
14
+ * window plus a margin, so the request doesn't abort before the call can be
15
+ * answered. The ring window is the request's `ringingTimeout` when set, else
16
+ * {@link DEFAULT_RINGING_TIMEOUT_SECONDS}. A longer user-supplied `timeout` is
17
+ * honored; a shorter one is raised to the floor.
18
+ */
19
+ declare function dialRequestTimeout(timeout: number | undefined, ringingTimeout: number | undefined): number;
20
+
21
+ export { DEFAULT_RINGING_TIMEOUT_SECONDS, RINGING_TIMEOUT_MARGIN_SECONDS, dialRequestTimeout };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialTimeout.d.ts","sourceRoot":"","sources":["../src/dialTimeout.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,MAAM,CAIR"}
@@ -0,0 +1,13 @@
1
+ const DEFAULT_RINGING_TIMEOUT_SECONDS = 30;
2
+ const RINGING_TIMEOUT_MARGIN_SECONDS = 2;
3
+ function dialRequestTimeout(timeout, ringingTimeout) {
4
+ const ring = ringingTimeout ?? DEFAULT_RINGING_TIMEOUT_SECONDS;
5
+ const floor = ring + RINGING_TIMEOUT_MARGIN_SECONDS;
6
+ return Math.max(timeout ?? floor, floor);
7
+ }
8
+ export {
9
+ DEFAULT_RINGING_TIMEOUT_SECONDS,
10
+ RINGING_TIMEOUT_MARGIN_SECONDS,
11
+ dialRequestTimeout
12
+ };
13
+ //# sourceMappingURL=dialTimeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dialTimeout.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n// Shared request-timeout handling for calls that may block until a call is\n// answered (SIP CreateSIPParticipant/TransferSIPParticipant, WhatsApp\n// AcceptWhatsAppCall). These take longer than a normal API call, and the request\n// must outlast the wait or it would abort before the call is answered.\n\n/**\n * Ring window (seconds) assumed when a request doesn't set a ringing timeout;\n * matches the server default. A dialing request must outlast it.\n */\nexport const DEFAULT_RINGING_TIMEOUT_SECONDS = 30;\n\n/**\n * When a call waits on ringing, the request must outlast the ringing window or\n * it would abort before the call can be answered. We keep at least this margin\n * (seconds) of request timeout above the ringing timeout.\n */\nexport const RINGING_TIMEOUT_MARGIN_SECONDS = 2;\n\n/**\n * Resolves the request timeout (seconds) for a phone-dialing call: the ring\n * window plus a margin, so the request doesn't abort before the call can be\n * answered. The ring window is the request's `ringingTimeout` when set, else\n * {@link DEFAULT_RINGING_TIMEOUT_SECONDS}. A longer user-supplied `timeout` is\n * honored; a shorter one is raised to the floor.\n */\nexport function dialRequestTimeout(\n timeout: number | undefined,\n ringingTimeout: number | undefined,\n): number {\n const ring = ringingTimeout ?? DEFAULT_RINGING_TIMEOUT_SECONDS;\n const floor = ring + RINGING_TIMEOUT_MARGIN_SECONDS;\n return Math.max(timeout ?? floor, floor);\n}\n"],"mappings":"AAaO,MAAM,kCAAkC;AAOxC,MAAM,iCAAiC;AASvC,SAAS,mBACd,SACA,gBACQ;AACR,QAAM,OAAO,kBAAkB;AAC/B,QAAM,QAAQ,OAAO;AACrB,SAAO,KAAK,IAAI,WAAW,OAAO,KAAK;AACzC;","names":[]}
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var failover_exports = {};
20
+ __export(failover_exports, {
21
+ FAILOVER_BACKOFF_BASE_MS: () => FAILOVER_BACKOFF_BASE_MS,
22
+ FAILOVER_MAX_ATTEMPTS: () => FAILOVER_MAX_ATTEMPTS,
23
+ MIN_FAILOVER_TIMEOUT_SECONDS: () => MIN_FAILOVER_TIMEOUT_SECONDS,
24
+ failoverAttempts: () => failoverAttempts,
25
+ hostKey: () => hostKey,
26
+ parseMaxAge: () => parseMaxAge,
27
+ pickNext: () => pickNext,
28
+ regionOrigins: () => regionOrigins,
29
+ sleep: () => sleep
30
+ });
31
+ module.exports = __toCommonJS(failover_exports);
32
+ const FAILOVER_MAX_ATTEMPTS = 3;
33
+ const FAILOVER_BACKOFF_BASE_MS = 200;
34
+ const MIN_FAILOVER_TIMEOUT_SECONDS = 5;
35
+ function failoverAttempts(enabled, hostname, force = false, timeoutSeconds = 0) {
36
+ if (!enabled || !(force || isCloud(hostname))) {
37
+ return 1;
38
+ }
39
+ if (timeoutSeconds > 0 && timeoutSeconds < MIN_FAILOVER_TIMEOUT_SECONDS) {
40
+ return 1;
41
+ }
42
+ return FAILOVER_MAX_ATTEMPTS;
43
+ }
44
+ function isCloud(hostname) {
45
+ return hostname.endsWith(".livekit.cloud");
46
+ }
47
+ function toHttp(url) {
48
+ return url.startsWith("ws") ? `http${url.slice(2)}` : url;
49
+ }
50
+ function hostKey(url) {
51
+ return url.host.toLowerCase();
52
+ }
53
+ function pickNext(regionOrigins2, attempted) {
54
+ for (const origin of regionOrigins2) {
55
+ try {
56
+ if (!attempted.has(hostKey(new URL(origin)))) {
57
+ return origin;
58
+ }
59
+ } catch {
60
+ }
61
+ }
62
+ return void 0;
63
+ }
64
+ function sleep(ms) {
65
+ return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
66
+ }
67
+ const regionCache = /* @__PURE__ */ new Map();
68
+ const inflight = /* @__PURE__ */ new Map();
69
+ async function regionOrigins(origin, headers) {
70
+ const key = hostKey(origin);
71
+ const cached = regionCache.get(key);
72
+ if (cached && Date.now() - cached.fetchedAt < cached.ttl) {
73
+ return cached.origins;
74
+ }
75
+ const existing = inflight.get(key);
76
+ if (existing) {
77
+ return existing;
78
+ }
79
+ const request = (async () => {
80
+ try {
81
+ const { origins, ttl } = await fetchRegions(origin, headers);
82
+ if (ttl > 0) {
83
+ regionCache.set(key, { origins, fetchedAt: Date.now(), ttl });
84
+ }
85
+ return origins;
86
+ } catch {
87
+ return (cached == null ? void 0 : cached.origins) ?? [];
88
+ } finally {
89
+ inflight.delete(key);
90
+ }
91
+ })();
92
+ inflight.set(key, request);
93
+ return request;
94
+ }
95
+ async function fetchRegions(origin, headers) {
96
+ const fetchHeaders = {};
97
+ for (const [k, v] of Object.entries(headers ?? {})) {
98
+ if (k.toLowerCase() === "content-type" || k.toLowerCase() === "content-length") continue;
99
+ fetchHeaders[k] = v;
100
+ }
101
+ const response = await fetch(new URL("/settings/regions", origin.origin), {
102
+ method: "GET",
103
+ headers: fetchHeaders,
104
+ // Short timeout so a slow/unreachable discovery endpoint doesn't stall the
105
+ // failover path.
106
+ signal: AbortSignal.timeout(2e3)
107
+ });
108
+ if (!response.ok) {
109
+ throw new Error(`region discovery failed: ${response.status}`);
110
+ }
111
+ const ttl = parseMaxAge(response.headers.get("cache-control"));
112
+ const body = await response.json();
113
+ const origins = (body.regions ?? []).filter((r) => !!r.url).map((r) => new URL(toHttp(r.url)).origin);
114
+ return { origins, ttl };
115
+ }
116
+ function parseMaxAge(cacheControl) {
117
+ if (!cacheControl) return 0;
118
+ for (const directive of cacheControl.split(",")) {
119
+ const trimmed = directive.trim().toLowerCase();
120
+ if (trimmed.startsWith("max-age=")) {
121
+ const secs = parseInt(trimmed.slice("max-age=".length), 10);
122
+ return Number.isFinite(secs) && secs > 0 ? secs * 1e3 : 0;
123
+ }
124
+ }
125
+ return 0;
126
+ }
127
+ // Annotate the CommonJS export names for ESM import in node:
128
+ 0 && (module.exports = {
129
+ FAILOVER_BACKOFF_BASE_MS,
130
+ FAILOVER_MAX_ATTEMPTS,
131
+ MIN_FAILOVER_TIMEOUT_SECONDS,
132
+ failoverAttempts,
133
+ hostKey,
134
+ parseMaxAge,
135
+ pickNext,
136
+ regionOrigins,
137
+ sleep
138
+ });
139
+ //# sourceMappingURL=failover.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/failover.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n// Region failover for the Twirp API clients.\n//\n// On a retryable failure (any transport error or HTTP 5xx) the client discovers\n// alternative LiveKit Cloud regions via /settings/regions and replays the\n// request against the next region, with exponential backoff. 4xx responses are\n// returned immediately.\n\n// Total attempts (the original request + fallback regions) and the base retry\n// backoff are fixed, not user-configurable, so retries can't be tuned to values\n// that could overwhelm the server.\nexport const FAILOVER_MAX_ATTEMPTS = 3;\nexport const FAILOVER_BACKOFF_BASE_MS = 200;\n// Below this per-request timeout, a retry is unlikely to help and many clients\n// would retry in lockstep across regions, so a short request gets a single\n// attempt (thundering-herd guard).\nexport const MIN_FAILOVER_TIMEOUT_SECONDS = 5;\n\n/**\n * Total request attempts for a host; 1 means no failover. Failover only engages\n * when enabled, the host is a LiveKit Cloud domain, and the request timeout is\n * long enough to retry. `force` bypasses the cloud-host check (test-only).\n */\nexport function failoverAttempts(\n enabled: boolean,\n hostname: string,\n force = false,\n timeoutSeconds = 0,\n): number {\n if (!enabled || !(force || isCloud(hostname))) {\n return 1;\n }\n if (timeoutSeconds > 0 && timeoutSeconds < MIN_FAILOVER_TIMEOUT_SECONDS) {\n return 1;\n }\n return FAILOVER_MAX_ATTEMPTS;\n}\n\n// Failover only engages for LiveKit Cloud project domains.\nfunction isCloud(hostname: string): boolean {\n return hostname.endsWith('.livekit.cloud');\n}\n\n/** Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https). */\nfunction toHttp(url: string): string {\n return url.startsWith('ws') ? `http${url.slice(2)}` : url;\n}\n\n/** A stable key identifying a host (including port) for dedup across attempts. */\nexport function hostKey(url: URL): string {\n return url.host.toLowerCase();\n}\n\n/** Returns the first region origin whose host has not yet been attempted. */\nexport function pickNext(regionOrigins: string[], attempted: Set<string>): string | undefined {\n for (const origin of regionOrigins) {\n try {\n if (!attempted.has(hostKey(new URL(origin)))) {\n return origin;\n }\n } catch {\n // skip malformed URLs\n }\n }\n return undefined;\n}\n\nexport function sleep(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\ntype CacheEntry = {\n origins: string[];\n fetchedAt: number;\n ttl: number; // ms\n};\n\n// Shared across all clients in the process so the region list is fetched once.\nconst regionCache = new Map<string, CacheEntry>();\n// Coalesces concurrent discovery fetches per origin: while one /settings/regions\n// request is in flight, other callers for the same origin await its result\n// rather than issuing their own (avoids a thundering herd when many requests\n// fail over at once).\nconst inflight = new Map<string, Promise<string[]>>();\n\n/**\n * Returns alternative region origins for `origin`, fetching /settings/regions\n * if the cache is stale. Best-effort: on a fetch failure it serves a stale\n * cached list when available, otherwise an empty list. Forwards `headers` so a\n * valid token — and any test directives — reach the discovery endpoint.\n */\nexport async function regionOrigins(origin: URL, headers: unknown): Promise<string[]> {\n const key = hostKey(origin);\n const cached = regionCache.get(key);\n if (cached && Date.now() - cached.fetchedAt < cached.ttl) {\n return cached.origins;\n }\n\n const existing = inflight.get(key);\n if (existing) {\n return existing;\n }\n const request = (async () => {\n try {\n const { origins, ttl } = await fetchRegions(origin, headers);\n // A zero TTL (e.g. Cache-Control: max-age=0) means \"do not cache\".\n if (ttl > 0) {\n regionCache.set(key, { origins, fetchedAt: Date.now(), ttl });\n }\n return origins;\n } catch {\n return cached?.origins ?? [];\n } finally {\n inflight.delete(key);\n }\n })();\n\n inflight.set(key, request);\n return request;\n}\n\nasync function fetchRegions(\n origin: URL,\n headers: unknown,\n): Promise<{ origins: string[]; ttl: number }> {\n // Forward the caller's headers (auth + any custom), minus body-specific ones.\n const fetchHeaders: Record<string, string> = {};\n for (const [k, v] of Object.entries((headers as Record<string, string>) ?? {})) {\n if (k.toLowerCase() === 'content-type' || k.toLowerCase() === 'content-length') continue;\n fetchHeaders[k] = v;\n }\n\n const response = await fetch(new URL('/settings/regions', origin.origin), {\n method: 'GET',\n headers: fetchHeaders,\n // Short timeout so a slow/unreachable discovery endpoint doesn't stall the\n // failover path.\n signal: AbortSignal.timeout(2000),\n });\n if (!response.ok) {\n throw new Error(`region discovery failed: ${response.status}`);\n }\n const ttl = parseMaxAge(response.headers.get('cache-control'));\n const body = (await response.json()) as { regions?: Array<{ url?: string }> };\n const origins = (body.regions ?? [])\n .filter((r) => !!r.url)\n .map((r) => new URL(toHttp(r.url!)).origin);\n return { origins, ttl };\n}\n\n/**\n * Returns the `max-age` from a Cache-Control header in milliseconds, or 0 when\n * absent, non-positive, or unparseable (meaning \"do not cache\"). Only `max-age`\n * is honored; other directives (including `s-maxage`, which targets shared\n * proxies) are ignored.\n */\nexport function parseMaxAge(cacheControl: string | null): number {\n if (!cacheControl) return 0;\n for (const directive of cacheControl.split(',')) {\n const trimmed = directive.trim().toLowerCase();\n if (trimmed.startsWith('max-age=')) {\n const secs = parseInt(trimmed.slice('max-age='.length), 10);\n return Number.isFinite(secs) && secs > 0 ? secs * 1000 : 0;\n }\n }\n return 0;\n}\n"],"mappings":"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","names":["regionOrigins"]}
@@ -0,0 +1,30 @@
1
+ declare const FAILOVER_MAX_ATTEMPTS = 3;
2
+ declare const FAILOVER_BACKOFF_BASE_MS = 200;
3
+ declare const MIN_FAILOVER_TIMEOUT_SECONDS = 5;
4
+ /**
5
+ * Total request attempts for a host; 1 means no failover. Failover only engages
6
+ * when enabled, the host is a LiveKit Cloud domain, and the request timeout is
7
+ * long enough to retry. `force` bypasses the cloud-host check (test-only).
8
+ */
9
+ declare function failoverAttempts(enabled: boolean, hostname: string, force?: boolean, timeoutSeconds?: number): number;
10
+ /** A stable key identifying a host (including port) for dedup across attempts. */
11
+ declare function hostKey(url: URL): string;
12
+ /** Returns the first region origin whose host has not yet been attempted. */
13
+ declare function pickNext(regionOrigins: string[], attempted: Set<string>): string | undefined;
14
+ declare function sleep(ms: number): Promise<void>;
15
+ /**
16
+ * Returns alternative region origins for `origin`, fetching /settings/regions
17
+ * if the cache is stale. Best-effort: on a fetch failure it serves a stale
18
+ * cached list when available, otherwise an empty list. Forwards `headers` so a
19
+ * valid token — and any test directives — reach the discovery endpoint.
20
+ */
21
+ declare function regionOrigins(origin: URL, headers: unknown): Promise<string[]>;
22
+ /**
23
+ * Returns the `max-age` from a Cache-Control header in milliseconds, or 0 when
24
+ * absent, non-positive, or unparseable (meaning "do not cache"). Only `max-age`
25
+ * is honored; other directives (including `s-maxage`, which targets shared
26
+ * proxies) are ignored.
27
+ */
28
+ declare function parseMaxAge(cacheControl: string | null): number;
29
+
30
+ export { FAILOVER_BACKOFF_BASE_MS, FAILOVER_MAX_ATTEMPTS, MIN_FAILOVER_TIMEOUT_SECONDS, failoverAttempts, hostKey, parseMaxAge, pickNext, regionOrigins, sleep };
@@ -0,0 +1,30 @@
1
+ declare const FAILOVER_MAX_ATTEMPTS = 3;
2
+ declare const FAILOVER_BACKOFF_BASE_MS = 200;
3
+ declare const MIN_FAILOVER_TIMEOUT_SECONDS = 5;
4
+ /**
5
+ * Total request attempts for a host; 1 means no failover. Failover only engages
6
+ * when enabled, the host is a LiveKit Cloud domain, and the request timeout is
7
+ * long enough to retry. `force` bypasses the cloud-host check (test-only).
8
+ */
9
+ declare function failoverAttempts(enabled: boolean, hostname: string, force?: boolean, timeoutSeconds?: number): number;
10
+ /** A stable key identifying a host (including port) for dedup across attempts. */
11
+ declare function hostKey(url: URL): string;
12
+ /** Returns the first region origin whose host has not yet been attempted. */
13
+ declare function pickNext(regionOrigins: string[], attempted: Set<string>): string | undefined;
14
+ declare function sleep(ms: number): Promise<void>;
15
+ /**
16
+ * Returns alternative region origins for `origin`, fetching /settings/regions
17
+ * if the cache is stale. Best-effort: on a fetch failure it serves a stale
18
+ * cached list when available, otherwise an empty list. Forwards `headers` so a
19
+ * valid token — and any test directives — reach the discovery endpoint.
20
+ */
21
+ declare function regionOrigins(origin: URL, headers: unknown): Promise<string[]>;
22
+ /**
23
+ * Returns the `max-age` from a Cache-Control header in milliseconds, or 0 when
24
+ * absent, non-positive, or unparseable (meaning "do not cache"). Only `max-age`
25
+ * is honored; other directives (including `s-maxage`, which targets shared
26
+ * proxies) are ignored.
27
+ */
28
+ declare function parseMaxAge(cacheControl: string | null): number;
29
+
30
+ export { FAILOVER_BACKOFF_BASE_MS, FAILOVER_MAX_ATTEMPTS, MIN_FAILOVER_TIMEOUT_SECONDS, failoverAttempts, hostKey, parseMaxAge, pickNext, regionOrigins, sleep };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failover.d.ts","sourceRoot":"","sources":["../src/failover.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAI5C,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,UAAQ,EACb,cAAc,SAAI,GACjB,MAAM,CAQR;AAYD,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAExC;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAW5F;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAgBD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA4BpF;AA+BD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAU/D"}