@decartai/sdk 0.1.5 → 0.1.7

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.
@@ -3,10 +3,43 @@ import { createConsoleLogger } from "../utils/logger.js";
3
3
  import { REALTIME_CONFIG } from "./config-realtime.js";
4
4
  import mitt from "mitt";
5
5
  //#region src/realtime/signaling-channel.ts
6
+ function buildInitialStateRequest(initialState) {
7
+ if (!initialState) return null;
8
+ if (initialState.imageRef !== void 0 || initialState.image !== void 0) {
9
+ const message = initialState.imageRef !== void 0 ? {
10
+ type: "set_image",
11
+ image_ref: initialState.imageRef
12
+ } : {
13
+ type: "set_image",
14
+ image_data: initialState.image ?? null
15
+ };
16
+ if (initialState.prompt !== void 0) message.prompt = initialState.prompt;
17
+ if (initialState.enhance !== void 0) message.enhance_prompt = initialState.enhance;
18
+ return {
19
+ message,
20
+ matchAck: (msg) => msg.type === "set_image_ack",
21
+ label: "Image send"
22
+ };
23
+ }
24
+ if (initialState.prompt !== void 0 && initialState.prompt !== null) {
25
+ const text = initialState.prompt;
26
+ return {
27
+ message: {
28
+ type: "prompt",
29
+ prompt: text,
30
+ enhance_prompt: initialState.enhance ?? true
31
+ },
32
+ matchAck: (msg) => msg.type === "prompt_ack" && msg.prompt === text,
33
+ label: "Prompt send"
34
+ };
35
+ }
36
+ return null;
37
+ }
6
38
  var SignalingChannel = class {
7
39
  ws = null;
8
40
  events = mitt();
9
41
  pendingAcks = [];
42
+ bufferedAcks = [];
10
43
  pendingRoomInfo = null;
11
44
  connected = false;
12
45
  closing = false;
@@ -29,7 +62,12 @@ var SignalingChannel = class {
29
62
  this.config.observability?.endPhase("websocket-open", { success: true });
30
63
  this.config.observability?.startPhase("room-join");
31
64
  const roomInfoWait = this.waitForRoomInfo(handshakeTimeout);
32
- if (!this.writeMessage({ type: "livekit_join" })) {
65
+ const initialStateRequest = buildInitialStateRequest(opts.initialState);
66
+ const joinMessage = {
67
+ type: "livekit_join",
68
+ initial_state: initialStateRequest ? initialStateRequest.message : null
69
+ };
70
+ if (!this.writeMessage(joinMessage)) {
33
71
  roomInfoWait.cancel();
34
72
  throw new Error("WebSocket is not open");
35
73
  }
@@ -42,18 +80,24 @@ var SignalingChannel = class {
42
80
  }
43
81
  this.config.observability?.endPhase("room-join", { success: true });
44
82
  this.connected = true;
45
- const initialStateAck = this.sendInitialStateTracked(opts.initialState);
83
+ const initialStateAck = initialStateRequest ? this.flushInitialState(initialStateRequest) : Promise.resolve();
46
84
  initialStateAck.catch(() => {});
47
85
  return {
48
86
  roomInfo,
49
87
  initialStateAck
50
88
  };
51
89
  }
52
- async sendInitialStateTracked(initialState) {
53
- if (!initialState) return;
90
+ async flushInitialState(request) {
54
91
  this.config.observability?.startPhase("initial-state-handshake");
55
- await this.sendInitialState(initialState);
92
+ const ack = await this.request({
93
+ message: request.message,
94
+ matchAck: request.matchAck,
95
+ timeoutMs: REALTIME_CONFIG.signaling.requestTimeoutMs,
96
+ label: request.label,
97
+ write: false
98
+ });
56
99
  this.config.observability?.endPhase("initial-state-handshake", { success: true });
100
+ if (!ack.success) throw new Error(ack.error ?? `Failed: ${request.label}`);
57
101
  }
58
102
  close() {
59
103
  this.closing = true;
@@ -180,32 +224,14 @@ var SignalingChannel = class {
180
224
  cancel: cleanup
181
225
  };
182
226
  }
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 }) {
227
+ async request({ message, matchAck, timeoutMs, label, write = true }) {
208
228
  return new Promise((resolve, reject) => {
229
+ const buffered = this.bufferedAcks.findIndex((m) => matchAck(m));
230
+ if (buffered !== -1) {
231
+ const [claimed] = this.bufferedAcks.splice(buffered, 1);
232
+ resolve(claimed);
233
+ return;
234
+ }
209
235
  const timer = setTimeout(() => {
210
236
  cleanup();
211
237
  this.logger.warn("signaling: ack timed out", {
@@ -230,7 +256,7 @@ var SignalingChannel = class {
230
256
  this.pendingAcks = this.pendingAcks.filter((e) => e !== entry);
231
257
  };
232
258
  this.pendingAcks.push(entry);
233
- if (!this.writeMessage(message)) {
259
+ if (write && !this.writeMessage(message)) {
234
260
  cleanup();
235
261
  reject(/* @__PURE__ */ new Error("WebSocket is not open"));
236
262
  }
@@ -244,7 +270,11 @@ var SignalingChannel = class {
244
270
  handleMessage(msg) {
245
271
  for (const ack of [...this.pendingAcks]) if (ack.matches(msg)) {
246
272
  ack.onMatch(msg);
247
- break;
273
+ return;
274
+ }
275
+ if (!this.connected && (msg.type === "set_image_ack" || msg.type === "prompt_ack")) {
276
+ this.bufferedAcks.push(msg);
277
+ return;
248
278
  }
249
279
  switch (msg.type) {
250
280
  case "livekit_room_info":
@@ -298,6 +328,7 @@ var SignalingChannel = class {
298
328
  rejectAllPending(error) {
299
329
  const pending = this.pendingAcks;
300
330
  this.pendingAcks = [];
331
+ this.bufferedAcks = [];
301
332
  for (const entry of pending) entry.reject(error);
302
333
  }
303
334
  };
@@ -1,6 +1,5 @@
1
1
  import { createConsoleLogger } from "../utils/logger.js";
2
2
  import { REALTIME_CONFIG } from "./config-realtime.js";
3
- import { InitialStateGate } from "./initial-state-gate.js";
4
3
  import { MediaChannel } from "./media-channel.js";
5
4
  import { SignalingChannel } from "./signaling-channel.js";
6
5
  import mitt from "mitt";
@@ -25,7 +24,7 @@ var StreamSession = class {
25
24
  queue = null;
26
25
  disposed = false;
27
26
  currentAttempt = 0;
28
- initialStateGate = new InitialStateGate();
27
+ teardownGeneration = 0;
29
28
  logger;
30
29
  constructor(config) {
31
30
  this.config = config;
@@ -101,11 +100,12 @@ var StreamSession = class {
101
100
  this.resetHandshakeState();
102
101
  const initialState = this.getInitialState();
103
102
  this.config.observability?.beginConnectionBreakdown(attempt, getInitialImageSizeKb(initialState?.image));
104
- const gateAttempt = this.initialStateGate.startAttempt(initialState);
103
+ this.config.observability?.markGlassToGlassStart();
105
104
  const { roomInfo, initialStateAck } = await this.signaling.openAndJoin({
106
105
  connectTimeout: REALTIME_CONFIG.session.connectionTimeoutMs,
107
106
  initialState
108
107
  });
108
+ this.watchInitialStateAck(initialStateAck, attempt);
109
109
  if (this.disposed || this.currentAttempt !== attempt) {
110
110
  this.tearDown();
111
111
  throw new AbortError("Stale connect attempt");
@@ -116,7 +116,6 @@ var StreamSession = class {
116
116
  url: roomInfo.livekitUrl,
117
117
  token: roomInfo.token
118
118
  });
119
- if (!await gateAttempt.waitForReadiness(initialStateAck)) throw new AbortError("Stale connect attempt");
120
119
  await this.media.publishLocalTracks();
121
120
  } catch (error) {
122
121
  this.tearDown();
@@ -140,6 +139,13 @@ var StreamSession = class {
140
139
  throw error;
141
140
  }
142
141
  }
142
+ watchInitialStateAck(initialStateAck, attempt) {
143
+ const generation = this.teardownGeneration;
144
+ initialStateAck.catch((error) => {
145
+ if (this.disposed || this.currentAttempt !== attempt || this.teardownGeneration !== generation) return;
146
+ this.events.emit("error", error instanceof Error ? error : new Error(String(error)));
147
+ });
148
+ }
143
149
  getInitialState() {
144
150
  if (this.config.initialImageRef !== void 0) return {
145
151
  imageRef: this.config.initialImageRef,
@@ -239,9 +245,9 @@ var StreamSession = class {
239
245
  this.wireMediaEvents();
240
246
  }
241
247
  tearDown() {
248
+ this.teardownGeneration++;
242
249
  this.signaling.close();
243
250
  this.media.disconnect();
244
- this.initialStateGate.reset();
245
251
  this.resetHandshakeState();
246
252
  }
247
253
  resetHandshakeState() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,21 +0,0 @@
1
- //#region src/realtime/initial-state-gate.ts
2
- var InitialStateGate = class {
3
- attemptId = 0;
4
- startAttempt(initialState) {
5
- const attemptId = ++this.attemptId;
6
- const shouldWait = hasCallerProvidedInitialState(initialState);
7
- return { waitForReadiness: async (initialStateAck) => {
8
- if (shouldWait) await initialStateAck;
9
- return this.attemptId === attemptId;
10
- } };
11
- }
12
- reset() {
13
- this.attemptId++;
14
- }
15
- };
16
- function hasCallerProvidedInitialState(state) {
17
- if (!state) return false;
18
- return state.image !== void 0 && state.image !== null || state.prompt !== void 0 && state.prompt !== null;
19
- }
20
- //#endregion
21
- export { InitialStateGate };