@decartai/sdk 0.0.68 → 0.1.1

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 (47) hide show
  1. package/README.md +49 -9
  2. package/dist/files/client.d.ts +33 -0
  3. package/dist/files/client.js +65 -0
  4. package/dist/files/types.d.ts +22 -0
  5. package/dist/files/types.js +4 -0
  6. package/dist/index.d.ts +19 -4
  7. package/dist/index.js +50 -28
  8. package/dist/process/client.js +1 -3
  9. package/dist/process/request.js +1 -3
  10. package/dist/queue/client.js +1 -3
  11. package/dist/queue/polling.js +1 -2
  12. package/dist/queue/request.js +1 -3
  13. package/dist/realtime/client.d.ts +23 -11
  14. package/dist/realtime/client.js +85 -156
  15. package/dist/realtime/config-realtime.js +49 -0
  16. package/dist/realtime/event-buffer.js +1 -3
  17. package/dist/realtime/initial-state-gate.js +21 -0
  18. package/dist/realtime/media-channel.js +82 -0
  19. package/dist/realtime/methods.js +25 -43
  20. package/dist/realtime/mirror-stream.js +1 -2
  21. package/dist/realtime/observability/diagnostics.d.ts +14 -53
  22. package/dist/realtime/observability/livekit-stats-provider.js +25 -0
  23. package/dist/realtime/observability/realtime-observability.js +70 -6
  24. package/dist/realtime/observability/telemetry-reporter.js +9 -28
  25. package/dist/realtime/observability/webrtc-stats.d.ts +5 -4
  26. package/dist/realtime/observability/webrtc-stats.js +3 -5
  27. package/dist/realtime/signaling-channel.js +302 -0
  28. package/dist/realtime/stream-session.js +257 -0
  29. package/dist/realtime/subscribe-client.d.ts +2 -3
  30. package/dist/realtime/subscribe-client.js +115 -11
  31. package/dist/realtime/types.d.ts +25 -1
  32. package/dist/shared/model.d.ts +11 -1
  33. package/dist/shared/model.js +51 -14
  34. package/dist/shared/request.js +1 -3
  35. package/dist/shared/types.js +1 -3
  36. package/dist/tokens/client.js +1 -3
  37. package/dist/utils/env.js +1 -2
  38. package/dist/utils/errors.d.ts +3 -0
  39. package/dist/utils/errors.js +4 -2
  40. package/dist/utils/logger.js +1 -2
  41. package/dist/utils/media.js +43 -0
  42. package/dist/utils/platform.js +13 -0
  43. package/dist/utils/user-agent.js +1 -3
  44. package/dist/version.js +1 -2
  45. package/package.json +2 -1
  46. package/dist/realtime/webrtc-connection.js +0 -500
  47. package/dist/realtime/webrtc-manager.js +0 -210
@@ -0,0 +1,25 @@
1
+ //#region src/realtime/observability/livekit-stats-provider.ts
2
+ function createLiveKitStatsProvider(room) {
3
+ let uid = 0;
4
+ const collectFromTrack = async (track, entries) => {
5
+ if (!track) return;
6
+ let report;
7
+ try {
8
+ report = await track.getRTCStatsReport();
9
+ } catch {
10
+ return;
11
+ }
12
+ if (!report) return;
13
+ report.forEach((stat, id) => {
14
+ entries.push([`${id}#${uid++}`, stat]);
15
+ });
16
+ };
17
+ return { async getStats() {
18
+ const entries = [];
19
+ for (const pub of room.localParticipant.trackPublications.values()) await collectFromTrack(pub.track, entries);
20
+ for (const participant of room.remoteParticipants.values()) for (const pub of participant.trackPublications.values()) await collectFromTrack(pub.track, entries);
21
+ return new Map(entries);
22
+ } };
23
+ }
24
+ //#endregion
25
+ export { createLiveKitStatsProvider };
@@ -1,16 +1,18 @@
1
+ import { REALTIME_CONFIG } from "../config-realtime.js";
2
+ import { createLiveKitStatsProvider } from "./livekit-stats-provider.js";
1
3
  import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
