@vinaverse/client 0.1.0-alpha.1 → 0.1.0-alpha.2

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.
@@ -219,6 +219,12 @@ interface ConversationOptions {
219
219
  readonly createId?: () => string;
220
220
  /** Something failed that no call returned: getting a session, an input that could not be delivered. */
221
221
  readonly onError?: (error: unknown) => void;
222
+ /**
223
+ * Close the connection while the app is in the background, and open it again in the foreground (a backend that
224
+ * mints a client secret per connection: with `refreshSecret`, a fresh one is asked for first). Default false: the
225
+ * connection stays, with no pings, until the session idles out. Nothing typed or answered is lost either way.
226
+ */
227
+ readonly closeInBackground?: boolean;
222
228
  }
223
229
  /** A text is at most 2000 code points (`TurnInput`). */
224
230
  declare const MAX_TEXT = 2000;
@@ -249,6 +255,10 @@ declare class ConversationStore {
249
255
  private historyState;
250
256
  /** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
251
257
  private transcriptSeen;
258
+ /** The session of the last session.ready: a card of another one takes no tap (§6). */
259
+ private readySession;
260
+ private cardOrder;
261
+ private kept;
252
262
  private locale;
253
263
  /** The request for a session in flight, to abort when it is no longer wanted. */
254
264
  private fetching;
@@ -284,6 +294,11 @@ declare class ConversationStore {
284
294
  setLocale(locale: string | undefined): void;
285
295
  /** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
286
296
  setForeground(foreground: boolean): void;
297
+ /** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
298
+ private suspended;
299
+ private suspend;
300
+ /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
301
+ private wake;
287
302
  /** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
288
303
  setOnline(online: boolean | null): void;
289
304
  /** Send a typed message; returns its `client_event_id`. Typed before a session exists, it is sent once one does. */
@@ -323,6 +338,13 @@ declare class ConversationStore {
323
338
  private reconnectIfDown;
324
339
  private statusChanged;
325
340
  private event;
341
+ /**
342
+ * A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
343
+ * the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
344
+ * message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
345
+ * card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
346
+ */
347
+ private anchorCards;
326
348
  /**
327
349
  * Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
328
350
  * per frame, both channels in the same flush: a streamed reply that completes becomes its message in one frame.
@@ -219,6 +219,12 @@ interface ConversationOptions {
219
219
  readonly createId?: () => string;
220
220
  /** Something failed that no call returned: getting a session, an input that could not be delivered. */
221
221
  readonly onError?: (error: unknown) => void;
222
+ /**
223
+ * Close the connection while the app is in the background, and open it again in the foreground (a backend that
224
+ * mints a client secret per connection: with `refreshSecret`, a fresh one is asked for first). Default false: the
225
+ * connection stays, with no pings, until the session idles out. Nothing typed or answered is lost either way.
226
+ */
227
+ readonly closeInBackground?: boolean;
222
228
  }
223
229
  /** A text is at most 2000 code points (`TurnInput`). */
224
230
  declare const MAX_TEXT = 2000;
@@ -249,6 +255,10 @@ declare class ConversationStore {
249
255
  private historyState;
250
256
  /** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
251
257
  private transcriptSeen;
258
+ /** The session of the last session.ready: a card of another one takes no tap (§6). */
259
+ private readySession;
260
+ private cardOrder;
261
+ private kept;
252
262
  private locale;
253
263
  /** The request for a session in flight, to abort when it is no longer wanted. */
254
264
  private fetching;
@@ -284,6 +294,11 @@ declare class ConversationStore {
284
294
  setLocale(locale: string | undefined): void;
285
295
  /** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
286
296
  setForeground(foreground: boolean): void;
297
+ /** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
298
+ private suspended;
299
+ private suspend;
300
+ /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
301
+ private wake;
287
302
  /** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
288
303
  setOnline(online: boolean | null): void;
289
304
  /** Send a typed message; returns its `client_event_id`. Typed before a session exists, it is sent once one does. */
@@ -323,6 +338,13 @@ declare class ConversationStore {
323
338
  private reconnectIfDown;
324
339
  private statusChanged;
325
340
  private event;
341
+ /**
342
+ * A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
343
+ * the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
344
+ * message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
345
+ * card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
346
+ */
347
+ private anchorCards;
326
348
  /**
327
349
  * Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
328
350
  * per frame, both channels in the same flush: a streamed reply that completes becomes its message in one frame.
@@ -156,10 +156,13 @@ function buildTimeline(input, previous) {
156
156
  busy: input.busy.has(card.id),
157
157
  ...input.decisionErrors?.has(card.id) ? { decisionError: true } : {}
158
158
  },
159
- order: card.seq
159
+ order: input.cardOrder?.get(card.id) ?? card.seq
160
160
  });
161
161
  }
162
162
  for (const { item, order } of input.system) live.push({ item, order });
163
+ const held = new Set(state.cards.map((c) => c.id));
164
+ for (const { item, order } of input.kept ?? [])
165
+ if (!held.has(item.key)) live.push({ item, order });
163
166
  for (const [id, sent] of input.sent) {
164
167
  const pending = pendingState(id, input);
165
168
  if (pending === null) continue;
@@ -269,6 +272,10 @@ var ConversationStore = class {
269
272
  historyState = "none";
270
273
  /** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
271
274
  transcriptSeen = false;
275
+ /** The session of the last session.ready: a card of another one takes no tap (§6). */
276
+ readySession = null;
277
+ cardOrder = /* @__PURE__ */ new Map();
278
+ kept = [];
272
279
  locale;
273
280
  /** The request for a session in flight, to abort when it is no longer wanted. */
274
281
  fetching = null;
@@ -350,9 +357,49 @@ var ConversationStore = class {
350
357
  /** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
351
358
  setForeground(foreground) {
352
359
  this.foreground = foreground;
360
+ if (this.options.closeInBackground && !this.disposed) {
361
+ if (!foreground) {
362
+ this.suspend();
363
+ return;
364
+ }
365
+ if (this.suspended) {
366
+ void this.wake();
367
+ return;
368
+ }
369
+ }
353
370
  this.session?.setForeground(foreground);
354
371
  if (foreground) this.reconnectIfDown();
355
372
  }
373
+ /** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
374
+ suspended = false;
375
+ suspend() {
376
+ const session = this.session;
377
+ if (!session || this.suspended || session.status === "ended") return;
378
+ this.suspended = true;
379
+ for (const off of this.unsubscribe) off();
380
+ this.unsubscribe = [];
381
+ session.close();
382
+ this.update();
383
+ }
384
+ /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
385
+ async wake() {
386
+ const info = this.info;
387
+ this.suspended = false;
388
+ if (!info) return;
389
+ let next = info;
390
+ if (this.options.refreshSecret) {
391
+ try {
392
+ next = {
393
+ ...info,
394
+ client_secret: await this.options.refreshSecret(info.id)
395
+ };
396
+ } catch (error) {
397
+ this.options.onError?.(error);
398
+ }
399
+ }
400
+ if (this.disposed || this.suspended) return;
401
+ this.open(next);
402
+ }
356
403
  /** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
357
404
  setOnline(online) {
358
405
  this.online = online;
@@ -374,6 +421,12 @@ var ConversationStore = class {
374
421
  this.update();
375
422
  return id;
376
423
  }
424
+ if (this.suspended) {
425
+ this.early.set(id, text);
426
+ void this.wake();
427
+ this.update();
428
+ return id;
429
+ }
377
430
  if (this.nextFrom(this.engine.state()) === "continue_here") {
378
431
  this.early.set(id, text);
379
432
  void this.resumeWith("continue_here");
@@ -575,10 +628,16 @@ var ConversationStore = class {
575
628
  ];
576
629
  };
577
630
  switch (frame.type) {
578
- case "session.ready":
579
- if (frame.resumed === true && Array.isArray(frame.transcript))
631
+ case "session.ready": {
632
+ const session = frame.session?.id;
633
+ const moved = typeof session === "string" && session !== this.readySession;
634
+ if (typeof session === "string") this.readySession = session;
635
+ if (frame.resumed === true && Array.isArray(frame.transcript)) {
580
636
  this.transcriptSeen = true;
637
+ this.anchorCards(moved);
638
+ }
581
639
  return;
640
+ }
582
641
  // `at` is required (v1-draft.15); the same takeover announced again after a reconnect has the same `at`, so it
583
642
  // draws one row. A frame without one still gets a row of its own.
584
643
  case "takeover.started":
@@ -624,6 +683,38 @@ var ConversationStore = class {
624
683
  return;
625
684
  }
626
685
  }
686
+ /**
687
+ * A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
688
+ * the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
689
+ * message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
690
+ * card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
691
+ */
692
+ anchorCards(sessionChanged) {
693
+ const state = this.engine.state();
694
+ const seqOf = new Map(state.messages.map((m) => [m.key, m.seq]));
695
+ const first = Math.min(...state.messages.map((m) => m.seq), 0);
696
+ const held = new Set(state.cards.map((c) => c.id));
697
+ const order = /* @__PURE__ */ new Map();
698
+ const kept = [];
699
+ let anchor = first - 1;
700
+ let step = 0;
701
+ for (const item of this.view.items) {
702
+ if (item.type === "message" && seqOf.has(item.key)) {
703
+ anchor = seqOf.get(item.key);
704
+ step = 0;
705
+ } else if (item.type === "card") {
706
+ const at = anchor + 0.5 + ++step / 1e3;
707
+ order.set(item.key, at);
708
+ if (!held.has(item.key))
709
+ kept.push({
710
+ item: sessionChanged ? { ...item, busy: false, restored: true } : item,
711
+ order: at
712
+ });
713
+ }
714
+ }
715
+ this.cardOrder = order;
716
+ this.kept = kept;
717
+ }
627
718
  // ------------------------------------------------------------ the view
628
719
  /**
629
720
  * Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
@@ -680,6 +771,8 @@ var ConversationStore = class {
680
771
  busy: this.busy,
681
772
  decisionErrors: this.decisionErrors,
682
773
  transcriptSeen: this.transcriptSeen,
774
+ cardOrder: this.cardOrder,
775
+ kept: this.kept,
683
776
  system: this.system
684
777
  },
685
778
  prev?.items ?? NO_ITEMS
@@ -709,6 +802,7 @@ var ConversationStore = class {
709
802
  }
710
803
  status() {
711
804
  if (this.disposed) return "closed";
805
+ if (this.suspended) return "idle";
712
806
  const session = this.session;
713
807
  if (!session) {
714
808
  if (this.failed) return "unavailable";