@decartai/sdk 0.2.3 → 0.2.4

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 CHANGED
@@ -103,6 +103,31 @@ models.realtime("lucy-2.1").supportedSpeeds; // undefined
103
103
 
104
104
  Passing `speed` for a model without the capability logs a warning through the client's logger and the session runs at standard speed.
105
105
 
106
+ #### Connect retries and refused sessions
107
+
108
+ A failed connect is re-dialled up to 5 times with exponential backoff (1 s to 10 s) before
109
+ `connect()` rejects. Two refusals are never retried, because the server would give the same answer:
110
+ a policy close (code 1008) and the API key's concurrent-session limit (close 1013 "Session Limit
111
+ Reached"). The session limit rejects `connect()` on the first attempt with the server's message
112
+ (`Concurrent session limit reached.`); if it lands after the session is established, the session ends
113
+ with a `sessionEnded` event whose `reason` is `"session_limit"`. A capacity refusal (1013 "Try Again
114
+ Later") is transient and still retries.
115
+
116
+ Apps that hand out connects from their own session queue should turn retries off: every retry is a
117
+ fresh dial that takes a freed slot ahead of the people waiting in line.
118
+
119
+ ```ts
120
+ const realtimeClient = await client.realtime.connect(stream, {
121
+ model,
122
+ retries: 0, // dial once; on failure, rejoin your own queue
123
+ // ...
124
+ });
125
+
126
+ realtimeClient.on("sessionEnded", ({ reason }) => {
127
+ // "session_limit" | "policy_violation" | "moderation_violation" | "insufficient_credits" | ...
128
+ });
129
+ ```
130
+
106
131
  ### Watch a Stream
107
132
 
108
133
  A connected realtime session exposes an SDK `subscribeToken` once it reaches a
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, SessionEnded } from "./realtime/types.js";
14
+ import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEndReason, 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";
@@ -35,4 +35,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
35
35
  files: FilesClient;
36
36
  };
37
37
  //#endregion
38
- export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ClientTokenClaims, 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, RealtimeSpeed, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VerifyClientTokenOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
38
+ export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ClientTokenClaims, 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, RealtimeSpeed, ReconnectEvent, SessionEndReason, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VerifyClientTokenOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
@@ -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, SessionEnded } from "./realtime/types.js";
14
+ import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEndReason, 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";
@@ -35,4 +35,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
35
35
  files: FilesClient;
36
36
  };
37
37
  //#endregion
38
- export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ClientTokenClaims, 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, RealtimeSpeed, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VerifyClientTokenOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
38
+ export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ClientTokenClaims, 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, RealtimeSpeed, ReconnectEvent, SessionEndReason, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VerifyClientTokenOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.2.3";
2
+ var version = "0.2.4";
3
3
  //#endregion
4
4
  export { version };
@@ -61,6 +61,7 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
61
61
  speed: z.ZodOptional<z.ZodEnum<{
62
62
  fast: "fast";
63
63
  }>>;
