@q9labsai/chalk-client 5.1.0 → 5.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/chat-files/types.d.ts +3 -0
  3. package/dist/chunk-6LNRDKJF.js +162 -0
  4. package/dist/{chunk-3ZN7IXOZ.js → chunk-GA3JDPFI.js} +252 -2
  5. package/dist/{chunk-RJRSLXVT.js → chunk-RVUN6LW4.js} +764 -265
  6. package/dist/{chunk-K7PNW3KI.js → chunk-TVNI2XUZ.js} +172 -18
  7. package/dist/client.d.ts +205 -0
  8. package/dist/connection/dependencies.d.ts +3 -0
  9. package/dist/connection/diagnostics.d.ts +3 -2
  10. package/dist/connection/index.d.ts +1 -0
  11. package/dist/connection/lifecycle.d.ts +1 -0
  12. package/dist/connection/types.d.ts +2 -0
  13. package/dist/effect.js +62 -4
  14. package/dist/generated/http-api.d.ts +201 -0
  15. package/dist/generated/schemas.d.ts +373 -0
  16. package/dist/generated/sync.d.ts +74 -3
  17. package/dist/generated/whiteboard-v1.d.ts +71 -9
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +20 -14
  20. package/dist/public-invites/code.d.ts +1 -0
  21. package/dist/public-invites/index.d.ts +1 -0
  22. package/dist/public-invites/index.js +6 -4
  23. package/dist/public-invites/words.d.ts +2 -0
  24. package/dist/space-client/reload-rejoin.d.ts +1 -1
  25. package/dist/space-client/types.d.ts +10 -1
  26. package/dist/sync/connect-diagnostics.d.ts +10 -0
  27. package/dist/sync/index.js +1 -1
  28. package/dist/sync/types.d.ts +1 -0
  29. package/dist/sync/v1-client.d.ts +2 -0
  30. package/dist/sync/v1-types.d.ts +8 -0
  31. package/dist/whiteboard/index.d.ts +2 -5
  32. package/dist/whiteboard/types.d.ts +26 -3
  33. package/dist/whiteboard/v1-client.d.ts +3 -1
  34. package/dist/whiteboard/v1-codec.d.ts +5 -1
  35. package/dist/whiteboard/v1-multipart.d.ts +1 -1
  36. package/dist/whiteboard/view-bounds.d.ts +5 -0
  37. package/dist/whiteboard/view-versions.d.ts +9 -0
  38. package/package.json +1 -1
  39. package/dist/chunk-KHHULLBZ.js +0 -142
@@ -120,7 +120,7 @@ var BrowserSyncSocket = class {
120
120
  this.#socket = socket;
121
121
  this.#socket.addEventListener("open", () => this.onopen?.());
122
122
  this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
123
- this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code }));
123
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
124
124
  this.#socket.addEventListener("error", () => this.onerror?.());
125
125
  }
126
126
  send(data) {
@@ -154,7 +154,9 @@ function createReactNativeWebSocketFactory(WebSocketConstructor) {
154
154
  function createReactNativeSyncLifecycle(environment) {
155
155
  return {
156
156
  subscribe(listener) {
157
- const appState = environment.appState.addEventListener("change", (state) => listener(state === "active" ? "active" : "inactive"));
157
+ const appState = environment.appState.addEventListener("change", (state) => {
158
+ if (state !== "inactive") listener(state === "active" ? "active" : "inactive");
159
+ });
158
160
  const network = environment.networkInfo?.addEventListener((state) => listener(isNetworkOnline(state) ? "online" : "offline"));
159
161
  listener(environment.appState.currentState === "active" ? "active" : "inactive");
160
162
  return () => {
@@ -176,7 +178,7 @@ var ReactNativeSyncSocket = class {
176
178
  this.#socket = socket;
177
179
  this.#socket.onopen = () => this.onopen?.();
178
180
  this.#socket.onmessage = (event) => this.onmessage?.({ data: event.data });
179
- this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006 });
181
+ this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0 });
180
182
  this.#socket.onerror = () => this.onerror?.();
181
183
  }
182
184
  send(data) {
@@ -196,7 +198,7 @@ var BrowserFallbackSyncSocket = class {
196
198
  this.#socket = socket;
197
199
  this.#socket.addEventListener("open", () => this.onopen?.());
198
200
  this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
199
- this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code }));
201
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
200
202
  this.#socket.addEventListener("error", () => this.onerror?.());
201
203
  }
