@decartai/sdk 0.2.0 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ import { G2GMetrics } from "./realtime/observability/g2g.js";
11
11
  import { WebRTCStats } from "./realtime/observability/webrtc-stats.js";
12
12
  import { ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport } from "./realtime/observability/connection-quality.js";
13
13
  import { ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ReconnectEvent, VideoStallEvent } from "./realtime/observability/diagnostics.js";
14
- import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage } from "./realtime/types.js";
14
+ import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEnded } from "./realtime/types.js";
15
15
  import { SetInput } from "./realtime/methods.js";
16
16
  import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, RealtimeMediaStream } from "./realtime/client.js";
17
17
  import { CheckConnectivityOptions, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport } from "./realtime/preflight-types.js";
@@ -33,4 +33,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
33
33
  files: FilesClient;
34
34
  };
35
35
  //#endregion
36
- export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, ReconnectEvent, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
36
+ export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
@@ -11,7 +11,7 @@ import { G2GMetrics } from "./realtime/observability/g2g.js";
11
11
  import { WebRTCStats } from "./realtime/observability/webrtc-stats.js";
12
12
  import { ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport } from "./realtime/observability/connection-quality.js";
13
13
  import { ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ReconnectEvent, VideoStallEvent } from "./realtime/observability/diagnostics.js";
14
- import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage } from "./realtime/types.js";
14
+ import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEnded } from "./realtime/types.js";
15
15
  import { SetInput } from "./realtime/methods.js";
16
16
  import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, RealtimeMediaStream } from "./realtime/client.js";
17
17
  import { CheckConnectivityOptions, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport } from "./realtime/preflight-types.js";
@@ -33,4 +33,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
33
33
  files: FilesClient;
34
34
  };
35
35
  //#endregion
36
- export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, ReconnectEvent, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
36
+ export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.2.0";
2
+ var version = "0.2.1";
3
3
  //#endregion
4
4
  export { version };
@@ -3,7 +3,7 @@ import { DecartSDKError } from "../utils/errors.js";
3
3
  import { WebRTCStats } from "./observability/webrtc-stats.js";
4
4
  import { ConnectionQualityReport } from "./observability/connection-quality.js";
5
5
  import { DiagnosticEvent } from "./observability/diagnostics.js";
6
- import { ConnectionState, GenerationEnded, GenerationTick, ImageSetOptions, QueuePosition } from "./types.js";
6
+ import { ConnectionState, GenerationEnded, GenerationTick, ImageSetOptions, QueuePosition, SessionEnded } from "./types.js";
7
7
  import { SetInput } from "./methods.js";
8
8
  import { z } from "zod";
9
9
 
@@ -78,6 +78,8 @@ type Events = {
78
78
  error: DecartSDKError;
79
79
  generationTick: GenerationTick;
80
80
  generationEnded: GenerationEnded;
81
+ /** Terminal: the server ended the session and no reconnect is coming. */
82
+ sessionEnded: SessionEnded;
81
83
  diagnostic: DiagnosticEvent;
82
84
  stats: WebRTCStats;
83
85
  };
@@ -112,6 +112,7 @@ const createRealTimeClient = (opts) => {
112
112
  });
113
113
  session.on("generationTick", (e) => emitOrBuffer("generationTick", e));
114
114
  session.on("generationEnded", (e) => emitOrBuffer("generationEnded", e));
