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.
- package/README.md +82 -15
- package/dist/AgentDispatchClient.cjs +5 -3
- package/dist/AgentDispatchClient.cjs.map +1 -1
- package/dist/AgentDispatchClient.js +5 -3
- package/dist/AgentDispatchClient.js.map +1 -1
- package/dist/ClientOptions.cjs.map +1 -1
- package/dist/ClientOptions.d.cts +12 -0
- package/dist/ClientOptions.d.ts +12 -0
- package/dist/ClientOptions.d.ts.map +1 -1
- package/dist/ConnectorClient.cjs +9 -5
- package/dist/ConnectorClient.cjs.map +1 -1
- package/dist/ConnectorClient.d.cts +6 -3
- package/dist/ConnectorClient.d.ts +6 -3
- package/dist/ConnectorClient.d.ts.map +1 -1
- package/dist/ConnectorClient.js +9 -5
- package/dist/ConnectorClient.js.map +1 -1
- package/dist/EgressClient.cjs +19 -3
- package/dist/EgressClient.cjs.map +1 -1
- package/dist/EgressClient.d.cts +7 -1
- package/dist/EgressClient.d.ts +7 -1
- package/dist/EgressClient.d.ts.map +1 -1
- package/dist/EgressClient.js +20 -3
- package/dist/EgressClient.js.map +1 -1
- package/dist/IngressClient.cjs +5 -3
- package/dist/IngressClient.cjs.map +1 -1
- package/dist/IngressClient.js +5 -3
- package/dist/IngressClient.js.map +1 -1
- package/dist/LiveKitAPI.cjs +80 -0
- package/dist/LiveKitAPI.cjs.map +1 -0
- package/dist/LiveKitAPI.d.cts +77 -0
- package/dist/LiveKitAPI.d.ts +77 -0
- package/dist/LiveKitAPI.d.ts.map +1 -0
- package/dist/LiveKitAPI.js +56 -0
- package/dist/LiveKitAPI.js.map +1 -0
- package/dist/RoomServiceClient.cjs +5 -3
- package/dist/RoomServiceClient.cjs.map +1 -1
- package/dist/RoomServiceClient.js +5 -3
- package/dist/RoomServiceClient.js.map +1 -1
- package/dist/ServiceBase.cjs +9 -9
- package/dist/ServiceBase.cjs.map +1 -1
- package/dist/ServiceBase.d.cts +20 -1
- package/dist/ServiceBase.d.ts +20 -1
- package/dist/ServiceBase.d.ts.map +1 -1
- package/dist/ServiceBase.js +9 -9
- package/dist/ServiceBase.js.map +1 -1
- package/dist/SipClient.cjs +40 -19
- package/dist/SipClient.cjs.map +1 -1
- package/dist/SipClient.d.cts +4 -2
- package/dist/SipClient.d.ts +4 -2
- package/dist/SipClient.d.ts.map +1 -1
- package/dist/SipClient.js +41 -20
- package/dist/SipClient.js.map +1 -1
- package/dist/TwirpRPC.cjs +135 -48
- package/dist/TwirpRPC.cjs.map +1 -1
- package/dist/TwirpRPC.d.cts +38 -2
- package/dist/TwirpRPC.d.ts +38 -2
- package/dist/TwirpRPC.d.ts.map +1 -1
- package/dist/TwirpRPC.js +140 -38
- package/dist/TwirpRPC.js.map +1 -1
- package/dist/dialTimeout.cjs +39 -0
- package/dist/dialTimeout.cjs.map +1 -0
- package/dist/dialTimeout.d.cts +21 -0
- package/dist/dialTimeout.d.ts +21 -0
- package/dist/dialTimeout.d.ts.map +1 -0
- package/dist/dialTimeout.js +13 -0
- package/dist/dialTimeout.js.map +1 -0
- package/dist/failover.cjs +139 -0
- package/dist/failover.cjs.map +1 -0
- package/dist/failover.d.cts +30 -0
- package/dist/failover.d.ts +30 -0
- package/dist/failover.d.ts.map +1 -0
- package/dist/failover.js +107 -0
- package/dist/failover.js.map +1 -0
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/version.cjs +29 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.d.cts +3 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -3
- package/src/AgentDispatchClient.ts +5 -5
- package/src/ClientOptions.ts +12 -0
- package/src/ConnectorClient.ts +20 -11
- package/src/EgressClient.ts +21 -5
- package/src/IngressClient.ts +5 -5
- package/src/LiveKitAPI.ts +126 -0
- package/src/RoomServiceClient.ts +5 -5
- package/src/ServiceBase.ts +35 -4
- package/src/SipClient.ts +55 -23
- package/src/TwirpRPC.test.ts +42 -0
- package/src/TwirpRPC.ts +187 -39
- package/src/dialTimeout.test.ts +41 -0
- package/src/dialTimeout.ts +37 -0
- package/src/failover.test.ts +188 -0
- package/src/failover.ts +170 -0
- package/src/index.ts +11 -1
- package/src/version.ts +1 -0
package/src/failover.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
// Region failover for the Twirp API clients.
|
|
6
|
+
//
|
|
7
|
+
// On a retryable failure (any transport error or HTTP 5xx) the client discovers
|
|
8
|
+
// alternative LiveKit Cloud regions via /settings/regions and replays the
|
|
9
|
+
// request against the next region, with exponential backoff. 4xx responses are
|
|
10
|
+
// returned immediately.
|
|
11
|
+
|
|
12
|
+
// Total attempts (the original request + fallback regions) and the base retry
|
|
13
|
+
// backoff are fixed, not user-configurable, so retries can't be tuned to values
|
|
14
|
+
// that could overwhelm the server.
|
|
15
|
+
export const FAILOVER_MAX_ATTEMPTS = 3;
|
|
16
|
+
export const FAILOVER_BACKOFF_BASE_MS = 200;
|
|
17
|
+
// Below this per-request timeout, a retry is unlikely to help and many clients
|
|
18
|
+
// would retry in lockstep across regions, so a short request gets a single
|
|
19
|
+
// attempt (thundering-herd guard).
|
|
20
|
+
export const MIN_FAILOVER_TIMEOUT_SECONDS = 5;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Total request attempts for a host; 1 means no failover. Failover only engages
|
|
24
|
+
* when enabled, the host is a LiveKit Cloud domain, and the request timeout is
|
|
25
|
+
* long enough to retry. `force` bypasses the cloud-host check (test-only).
|
|
26
|
+
*/
|
|
27
|
+
export function failoverAttempts(
|
|
28
|
+
enabled: boolean,
|
|
29
|
+
hostname: string,
|
|
30
|
+
force = false,
|
|
31
|
+
timeoutSeconds = 0,
|
|
32
|
+
): number {
|
|
33
|
+
if (!enabled || !(force || isCloud(hostname))) {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
if (timeoutSeconds > 0 && timeoutSeconds < MIN_FAILOVER_TIMEOUT_SECONDS) {
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
return FAILOVER_MAX_ATTEMPTS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Failover only engages for LiveKit Cloud project domains.
|
|
43
|
+
function isCloud(hostname: string): boolean {
|
|
44
|
+
return hostname.endsWith('.livekit.cloud');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https). */
|
|
48
|
+
function toHttp(url: string): string {
|
|
49
|
+
return url.startsWith('ws') ? `http${url.slice(2)}` : url;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A stable key identifying a host (including port) for dedup across attempts. */
|
|
53
|
+
export function hostKey(url: URL): string {
|
|
54
|
+
return url.host.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns the first region origin whose host has not yet been attempted. */
|
|
58
|
+
export function pickNext(regionOrigins: string[], attempted: Set<string>): string | undefined {
|
|
59
|
+
for (const origin of regionOrigins) {
|
|
60
|
+
try {
|
|
61
|
+
if (!attempted.has(hostKey(new URL(origin)))) {
|
|
62
|
+
return origin;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// skip malformed URLs
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function sleep(ms: number): Promise<void> {
|
|
72
|
+
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type CacheEntry = {
|
|
76
|
+
origins: string[];
|
|
77
|
+
fetchedAt: number;
|
|
78
|
+
ttl: number; // ms
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Shared across all clients in the process so the region list is fetched once.
|
|
82
|
+
const regionCache = new Map<string, CacheEntry>();
|
|
83
|
+
// Coalesces concurrent discovery fetches per origin: while one /settings/regions
|
|
84
|
+
// request is in flight, other callers for the same origin await its result
|
|
85
|
+
// rather than issuing their own (avoids a thundering herd when many requests
|
|
86
|
+
// fail over at once).
|
|
87
|
+
const inflight = new Map<string, Promise<string[]>>();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns alternative region origins for `origin`, fetching /settings/regions
|
|
91
|
+
* if the cache is stale. Best-effort: on a fetch failure it serves a stale
|
|
92
|
+
* cached list when available, otherwise an empty list. Forwards `headers` so a
|
|
93
|
+
* valid token — and any test directives — reach the discovery endpoint.
|
|
94
|
+
*/
|
|
95
|
+
export async function regionOrigins(origin: URL, headers: unknown): Promise<string[]> {
|
|
96
|
+
const key = hostKey(origin);
|
|
97
|
+
const cached = regionCache.get(key);
|
|
98
|
+
if (cached && Date.now() - cached.fetchedAt < cached.ttl) {
|
|
99
|
+
return cached.origins;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const existing = inflight.get(key);
|
|
103
|
+
if (existing) {
|
|
104
|
+
return existing;
|
|
105
|
+
}
|
|
106
|
+
const request = (async () => {
|
|
107
|
+
try {
|
|
108
|
+
const { origins, ttl } = await fetchRegions(origin, headers);
|
|
109
|
+
// A zero TTL (e.g. Cache-Control: max-age=0) means "do not cache".
|
|
110
|
+
if (ttl > 0) {
|
|
111
|
+
regionCache.set(key, { origins, fetchedAt: Date.now(), ttl });
|
|
112
|
+
}
|
|
113
|
+
return origins;
|
|
114
|
+
} catch {
|
|
115
|
+
return cached?.origins ?? [];
|
|
116
|
+
} finally {
|
|
117
|
+
inflight.delete(key);
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
inflight.set(key, request);
|
|
122
|
+
return request;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function fetchRegions(
|
|
126
|
+
origin: URL,
|
|
127
|
+
headers: unknown,
|
|
128
|
+
): Promise<{ origins: string[]; ttl: number }> {
|
|
129
|
+
// Forward the caller's headers (auth + any custom), minus body-specific ones.
|
|
130
|
+
const fetchHeaders: Record<string, string> = {};
|
|
131
|
+
for (const [k, v] of Object.entries((headers as Record<string, string>) ?? {})) {
|
|
132
|
+
if (k.toLowerCase() === 'content-type' || k.toLowerCase() === 'content-length') continue;
|
|
133
|
+
fetchHeaders[k] = v;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const response = await fetch(new URL('/settings/regions', origin.origin), {
|
|
137
|
+
method: 'GET',
|
|
138
|
+
headers: fetchHeaders,
|
|
139
|
+
// Short timeout so a slow/unreachable discovery endpoint doesn't stall the
|
|
140
|
+
// failover path.
|
|
141
|
+
signal: AbortSignal.timeout(2000),
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`region discovery failed: ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
const ttl = parseMaxAge(response.headers.get('cache-control'));
|
|
147
|
+
const body = (await response.json()) as { regions?: Array<{ url?: string }> };
|
|
148
|
+
const origins = (body.regions ?? [])
|
|
149
|
+
.filter((r) => !!r.url)
|
|
150
|
+
.map((r) => new URL(toHttp(r.url!)).origin);
|
|
151
|
+
return { origins, ttl };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the `max-age` from a Cache-Control header in milliseconds, or 0 when
|
|
156
|
+
* absent, non-positive, or unparseable (meaning "do not cache"). Only `max-age`
|
|
157
|
+
* is honored; other directives (including `s-maxage`, which targets shared
|
|
158
|
+
* proxies) are ignored.
|
|
159
|
+
*/
|
|
160
|
+
export function parseMaxAge(cacheControl: string | null): number {
|
|
161
|
+
if (!cacheControl) return 0;
|
|
162
|
+
for (const directive of cacheControl.split(',')) {
|
|
163
|
+
const trimmed = directive.trim().toLowerCase();
|
|
164
|
+
if (trimmed.startsWith('max-age=')) {
|
|
165
|
+
const secs = parseInt(trimmed.slice('max-age='.length), 10);
|
|
166
|
+
return Number.isFinite(secs) && secs > 0 ? secs * 1000 : 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
EncodedFileType,
|
|
25
25
|
EncodingOptions,
|
|
26
26
|
EncodingOptionsPreset,
|
|
27
|
+
FileOutput,
|
|
27
28
|
GCPUpload,
|
|
28
29
|
ImageCodec,
|
|
29
30
|
ImageFileSuffix,
|
|
@@ -38,10 +39,13 @@ export {
|
|
|
38
39
|
IngressVideoEncodingPreset,
|
|
39
40
|
IngressVideoOptions,
|
|
40
41
|
JobRestartPolicy,
|
|
42
|
+
MediaSource,
|
|
43
|
+
Output,
|
|
41
44
|
ParticipantEgressRequest,
|
|
42
45
|
ParticipantInfo,
|
|
43
46
|
ParticipantInfo_State,
|
|
44
47
|
ParticipantPermission,
|
|
48
|
+
ParticipantVideo,
|
|
45
49
|
Room,
|
|
46
50
|
RoomAgentDispatch,
|
|
47
51
|
RoomCompositeEgressRequest,
|
|
@@ -61,14 +65,18 @@ export {
|
|
|
61
65
|
SIPCallStatus,
|
|
62
66
|
SegmentedFileOutput,
|
|
63
67
|
SegmentedFileProtocol,
|
|
68
|
+
StartEgressRequest,
|
|
69
|
+
StorageConfig,
|
|
64
70
|
StreamOutput,
|
|
65
71
|
StreamProtocol,
|
|
72
|
+
TemplateSource,
|
|
66
73
|
TrackCompositeEgressRequest,
|
|
67
74
|
TrackEgressRequest,
|
|
68
75
|
TrackInfo,
|
|
69
76
|
TrackSource,
|
|
70
77
|
TrackType,
|
|
71
78
|
WebEgressRequest,
|
|
79
|
+
WebSource,
|
|
72
80
|
VideoCodec,
|
|
73
81
|
WebhookConfig,
|
|
74
82
|
} from '@livekit/protocol';
|
|
@@ -78,7 +86,9 @@ export * from './ConnectorClient.js';
|
|
|
78
86
|
export * from './EgressClient.js';
|
|
79
87
|
export * from './grants.js';
|
|
80
88
|
export * from './IngressClient.js';
|
|
89
|
+
export * from './LiveKitAPI.js';
|
|
81
90
|
export * from './RoomServiceClient.js';
|
|
82
91
|
export * from './SipClient.js';
|
|
83
|
-
export { TwirpError } from './TwirpRPC.js';
|
|
92
|
+
export { ServerError, SipCallError, TwirpError } from './TwirpRPC.js';
|
|
93
|
+
export type { ClientOptions } from './ClientOptions.js';
|
|
84
94
|
export * from './WebhookReceiver.js';
|
package/src/version.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SDK_VERSION = "2.18.0";
|