2
4
  import { WebRTCStatsCollector } from "./webrtc-stats.js";
3
-
4
5
  //#region src/realtime/observability/realtime-observability.ts
5
- const STALL_FPS_THRESHOLD = .5;
6
6
  var RealtimeObservability = class {
7
7
  telemetryReporter = new NullTelemetryReporter();
8
8
  telemetryReporterReady = false;
9
9
  pendingTelemetryDiagnostics = [];
10
10
  statsCollector = null;
11
11
  statsCollectorSource = null;
12
+ liveKitRoom = null;
12
13
  videoStalled = false;
13
14
  stallStartMs = 0;
15
+ connectionBreakdown = null;
14
16
  constructor(options) {
15
17
  this.options = options;
16
18
  }
@@ -22,6 +24,56 @@ var RealtimeObservability = class {
22
24
  });
23
25
  this.addTelemetryDiagnostic(name, data, timestamp);
24
26
  }
27
+ beginConnectionBreakdown(attempt, initialImageSizeKb) {
28
+ this.connectionBreakdown = {
29
+ attempt,
30
+ connectStartedAt: Date.now(),
31
+ initialImageSizeKb,
32
+ phases: /* @__PURE__ */ new Map()
33
+ };
34
+ }
35
+ startPhase(name) {
36
+ if (!this.connectionBreakdown) return;
37
+ this.connectionBreakdown.phases.set(name, { startedAt: Date.now() });
38
+ }
39
+ endPhase(name, opts) {
40
+ if (!this.connectionBreakdown) return;
41
+ const entry = this.connectionBreakdown.phases.get(name);
42
+ if (!entry) {
43
+ this.options.logger.warn("observability: endPhase called for unknown phase", { phase: name });
44
+ return;
45
+ }
46
+ entry.endedAt = Date.now();
47
+ entry.success = opts.success;
48
+ if (opts.error !== void 0) entry.error = opts.error;
49
+ }
50
+ finishConnectionBreakdown(opts) {
51
+ const buffer = this.connectionBreakdown;
52
+ if (!buffer) return;
53
+ this.connectionBreakdown = null;
54
+ const now = Date.now();
55
+ const phases = [];
56
+ for (const [phase, entry] of buffer.phases) {
57
+ const unfinished = entry.endedAt === void 0;
58
+ const endedAt = entry.endedAt ?? now;
59
+ const success = entry.success ?? false;
60
+ const error = entry.error ?? (unfinished && !opts.success ? opts.error : void 0);
61
+ phases.push({
62
+ phase,
63
+ durationMs: endedAt - entry.startedAt,
64
+ success,
65
+ ...error !== void 0 ? { error } : {}
66
+ });
67
+ }
68
+ this.diagnostic("client-session-connection-breakdown", {
69
+ attempt: buffer.attempt,
70
+ success: opts.success,
71
+ totalDurationMs: now - buffer.connectStartedAt,
72
+ initialImageSizeKb: buffer.initialImageSizeKb,
73
+ phases,
74
+ ...opts.error !== void 0 ? { error: opts.error } : {}
75
+ }, now);
76
+ }
25
77
  sessionStarted(sessionId) {
26
78
  if (!this.options.telemetryEnabled) return;
27
79
  if (this.telemetryReporterReady) this.telemetryReporter.stop();
@@ -45,15 +97,27 @@ var RealtimeObservability = class {
45
97
  }
46
98
  if (source === this.statsCollectorSource) return;
47
99
  this.stopStats();
100
+ this.resetStallDetection();
48
101
  this.statsCollectorSource = source;
49
102
  if (!this.options.telemetryEnabled && !this.options.onStats) return;
50
103
  this.statsCollector = new WebRTCStatsCollector();
51
104
  this.statsCollector.start(source, (stats) => this.handleStats(stats));
52
105
  }