202
204
  send(data) {
@@ -217,6 +219,40 @@ function unsubscribe(subscription) {
217
219
  }
218
220
  }
219
221
 
222
+ // src/sync/connect-diagnostics.ts
223
+ var SAFE_REASONS = /* @__PURE__ */ new Set([
224
+ "hello timeout",
225
+ "invalid token",
226
+ "policy violation",
227
+ "dependency unavailable",
228
+ "server draining",
229
+ "heartbeat timeout",
230
+ "normal",
231
+ "transport error",
232
+ "authentication failed",
233
+ "delivery recovery required",
234
+ "space action delivery recovery required",
235
+ "text frames only",
236
+ "unsupported protocol",
237
+ "invalid frame",
238
+ "WebSocket dial timeout",
239
+ "lifecycle transport deadline",
240
+ "lifecycle unavailable",
241
+ "invalid_frame",
242
+ "state_digest_mismatch",
243
+ "state_digest_recovery_failed",
244
+ "terminal event acknowledged",
245
+ "client stopped",
246
+ "participant inactive",
247
+ "terminal recovery"
248
+ ]);
249
+ function safeSyncCloseReason(reason) {
250
+ return reason ? SAFE_REASONS.has(reason) ? reason : "redacted" : null;
251
+ }
252
+ function formatSyncConnectDiagnostic(diagnostic) {
253
+ return `[${diagnostic.stage}; open=${diagnostic.opened ? 1 : 0}; close=${diagnostic.closeCode ?? "none"}; reason=${safeSyncCloseReason(diagnostic.closeReason ?? void 0) ?? "none"}; ${Math.round(diagnostic.durationMs)}ms]`;
254
+ }
255
+
220
256
  // src/sync/v1-health.ts
