@q9labsai/chalk-client 5.0.1 → 5.2.0

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 (236) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/README.md +39 -0
  3. package/dist/access/base64-url.d.ts +0 -1
  4. package/dist/access/grant.d.ts +0 -1
  5. package/dist/access/manager.d.ts +0 -1
  6. package/dist/chat-files/http-transport.d.ts +0 -1
  7. package/dist/chat-files/index.d.ts +0 -1
  8. package/dist/chat-files/types.d.ts +3 -1
  9. package/dist/{chunk-IPAN4LUZ.js → chunk-BCO3AZUB.js} +1463 -583
  10. package/dist/{chunk-AVXJ4HQU.js → chunk-D2GXPX2P.js} +298 -47
  11. package/dist/{chunk-CMVO7USN.js → chunk-DHGAKJXP.js} +1 -1
  12. package/dist/{chunk-O2NOZKJP.js → chunk-DWKTGBHR.js} +20 -8
  13. package/dist/{chunk-2HJIJVE2.js → chunk-HO4745TX.js} +591 -5
  14. package/dist/client.d.ts +277 -1
  15. package/dist/collaboration/index.d.ts +0 -1
  16. package/dist/collaboration/types.d.ts +0 -1
  17. package/dist/collaboration/wire.d.ts +0 -1
  18. package/dist/connection/dependencies.d.ts +8 -3
  19. package/dist/connection/diagnostics.d.ts +3 -3
  20. package/dist/connection/index.d.ts +1 -1
  21. package/dist/connection/lifecycle.d.ts +3 -3
  22. package/dist/connection/media-devices.d.ts +0 -1
  23. package/dist/connection/production.d.ts +0 -1
  24. package/dist/connection/types.d.ts +4 -2
  25. package/dist/effect.d.ts +0 -1
  26. package/dist/effect.js +146 -4
  27. package/dist/foreground-deadline.d.ts +0 -1
  28. package/dist/generated/http-api.d.ts +273 -1
  29. package/dist/generated/schemas.d.ts +732 -7
  30. package/dist/generated/sync.d.ts +74 -4
  31. package/dist/generated/whiteboard-v1.d.ts +114 -9
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +21 -15
  34. package/dist/media/camera-uplink.d.ts +0 -1
  35. package/dist/media/capture-error.d.ts +0 -1
  36. package/dist/media/client.d.ts +3 -2
  37. package/dist/media/cloudflare-sfu.d.ts +0 -1
  38. package/dist/media/index.d.ts +0 -1
  39. package/dist/media/index.js +1 -1
  40. package/dist/media/observers.d.ts +0 -1
  41. package/dist/media/plane.d.ts +0 -1
  42. package/dist/media/remote-track-response.d.ts +0 -1
  43. package/dist/media/target.d.ts +0 -1
  44. package/dist/media/tracks.d.ts +0 -1
  45. package/dist/media/transport.d.ts +0 -1
  46. package/dist/media/types.d.ts +0 -1
  47. package/dist/public-invites/client.d.ts +0 -1
  48. package/dist/public-invites/index.d.ts +0 -1
  49. package/dist/public-invites/index.js +2 -2
  50. package/dist/public-invites/types.d.ts +0 -1
  51. package/dist/server/access-refresh.d.ts +0 -1
  52. package/dist/server/{chunk-C4XITY5U.js → chunk-XQOQTCUL.js} +22 -0
  53. package/dist/server/client.d.ts +0 -1
  54. package/dist/server/errors.d.cts +8 -1
  55. package/dist/server/errors.d.ts +8 -2
  56. package/dist/server/index.cjs +31 -7
  57. package/dist/server/index.d.cts +1 -1
  58. package/dist/server/index.d.ts +1 -2
  59. package/dist/server/index.js +10 -7
  60. package/dist/server/server-only.d.ts +0 -1
  61. package/dist/server/server-only.js +1 -1
  62. package/dist/server/transport.d.ts +0 -1
  63. package/dist/server/types.d.ts +0 -1
  64. package/dist/space-client/chat-controller-helpers.d.ts +0 -1
  65. package/dist/space-client/chat-controller.d.ts +0 -1
  66. package/dist/space-client/controller-effects.d.ts +0 -1
  67. package/dist/space-client/core.d.ts +0 -1
  68. package/dist/space-client/effect.d.ts +0 -1
  69. package/dist/space-client/errors.d.ts +4 -2
  70. package/dist/space-client/index.d.ts +1 -2
  71. package/dist/space-client/media-controller.d.ts +3 -2
  72. package/dist/space-client/media-device-selection.d.ts +0 -1
  73. package/dist/space-client/offline-actions-storage.d.ts +0 -1
  74. package/dist/space-client/offline-actions.d.ts +0 -1
  75. package/dist/space-client/participants-controller.d.ts +0 -1
  76. package/dist/space-client/promise-facade.d.ts +0 -1
  77. package/dist/space-client/reactions-controller.d.ts +0 -1
  78. package/dist/space-client/recording-controller.d.ts +0 -1
  79. package/dist/space-client/reload-rejoin.d.ts +1 -2
  80. package/dist/space-client/space-client.d.ts +0 -1
  81. package/dist/space-client/store.d.ts +0 -1
  82. package/dist/space-client/types.d.ts +15 -3
  83. package/dist/space-client/whiteboard-controller.d.ts +0 -1
  84. package/dist/sync/browser.d.ts +0 -1
  85. package/dist/sync/canonical.d.ts +0 -1
  86. package/dist/sync/connect-diagnostics.d.ts +10 -0
  87. package/dist/sync/dial-deadline.d.ts +3 -0
  88. package/dist/sync/errors.d.ts +0 -1
  89. package/dist/sync/index.d.ts +0 -1
  90. package/dist/sync/index.js +1 -1
  91. package/dist/sync/react-native.d.ts +0 -1
  92. package/dist/sync/types.d.ts +1 -1
  93. package/dist/sync/v1-client.d.ts +4 -2
  94. package/dist/sync/v1-codec.d.ts +0 -1
  95. package/dist/sync/v1-collaboration-state.d.ts +1 -1
  96. package/dist/sync/v1-command-scheduler.d.ts +0 -1
  97. package/dist/sync/v1-create.d.ts +0 -1
  98. package/dist/sync/v1-deferred.d.ts +0 -1
  99. package/dist/sync/v1-error.d.ts +0 -1
  100. package/dist/sync/v1-frame-signature.d.ts +0 -1
  101. package/dist/sync/v1-health.d.ts +15 -0
  102. package/dist/sync/v1-live-target-coordinator.d.ts +0 -1
  103. package/dist/sync/v1-persistence.d.ts +0 -1
  104. package/dist/sync/v1-platform-persistence.d.ts +0 -1
  105. package/dist/sync/v1-reducer.d.ts +3 -1
  106. package/dist/sync/v1-types.d.ts +14 -1
  107. package/dist/webhooks/errors.d.ts +0 -1
  108. package/dist/webhooks/generated/event-v1.d.ts +0 -1
  109. package/dist/webhooks/generated/fixtures-v1.d.ts +0 -1
  110. package/dist/webhooks/index.d.ts +0 -1
  111. package/dist/webhooks/processor.d.ts +0 -1
  112. package/dist/webhooks/server-only.d.ts +0 -1
  113. package/dist/webhooks/test.d.ts +0 -1
  114. package/dist/webhooks/types.d.ts +0 -1
  115. package/dist/webhooks/validate.d.ts +0 -1
  116. package/dist/webhooks/verify.d.ts +0 -1
  117. package/dist/whiteboard/file-download-transport.d.ts +0 -1
  118. package/dist/whiteboard/index.d.ts +2 -1
  119. package/dist/whiteboard/types.d.ts +54 -5
  120. package/dist/whiteboard/v1-client.d.ts +5 -2
  121. package/dist/whiteboard/v1-codec.d.ts +5 -2
  122. package/dist/whiteboard/v1-create.d.ts +0 -1
  123. package/dist/whiteboard/v1-multipart.d.ts +1 -2
  124. package/dist/whiteboard/v1-persistence.d.ts +0 -1
  125. package/dist/whiteboard/view-bounds.d.ts +5 -0
  126. package/dist/whiteboard/view-versions.d.ts +9 -0
  127. package/package.json +4 -2
  128. package/dist/access/base64-url.d.ts.map +0 -1
  129. package/dist/access/grant.d.ts.map +0 -1
  130. package/dist/access/grant.test.helpers.d.ts +0 -10
  131. package/dist/access/grant.test.helpers.d.ts.map +0 -1
  132. package/dist/access/manager.d.ts.map +0 -1
  133. package/dist/chat-files/http-transport.d.ts.map +0 -1
  134. package/dist/chat-files/index.d.ts.map +0 -1
  135. package/dist/chat-files/types.d.ts.map +0 -1
  136. package/dist/client.d.ts.map +0 -1
  137. package/dist/collaboration/index.d.ts.map +0 -1
  138. package/dist/collaboration/types.d.ts.map +0 -1
  139. package/dist/collaboration/wire.d.ts.map +0 -1
  140. package/dist/connection/dependencies.d.ts.map +0 -1
  141. package/dist/connection/diagnostics.d.ts.map +0 -1
  142. package/dist/connection/index.d.ts.map +0 -1
  143. package/dist/connection/lifecycle.d.ts.map +0 -1
  144. package/dist/connection/media-devices.d.ts.map +0 -1
  145. package/dist/connection/production.d.ts.map +0 -1
  146. package/dist/connection/types.d.ts.map +0 -1
  147. package/dist/effect.d.ts.map +0 -1
  148. package/dist/foreground-deadline.d.ts.map +0 -1
  149. package/dist/generated/http-api.d.ts.map +0 -1
  150. package/dist/generated/schemas.d.ts.map +0 -1
  151. package/dist/generated/sync.d.ts.map +0 -1
  152. package/dist/generated/whiteboard-v1.d.ts.map +0 -1
  153. package/dist/index.d.ts.map +0 -1
  154. package/dist/media/camera-uplink.d.ts.map +0 -1
  155. package/dist/media/capture-error.d.ts.map +0 -1
  156. package/dist/media/client.d.ts.map +0 -1
  157. package/dist/media/cloudflare-sfu.d.ts.map +0 -1
  158. package/dist/media/index.d.ts.map +0 -1
  159. package/dist/media/observers.d.ts.map +0 -1
  160. package/dist/media/plane.d.ts.map +0 -1
  161. package/dist/media/remote-track-response.d.ts.map +0 -1
  162. package/dist/media/target.d.ts.map +0 -1
  163. package/dist/media/tracks.d.ts.map +0 -1
  164. package/dist/media/transport.d.ts.map +0 -1
  165. package/dist/media/types.d.ts.map +0 -1
  166. package/dist/public-invites/client.d.ts.map +0 -1
  167. package/dist/public-invites/index.d.ts.map +0 -1
  168. package/dist/public-invites/types.d.ts.map +0 -1
  169. package/dist/server/access-refresh.d.ts.map +0 -1
  170. package/dist/server/client.d.ts.map +0 -1
  171. package/dist/server/errors.d.ts.map +0 -1
  172. package/dist/server/index.d.ts.map +0 -1
  173. package/dist/server/server-only.d.ts.map +0 -1
  174. package/dist/server/transport.d.ts.map +0 -1
  175. package/dist/server/types.d.ts.map +0 -1
  176. package/dist/space-client/chat-controller-helpers.d.ts.map +0 -1
  177. package/dist/space-client/chat-controller.d.ts.map +0 -1
  178. package/dist/space-client/controller-effects.d.ts.map +0 -1
  179. package/dist/space-client/core.d.ts.map +0 -1
  180. package/dist/space-client/core.test.helpers.d.ts +0 -15
  181. package/dist/space-client/core.test.helpers.d.ts.map +0 -1
  182. package/dist/space-client/effect.d.ts.map +0 -1
  183. package/dist/space-client/errors.d.ts.map +0 -1
  184. package/dist/space-client/index.d.ts.map +0 -1
  185. package/dist/space-client/media-controller.d.ts.map +0 -1
  186. package/dist/space-client/media-device-selection.d.ts.map +0 -1
  187. package/dist/space-client/offline-actions-storage.d.ts.map +0 -1
  188. package/dist/space-client/offline-actions.d.ts.map +0 -1
  189. package/dist/space-client/participants-controller.d.ts.map +0 -1
  190. package/dist/space-client/promise-facade.d.ts.map +0 -1
  191. package/dist/space-client/reactions-controller.d.ts.map +0 -1
  192. package/dist/space-client/recording-controller.d.ts.map +0 -1
  193. package/dist/space-client/reload-rejoin.d.ts.map +0 -1
  194. package/dist/space-client/space-client.d.ts.map +0 -1
  195. package/dist/space-client/store.d.ts.map +0 -1
  196. package/dist/space-client/terminal.test.helpers.d.ts +0 -136
  197. package/dist/space-client/terminal.test.helpers.d.ts.map +0 -1
  198. package/dist/space-client/types.d.ts.map +0 -1
  199. package/dist/space-client/whiteboard-controller.d.ts.map +0 -1
  200. package/dist/sync/browser.d.ts.map +0 -1
  201. package/dist/sync/canonical.d.ts.map +0 -1
  202. package/dist/sync/errors.d.ts.map +0 -1
  203. package/dist/sync/index.d.ts.map +0 -1
  204. package/dist/sync/react-native.d.ts.map +0 -1
  205. package/dist/sync/types.d.ts.map +0 -1
  206. package/dist/sync/v1-client.d.ts.map +0 -1
  207. package/dist/sync/v1-codec.d.ts.map +0 -1
  208. package/dist/sync/v1-collaboration-state.d.ts.map +0 -1
  209. package/dist/sync/v1-command-scheduler.d.ts.map +0 -1
  210. package/dist/sync/v1-create.d.ts.map +0 -1
  211. package/dist/sync/v1-deferred.d.ts.map +0 -1
  212. package/dist/sync/v1-error.d.ts.map +0 -1
  213. package/dist/sync/v1-frame-signature.d.ts.map +0 -1
  214. package/dist/sync/v1-live-target-coordinator.d.ts.map +0 -1
  215. package/dist/sync/v1-persistence.d.ts.map +0 -1
  216. package/dist/sync/v1-platform-persistence.d.ts.map +0 -1
  217. package/dist/sync/v1-reducer.d.ts.map +0 -1
  218. package/dist/sync/v1-types.d.ts.map +0 -1
  219. package/dist/webhooks/errors.d.ts.map +0 -1
  220. package/dist/webhooks/generated/event-v1.d.ts.map +0 -1
  221. package/dist/webhooks/generated/fixtures-v1.d.ts.map +0 -1
  222. package/dist/webhooks/index.d.ts.map +0 -1
  223. package/dist/webhooks/processor.d.ts.map +0 -1
  224. package/dist/webhooks/server-only.d.ts.map +0 -1
  225. package/dist/webhooks/test.d.ts.map +0 -1
  226. package/dist/webhooks/types.d.ts.map +0 -1
  227. package/dist/webhooks/validate.d.ts.map +0 -1
  228. package/dist/webhooks/verify.d.ts.map +0 -1
  229. package/dist/whiteboard/file-download-transport.d.ts.map +0 -1
  230. package/dist/whiteboard/index.d.ts.map +0 -1
  231. package/dist/whiteboard/types.d.ts.map +0 -1
  232. package/dist/whiteboard/v1-client.d.ts.map +0 -1
  233. package/dist/whiteboard/v1-codec.d.ts.map +0 -1
  234. package/dist/whiteboard/v1-create.d.ts.map +0 -1
  235. package/dist/whiteboard/v1-multipart.d.ts.map +0 -1
  236. package/dist/whiteboard/v1-persistence.d.ts.map +0 -1