106
+ setLiveKitRoom(room) {
107
+ if (!room) {
108
+ this.liveKitRoom = null;
109
+ this.setStatsProvider(null);
110
+ return;
111
+ }
112
+ if (room === this.liveKitRoom) return;
113
+ this.setStatsProvider(createLiveKitStatsProvider(room));
114
+ this.liveKitRoom = room;
115
+ }
53
116
  stopStats() {
54
117
  this.statsCollector?.stop();
55
118
  this.statsCollector = null;
56
119
  this.statsCollectorSource = null;
120
+ this.liveKitRoom = null;
57
121
  this.resetStallDetection();
58
122
  }
59
123
  stop() {
@@ -62,6 +126,7 @@ var RealtimeObservability = class {
62
126
  this.telemetryReporter = new NullTelemetryReporter();
63
127
  this.telemetryReporterReady = false;
64
128
  this.pendingTelemetryDiagnostics.length = 0;
129
+ this.connectionBreakdown = null;
65
130
  }
66
131
  handleStats(stats) {
67
132
  this.options.onStats?.(stats);
@@ -70,14 +135,14 @@ var RealtimeObservability = class {
70
135
  }
71
136
  detectVideoStall(stats) {
72
137
  const fps = stats.video?.framesPerSecond ?? 0;
73
- if (!this.videoStalled && stats.video && fps < STALL_FPS_THRESHOLD) {
138
+ if (!this.videoStalled && stats.video && fps < REALTIME_CONFIG.observability.stallFpsThreshold) {
74
139
  this.videoStalled = true;
75
140
  this.stallStartMs = Date.now();
76
141
  this.diagnostic("videoStall", {
77
142
  stalled: true,
78
143
  durationMs: 0
79
144
  }, this.stallStartMs);
80
- } else if (this.videoStalled && fps >= STALL_FPS_THRESHOLD) {
145
+ } else if (this.videoStalled && fps >= REALTIME_CONFIG.observability.stallFpsThreshold) {
81
146
  const durationMs = Date.now() - this.stallStartMs;
82
147
  this.videoStalled = false;
83
148
  this.diagnostic("videoStall", {
@@ -104,6 +169,5 @@ var RealtimeObservability = class {
104
169
  this.stallStartMs = 0;
105
170
  }
106
171
  };
107
-
108
172
  //#endregion
109
- export { RealtimeObservability };
173
+ export { RealtimeObservability };
@@ -1,14 +1,7 @@
1
1
  import { VERSION } from "../../version.js";
2
2
  import { buildAuthHeaders } from "../../shared/request.js";
3
-
3
+ import { REALTIME_CONFIG } from "../config-realtime.js";
4
4
  //#region src/realtime/observability/telemetry-reporter.ts
5
- const DEFAULT_REPORT_INTERVAL_MS = 1e4;
6
- const TELEMETRY_URL = "https://platform.decart.ai/api/v1/telemetry";
7
- /**
8
- * Maximum number of items per array (stats / diagnostics) in a single report.
9
- * Matches the backend Zod schema which enforces `z.array().max(120)`.
10
- */
11
- const MAX_ITEMS_PER_REPORT = 120;
12
5
  /** No-op reporter that silently discards all data. Used when telemetry is disabled. */
13
6
  var NullTelemetryReporter = class {
14
7
  start() {}
@@ -22,7 +15,6 @@ var TelemetryReporter = class {
22
15
  sessionId;
23
16
  model;
24
17
  integration;
25
- logger;
26
18
  reportIntervalMs;
27
19
  intervalId = null;
28
20
  statsBuffer = [];
@@ -32,8 +24,7 @@ var TelemetryReporter = class {
32
24
  this.sessionId = options.sessionId;
33
25
  this.model = options.model;
34
26
  this.integration = options.integration;
35
- this.logger = options.logger;
36
- this.reportIntervalMs = options.reportIntervalMs ?? DEFAULT_REPORT_INTERVAL_MS;
27
+ this.reportIntervalMs = options.reportIntervalMs ?? REALTIME_CONFIG.observability.telemetryReportIntervalMs;
37
28
  }
38
29
  /** Start the periodic reporting timer. */
39
30
  start() {
@@ -62,7 +53,7 @@ var TelemetryReporter = class {
62
53
  this.diagnosticsBuffer = [];
63
54
  }
64
55
  /**
65
- * Build a single chunk from the front of the buffers, respecting MAX_ITEMS_PER_REPORT.
56
+ * Build a single chunk from the front of the buffers, respecting the configured report item cap.
66
57
  * Returns null when both buffers are empty.
67
58
  */
68
59
  createReportChunk() {
@@ -79,8 +70,8 @@ var TelemetryReporter = class {
79
70
  sdkVersion: VERSION,
80
71
  ...this.model ? { model: this.model } : {},
81
72
  tags,
82
- stats: this.statsBuffer.splice(0, MAX_ITEMS_PER_REPORT),
83
- diagnostics: this.diagnosticsBuffer.splice(0, MAX_ITEMS_PER_REPORT)
73
+ stats: this.statsBuffer.splice(0, REALTIME_CONFIG.observability.telemetryMaxItemsPerReport),
74
+ diagnostics: this.diagnosticsBuffer.splice(0, REALTIME_CONFIG.observability.telemetryMaxItemsPerReport)
84
75
  };
85
76
  }
86
77
  sendReport() {
@@ -95,25 +86,15 @@ var TelemetryReporter = class {
95
86
  };
96
87
  let chunk = this.createReportChunk();
97
88
  while (chunk !== null) {
98
- fetch(TELEMETRY_URL, {
89
+ fetch(REALTIME_CONFIG.observability.telemetryUrl, {
99
90
  method: "POST",
100
91
  headers: commonHeaders,
101
92
  body: JSON.stringify(chunk)
102
- }).then((response) => {
103
- if (!response.ok) this.logger.warn("Telemetry report rejected", {
104
- status: response.status,
105
- statusText: response.statusText
106
- });
107
- }).catch((error) => {
108
- this.logger.debug("Telemetry report failed", { error: String(error) });
109
- });
93
+ }).catch(() => {});
110
94
  chunk = this.createReportChunk();
111
95
  }
112
- } catch (error) {
113
- this.logger.debug("Telemetry report failed", { error: String(error) });
114
- }
96
+ } catch {}
115
97
  }
116
98
  };
117
-
118
99
  //#endregion
119
- export { NullTelemetryReporter, TelemetryReporter };
100
+ export { NullTelemetryReporter, TelemetryReporter };
@@ -127,10 +127,7 @@ type WebRTCStats = {
127
127
  * jitter and failure modes, so this is essential signal for
128
128
  * benchmarking and incident triage.
129
129
  */
130
- selectedCandidatePairs: Array<{
131
- local: IceCandidateInfo;
132
- remote: IceCandidateInfo;
133
- }>;
130
+ selectedCandidatePairs: IceCandidatePair[];
134
131
  };
135
132
  };
136
133
  /** One side of an ICE candidate pair (sender or receiver). */
@@ -143,5 +140,9 @@ type IceCandidateInfo = {
143
140
  /** "udp" | "tcp" */
144
141
  protocol: string;
145
142
  };
143
+ type IceCandidatePair = {
144
+ local: IceCandidateInfo;
145
+ remote: IceCandidateInfo;
146
+ };
146
147
  //#endregion
147
148
  export { WebRTCStats };
@@ -1,6 +1,5 @@
1
+ import { REALTIME_CONFIG } from "../config-realtime.js";
1
2
  //#region src/realtime/observability/webrtc-stats.ts
2
- const DEFAULT_INTERVAL_MS = 1e3;
3
- const MIN_INTERVAL_MS = 500;
4
3
  var WebRTCStatsCollector = class {
5
4
  source = null;
6
5
  intervalId = null;
@@ -17,7 +16,7 @@ var WebRTCStatsCollector = class {
17
16
  onStats = null;
18
17
  intervalMs;
19
18
  constructor(options = {}) {
20
- this.intervalMs = Math.max(options.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
19
+ this.intervalMs = Math.max(options.intervalMs ?? REALTIME_CONFIG.observability.statsDefaultIntervalMs, REALTIME_CONFIG.observability.statsMinIntervalMs);
21
20
  }
22
21
  /** Attach to a stats provider and start polling. */
23
22
  start(source, onStats) {
@@ -273,6 +272,5 @@ var WebRTCStatsCollector = class {
273
272
  };
274
273
  }
275
274
  };
276
-
277
275
  //#endregion
278
- export { WebRTCStatsCollector };
276
+ export { WebRTCStatsCollector };
@@ -0,0 +1,302 @@
1
+ import { buildUserAgent } from "../utils/user-agent.js";
2
+ import { createConsoleLogger } from "../utils/logger.js";
3
+ import { REALTIME_CONFIG } from "./config-realtime.js";
4
+ import mitt from "mitt";
5
+ //#region src/realtime/signaling-channel.ts
6
+ var SignalingChannel = class {
7
+ ws = null;
8
+ events = mitt();
9
+ pendingAcks = [];
10
+ pendingRoomInfo = null;
11
+ connected = false;
12
+ closing = false;
13
+ logger;
14
+ constructor(config) {
15
+ this.config = config;
16
+ this.logger = config.logger ?? createConsoleLogger("warn");
17
+ }
18
+ on(event, handler) {
19
+ this.events.on(event, handler);
20
+ }
21
+ off(event, handler) {
22
+ this.events.off(event, handler);
23
+ }
24
+ async openAndJoin(opts = {}) {
25
+ const connectTimeout = opts.connectTimeout ?? REALTIME_CONFIG.signaling.connectTimeoutMs;
26
+ const handshakeTimeout = opts.handshakeTimeout ?? REALTIME_CONFIG.signaling.handshakeTimeoutMs;
27
+ this.config.observability?.startPhase("websocket-open");
28
+ await this.openSocket(connectTimeout);
29
+ this.config.observability?.endPhase("websocket-open", { success: true });
30
+ this.config.observability?.startPhase("room-join");
31
+ const roomInfoWait = this.waitForRoomInfo(handshakeTimeout);
32
+ if (!this.writeMessage({ type: "livekit_join" })) {
33
+ roomInfoWait.cancel();
34
+ throw new Error("WebSocket is not open");
35
+ }
36
+ let roomInfo;
37
+ try {
38
+ roomInfo = await roomInfoWait.promise;
39
+ } catch (error) {
40
+ this.rejectAllPending(error instanceof Error ? error : new Error(String(error)));
41
+ throw error;
42
+ }
43
+ this.config.observability?.endPhase("room-join", { success: true });
44
+ this.connected = true;
45
+ const initialStateAck = this.sendInitialStateTracked(opts.initialState);
46
+ initialStateAck.catch(() => {});
47
+ return {
48
+ roomInfo,
49
+ initialStateAck
50
+ };
51
+ }
52
+ async sendInitialStateTracked(initialState) {
53
+ if (!initialState) return;
54
+ this.config.observability?.startPhase("initial-state-handshake");
55
+ await this.sendInitialState(initialState);
56
+ this.config.observability?.endPhase("initial-state-handshake", { success: true });
57
+ }
58
+ close() {
59
+ this.closing = true;
60
+ this.connected = false;
61
+ const ws = this.ws;
62
+ this.ws = null;
63
+ if (ws) try {
64
+ ws.close();
65
+ } catch {}
66
+ this.rejectPendingRoomInfo(/* @__PURE__ */ new Error("Control channel closed"));
67
+ this.rejectAllPending(/* @__PURE__ */ new Error("Control channel closed"));
68
+ }
69
+ async sendPrompt(text, opts = {}) {
70
+ const ack = await this.request({
71
+ message: {
72
+ type: "prompt",
73
+ prompt: text,
74
+ enhance_prompt: opts.enhance ?? true
75
+ },
76
+ matchAck: (msg) => msg.type === "prompt_ack" && msg.prompt === text,
77
+ timeoutMs: opts.timeout ?? REALTIME_CONFIG.signaling.requestTimeoutMs,
78
+ label: "Prompt send"
79
+ });
80
+ if (!ack.success) throw new Error(ack.error ?? "Failed to send prompt");
81
+ }
82
+ async setImage(payload, opts = {}) {
83
+ const message = payload.kind === "ref" ? {
84
+ type: "set_image",
85
+ image_ref: payload.ref
86
+ } : {
87
+ type: "set_image",
88
+ image_data: payload.data
89
+ };
90
+ if (opts.prompt !== void 0) message.prompt = opts.prompt;
91
+ if (opts.enhance !== void 0) message.enhance_prompt = opts.enhance;
92
+ const ack = await this.request({
93
+ message,
94
+ matchAck: (msg) => msg.type === "set_image_ack",
95
+ timeoutMs: opts.timeout ?? REALTIME_CONFIG.signaling.requestTimeoutMs,
96
+ label: "Image send"
97
+ });
98
+ if (!ack.success) throw new Error(ack.error ?? "Failed to send image");
99
+ }
100
+ async openSocket(timeout) {
101
+ const userAgent = encodeURIComponent(buildUserAgent(this.config.integration));
102
+ const separator = this.config.url.includes("?") ? "&" : "?";
103
+ const wsUrl = `${this.config.url}${separator}user_agent=${userAgent}`;
104
+ this.closing = false;
105
+ await new Promise((resolve, reject) => {
106
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`WebSocket open timeout (${timeout}ms)`)), timeout);
107
+ const ws = new WebSocket(wsUrl);
108
+ this.ws = ws;
109
+ ws.onopen = () => {
110
+ clearTimeout(timer);
111
+ resolve();
112
+ };
113
+ ws.onclose = (e) => {
114
+ clearTimeout(timer);
115
+ const wasConnected = this.connected;
116
+ const pendingCount = this.pendingAcks.length;
117
+ this.connected = false;
118
+ this.ws = null;
119
+ this.logger.warn("signaling: websocket closed", {
120
+ code: e.code,
121
+ reason: e.reason,
122
+ wasConnected,
123
+ closing: this.closing,
124
+ pendingAcks: pendingCount
125
+ });
126
+ const error = /* @__PURE__ */ new Error(`WebSocket closed: ${e.code} ${e.reason}`);
127
+ this.rejectPendingRoomInfo(error);
128
+ this.rejectAllPending(error);
129
+ if (wasConnected || this.closing) this.events.emit("closed", {
130
+ code: e.code,
131
+ reason: e.reason
132
+ });
133
+ else reject(error);
134
+ };
135
+ ws.onerror = () => {};
136
+ ws.onmessage = (e) => {
137
+ try {
138
+ this.handleMessage(JSON.parse(e.data));
139
+ } catch {}
140
+ };
141
+ });
142
+ }
143
+ waitForRoomInfo(timeoutMs) {
144
+ let cleanup = () => {};
145
+ return {
146
+ promise: new Promise((resolve, reject) => {
147
+ let timer = setTimeout(() => {
148
+ cleanup();
149
+ this.logger.warn("signaling: livekit_room_info timeout", { timeoutMs });
150
+ reject(/* @__PURE__ */ new Error(`livekit_room_info timeout (${timeoutMs}ms)`));
151
+ }, timeoutMs);
152
+ const pendingRoomInfo = {
153
+ resolve: (info) => {
154
+ cleanup();
155
+ resolve(info);
156
+ },
157
+ reject: (err) => {
158
+ cleanup();
159
+ reject(err);
160
+ },
161
+ cancel: () => {
162
+ cleanup();
163
+ },
164
+ pauseTimeout: () => {
165
+ if (timer) {
166
+ clearTimeout(timer);
167
+ timer = null;
168
+ }
169
+ }
170
+ };
171
+ cleanup = () => {
172
+ if (timer) {
173
+ clearTimeout(timer);
174
+ timer = null;
175
+ }
176
+ if (this.pendingRoomInfo === pendingRoomInfo) this.pendingRoomInfo = null;
177
+ };
178
+ this.pendingRoomInfo = pendingRoomInfo;
179
+ }),
180
+ cancel: cleanup
181
+ };
182
+ }
183
+ async sendInitialState(initialState) {
184
+ if (!initialState) return;
185
+ if (initialState.imageRef !== void 0) {
186
+ await this.setImage({
187
+ kind: "ref",
188
+ ref: initialState.imageRef
189
+ }, {
190
+ prompt: initialState.prompt,
191
+ enhance: initialState.enhance
192
+ });
193
+ return;
194
+ }
195
+ if (initialState.image !== void 0) {
196
+ await this.setImage({
197
+ kind: "data",
198
+ data: initialState.image
199
+ }, {
200
+ prompt: initialState.prompt,
201
+ enhance: initialState.enhance
202
+ });
203
+ return;
204
+ }
205
+ if (initialState.prompt !== void 0 && initialState.prompt !== null) await this.sendPrompt(initialState.prompt, { enhance: initialState.enhance });
206
+ }
207
+ async request({ message, matchAck, timeoutMs, label }) {
208
+ return new Promise((resolve, reject) => {
209
+ const timer = setTimeout(() => {
210
+ cleanup();
211
+ this.logger.warn("signaling: ack timed out", {
212
+ label,
213
+ timeoutMs
214
+ });
215
+ reject(/* @__PURE__ */ new Error(`${label} timed out`));
216
+ }, timeoutMs);
217
+ const entry = {
218
+ matches: matchAck,
219
+ onMatch: (msg) => {
220
+ cleanup();
221
+ resolve(msg);
222
+ },
223
+ reject: (err) => {
224
+ cleanup();
225
+ reject(err);
226
+ }
227
+ };
228
+ const cleanup = () => {
229
+ clearTimeout(timer);
230
+ this.pendingAcks = this.pendingAcks.filter((e) => e !== entry);
231
+ };
232
+ this.pendingAcks.push(entry);
233
+ if (!this.writeMessage(message)) {
234
+ cleanup();
235
+ reject(/* @__PURE__ */ new Error("WebSocket is not open"));
236
+ }
237
+ });
238
+ }
239
+ writeMessage(message) {
240
+ if (this.ws?.readyState !== WebSocket.OPEN) return false;
241
+ this.ws.send(JSON.stringify(message));
242
+ return true;
243
+ }
244
+ handleMessage(msg) {
245
+ for (const ack of [...this.pendingAcks]) if (ack.matches(msg)) {
246
+ ack.onMatch(msg);
247
+ break;
248
+ }
249
+ switch (msg.type) {
250
+ case "livekit_room_info":
251
+ this.resolvePendingRoomInfo({
252
+ livekitUrl: msg.livekit_url,
253
+ token: msg.token,
254
+ roomName: msg.room_name,
255
+ sessionId: msg.session_id
256
+ });
257
+ break;
258
+ case "queue_position":
259
+ this.pendingRoomInfo?.pauseTimeout();
260
+ this.events.emit("queuePosition", {
261
+ position: msg.position,
262
+ queueSize: msg.queue_size
263
+ });
264
+ break;
265
+ case "generation_tick":
266
+ this.events.emit("generationTick", { seconds: msg.seconds });
267
+ break;
268
+ case "generation_ended":
269
+ this.events.emit("generationEnded", {
270
+ seconds: msg.seconds,
271
+ reason: msg.reason
272
+ });
273
+ break;
274
+ case "error": {
275
+ const error = new Error(msg.error);
276
+ error.source = "server";
277
+ this.logger.error("signaling: server error received", { error: msg.error });
278
+ this.events.emit("serverError", error);
279
+ this.rejectPendingRoomInfo(error);
280
+ this.rejectAllPending(error);
281
+ break;
282
+ }
283
+ }
284
+ }
285
+ resolvePendingRoomInfo(info) {
286
+ const pending = this.pendingRoomInfo;
287
+ if (!pending) return;
288
+ pending.resolve(info);
289
+ }
290
+ rejectPendingRoomInfo(error) {
291
+ const pending = this.pendingRoomInfo;
292
+ if (!pending) return;
293
+ pending.reject(error);
294
+ }
295
+ rejectAllPending(error) {
296
+ const pending = this.pendingAcks;
297
+ this.pendingAcks = [];
298
+ for (const entry of pending) entry.reject(error);
299
+ }
300
+ };
301
+ //#endregion
302
+ export { SignalingChannel };