115
+ session.on("sessionEnded", (e) => emitOrBuffer("sessionEnded", e));
115
116
  session.on("error", (error) => {
116
117
  logger.error("Realtime error", { error: error.message });
117
118
  emitOrBuffer("error", classifyWebrtcError(error));
@@ -25,6 +25,14 @@ const REALTIME_CONFIG = {
25
25
  minTimeout: 1e3,
26
26
  maxTimeout: 1e4
27
27
  },
28
+ /**
29
+ * `generation_ended` reasons the server will not change its mind about.
30
+ * The rest of its vocabulary (`error`, `disconnect`, `timeout`) is generic
31
+ * or overloaded, so those still retry.
32
+ */
33
+ terminalEndReasons: ["moderation_violation", "insufficient_credits"],
34
+ /** Close code for a policy termination. */
35
+ terminalCloseCode: 1008,
28
36
  permanentErrorSubstrings: [
29
37
  "permission denied",
30
38
  "not allowed",
@@ -4,6 +4,22 @@ import { SignalingChannel } from "./signaling-channel.js";
4
4
  import mitt from "mitt";
5
5
  import pRetry, { AbortError } from "p-retry";
6
6
  //#region src/realtime/stream-session.ts
7
+ function isTerminalEndReason(reason) {
8
+ return reason !== void 0 && REALTIME_CONFIG.session.terminalEndReasons.includes(reason);
9
+ }
10
+ /** The reason is only sent once generation has started; earlier kills carry just the code. */
11
+ function terminalReasonFromClose(cause) {
12
+ return cause.code === REALTIME_CONFIG.session.terminalCloseCode ? "policy_violation" : null;
13
+ }
14
+ /**
15
+ * SignalingChannel only emits `closed` after the handshake, so a close during the
16
+ * join reaches us as connect-failure text rather than an event.
17
+ */
18
+ function terminalReasonFromError(error) {
19
+ if (!(error instanceof Error)) return null;
20
+ const marker = `websocket closed: ${REALTIME_CONFIG.session.terminalCloseCode}`;
21
+ return error.message.toLowerCase().includes(marker) ? "policy_violation" : null;
22
+ }
7
23
  function encodeSubscribeToken(roomName, options = {}) {
8
24
  return btoa(JSON.stringify({
9
25
  room_name: roomName,
@@ -28,6 +44,13 @@ var StreamSession = class {
28
44
  disposed = false;
29
45
  currentAttempt = 0;
30
46
  teardownGeneration = 0;
47
+ terminalEndReason = null;
48
+ /**
49
+ * Replayed as a reconnect's initial state. Callers usually connect first and
50
+ * send the image after, so without this the new session joins with nothing
51
+ * applied and never starts generating.
52
+ */
53
+ appliedState = null;
31
54
  logger;
32
55
  constructor(config) {
33
56
  this.config = config;
@@ -54,6 +77,7 @@ var StreamSession = class {
54
77
  }
55
78
  async connect() {
56
79
  this.disposed = false;
80
+ this.terminalEndReason = null;
57
81
  const attempt = ++this.currentAttempt;
58
82
  this.setState("connecting");
59
83
  this.logger.info("realtime connect: starting", { attemptCycle: attempt });
@@ -61,6 +85,14 @@ var StreamSession = class {
61
85
  await pRetry(() => this.runOneConnect(attempt), this.retryOptionsFor(attempt));
62
86
  } catch (error) {
63
87
  const message = error instanceof Error ? error.message : String(error);
88
+ const terminal = this.terminalEndReason ?? terminalReasonFromError(error);
89
+ if (terminal && !this.disposed) {
90
+ this.finishTerminally(terminal, {
91
+ source: "connect",
92
+ error: message
93
+ });
94
+ throw error;
95
+ }
64
96
  this.logger.error("realtime connect: exhausted all retries", { error: message });
65
97
  if (this.currentAttempt === attempt && !this.disposed) this.setState("disconnected");
66
98
  throw error;
@@ -68,11 +100,25 @@ var StreamSession = class {
68
100
  }
69
101
  async sendPrompt(text, opts) {
70
102
  this.assertConnected();
71
- return this.signaling.sendPrompt(text, opts);
103
+ await this.signaling.sendPrompt(text, opts);
104
+ this.appliedState = {
105
+ ...this.getInitialState(),
106
+ prompt: text,
107
+ enhance: opts?.enhance
108
+ };
72
109
  }
73
110
  async setImage(payload, opts) {
74
111
  this.assertConnected();
75
- return this.signaling.setImage(payload, opts);
112
+ await this.signaling.setImage(payload, opts);
113
+ this.appliedState = payload.kind === "ref" ? {
114
+ imageRef: payload.ref,
115
+ prompt: opts?.prompt ?? null,
116
+ enhance: opts?.enhance
117
+ } : {
118
+ image: payload.data,
119
+ prompt: opts?.prompt ?? null,
120
+ enhance: opts?.enhance
121
+ };
76
122
  }
77
123
  async replaceVideoTrack(track) {
78
124
  this.assertConnected();
@@ -96,6 +142,12 @@ var StreamSession = class {
96
142
  },
97
143
  shouldRetry: (error) => {
98
144
  if (this.disposed || this.currentAttempt !== attempt) return false;
145
+ const terminal = this.terminalEndReason ?? terminalReasonFromError(error);
146
+ if (terminal) {
147
+ this.terminalEndReason = terminal;
148
+ this.logger.error("realtime connect: session refused, not retrying", { reason: terminal });
149
+ return false;
150
+ }
99
151
  const msg = error.message.toLowerCase();
100
152
  const permanent = REALTIME_CONFIG.session.permanentErrorSubstrings.some((err) => msg.includes(err));
101
153
  if (permanent) this.logger.error("realtime connect: permanent error, not retrying", { error: error.message });
@@ -158,6 +210,9 @@ var StreamSession = class {
158
210
  });
159
211
  }
160
212
  getInitialState() {
213
+ return this.appliedState ?? this.configInitialState();
214
+ }
215
+ configInitialState() {
161
216
  if (this.config.initialImageRef !== void 0) return {
162
217
  imageRef: this.config.initialImageRef,
163
218
  prompt: this.config.initialPrompt?.text,
@@ -183,12 +238,18 @@ var StreamSession = class {
183
238
  this.markGenerating();
184
239
  this.events.emit("generationTick", e);
185
240
  });
186
- this.signaling.on("generationEnded", (e) => this.events.emit("generationEnded", e));
241
+ this.signaling.on("generationEnded", (e) => {
242
+ if (isTerminalEndReason(e.reason)) this.terminalEndReason = e.reason;
243
+ this.events.emit("generationEnded", e);
244
+ });
187
245
  this.signaling.on("serverError", (err) => this.events.emit("error", err));
188
- this.signaling.on("closed", (info) => this.handleConnectionLoss({
189
- source: "signaling",
190
- ...info
191
- }));
246
+ this.signaling.on("closed", (info) => {
247
+ this.terminalEndReason ??= terminalReasonFromClose({ ...info });
248
+ this.handleConnectionLoss({
249
+ source: "signaling",
250
+ ...info
251
+ });
252
+ });
192
253
  }
193
254
  markGenerating() {
194
255
  if (this.state === "connected") this.setState("generating");
@@ -202,6 +263,11 @@ var StreamSession = class {
202
263
  }
203
264
  handleConnectionLoss(cause) {
204
265
  if (this.disposed) return;
266
+ const terminalReason = this.terminalEndReason ?? terminalReasonFromClose(cause);
267
+ if (terminalReason) {
268
+ this.finishTerminally(terminalReason, cause);
269
+ return;
270
+ }
205
271
  if (this.state !== "connected" && this.state !== "generating") {
206
272
  this.logger.debug("connection loss ignored (not connected)", {
207
273
  state: this.state,
@@ -215,6 +281,22 @@ var StreamSession = class {
215
281
  });
216
282
  this.scheduleReconnect();
217
283
  }
284
+ /**
285
+ * The server ended this session on purpose: retrying would get the same answer
286
+ * and bill another session, and "reconnecting" would hide the reason.
287
+ */
288
+ finishTerminally(reason, cause) {
289
+ this.logger.warn("realtime session ended by the server; not reconnecting", {
290
+ reason,
291
+ state: this.state,
292
+ ...cause
293
+ });
294
+ this.terminalEndReason = reason;
295
+ this.disposed = true;
296
+ this.tearDown();
297
+ this.setState("disconnected");
298
+ this.events.emit("sessionEnded", { reason });
299
+ }
218
300
  scheduleReconnect() {
219
301
  const attempt = ++this.currentAttempt;
220
302
  this.setState("reconnecting");
@@ -229,6 +311,14 @@ var StreamSession = class {
229
311
  }).catch((error) => {
230
312
  if (this.disposed || this.currentAttempt !== attempt) return;
231
313
  const message = error instanceof Error ? error.message : String(error);
314
+ const terminal = this.terminalEndReason ?? terminalReasonFromError(error);
315
+ if (terminal) {
316
+ this.finishTerminally(terminal, {
317
+ source: "reconnect",
318
+ error: message
319
+ });
320
+ return;
321
+ }
232
322
  this.logger.error("realtime reconnect: failed permanently", { error: message });
233
323
  this.tearDown();
234
324
  this.setState("disconnected");
@@ -20,10 +20,14 @@ type GenerationEnded = {
20
20
  seconds: number;
21
21
  reason: string;
22
22
  };
23
+ /** The server ended the session on purpose; the SDK will not reconnect. */
24
+ type SessionEnded = {
25
+ reason: string;
26
+ };
23
27
  type ImageSetOptions = {
24
28
  prompt?: string | null;
25
29
  enhance?: boolean;
26
30
  timeout?: number;
27
31
  };
28
32
  //#endregion
29
- export { ConnectionState, GenerationEnded, GenerationEndedMessage, GenerationTick, ImageSetOptions, QueuePosition, QueuePositionMessage };
33
+ export { ConnectionState, GenerationEnded, GenerationEndedMessage, GenerationTick, ImageSetOptions, QueuePosition, QueuePositionMessage, SessionEnded };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",