64
+ retries: z.ZodOptional<z.ZodNumber>;
64
65
  preferredVideoCodec: z.ZodOptional<z.ZodEnum<{
65
66
  h264: "h264";
66
67
  vp8: "vp8";
@@ -28,6 +28,15 @@ const realTimeClientConnectOptionsSchema = z.object({
28
28
  * model does not list it in `supportedSpeeds`). Omit it (the default) for standard mode.
29
29
  */
30
30
  speed: realtimeSpeedSchema.optional(),
31
+ /**
32
+ * How many times a failed connect is re-dialled before `connect()` rejects (default 5, exponential
33
+ * backoff from 1 s to 10 s). `0` dials exactly once. Apps that hand out connects from their own session
34
+ * queue should pass `0`: every retry is a fresh dial that competes with the people waiting in line.
35
+ * Refusals the server will not change its mind about (policy close 1008, concurrent-session limit
36
+ * 1013 "Session Limit Reached") are never retried regardless. Does not affect the automatic
37
+ * reconnect after an established session drops.
38
+ */
39
+ retries: z.number().int().min(0).optional(),
31
40
  /** Local track publish codec. Desktop Safari is always pinned to vp8 and ignores this value. */
32
41
  preferredVideoCodec: z.enum([
33
42
  "h264",
@@ -47,7 +56,7 @@ const createRealTimeClient = (opts) => {
47
56
  const connect = async (stream, options) => {
48
57
  const parsedOptions = realTimeClientConnectOptionsSchema.safeParse(options);
49
58
  if (!parsedOptions.success) throw parsedOptions.error;
50
- const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, speed, preferredVideoCodec } = parsedOptions.data;
59
+ const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, speed, retries, preferredVideoCodec } = parsedOptions.data;
51
60
  const mirror = parsedOptions.data.mirror ?? false;
52
61
  let session;
53
62
  let observability;
@@ -104,6 +113,7 @@ const createRealTimeClient = (opts) => {
104
113
  initialImageRef,
105
114
  initialPrompt,
106
115
  initialPassthrough: initialState?.passthrough,
116
+ connectRetries: retries,
107
117
  logger,
108
118
  videoCodec: preparedConnection.videoCodec,
109
119
  createMediaChannel: preparedConnection.createMediaChannel
@@ -33,6 +33,18 @@ const REALTIME_CONFIG = {
33
33
  terminalEndReasons: ["moderation_violation", "insufficient_credits"],
34
34
  /** Close code for a policy termination. */
35
35
  terminalCloseCode: 1008,
36
+ /**
37
+ * The bouncer's concurrent-session refusal: an `error` message carrying
38
+ * `errorText`, then close `closeCode` with `closeReason`. Retrying it only
39
+ * takes a slot from whoever is next in line. 1013 with any other reason
40
+ * ("Try Again Later": upstream at capacity) is transient and still retries.
41
+ * Matched case-insensitively.
42
+ */
43
+ sessionLimit: {
44
+ closeCode: 1013,
45
+ closeReason: "session limit reached",
46
+ errorText: "concurrent session limit reached"
47
+ },
36
48
  permanentErrorSubstrings: [
37
49
  "permission denied",
38
50
  "not allowed",
@@ -7,18 +7,36 @@ import pRetry, { AbortError } from "p-retry";
7
7
  function isTerminalEndReason(reason) {
8
8
  return reason !== void 0 && REALTIME_CONFIG.session.terminalEndReasons.includes(reason);
9
9
  }
10
- /** The reason is only sent once generation has started; earlier kills carry just the code. */
10
+ const POLICY_VIOLATION = "policy_violation";
11
+ const SESSION_LIMIT = "session_limit";
12
+ function isSessionLimitClose(code, reason) {
13
+ const { closeCode, closeReason } = REALTIME_CONFIG.session.sessionLimit;
14
+ return code === closeCode && typeof reason === "string" && reason.toLowerCase().includes(closeReason);
15
+ }
16
+ /**
17
+ * The 1008 reason is only sent once generation has started; earlier kills carry just
18
+ * the code. 1013 is terminal only with the session-limit reason: its other reason,
19
+ * "Try Again Later", is a transient capacity refusal.
20
+ */
11
21
  function terminalReasonFromClose(cause) {
12
- return cause.code === REALTIME_CONFIG.session.terminalCloseCode ? "policy_violation" : null;
22
+ if (cause.code === REALTIME_CONFIG.session.terminalCloseCode) return POLICY_VIOLATION;
23
+ if (isSessionLimitClose(cause.code, cause.reason)) return SESSION_LIMIT;
24
+ return null;
13
25
  }
14
26
  /**
15
27
  * SignalingChannel only emits `closed` after the handshake, so a close during the
16
- * join reaches us as connect-failure text rather than an event.
28
+ * join reaches us as connect-failure text rather than an event. The session-limit
29
+ * refusal is preceded by an `error` message, which is usually what rejects the join;
30
+ * the 1013 close lands a tick later, so both texts are recognised.
17
31
  */
18
32
  function terminalReasonFromError(error) {
19
33
  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;
34
+ const message = error.message.toLowerCase();
35
+ const { terminalCloseCode, sessionLimit } = REALTIME_CONFIG.session;
36
+ if (message.includes(`websocket closed: ${terminalCloseCode}`)) return POLICY_VIOLATION;
37
+ if (message.includes(`websocket closed: ${sessionLimit.closeCode} ${sessionLimit.closeReason}`)) return SESSION_LIMIT;
38
+ if (message.includes(sessionLimit.errorText)) return SESSION_LIMIT;
39
+ return null;
22
40
  }
23
41
  function encodeSubscribeToken(roomName, options = {}) {
24
42
  return btoa(JSON.stringify({
@@ -82,12 +100,12 @@ var StreamSession = class {
82
100
  this.setState("connecting");
83
101
  this.logger.info("realtime connect: starting", { attemptCycle: attempt });
84
102
  try {
85
- await pRetry(() => this.runOneConnect(attempt), this.retryOptionsFor(attempt));
103
+ await pRetry(() => this.runOneConnect(attempt), this.retryOptionsFor(attempt, this.config.connectRetries));
86
104
  } catch (error) {
87
105
  const message = error instanceof Error ? error.message : String(error);
88
106
  const terminal = this.terminalEndReason ?? terminalReasonFromError(error);
89
- if (terminal && !this.disposed) {
90
- this.finishTerminally(terminal, {
107
+ if (terminal) {
108
+ if (!this.disposed) this.finishTerminally(terminal, {
91
109
  source: "connect",
92
110
  error: message
93
111
  });
@@ -134,9 +152,10 @@ var StreamSession = class {
134
152
  assertConnected() {
135
153
  if (!this.isConnected()) throw new Error(`Cannot send message: connection is ${this.state}`);
136
154
  }
137
- retryOptionsFor(attempt) {
155
+ retryOptionsFor(attempt, retries = REALTIME_CONFIG.session.retry.retries) {
138
156
  return {
139
157
  ...REALTIME_CONFIG.session.retry,
158
+ retries,
140
159
  onFailedAttempt: (_error) => {
141
160
  this.tearDown();
142
161
  },
@@ -287,9 +306,9 @@ var StreamSession = class {
287
306
  */
288
307
  finishTerminally(reason, cause) {
289
308
  this.logger.warn("realtime session ended by the server; not reconnecting", {
309
+ ...cause,
290
310
  reason,
291
- state: this.state,
292
- ...cause
311
+ state: this.state
293
312
  });
294
313
  this.terminalEndReason = reason;
295
314
  this.disposed = true;
@@ -20,9 +20,18 @@ type GenerationEnded = {
20
20
  seconds: number;
21
21
  reason: string;
22
22
  };
23
+ /**
24
+ * Why the server ended a session on purpose. Terminal `generation_ended` reasons
25
+ * (`moderation_violation`, `insufficient_credits`) pass through as sent; the SDK
26
+ * derives the rest from the close:
27
+ * - `policy_violation`: close 1008 before a reason was sent.
28
+ * - `session_limit`: the API key's concurrent-session limit was reached (close 1013
29
+ * "Session Limit Reached"). Rejoin your own queue instead of dialling again.
30
+ */
31
+ type SessionEndReason = "moderation_violation" | "insufficient_credits" | "policy_violation" | "session_limit" | (string & {});
23
32
  /** The server ended the session on purpose; the SDK will not reconnect. */
24
33
  type SessionEnded = {
25
- reason: string;
34
+ reason: SessionEndReason;
26
35
  };
27
36
  type ImageSetOptions = {
28
37
  prompt?: string | null;
@@ -30,4 +39,4 @@ type ImageSetOptions = {
30
39
  timeout?: number;
31
40
  };
32
41
  //#endregion
33
- export { ConnectionState, GenerationEnded, GenerationEndedMessage, GenerationTick, ImageSetOptions, QueuePosition, QueuePositionMessage, SessionEnded };
42
+ export { ConnectionState, GenerationEnded, GenerationEndedMessage, GenerationTick, ImageSetOptions, QueuePosition, QueuePositionMessage, SessionEndReason, SessionEnded };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",