@@ -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) {
@@ -176,7 +176,7 @@ var ReactNativeSyncSocket = class {
176
176
  this.#socket = socket;
177
177
  this.#socket.onopen = () => this.onopen?.();
178
178
  this.#socket.onmessage = (event) => this.onmessage?.({ data: event.data });
179
- this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006 });
179
+ this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0 });
180
180
  this.#socket.onerror = () => this.onerror?.();
181
181
  }
182
182
  send(data) {
@@ -196,7 +196,7 @@ var BrowserFallbackSyncSocket = class {
196
196
  this.#socket = socket;
197
197
  this.#socket.addEventListener("open", () => this.onopen?.());
198
198
  this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
199
- this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code }));
199
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
200
200
  this.#socket.addEventListener("error", () => this.onerror?.());
201
201
  }
202
202
  send(data) {
@@ -217,6 +217,81 @@ function unsubscribe(subscription) {
217
217
  }
218
218
  }
219
219
 
220
+ // src/sync/connect-diagnostics.ts
221
+ var SAFE_REASONS = /* @__PURE__ */ new Set([
222
+ "hello timeout",
223
+ "invalid token",
224
+ "policy violation",
225
+ "dependency unavailable",
226
+ "server draining",
227
+ "heartbeat timeout",
228
+ "normal",
229
+ "transport error",
230
+ "authentication failed",
231
+ "delivery recovery required",
232
+ "space action delivery recovery required",
233
+ "text frames only",
234
+ "unsupported protocol",
235
+ "invalid frame",
236
+ "WebSocket dial timeout",
237
+ "lifecycle transport deadline",
238
+ "lifecycle unavailable",
239
+ "invalid_frame",
240
+ "state_digest_mismatch",
241
+ "state_digest_recovery_failed",
242
+ "terminal event acknowledged",
243
+ "client stopped",
244
+ "participant inactive",
245
+ "terminal recovery"
246
+ ]);
247
+ function safeSyncCloseReason(reason) {
248
+ return reason ? SAFE_REASONS.has(reason) ? reason : "redacted" : null;
249
+ }
250
+ function formatSyncConnectDiagnostic(diagnostic) {
251
+ return `[${diagnostic.stage}; open=${diagnostic.opened ? 1 : 0}; close=${diagnostic.closeCode ?? "none"}; reason=${safeSyncCloseReason(diagnostic.closeReason ?? void 0) ?? "none"}; ${Math.round(diagnostic.durationMs)}ms]`;
252
+ }
253
+
254
+ // src/sync/v1-health.ts
255
+ var SYNC_HEALTH_BUDGET = {
256
+ probeIntervalMs: 500,
257
+ degradedAfterMs: 750,
258
+ recoveringAfterMs: 4e3,
259
+ stablePongsForMs: 2e3
260
+ };
261
+ var V1SyncHealth = class {
262
+ health = "healthy";
263
+ #lastPongAt;
264
+ #stableSince = null;
265
+ constructor(now) {
266
+ this.#lastPongAt = now;
267
+ }
268
+ observePong(now) {
269
+ this.check(now);
270
+ const gap = now - this.#lastPongAt;
271
+ this.#lastPongAt = now;
272
+ if (this.health === "healthy") return;
273
+ if (gap > SYNC_HEALTH_BUDGET.degradedAfterMs || this.#stableSince === null) this.#stableSince = now;
274
+ if (now - this.#stableSince >= SYNC_HEALTH_BUDGET.stablePongsForMs) {
275
+ this.health = "healthy";
276
+ this.#stableSince = null;
277
+ }
278
+ }
279
+ check(now) {
280
+ const silence = now - this.#lastPongAt;
281
+ if (silence >= SYNC_HEALTH_BUDGET.degradedAfterMs) this.#stableSince = null;
282
+ if (silence >= SYNC_HEALTH_BUDGET.recoveringAfterMs) this.health = "recovering";
283
+ else if (silence >= SYNC_HEALTH_BUDGET.degradedAfterMs && this.health === "healthy") this.health = "degraded";
284
+ }
285
+ };
286
+
287
+ // src/sync/dial-deadline.ts
288
+ var SYNC_DIAL_TIMEOUT_MS = 1e4;
289
+ function scheduleSyncDialDeadline(clock, onTimeout, timeoutMs = SYNC_DIAL_TIMEOUT_MS) {
290
+ const allowance = Number.isFinite(timeoutMs) ? Math.min(6e4, Math.max(SYNC_DIAL_TIMEOUT_MS, timeoutMs)) : SYNC_DIAL_TIMEOUT_MS;
291
+ const timer = clock.setTimeout(onTimeout, allowance);
292
+ return () => clock.clearTimeout(timer);
293
+ }
294
+
220
295
  // src/generated/sync.ts
221
296
  import { Schema } from "effect";
222
297
  var SyncProtocolMetadata = {
@@ -225,7 +300,14 @@ var SyncProtocolMetadata = {
225
300
  field: "protocol",
226
301
  value: 1,
227
302
  transport: "websocket-json-text",
228
- route: "/v1/sync"
303
+ route: "/v1/sync",
304
+ entryGeneration: {
305
+ queryParameter: "entry_generation",
306
+ newEntry: "new",
307
+ resume: "previous welcome entry_generation",
308
+ confirmation: "monotonic compare-and-set",
309
+ scope: "participant"
310
+ }
229
311
  },
230
312
  directions: {
231
313
  clientToServer: "client-to-server",
@@ -249,7 +331,7 @@ var SyncProtocolMetadata = {
249
331
  },
250
332
  {
251
333
  id: "live",
252
- accepts: ["command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"],
334
+ 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"],
253
335
  heartbeat: {
254
336
  intervalMs: 2e4,
255
337
  missedDeadlinesBeforeClose: 2
@@ -529,7 +611,7 @@ var SyncProtocolMetadata = {
529
611
  {
530
612
  code: 1008,
531
613
  connection: "closed",
532
- reasons: ["invalid token", "policy violation"]
614
+ reasons: ["invalid token", "policy violation", "entry timeout"]
533
615
  },
534
616
  {
535
617
  code: 1009,
@@ -701,6 +783,8 @@ function strictClientFrame(value) {
701
783
  return exactObject(value, ["type", "stream", "revision", "state_digest"]);
702
784
  case "recovery_ack":
703
785
  return exactObject(value, ["type", "recovery_id", "revision", "state_digest"]);
786
+ case "entered":
787
+ return exactObject(value, ["type"]);
704
788
  case "ping":
705
789
  return exactObject(value, ["type"]);
706
790
  default:
@@ -783,7 +867,8 @@ function strictWelcome(value) {
783
867
  const base = ["type", "protocol", "participant_id", "participant_generation", "recovery_id", "head", "mode"];
784
868
  const extension = Array.isArray(value.extensions) && value.extensions.length === 1 ? value.extensions[0] : void 0;
785
869
  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);
786
- const fields = extensionValid ? [...base, "extensions"] : base;
870
+ if (Object.hasOwn(value, "entry_confirmation") && value.entry_confirmation !== true) return false;
871
+ const fields = [...base, ...extensionValid ? ["extensions"] : [], ...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [], ...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : []];
787
872
  if (!strictCursor(value.head)) return false;
788
873
  if (value.mode === "snapshot") return exactObjectWithCorrelation(value, [...fields, "snapshot"]) && strictSnapshot(value.snapshot);
789
874
  if (value.mode === "terminal") return exactObjectWithCorrelation(value, [...fields, "reason"]);
@@ -842,6 +927,8 @@ function strictServerFrame(value) {
842
927
  return exactObjectWithCorrelation(value, ["type", "command_id", "code"]);
843
928
  case "error":
844
929
  return exactObjectWithCorrelation(value, ["type", "code", "detail"]);
930
+ case "entered_ack":
931
+ return exactObjectWithCorrelation(value, ["type"]);
845
932
  case "pong":
846
933
  return exactObjectWithCorrelation(value, ["type"]);
847
934
  default:
@@ -1382,7 +1469,16 @@ var CollaborationWelcomeExtensionSchema = Schema.Struct({
1382
1469
  retained_floor_sequence: Schema.NullOr(UnsignedDecimalSchema),
1383
1470
  read_receipts: Schema.Array(ChatReadReceiptSchema).check(Schema.isMaxLength(500))
1384
1471
  }).check(exactKeys(["name", "capabilities", "participant_capabilities", "chat_head_sequence", "retained_floor_sequence", "read_receipts"]));
1385
- var WelcomeBase = { type: Schema.Literal("welcome"), protocol: Schema.Literal(1), participant_id: UuidSchema, participant_generation: PositiveIntegerSchema, recovery_id: UuidSchema, head: ControlCursorSchema };
1472
+ var WelcomeBase = {
1473
+ type: Schema.Literal("welcome"),
1474
+ protocol: Schema.Literal(1),
1475
+ participant_id: UuidSchema,
1476
+ participant_generation: PositiveIntegerSchema,
1477
+ recovery_id: UuidSchema,
1478
+ head: ControlCursorSchema,
1479
+ entry_confirmation: Schema.optionalKey(Schema.Literal(true)),
1480
+ entry_generation: Schema.optionalKey(PositiveIntegerSchema.check(Schema.isLessThanOrEqualTo(9007199254740991)))
1481
+ };
1386
1482
  var ExtendedWelcomeBase = { ...WelcomeBase, extensions: Schema.Tuple([CollaborationWelcomeExtensionSchema]) };
1387
1483
  var WelcomeSnapshotFrameSchema = Schema.Struct({ ...WelcomeBase, mode: Schema.Literal("snapshot"), snapshot: SnapshotSchema, ...ServerCorrelationFields }).check(boundedServerFrame(SyncProtocolLimits.snapshotEncodedBytes));
1388
1484
  var WelcomeReplayFrameSchema = Schema.Struct({ ...WelcomeBase, mode: Schema.Literal("replay"), ...ServerCorrelationFields });
@@ -1558,6 +1654,8 @@ var RejectedChatReadResultFrameSchema = Schema.Struct({ type: Schema.Literal("ch
1558
1654
  var ChatReadResultFrameSchema = Schema.Union([AcceptedChatReadResultFrameSchema, RejectedChatReadResultFrameSchema]);
1559
1655
  var RetryableErrorFrameSchema = Schema.Struct({ type: Schema.Literal("retryable_error"), command_id: CommandIdSchema, code: RetryableErrorCodeSchema, ...ServerCorrelationFields });
1560
1656
  var ErrorFrameSchema = Schema.Struct({ type: Schema.Literal("error"), code: ProtocolErrorCodeSchema, detail: boundedUtf8String(1024), ...ServerCorrelationFields });
1657
+ var EnteredFrameSchema = Schema.Struct({ type: Schema.Literal("entered") });
1658
+ var EnteredAckFrameSchema = Schema.Struct({ type: Schema.Literal("entered_ack"), ...ServerCorrelationFields });
1561
1659
  var PingFrameSchema = Schema.Struct({ type: Schema.Literal("ping") });
1562
1660
  var PongFrameSchema = Schema.Struct({ type: Schema.Literal("pong"), ...ServerCorrelationFields });
1563
1661
  var SyncV1ClientFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) => strictClientFrame(frame) ? void 0 : { path: [], issue: "must not contain unknown or missing fields" })).pipe(
@@ -1591,6 +1689,7 @@ var SyncV1ClientFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) =>
1591
1689
  ChatReactionSetFrameSchema,
1592
1690
  DeliveryAckFrameSchema,
1593
1691
  RecoveryAckFrameSchema,
1692
+ EnteredFrameSchema,
1594
1693
  PingFrameSchema
1595
1694
  ])
1596
1695
  )
@@ -1622,6 +1721,7 @@ var SyncV1ServerFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) =>
1622
1721
  ChatReactionResultFrameSchema,
1623
1722
  RetryableErrorFrameSchema,
1624
1723
  ErrorFrameSchema,
1724
+ EnteredAckFrameSchema,
1625
1725
  PongFrameSchema
1626
1726
  ])
1627
1727
  )
@@ -1719,6 +1819,12 @@ var V1ReplicaError = class extends Error {
1719
1819
  this.name = "V1ReplicaError";
1720
1820
  }
1721
1821
  };
1822
+ var V1StateDigestMismatchError = class extends V1ReplicaError {
1823
+ constructor() {
1824
+ super("control state digest mismatch");
1825
+ this.name = "V1StateDigestMismatchError";
1826
+ }
1827
+ };
1722
1828
  async function restoreV1Snapshot(snapshot) {
1723
1829
  const state = snapshotToState(snapshot);
1724
1830
  assertV1ControlSemantics(state);
@@ -1832,7 +1938,7 @@ function durableProjection(state) {
1832
1938
  };
1833
1939
  }
1834
1940
  async function assertDigest(state) {
1835
- if (await computeV1StateDigest(state) !== state.stateDigest) throw new V1ReplicaError("control state digest mismatch");
1941
+ if (await computeV1StateDigest(state) !== state.stateDigest) throw new V1StateDigestMismatchError();
1836
1942
  }
1837
1943
  function reduceEvent(state, event) {
1838
1944
  switch (event.name) {
@@ -1971,9 +2077,11 @@ function sameStrings2(left, right) {
1971
2077
 
1972
2078
  // src/sync/v1-collaboration-state.ts
1973
2079
  var MAX_COLLABORATION_REQUESTS_IN_FLIGHT = 64;
2080
+ var REACTION_ACK_TIMEOUT_MS = 5e3;
1974
2081
  var V1CollaborationState = class {
1975
2082
  #options;
1976
2083
  #listeners = /* @__PURE__ */ new Set();
2084
+ #reactionTimers = /* @__PURE__ */ new Map();
1977
2085
  #reactions = /* @__PURE__ */ new Map();
1978
2086
  #chatSends = /* @__PURE__ */ new Map();
1979
2087
  #chatPages = /* @__PURE__ */ new Map();
@@ -2044,7 +2152,18 @@ var V1CollaborationState = class {
2044
2152
  this.#assertCapacity();
2045
2153
  const operationId = this.#nextRequestId();
2046
2154
  const frame = { type: "reaction_send", operation_id: operationId, reaction };
2047
- return this.#sendTrackedRequest(operationId, frame, this.#reactions);
2155
+ const pending = this.#sendTrackedRequest(operationId, frame, this.#reactions);
2156
+ this.#reactionTimers.set(
2157
+ operationId,
2158
+ this.#options.clock().setTimeout(() => {
2159
+ this.#reactionTimers.delete(operationId);
2160
+ const deferred = this.#reactions.get(operationId);
2161
+ if (!deferred) return;
2162
+ this.#reactions.delete(operationId);
2163
+ rejectV1Deferred(deferred, new V1SyncError("Reaction acknowledgement timed out", "reaction_ack_timeout"));
2164
+ }, REACTION_ACK_TIMEOUT_MS)
2165
+ );
2166
+ return pending;
2048
2167
  }
2049
2168
  sendChatMessage(input) {
2050
2169
  this.#assertReady("sendChat");
@@ -2108,6 +2227,8 @@ var V1CollaborationState = class {
2108
2227
  this.#requireNegotiated();
2109
2228
  const deferred = this.#reactions.get(frame.operation_id);
2110
2229
  if (!deferred) return;
2230
+ this.#options.clock().clearTimeout(this.#reactionTimers.get(frame.operation_id));
2231
+ this.#reactionTimers.delete(frame.operation_id);
2111
2232
  this.#reactions.delete(frame.operation_id);
2112
2233
  if (frame.outcome === "accepted") resolveV1Deferred(deferred, reactionFromFrame(frame.reaction));
2113
2234
  else rejectV1Deferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
@@ -2178,6 +2299,8 @@ var V1CollaborationState = class {
2178
2299
  for (const deferred of this.#chatSends.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
2179
2300
  for (const deferred of this.#chatPages.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
2180
2301
  for (const deferred of this.#chatReads.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
2302
+ for (const timer of this.#reactionTimers.values()) this.#options.clock().clearTimeout(timer);
2303
+ this.#reactionTimers.clear();
2181
2304
  this.#reactions.clear();
2182
2305
  this.#chatSends.clear();
2183
2306
  this.#chatPages.clear();
@@ -2863,17 +2986,21 @@ var CLIENT_RESTART_CLOSE_CODE = 4e3;
2863
2986
  var DEFAULT_RECONNECT_DELAY_MS = 250;
2864
2987
  var MAX_RECONNECT_DELAY_MS = 5e3;
2865
2988
  var FAST_RECONNECT_ATTEMPTS = 8;
2866
- var NOTICE_PROBE_INTERVAL_MS = 500;
2867
- var NOTICE_SILENCE_MS = 750;
2989
+ var DIGEST_RECOVERY_COOLDOWN_MS = 6e4;
2868
2990
  var V1SyncClient = class {
2869
2991
  #options;
2870
2992
  #commandScheduler;
2871
2993
  #listeners = /* @__PURE__ */ new Set();
2994
+ #mediaPauseListeners = /* @__PURE__ */ new Set();
2872
2995
  #requestListeners = /* @__PURE__ */ new Set();
2873
2996
  #collaborationState;
2874
2997
  #controlHeads = /* @__PURE__ */ new Map();
2998
+ #snapshotRevision = null;
2875
2999
  #controlEvents = /* @__PURE__ */ new Map();
2876
3000
  #liveTargets;
3001
+ #entryPending = false;
3002
+ // Older Sync rejects new hello fields, so entry confirmation is requested on the URL and granted in welcome.
3003
+ #entryNegotiated = false;
2877
3004
  #requests = /* @__PURE__ */ new Map();
2878
3005
  #mediaEventEvidence = /* @__PURE__ */ new Map();
2879
3006
  #presenceEventEvidence = /* @__PURE__ */ new Map();
@@ -2884,18 +3011,28 @@ var V1SyncClient = class {
2884
3011
  #presence = null;
2885
3012
  #participantId = null;
2886
3013
  #participantGeneration = null;
3014
+ #entryGeneration = null;
2887
3015
  #recovery = null;
3016
+ #snapshotRecoveryRequested = false;
3017
+ #lastDigestRecoveryAt = null;
2888
3018
  #started = false;
2889
3019
  #startupGeneration = 0;
2890
3020
  #connectionGeneration = 0;
2891
3021
  #reconnectTimer;
3022
+ #cancelDial;
3023
+ #dialTimeoutCount = 0;
3024
+ #connectStartedAt = null;
3025
+ #opened = false;
3026
+ #connectFailure;
3027
+ #connectDiagnostic() {
3028
+ 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())) };
3029
+ }
2892
3030
  #reconnectAttempt = 0;
2893
3031
  #heartbeatTimer;
2894
3032
  #missedHeartbeats = 0;
2895
3033
  #noticeTimer;
2896
3034
  #noticeProbeTimer;
2897
- #noticeLastInboundAt = 0;
2898
- #noticeUnresponsive = false;
3035
+ #health = new V1SyncHealth(0);
2899
3036
  #unsubscribeLifecycle;
2900
3037
  #inbound = Promise.resolve();
2901
3038
  #transportAvailable = true;
@@ -2935,6 +3072,7 @@ var V1SyncClient = class {
2935
3072
  stateChanged: () => this.#emit()
2936
3073
  });
2937
3074
  this.#collaborationState = new V1CollaborationState({
3075
+ clock: () => this.#clock(),
2938
3076
  request: options.collaboration,
2939
3077
  requestIds: options.requestIds,
2940
3078
  maxPendingRequests: options.maxPendingCollaborationRequests,
@@ -2967,10 +3105,13 @@ var V1SyncClient = class {
2967
3105
  }
2968
3106
  stop() {
2969
3107
  this.#started = false;
3108
+ this.#entryPending = false;
3109
+ this.#entryGeneration = null;
2970
3110
  this.#startupGeneration += 1;
2971
3111
  this.#unsubscribeLifecycle?.();
2972
3112
  this.#unsubscribeLifecycle = void 0;
2973
3113
  this.#clearReconnect();
3114
+ this.#clearDial();
2974
3115
  this.#clearHeartbeat();
2975
3116
  this.#clearNoticeProbe();
2976
3117
  this.#commandScheduler.stop("client_stopped");
@@ -2985,11 +3126,24 @@ var V1SyncClient = class {
2985
3126
  this.#liveTargets.resetLocalMedia();
2986
3127
  this.#emit();
2987
3128
  }
3129
+ /** Announces completed startup; transport recovery resends until acknowledged. */
3130
+ async confirmEntered() {
3131
+ this.#requireLive();
3132
+ this.#entryPending = true;
3133
+ if (!this.#entryNegotiated) return;
3134
+ this.#send({ type: "entered" });
3135
+ }
2988
3136
  getSnapshot() {
2989
3137
  const pendingCommands = this.#commandScheduler.pendingCommands;
2990
3138
  const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
2991
3139
  return {
2992
- connection: this.#phase.phase === "live" ? { ...this.#phase, noticeUnresponsive: this.#noticeUnresponsive } : { ...this.#phase },
3140
+ connection: {
3141
+ ...this.#phase,
3142
+ ...this.#connectStartedAt !== null && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectAttempt: this.#connectDiagnostic() } : {},
3143
+ ...this.#connectFailure && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectFailure: this.#connectFailure } : {},
3144
+ ...this.#phase.phase === "live" ? { noticeUnresponsive: this.#health.health === "recovering", health: this.#health.health } : {},
3145
+ ...this.#dialTimeoutCount ? { dialTimeoutCount: this.#dialTimeoutCount } : {}
3146
+ },
2993
3147
  participantId: this.#participantId,
2994
3148
  participantGeneration: this.#participantGeneration,
2995
3149
  control: this.#control,
@@ -3006,6 +3160,16 @@ var V1SyncClient = class {
3006
3160
  listener(this.getSnapshot());
3007
3161
  return () => this.#listeners.delete(listener);
3008
3162
  }
3163
+ onMediaPause(listener) {
3164
+ this.#mediaPauseListeners.add(listener);
3165
+ return () => this.#mediaPauseListeners.delete(listener);
3166
+ }
3167
+ #notifyMediaPause(event) {
3168
+ const source = event.name === "participant_microphone_stopped" ? "microphone" : event.name === "participant_camera_stopped" ? "camera" : event.name === "participant_screen_share_stopped" ? "screen" : null;
3169
+ if (source && "participant_id" in event.payload && event.payload.participant_id === this.#participantId) {
3170
+ for (const listener of this.#mediaPauseListeners) listener({ source, revision: event.revision });
3171
+ }
3172
+ }
3009
3173
  onDirectedRequest(listener) {
3010
3174
  this.#requestListeners.add(listener);
3011
3175
  return () => this.#requestListeners.delete(listener);
@@ -3117,25 +3281,66 @@ var V1SyncClient = class {
3117
3281
  return promise;
3118
3282
  }
3119
3283
  #connect() {
3120
- if (!this.#started || !this.#transportAvailable || this.#socket) return;
3284
+ if (!this.#started || !this.#transportAvailable || this.#socket || this.#phase.phase === "terminal") return;
3121
3285
  this.#phase = { phase: "connecting" };
3286
+ this.#connectStartedAt = this.#clock().now();
3287
+ this.#opened = false;
3288
+ this.#connectFailure = void 0;
3122
3289
  this.#emit();
3123
- const socket = this.#options.webSocket.connect(this.#options.url);
3290
+ this.#entryNegotiated = false;
3291
+ let socket;
3292
+ try {
3293
+ socket = this.#options.webSocket.connect(entryUrl(this.#options.url, this.#entryGeneration, this.#options.entryConfirmation === true));
3294
+ } catch {
3295
+ this.#connectFailure = this.#connectDiagnostic();
3296
+ this.#emit();
3297
+ throw new V1SyncError("Sync WebSocket could not be created", "sync_hello_timeout");
3298
+ }
3124
3299
  const connectionGeneration = ++this.#connectionGeneration;
3125
3300
  this.#socket = socket;
3301
+ this.#cancelDial = scheduleSyncDialDeadline(
3302
+ this.#clock(),
3303
+ () => {
3304
+ this.#cancelDial = void 0;
3305
+ this.#dialTimeoutCount += 1;
3306
+ this.#connectFailure = this.#connectDiagnostic();
3307
+ this.#recover("WebSocket dial timeout");
3308
+ },
3309
+ this.#options.dialTimeoutMs
3310
+ );
3126
3311
  socket.onopen = () => {
3312
+ if (socket !== this.#socket) return;
3313
+ this.#clearDial();
3314
+ this.#opened = true;
3315
+ this.#connectFailure = void 0;
3127
3316
  void this.#authenticate(socket);
3128
3317
  };
3129
3318
  socket.onmessage = (event) => {
3130
3319
  this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
3131
3320
  };
3132
- socket.onclose = () => {
3321
+ socket.onclose = (event) => {
3322
+ if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
3323
+ this.#connectFailure = { ...this.#connectDiagnostic(), closeCode: event.code, closeReason: safeSyncCloseReason(event.reason) };
3324
+ this.#emit();
3325
+ }
3133
3326
  this.#clock().setTimeout(() => {
3134
3327
  if (connectionGeneration !== this.#connectionGeneration) return;
3135
- this.#inbound = this.#inbound.then(() => this.#disconnected(socket));
3328
+ this.#inbound = this.#inbound.then(() => {
3329
+ if (socket === this.#socket && event.code === 1008 && (event.reason === "" || event.reason === "invalid token" || event.reason === "policy violation" || event.reason === "entry timeout")) {
3330
+ this.#phase = { phase: "terminal", terminalReason: event.reason === "entry timeout" ? "entry_timeout" : "access_rejected" };
3331
+ this.#emit();
3332
+ }
3333
+ this.#disconnected(socket);
3334
+ });
3136
3335
  }, 0);
3137
3336
  };
3138
- socket.onerror = () => socket.close(CLIENT_RESTART_CLOSE_CODE, "transport error");
3337
+ socket.onerror = () => {
3338
+ if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
3339
+ this.#connectFailure = this.#connectDiagnostic();
3340
+ this.#emit();
3341
+ }
3342
+ socket.close(CLIENT_RESTART_CLOSE_CODE, "transport error");
3343
+ };
3139
3344
  }
3140
3345
  async #authenticate(socket) {
3141
3346
  try {
@@ -3169,17 +3374,11 @@ var V1SyncClient = class {
3169
3374
  try {
3170
3375
  if (typeof data !== "string" || encoder3.encode(data).byteLength > SyncProtocolLimits.snapshotEncodedBytes) throw new V1ReplicaError("invalid inbound frame size");
3171
3376
  const frame = decodeV1ServerFrame(data);
3172
- if (this.#phase.phase === "live") {
3173
- this.#noticeLastInboundAt = this.#now();
3174
- this.#scheduleNoticeCheck();
3175
- if (this.#noticeUnresponsive) {
3176
- this.#noticeUnresponsive = false;
3177
- this.#emit();
3178
- }
3179
- }
3180
3377
  await this.#handleFrame(frame);
3181
- } catch {
3182
- this.#recover("invalid_frame");
3378
+ } catch (error) {
3379
+ if (this.#snapshotRecoveryRequested) this.#failDigestRecovery();
3380
+ else if (error instanceof V1StateDigestMismatchError) this.#recoverDigestMismatch();
3381
+ else this.#recover("invalid_frame");
3183
3382
  }
3184
3383
  }
3185
3384
  async #handleFrame(frame) {
@@ -3207,6 +3406,7 @@ var V1SyncClient = class {
3207
3406
  this.#control = await applyV1Event(this.#requireControl(), frame);
3208
3407
  this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
3209
3408
  this.#rememberControlEvent(frame);
3409
+ this.#notifyMediaPause(frame);
3210
3410
  }
3211
3411
  await this.#commandScheduler.settleTerminalLifecycle(frame, this.#participantId);
3212
3412
  await this.#commandScheduler.settleProven();
@@ -3269,15 +3469,25 @@ var V1SyncClient = class {
3269
3469
  return;
3270
3470
  case "error":
3271
3471
  throw new V1ReplicaError(frame.code);
3272
- case "pong":
3472
+ case "entered_ack":
3473
+ this.#requireLive();
3474
+ this.#entryPending = false;
3475
+ return;
3476
+ case "pong": {
3273
3477
  this.#requireLive();
3274
3478
  this.#missedHeartbeats = 0;
3479
+ const previous = this.#health.health;
3480
+ this.#health.observePong(this.#now());
3481
+ if (previous !== this.#health.health) this.#emit();
3275
3482
  return;
3483
+ }
3276
3484
  }
3277
3485
  }
3278
3486
  async #welcome(frame) {
3279
3487
  if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
3280
3488
  this.#collaborationState.acceptWelcome(frame);
3489
+ this.#entryNegotiated = frame.entry_confirmation === true;
3490
+ this.#entryGeneration = frame.entry_generation ?? this.#entryGeneration;
3281
3491
  this.#participantId = frame.participant_id;
3282
3492
  this.#participantGeneration = frame.participant_generation;
3283
3493
  this.#updateLocalMediaStates();
@@ -3288,15 +3498,20 @@ var V1SyncClient = class {
3288
3498
  this.#emit();
3289
3499
  return;
3290
3500
  }
3501
+ if (this.#snapshotRecoveryRequested && frame.mode !== "snapshot") throw new V1ReplicaError("digest recovery requires a full snapshot");
3291
3502
  if (frame.mode === "snapshot") {
3292
- this.#control = await restoreV1Snapshot(frame.snapshot);
3503
+ const control = await restoreV1Snapshot(frame.snapshot);
3504
+ if (!sameHead(control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
3505
+ this.#control = control;
3506
+ this.#snapshotRevision = control.revision;
3507
+ this.#snapshotRecoveryRequested = false;
3293
3508
  this.#controlHeads.clear();
3294
3509
  this.#controlEvents.clear();
3295
3510
  this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
3511
+ await this.#commandScheduler.settleProven();
3296
3512
  }
3297
3513
  if (frame.mode === "replay" && (!this.#control || this.#control.revision >= frame.head.revision)) throw new V1ReplicaError("invalid replay welcome");
3298
3514
  if (frame.mode === "up_to_date" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("up-to-date head mismatch");
3299
- if (frame.mode === "snapshot" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
3300
3515
  this.#recovery = { id: frame.recovery_id, head: frame.head, replayEvents: 0, replayBytes: 0, controlComplete: false };
3301
3516
  if (frame.mode === "snapshot") this.#ackRecovery();
3302
3517
  this.#emit();
@@ -3311,6 +3526,7 @@ var V1SyncClient = class {
3311
3526
  this.#control = await applyV1Event(this.#requireControl(), event);
3312
3527
  this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
3313
3528
  this.#rememberControlEvent(event);
3529
+ this.#notifyMediaPause(event);
3314
3530
  }
3315
3531
  this.#ackRecovery();
3316
3532
  this.#emit();
@@ -3390,8 +3606,11 @@ var V1SyncClient = class {
3390
3606
  if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
3391
3607
  this.#recovery = null;
3392
3608
  this.#phase = { phase: "live" };
3609
+ this.#connectStartedAt = null;
3610
+ this.#connectFailure = void 0;
3393
3611
  this.#reconnectAttempt = 0;
3394
3612
  this.#missedHeartbeats = 0;
3613
+ if (this.#entryPending && this.#entryNegotiated) this.#send({ type: "entered" });
3395
3614
  this.#commandScheduler.enterLive();
3396
3615
  this.#liveTargets.enterLive();
3397
3616
  this.#startHeartbeat();
@@ -3404,7 +3623,7 @@ var V1SyncClient = class {
3404
3623
  this.#send({ type: "recovery_ack", recovery_id: recovery.id, revision: control.revision, state_digest: control.stateDigest });
3405
3624
  }
3406
3625
  #ackHeadIsProven(ack) {
3407
- return this.#controlHeads.get(ack.revision) === ack.state_digest;
3626
+ return this.#snapshotRevision !== null && ack.revision < this.#snapshotRevision || this.#controlHeads.get(ack.revision) === ack.state_digest;
3408
3627
  }
3409
3628
  #rememberControlHead(revision, digest) {
3410
3629
  this.#controlHeads.set(revision, digest);
@@ -3423,6 +3642,7 @@ var V1SyncClient = class {
3423
3642
  #disconnected(socket) {
3424
3643
  if (socket !== this.#socket) return;
3425
3644
  this.#socket = null;
3645
+ this.#clearDial();
3426
3646
  this.#recovery = null;
3427
3647
  this.#clearHeartbeat();
3428
3648
  this.#clearNoticeProbe();
@@ -3452,6 +3672,29 @@ var V1SyncClient = class {
3452
3672
  socket.close(CLIENT_RESTART_CLOSE_CODE, reason);
3453
3673
  this.#disconnected(socket);
3454
3674
  }
3675
+ #recoverDigestMismatch() {
3676
+ const now = this.#clock().now();
3677
+ if (this.#lastDigestRecoveryAt !== null && now - this.#lastDigestRecoveryAt < DIGEST_RECOVERY_COOLDOWN_MS) {
3678
+ this.#failDigestRecovery();
3679
+ return;
3680
+ }
3681
+ this.#lastDigestRecoveryAt = now;
3682
+ this.#snapshotRecoveryRequested = true;
3683
+ this.#control = null;
3684
+ this.#snapshotRevision = null;
3685
+ this.#controlHeads.clear();
3686
+ this.#controlEvents.clear();
3687
+ this.#recover("state_digest_mismatch");
3688
+ }
3689
+ #failDigestRecovery() {
3690
+ this.#phase = { phase: "terminal", terminalReason: "state_digest_recovery_failed" };
3691
+ this.#clearReconnect();
3692
+ this.#commandScheduler.stop("state_digest_recovery_failed");
3693
+ const socket = this.#socket;
3694
+ socket?.close(CLIENT_RESTART_CLOSE_CODE, "state_digest_recovery_failed");
3695
+ if (socket) this.#disconnected(socket);
3696
+ this.#emit();
3697
+ }
3455
3698
  #handleLifecycle(event) {
3456
3699
  if (event === "online" || event === "offline") this.#online = event === "online";
3457
3700
  else this.#active = event === "active";
@@ -3500,6 +3743,10 @@ var V1SyncClient = class {
3500
3743
  #now() {
3501
3744
  return this.#clock().now();
3502
3745
  }
3746
+ #clearDial() {
3747
+ this.#cancelDial?.();
3748
+ this.#cancelDial = void 0;
3749
+ }
3503
3750
  #clearReconnect() {
3504
3751
  if (this.#reconnectTimer === void 0) return;
3505
3752
  this.#clock().clearTimeout(this.#reconnectTimer);
@@ -3557,33 +3804,29 @@ var V1SyncClient = class {
3557
3804
  }
3558
3805
  #startNoticeProbe() {
3559
3806
  this.#clearNoticeProbe();
3560
- this.#noticeLastInboundAt = this.#now();
3561
- this.#scheduleNoticeCheck();
3807
+ this.#send({ type: "ping" });
3808
+ this.#health = new V1SyncHealth(this.#now());
3562
3809
  const tick = () => {
3563
3810
  this.#noticeProbeTimer = void 0;
3564
3811
  if (this.#phase.phase !== "live") return;
3812
+ const previous = this.#health.health;
3813
+ this.#health.check(this.#now());
3814
+ if (previous !== this.#health.health) this.#emit();
3565
3815
  this.#send({ type: "ping" });
3566
- this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
3816
+ this.#noticeProbeTimer = this.#clock().setTimeout(tick, SYNC_HEALTH_BUDGET.probeIntervalMs);
3567
3817
  };
3568
- this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
3569
- }
3570
- #scheduleNoticeCheck() {
3571
- if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
3572
- const remaining = Math.max(0, NOTICE_SILENCE_MS - (this.#now() - this.#noticeLastInboundAt));
3818
+ this.#noticeProbeTimer = this.#clock().setTimeout(tick, SYNC_HEALTH_BUDGET.probeIntervalMs);
3573
3819
  this.#noticeTimer = this.#clock().setTimeout(() => {
3574
3820
  this.#noticeTimer = void 0;
3575
- if (this.#phase.phase !== "live" || this.#noticeUnresponsive) return;
3576
- if (this.#now() - this.#noticeLastInboundAt < NOTICE_SILENCE_MS) return this.#scheduleNoticeCheck();
3577
- this.#noticeUnresponsive = true;
3821
+ this.#health.check(this.#now());
3578
3822
  this.#emit();
3579
- }, remaining);
3823
+ }, SYNC_HEALTH_BUDGET.degradedAfterMs);
3580
3824
  }
3581
3825
  #clearNoticeProbe() {
3582
3826
  if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
3583
3827
  if (this.#noticeProbeTimer !== void 0) this.#clock().clearTimeout(this.#noticeProbeTimer);
3584
3828
  this.#noticeTimer = void 0;
3585
3829
  this.#noticeProbeTimer = void 0;
3586
- this.#noticeUnresponsive = false;
3587
3830
  }
3588
3831
  #emit() {
3589
3832
  const snapshot = this.getSnapshot();
@@ -3626,6 +3869,12 @@ function copyPublication(publication) {
3626
3869
  function validMediaPublication(publication) {
3627
3870
  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);
3628
3871
  }
3872
+ function entryUrl(value, generation, confirmation) {
3873
+ const url = new URL(value);
3874
+ url.searchParams.set("entry_generation", generation === null ? "new" : String(generation));
3875
+ if (confirmation) url.searchParams.set("entry_confirmation", "1");
3876
+ return url.toString();
3877
+ }
3629
3878
  function assertV1Url(value) {
3630
3879
  const url = new URL(value);
3631
3880
  if (url.pathname !== "/v1/sync") throw new V1SyncError("SyncEngine v1 websocket URL must use /v1/sync", "invalid_url");
@@ -3773,6 +4022,7 @@ function createV1SyncClient(options) {
3773
4022
  }
3774
4023
 
3775
4024
  export {
4025
+ formatSyncConnectDiagnostic,
3776
4026
  UuidSchema,
3777
4027
  ReactionValues,
3778
4028
  ReactionSchema,
@@ -3786,6 +4036,7 @@ export {
3786
4036
  createBrowserSyncLifecycle,
3787
4037
  createReactNativeWebSocketFactory,
3788
4038
  createReactNativeSyncLifecycle,
4039
+ scheduleSyncDialDeadline,
3789
4040
  encodeV1ClientFrame,
3790
4041
  decodeV1ServerFrame,
3791
4042
  decodeV1ClientFrame,