221
257
  var SYNC_HEALTH_BUDGET = {
222
258
  probeIntervalMs: 500,
@@ -266,7 +302,14 @@ var SyncProtocolMetadata = {
266
302
  field: "protocol",
267
303
  value: 1,
268
304
  transport: "websocket-json-text",
269
- route: "/v1/sync"
305
+ route: "/v1/sync",
306
+ entryGeneration: {
307
+ queryParameter: "entry_generation",
308
+ newEntry: "new",
309
+ resume: "previous welcome entry_generation",
310
+ confirmation: "monotonic compare-and-set",
311
+ scope: "participant"
312
+ }
270
313
  },
271
314
  directions: {
272
315
  clientToServer: "client-to-server",
@@ -290,7 +333,7 @@ var SyncProtocolMetadata = {
290
333
  },
291
334
  {
292
335
  id: "live",
293
- accepts: ["command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"],
336
+ accepts: ["entered", "command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"],
294
337
  heartbeat: {
295
338
  intervalMs: 2e4,
296
339
  missedDeadlinesBeforeClose: 2
@@ -570,7 +613,7 @@ var SyncProtocolMetadata = {
570
613
  {
571
614
  code: 1008,
572
615
  connection: "closed",
573
- reasons: ["invalid token", "policy violation"]
616
+ reasons: ["invalid token", "policy violation", "entry timeout"]
574
617
  },
575
618
  {
576
619
  code: 1009,
@@ -742,6 +785,8 @@ function strictClientFrame(value) {
742
785
  return exactObject(value, ["type", "stream", "revision", "state_digest"]);
743
786
  case "recovery_ack":
744
787
  return exactObject(value, ["type", "recovery_id", "revision", "state_digest"]);
788
+ case "entered":
789
+ return exactObject(value, ["type"]);
745
790
  case "ping":
746
791
  return exactObject(value, ["type"]);
747
792
  default:
@@ -824,7 +869,8 @@ function strictWelcome(value) {
824
869
  const base = ["type", "protocol", "participant_id", "participant_generation", "recovery_id", "head", "mode"];
825
870
  const extension = Array.isArray(value.extensions) && value.extensions.length === 1 ? value.extensions[0] : void 0;
826
871
  const extensionValid = isObject(extension) && ["collaboration_v1", "collaboration_v2"].includes(String(extension.name)) && exactObject(extension, ["name", "capabilities", "participant_capabilities", "chat_head_sequence", "retained_floor_sequence", "read_receipts"]) && Array.isArray(extension.read_receipts);
827
- const fields = extensionValid ? [...base, "extensions"] : base;
872
+ if (Object.hasOwn(value, "entry_confirmation") && value.entry_confirmation !== true) return false;
873
+ const fields = [...base, ...extensionValid ? ["extensions"] : [], ...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [], ...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : []];
828
874
  if (!strictCursor(value.head)) return false;
829
875
  if (value.mode === "snapshot") return exactObjectWithCorrelation(value, [...fields, "snapshot"]) && strictSnapshot(value.snapshot);
830
876
  if (value.mode === "terminal") return exactObjectWithCorrelation(value, [...fields, "reason"]);
@@ -883,6 +929,8 @@ function strictServerFrame(value) {
883
929
  return exactObjectWithCorrelation(value, ["type", "command_id", "code"]);
884
930
  case "error":
885
931
  return exactObjectWithCorrelation(value, ["type", "code", "detail"]);
932
+ case "entered_ack":
933
+ return exactObjectWithCorrelation(value, ["type"]);
886
934
  case "pong":
887
935
  return exactObjectWithCorrelation(value, ["type"]);
888
936
  default:
@@ -1423,7 +1471,16 @@ var CollaborationWelcomeExtensionSchema = Schema.Struct({
1423
1471
  retained_floor_sequence: Schema.NullOr(UnsignedDecimalSchema),
1424
1472
  read_receipts: Schema.Array(ChatReadReceiptSchema).check(Schema.isMaxLength(500))
1425
1473
  }).check(exactKeys(["name", "capabilities", "participant_capabilities", "chat_head_sequence", "retained_floor_sequence", "read_receipts"]));
1426
- var WelcomeBase = { type: Schema.Literal("welcome"), protocol: Schema.Literal(1), participant_id: UuidSchema, participant_generation: PositiveIntegerSchema, recovery_id: UuidSchema, head: ControlCursorSchema };
1474
+ var WelcomeBase = {
1475
+ type: Schema.Literal("welcome"),
1476
+ protocol: Schema.Literal(1),
1477
+ participant_id: UuidSchema,
1478
+ participant_generation: PositiveIntegerSchema,
1479
+ recovery_id: UuidSchema,
1480
+ head: ControlCursorSchema,
1481
+ entry_confirmation: Schema.optionalKey(Schema.Literal(true)),
1482
+ entry_generation: Schema.optionalKey(PositiveIntegerSchema.check(Schema.isLessThanOrEqualTo(9007199254740991)))
1483
+ };
1427
1484
  var ExtendedWelcomeBase = { ...WelcomeBase, extensions: Schema.Tuple([CollaborationWelcomeExtensionSchema]) };
1428
1485
  var WelcomeSnapshotFrameSchema = Schema.Struct({ ...WelcomeBase, mode: Schema.Literal("snapshot"), snapshot: SnapshotSchema, ...ServerCorrelationFields }).check(boundedServerFrame(SyncProtocolLimits.snapshotEncodedBytes));
1429
1486
  var WelcomeReplayFrameSchema = Schema.Struct({ ...WelcomeBase, mode: Schema.Literal("replay"), ...ServerCorrelationFields });
@@ -1599,6 +1656,8 @@ var RejectedChatReadResultFrameSchema = Schema.Struct({ type: Schema.Literal("ch
1599
1656
  var ChatReadResultFrameSchema = Schema.Union([AcceptedChatReadResultFrameSchema, RejectedChatReadResultFrameSchema]);
1600
1657
  var RetryableErrorFrameSchema = Schema.Struct({ type: Schema.Literal("retryable_error"), command_id: CommandIdSchema, code: RetryableErrorCodeSchema, ...ServerCorrelationFields });
1601
1658
  var ErrorFrameSchema = Schema.Struct({ type: Schema.Literal("error"), code: ProtocolErrorCodeSchema, detail: boundedUtf8String(1024), ...ServerCorrelationFields });
1659
+ var EnteredFrameSchema = Schema.Struct({ type: Schema.Literal("entered") });
1660
+ var EnteredAckFrameSchema = Schema.Struct({ type: Schema.Literal("entered_ack"), ...ServerCorrelationFields });
1602
1661
  var PingFrameSchema = Schema.Struct({ type: Schema.Literal("ping") });
1603
1662
  var PongFrameSchema = Schema.Struct({ type: Schema.Literal("pong"), ...ServerCorrelationFields });
1604
1663
  var SyncV1ClientFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) => strictClientFrame(frame) ? void 0 : { path: [], issue: "must not contain unknown or missing fields" })).pipe(
@@ -1632,6 +1691,7 @@ var SyncV1ClientFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) =>
1632
1691
  ChatReactionSetFrameSchema,
1633
1692
  DeliveryAckFrameSchema,
1634
1693
  RecoveryAckFrameSchema,
1694
+ EnteredFrameSchema,
1635
1695
  PingFrameSchema
1636
1696
  ])
1637
1697
  )
@@ -1663,6 +1723,7 @@ var SyncV1ServerFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) =>
1663
1723
  ChatReactionResultFrameSchema,
1664
1724
  RetryableErrorFrameSchema,
1665
1725
  ErrorFrameSchema,
1726
+ EnteredAckFrameSchema,
1666
1727
  PongFrameSchema
1667
1728
  ])
1668
1729
  )
@@ -1897,7 +1958,7 @@ function reduceEvent(state, event) {
1897
1958
  case "episode_started":
1898
1959
  return state;
1899
1960
  case "episode_ended":
1900
- return { ...state, status: "ended", participants: [], admissionRequests: [], recording: null };
1961
+ return { ...state, status: "ended", endReason: event.payload.reason, participants: [], admissionRequests: [], recording: null };
1901
1962
  case "hand_raised":
1902
1963
  case "hand_lowered":
1903
1964
  return updateParticipant(state, event.payload.participant_id, (participant) => ({ ...participant, handRaised: event.name === "hand_raised" }));
@@ -2939,6 +3000,9 @@ var V1SyncClient = class {
2939
3000
  #snapshotRevision = null;
2940
3001
  #controlEvents = /* @__PURE__ */ new Map();
2941
3002
  #liveTargets;
3003
+ #entryPending = false;
3004
+ // Older Sync rejects new hello fields, so entry confirmation is requested on the URL and granted in welcome.
3005
+ #entryNegotiated = false;
2942
3006
  #requests = /* @__PURE__ */ new Map();
2943
3007
  #mediaEventEvidence = /* @__PURE__ */ new Map();
2944
3008
  #presenceEventEvidence = /* @__PURE__ */ new Map();
@@ -2949,6 +3013,7 @@ var V1SyncClient = class {
2949
3013
  #presence = null;
2950
3014
  #participantId = null;
2951
3015
  #participantGeneration = null;
3016
+ #entryGeneration = null;
2952
3017
  #recovery = null;
2953
3018
  #snapshotRecoveryRequested = false;
2954
3019
  #lastDigestRecoveryAt = null;
@@ -2958,7 +3023,14 @@ var V1SyncClient = class {
2958
3023
  #reconnectTimer;
2959
3024
  #cancelDial;
2960
3025
  #dialTimeoutCount = 0;
3026
+ #connectStartedAt = null;
3027
+ #opened = false;
3028
+ #connectFailure;
3029
+ #connectDiagnostic() {
3030
+ return { stage: this.#opened ? "hello" : "upgrade", opened: this.#opened, closeCode: null, closeReason: null, durationMs: Math.max(0, this.#clock().now() - (this.#connectStartedAt ?? this.#clock().now())) };
3031
+ }
2961
3032
  #reconnectAttempt = 0;
3033
+ #episodeEndTimer;
2962
3034
  #heartbeatTimer;
2963
3035
  #missedHeartbeats = 0;
2964
3036
  #noticeTimer;
@@ -3036,11 +3108,15 @@ var V1SyncClient = class {
3036
3108
  }
3037
3109
  stop() {
3038
3110
  this.#started = false;
3111
+ this.#entryPending = false;
3112
+ this.#entryGeneration = null;
3039
3113
  this.#startupGeneration += 1;
3040
3114
  this.#unsubscribeLifecycle?.();
3041
3115
  this.#unsubscribeLifecycle = void 0;
3042
3116
  this.#clearReconnect();
3043
3117
  this.#clearDial();
3118
+ this.#clock().clearTimeout(this.#episodeEndTimer);
3119
+ this.#episodeEndTimer = void 0;
3044
3120
  this.#clearHeartbeat();
3045
3121
  this.#clearNoticeProbe();
3046
3122
  this.#commandScheduler.stop("client_stopped");
@@ -3055,11 +3131,24 @@ var V1SyncClient = class {
3055
3131
  this.#liveTargets.resetLocalMedia();
3056
3132
  this.#emit();
3057
3133
  }
3134
+ /** Announces completed startup; transport recovery resends until acknowledged. */
3135
+ async confirmEntered() {
3136
+ this.#requireLive();
3137
+ this.#entryPending = true;
3138
+ if (!this.#entryNegotiated) return;
3139
+ this.#send({ type: "entered" });
3140
+ }
3058
3141
  getSnapshot() {
3059
3142
  const pendingCommands = this.#commandScheduler.pendingCommands;
3060
3143
  const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
3061
3144
  return {
3062
- connection: { ...this.#phase, ...this.#phase.phase === "live" ? { noticeUnresponsive: this.#health.health === "recovering", health: this.#health.health } : {}, ...this.#dialTimeoutCount ? { dialTimeoutCount: this.#dialTimeoutCount } : {} },
3145
+ connection: {
3146
+ ...this.#phase,
3147
+ ...this.#connectStartedAt !== null && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectAttempt: this.#connectDiagnostic() } : {},
3148
+ ...this.#connectFailure && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectFailure: this.#connectFailure } : {},
3149
+ ...this.#phase.phase === "live" ? { noticeUnresponsive: this.#health.health === "recovering", health: this.#health.health } : {},
3150
+ ...this.#dialTimeoutCount ? { dialTimeoutCount: this.#dialTimeoutCount } : {}
3151
+ },
3063
3152
  participantId: this.#participantId,
3064
3153
  participantGeneration: this.#participantGeneration,
3065
3154
  control: this.#control,
@@ -3199,8 +3288,19 @@ var V1SyncClient = class {
3199
3288
  #connect() {
3200
3289
  if (!this.#started || !this.#transportAvailable || this.#socket || this.#phase.phase === "terminal") return;
3201
3290
  this.#phase = { phase: "connecting" };
3291
+ this.#connectStartedAt = this.#clock().now();
3292
+ this.#opened = false;
3293
+ this.#connectFailure = void 0;
3202
3294
  this.#emit();
3203
- const socket = this.#options.webSocket.connect(this.#options.url);
3295
+ this.#entryNegotiated = false;
3296
+ let socket;
3297
+ try {
3298
+ socket = this.#options.webSocket.connect(entryUrl(this.#options.url, this.#entryGeneration, this.#options.entryConfirmation === true));
3299
+ } catch {
3300
+ this.#connectFailure = this.#connectDiagnostic();
3301
+ this.#emit();
3302
+ throw new V1SyncError("Sync WebSocket could not be created", "sync_hello_timeout");
3303
+ }
3204
3304
  const connectionGeneration = ++this.#connectionGeneration;
3205
3305
  this.#socket = socket;
3206
3306
  this.#cancelDial = scheduleSyncDialDeadline(
@@ -3208,6 +3308,7 @@ var V1SyncClient = class {
3208
3308
  () => {
3209
3309
  this.#cancelDial = void 0;
3210
3310
  this.#dialTimeoutCount += 1;
3311
+ this.#connectFailure = this.#connectDiagnostic();
3211
3312
  this.#recover("WebSocket dial timeout");
3212
3313
  },
3213
3314
  this.#options.dialTimeoutMs
@@ -3215,18 +3316,36 @@ var V1SyncClient = class {
3215
3316
  socket.onopen = () => {
3216
3317
  if (socket !== this.#socket) return;
3217
3318
  this.#clearDial();
3319
+ this.#opened = true;
3320
+ this.#connectFailure = void 0;
3218
3321
  void this.#authenticate(socket);
3219
3322
  };
3220
3323
  socket.onmessage = (event) => {
3221
3324
  this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
3222
3325
  };
3223
- socket.onclose = () => {
3326
+ socket.onclose = (event) => {
3327
+ if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
3328
+ this.#connectFailure = { ...this.#connectDiagnostic(), closeCode: event.code, closeReason: safeSyncCloseReason(event.reason) };
3329
+ this.#emit();
3330
+ }
3224
3331
  this.#clock().setTimeout(() => {
3225
3332
  if (connectionGeneration !== this.#connectionGeneration) return;
3226
- this.#inbound = this.#inbound.then(() => this.#disconnected(socket));
3333
+ this.#inbound = this.#inbound.then(() => {
3334
+ if (socket === this.#socket && event.code === 1008 && (event.reason === "" || event.reason === "invalid token" || event.reason === "policy violation" || event.reason === "entry timeout")) {
3335
+ this.#phase = { phase: "terminal", terminalReason: event.reason === "entry timeout" ? "entry_timeout" : "access_rejected" };
3336
+ this.#emit();
3337
+ }
3338
+ this.#disconnected(socket);
3339
+ });
3227
3340
  }, 0);
3228
3341
  };
3229
- socket.onerror = () => socket.close(CLIENT_RESTART_CLOSE_CODE, "transport error");
3342
+ socket.onerror = () => {
3343
+ if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
3344
+ this.#connectFailure = this.#connectDiagnostic();
3345
+ this.#emit();
3346
+ }
3347
+ socket.close(CLIENT_RESTART_CLOSE_CODE, "transport error");
3348
+ };
3230
3349
  }
3231
3350
  async #authenticate(socket) {
3232
3351
  try {
@@ -3309,6 +3428,19 @@ var V1SyncClient = class {
3309
3428
  case "ack":
3310
3429
  this.#requireLive();
3311
3430
  await this.#commandScheduler.acknowledge(frame);
3431
+ if (frame.outcome === "rejected" && frame.reason === "episode_ended") {
3432
+ if (this.#episodeEndTimer === void 0) {
3433
+ this.#episodeEndTimer = this.#clock().setTimeout(() => {
3434
+ this.#episodeEndTimer = void 0;
3435
+ if (this.#control?.status === "ended" || this.#phase.phase === "terminal") return;
3436
+ this.#phase = { phase: "terminal", terminalReason: "episode_ended" };
3437
+ this.#clearReconnect();
3438
+ this.#socket?.close(1e3, "episode ended");
3439
+ this.#emit();
3440
+ }, 5e3);
3441
+ }
3442
+ this.#recover("episode ended");
3443
+ }
3312
3444
  return;
3313
3445
  case "retryable_error":
3314
3446
  this.#requireLive();
@@ -3355,6 +3487,10 @@ var V1SyncClient = class {
3355
3487
  return;
3356
3488
  case "error":
3357
3489
  throw new V1ReplicaError(frame.code);
3490
+ case "entered_ack":
3491
+ this.#requireLive();
3492
+ this.#entryPending = false;
3493
+ return;
3358
3494
  case "pong": {
3359
3495
  this.#requireLive();
3360
3496
  this.#missedHeartbeats = 0;
@@ -3368,6 +3504,8 @@ var V1SyncClient = class {
3368
3504
  async #welcome(frame) {
3369
3505
  if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
3370
3506
  this.#collaborationState.acceptWelcome(frame);
3507
+ this.#entryNegotiated = frame.entry_confirmation === true;
3508
+ this.#entryGeneration = frame.entry_generation ?? this.#entryGeneration;
3371
3509
  this.#participantId = frame.participant_id;
3372
3510
  this.#participantGeneration = frame.participant_generation;
3373
3511
  this.#updateLocalMediaStates();
@@ -3486,8 +3624,11 @@ var V1SyncClient = class {
3486
3624
  if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
3487
3625
  this.#recovery = null;
3488
3626
  this.#phase = { phase: "live" };
3627
+ this.#connectStartedAt = null;
3628
+ this.#connectFailure = void 0;
3489
3629
  this.#reconnectAttempt = 0;
3490
3630
  this.#missedHeartbeats = 0;
3631
+ if (this.#entryPending && this.#entryNegotiated) this.#send({ type: "entered" });
3491
3632
  this.#commandScheduler.enterLive();
3492
3633
  this.#liveTargets.enterLive();
3493
3634
  this.#startHeartbeat();
@@ -3529,9 +3670,10 @@ var V1SyncClient = class {
3529
3670
  this.#presence = null;
3530
3671
  this.#rejectRequests("disconnected_before_delivery");
3531
3672
  this.#collaborationState.disconnect("disconnected_before_delivery");
3532
- if (!this.#started || !this.#transportAvailable || this.#phase.phase === "terminal") return;
3673
+ if (!this.#started || this.#phase.phase === "terminal") return;
3533
3674
  this.#phase = { phase: "connecting" };
3534
3675
  this.#emit();
3676
+ if (!this.#transportAvailable) return;
3535
3677
  this.#clearReconnect();
3536
3678
  const configuredDelay = this.#options.reconnectDelayMs;
3537
3679
  const exponentialAttempt = configuredDelay === void 0 ? Math.max(0, this.#reconnectAttempt - FAST_RECONNECT_ATTEMPTS + 1) : this.#reconnectAttempt;
@@ -3576,8 +3718,13 @@ var V1SyncClient = class {
3576
3718
  if (event === "online" || event === "offline") this.#online = event === "online";
3577
3719
  else this.#active = event === "active";
3578
3720
  this.#transportAvailable = this.#online && this.#active;
3579
- if (!this.#transportAvailable) this.#socket?.close(CLIENT_RESTART_CLOSE_CODE, "lifecycle unavailable");
3580
- else this.#connect();
3721
+ if (!this.#transportAvailable) {
3722
+ const socket = this.#socket;
3723
+ if (socket) {
3724
+ socket.close(CLIENT_RESTART_CLOSE_CODE, "lifecycle unavailable");
3725
+ this.#disconnected(socket);
3726
+ }
3727
+ } else this.#connect();
3581
3728
  }
3582
3729
  #rejectEphemeral(code) {
3583
3730
  this.#rejectRequests(code);
@@ -3746,6 +3893,12 @@ function copyPublication(publication) {
3746
3893
  function validMediaPublication(publication) {
3747
3894
  return typeof publication.participantId === "string" && publication.participantId.length > 0 && (publication.source === "microphone" || publication.source === "camera" || publication.source === "screen" || publication.source === "screen_audio") && typeof publication.enabled === "boolean" && (publication.publicationId === null || typeof publication.publicationId === "string") && (!publication.enabled || publication.publicationId !== null);
3748
3895
  }
3896
+ function entryUrl(value, generation, confirmation) {
3897
+ const url = new URL(value);
3898
+ url.searchParams.set("entry_generation", generation === null ? "new" : String(generation));
3899
+ if (confirmation) url.searchParams.set("entry_confirmation", "1");
3900
+ return url.toString();
3901
+ }
3749
3902
  function assertV1Url(value) {
3750
3903
  const url = new URL(value);
3751
3904
  if (url.pathname !== "/v1/sync") throw new V1SyncError("SyncEngine v1 websocket URL must use /v1/sync", "invalid_url");
@@ -3893,6 +4046,7 @@ function createV1SyncClient(options) {
3893
4046
  }
3894
4047
 
3895
4048
  export {
4049
+ formatSyncConnectDiagnostic,
3896
4050
  UuidSchema,
3897
4051
  ReactionValues,
3898
4052
  ReactionSchema,