@q9labsai/chalk-client 5.2.2 → 5.2.3

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.
@@ -20,7 +20,7 @@ import {
20
20
  formatSyncConnectDiagnostic,
21
21
  scheduleSyncDialDeadline,
22
22
  syncConnectFailureCode
23
- } from "./chunk-AIJ5Z6HA.js";
23
+ } from "./chunk-IMMT22US.js";
24
24
 
25
25
  // src/access/manager.ts
26
26
  import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
@@ -333,7 +333,8 @@ var WhiteboardV1ProtocolMetadata = {
333
333
  extensions: {
334
334
  presentation: {
335
335
  name: "presentation_v1",
336
- exactFields: ["name"]
336
+ exactFields: ["name"],
337
+ presenterExactFields: ["name", "presenter"]
337
338
  },
338
339
  view: {
339
340
  name: "view_v1",
@@ -655,7 +656,7 @@ function isExtensions(value) {
655
656
  const drawPolicyLast = value.length > 0 && isExtension(value[value.length - 1], drawPolicy);
656
657
  const base = drawPolicyLast ? value.slice(0, -1) : value;
657
658
  if (base.length === 0) return drawPolicyLast;
658
- return (base.length === 1 || base.length === 2) && isExtension(base[0], presentation) && (base.length === 1 || isExtension(base[1], view) || hasExactKeys(base[1], view.ownerExactFields) && base[1].name === view.name && base[1].origins === true && base[1].owner === true || hasExactKeys(base[1], view.extendedExactFields) && base[1].name === view.name && base[1].origins === true);
659
+ return (base.length === 1 || base.length === 2) && (isExtension(base[0], presentation) || hasExactKeys(base[0], presentation.presenterExactFields) && base[0].name === presentation.name && base[0].presenter === true) && (base.length === 1 || isExtension(base[1], view) || hasExactKeys(base[1], view.ownerExactFields) && base[1].name === view.name && base[1].origins === true && base[1].owner === true || hasExactKeys(base[1], view.extendedExactFields) && base[1].name === view.name && base[1].origins === true);
659
660
  }
660
661
  function isDrawPolicy(value) {
661
662
  return typeof value === "string" && WhiteboardV1ProtocolMetadata.drawPolicies.some((policy) => policy === value);
@@ -664,6 +665,10 @@ function isDrawGrant(value) {
664
665
  return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.drawAccess.grantExactFields) && isUuid(value.participant_id) && typeof value.can_draw === "boolean";
665
666
  }
666
667
  function isWelcome(value) {
668
+ if ("presenter_id" in value) {
669
+ const { presenter_id, ...welcome } = value;
670
+ return (presenter_id === null || isUuid(presenter_id)) && isWelcome(welcome);
671
+ }
667
672
  if ("view_owner_id" in value) {
668
673
  const { view_owner_id, ...welcome } = value;
669
674
  return isUuid(view_owner_id) && isWelcome(welcome);
@@ -671,6 +676,13 @@ function isWelcome(value) {
671
676
  const exact = hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.welcome.exactFields) || hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.welcome.extendedExactFields) && typeof value.presenting === "boolean";
672
677
  return exact && value.protocol === "whiteboard-v1" && isUuid(value.participant_id) && Number.isSafeInteger(value.participant_generation) && Number(value.participant_generation) > 0 && isCapabilities(value.capabilities) && isCapabilities(value.participant_capabilities) && isUuid(value.scene_id) && isUnsignedDecimal(value.revision) && typeof value.can_draw === "boolean";
673
678
  }
679
+ function isPresentationUpdated(value) {
680
+ if ("presenter_id" in value) {
681
+ const { presenter_id, ...frame } = value;
682
+ return (presenter_id === null || isUuid(presenter_id)) && isPresentationUpdated(frame);
683
+ }
684
+ return hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.presentationUpdated.exactFields) && isUuid(value.scene_id) && isUnsignedDecimal(value.revision) && typeof value.presenting === "boolean";
685
+ }
674
686
  function isServerFrame(value) {
675
687
  if (!isRecord2(value)) return false;
676
688
  switch (value.type) {
@@ -693,7 +705,7 @@ function isServerFrame(value) {
693
705
  case "draw_access":
694
706
  return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.drawAccess.exactFields) && isDrawPolicy(value.policy) && typeof value.can_draw === "boolean" && isCapabilities(value.capabilities) && Array.isArray(value.grants) && value.grants.length <= WhiteboardV1ProtocolLimits.drawAccessMaxGrants && value.grants.every(isDrawGrant);
695
707
  case "presentation_updated":
696
- return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.presentationUpdated.exactFields) && isUuid(value.scene_id) && isUnsignedDecimal(value.revision) && typeof value.presenting === "boolean";
708
+ return isPresentationUpdated(value);
697
709
  case "reset_required":
698
710
  return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.resetRequired.exactFields) && isUuid(value.scene_id) && typeof value.reason === "string" && WhiteboardV1ProtocolMetadata.resetReasons.some((reason) => reason === value.reason);
699
711
  case "operation_error":
@@ -1066,7 +1078,8 @@ var ChalkWhiteboardV1Client = class {
1066
1078
  #presentationSupported = false;
1067
1079
  #viewsSupported = false;
1068
1080
  #presenting = false;
1069
- #hello = "owner-view";
1081
+ #presenterId = null;
1082
+ #hello = "presenter-view";
1070
1083
  #viewSequence = 0;
1071
1084
  #viewOwnerId = null;
1072
1085
  #viewVersions = new ChalkWhiteboardViewVersions();
@@ -1119,7 +1132,7 @@ var ChalkWhiteboardV1Client = class {
1119
1132
  if (this.#started && this.#startPromise) return this.#startPromise;
1120
1133
  this.#started = true;
1121
1134
  globalThis.document?.addEventListener("visibilitychange", this.#visibilityChanged);
1122
- this.#hello = "owner-view";
1135
+ this.#hello = "presenter-view";
1123
1136
  this.#rejectedHelloRetries = 0;
1124
1137
  this.#viewsSupported = false;
1125
1138
  this.#phase = "connecting";
@@ -1173,6 +1186,7 @@ var ChalkWhiteboardV1Client = class {
1173
1186
  this.#drawPolicy = null;
1174
1187
  this.#drawPermissions = [];
1175
1188
  this.#presenting = false;
1189
+ this.#presenterId = null;
1176
1190
  this.#publishSummary("unsubscribed", null);
1177
1191
  }
1178
1192
  subscribe(listener) {
@@ -1489,6 +1503,7 @@ var ChalkWhiteboardV1Client = class {
1489
1503
  this.#sceneId = frame.scene_id;
1490
1504
  this.#revision = frame.revision;
1491
1505
  this.#presenting = frame.presenting;
1506
+ this.#presenterId = frame.presenter_id ?? null;
1492
1507
  this.#publishSummary("ready", null);
1493
1508
  return;
1494
1509
  case "reset_required":
@@ -1561,7 +1576,13 @@ var ChalkWhiteboardV1Client = class {
1561
1576
  this.#drawPolicy = null;
1562
1577
  this.#drawPermissions = [];
1563
1578
  this.#presentationSupported = "presenting" in frame;
1564
- this.#presenting = "presenting" in frame ? frame.presenting : false;
1579
+ if ("presenting" in frame) {
1580
+ this.#presenting = frame.presenting;
1581
+ this.#presenterId = frame.presenter_id ?? null;
1582
+ } else {
1583
+ this.#presenting = false;
1584
+ this.#presenterId = null;
1585
+ }
1565
1586
  const refused = !drawPolicyNegotiated(this.#hello) && this.#dropPolicyChanges();
1566
1587
  this.#publishSummary("loading", refused ? policyUnavailable() : null);
1567
1588
  this.#clearHeartbeat();
@@ -1865,7 +1886,7 @@ var ChalkWhiteboardV1Client = class {
1865
1886
  if (step === "fail") return false;
1866
1887
  if (step === "step-down") return this.#stepDownHello();
1867
1888
  if (step === "retry-hello") return this.#retryHello();
1868
- if (phase === "live") this.#hello = "owner-view";
1889
+ if (phase === "live") this.#hello = "presenter-view";
1869
1890
  return true;
1870
1891
  }
1871
1892
  #stepDownHello() {
@@ -1989,6 +2010,7 @@ var ChalkWhiteboardV1Client = class {
1989
2010
  drawPolicy: this.#drawPolicy,
1990
2011
  drawPermissions: this.#drawPermissions,
1991
2012
  presenting: this.#presenting,
2013
+ presenterId: this.#presenterId,
1992
2014
  error: this.#summaryError
1993
2015
  };
1994
2016
  }
@@ -2193,19 +2215,20 @@ function operationName(frame) {
2193
2215
  function retryableOperationCode(code) {
2194
2216
  return code === "unavailable" || code === "overloaded" || code === "rate_limited" || code === "storage_unavailable";
2195
2217
  }
2196
- var HELLO_LEVELS = ["owner-view", "draw-policy", "versioned-view", "view", "presentation", "legacy"];
2218
+ var HELLO_LEVELS = ["presenter-view", "owner-view", "draw-policy", "versioned-view", "view", "presentation", "legacy"];
2197
2219
  function viewsNegotiated(level) {
2198
- return level === "owner-view" || level === "draw-policy" || level === "versioned-view" || level === "view";
2220
+ return level === "presenter-view" || level === "owner-view" || level === "draw-policy" || level === "versioned-view" || level === "view";
2199
2221
  }
2200
2222
  function viewOriginsNegotiated(level) {
2201
- return level === "owner-view" || level === "draw-policy" || level === "versioned-view";
2223
+ return level === "presenter-view" || level === "owner-view" || level === "draw-policy" || level === "versioned-view";
2202
2224
  }
2203
2225
  function drawPolicyNegotiated(level) {
2204
- return level === "owner-view" || level === "draw-policy";
2226
+ return level === "presenter-view" || level === "owner-view" || level === "draw-policy";
2205
2227
  }
2206
2228
  function helloFrame(level, token, cursor) {
2207
2229
  const hello = { type: "hello", protocol: "whiteboard-v1", token, cursor };
2208
2230
  if (level === "legacy") return hello;
2231
+ if (level === "presenter-view") return { ...hello, extensions: [{ name: "presentation_v1", presenter: true }, { name: "view_v1", origins: true, owner: true }, { name: "draw_policy_v1" }] };
2209
2232
  if (level === "presentation") return { ...hello, extensions: [{ name: "presentation_v1" }] };
2210
2233
  if (level === "view") return { ...hello, extensions: [{ name: "presentation_v1" }, { name: "view_v1" }] };
2211
2234
  if (level === "versioned-view") return { ...hello, extensions: [{ name: "presentation_v1" }, { name: "view_v1", origins: true }] };
@@ -2518,6 +2541,22 @@ var ConnectionDiagnostics = class {
2518
2541
  } catch {
2519
2542
  }
2520
2543
  }
2544
+ recordRecoveryExhausted(input) {
2545
+ const { state, epoch, code, attempts, transport } = input;
2546
+ this.record({
2547
+ event: "recovery_exhausted",
2548
+ state,
2549
+ epoch,
2550
+ code,
2551
+ recovery: {
2552
+ reference: `connection.${code}`,
2553
+ attempts,
2554
+ transport,
2555
+ lastHeartbeatReplyAgoMs: transport?.lastHeartbeatReplyAt == null ? null : Math.max(0, this.#now() - transport.lastHeartbeatReplyAt),
2556
+ visibilityState: typeof document === "undefined" ? null : document.visibilityState
2557
+ }
2558
+ });
2559
+ }
2521
2560
  snapshot() {
2522
2561
  return Object.freeze([...this.#events]);
2523
2562
  }
@@ -3049,7 +3088,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3049
3088
  const syncAttempt = { client: null, expiresAt: null, started: true };
3050
3089
  while (model.epoch === epoch && active(model)) {
3051
3090
  const plan = yield* recoveryPlan(model.recoveryQueued ?? kind);
3052
- const outcome = yield* attemptRecovery(plan, mediaAttempt, syncAttempt).pipe(
3091
+ let attempts = 0;
3092
+ const outcome = yield* attemptRecovery(plan, mediaAttempt, syncAttempt, () => attempts++).pipe(
3053
3093
  Effect2.match({
3054
3094
  onSuccess: (attempt) => ({ attempt, failure: null }),
3055
3095
  onFailure: (failure2) => ({ attempt: null, failure: failure2 })
@@ -3067,9 +3107,18 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3067
3107
  yield* enqueue(
3068
3108
  Effect2.gen(function* () {
3069
3109
  if (model.epoch !== epoch || !active(model)) return;
3110
+ if (plan.kind === "sync" && model.sync?.getSnapshot().connection.phase === "live") {
3111
+ model.recoveryQueued = "media";
3112
+ return;
3113
+ }
3070
3114
  const code = plan.kind === "media" ? "media_recovery_exhausted" : "sync_recovery_exhausted";
3115
+ const transport = model.sync?.getSnapshot().connection.recoveryDiagnostic ?? null;
3116
+ diagnostics.recordRecoveryExhausted({ state: model.state, epoch, code, attempts, transport });
3117
+ if (plan.kind === "sync") {
3118
+ yield* failForSnapshot(lifecycleFailure(code, true, "Connection lost. Rejoin to continue."));
3119
+ return;
3120
+ }
3071
3121
  model.failure = toFailure(lifecycleFailure(code, true, "Connection interrupted. Reconnecting in the background."));
3072
- diagnostics.record({ event: "recovery_exhausted", state: model.state, epoch, code });
3073
3122
  yield* publish();
3074
3123
  })
3075
3124
  );
@@ -3090,14 +3139,14 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3090
3139
  delays: options.recovery?.backoffMs?.length ? options.recovery.backoffMs : [100, 250, 500]
3091
3140
  }))
3092
3141
  );
3093
- const attemptRecovery = (plan, mediaAttempt2, syncAttempt) => Effect2.gen(function* () {
3142
+ const attemptRecovery = (plan, mediaAttempt2, syncAttempt, onAttempt) => Effect2.gen(function* () {
3094
3143
  for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
3095
3144
  const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
3096
3145
  if (remaining <= 0) return null;
3146
+ onAttempt();
3097
3147
  diagnostics.record({ event: "recovery_attempt", state: model.state, epoch: model.epoch, attempt });
3098
3148
  const failure2 = yield* recoveryOperation(mediaAttempt2, syncAttempt).pipe(
3099
- Effect2.timeout(remaining),
3100
- Effect2.catchTag("TimeoutError", () => Effect2.fail(lifecycleFailure("sync_start_failed", true, "Connection recovery is still waiting for the transport"))),
3149
+ (effect) => withinForegroundBudget(effect, remaining, lifecycleFailure("sync_start_failed", true, "Connection recovery is still waiting for the transport")),
3101
3150
  Effect2.match({ onSuccess: () => null, onFailure: (cause) => cause })
3102
3151
  );
3103
3152
  if (failure2 === null) return attempt;
@@ -6237,6 +6286,7 @@ var WhiteboardControllerRuntime = class {
6237
6286
  drawPolicy: summary.drawPolicy,
6238
6287
  drawPermissions: Object.freeze(summary.drawPermissions.map((permission) => Object.freeze({ ...permission }))),
6239
6288
  presenting: summary.presenting,
6289
+ presenterId: summary.presenterId ?? null,
6240
6290
  error: summary.error ? Object.freeze({ ...summary.error }) : null
6241
6291
  });
6242
6292
  this.#publish();
@@ -6255,7 +6305,7 @@ var WhiteboardControllerRuntime = class {
6255
6305
  #publish() {
6256
6306
  const slice = Object.freeze({
6257
6307
  open: this.#summary.status !== "unsubscribed",
6258
- engine: Object.freeze({ status: this.#summary.status, sceneId: this.#summary.sceneId, revision: this.#summary.revision, presenting: this.#summary.presenting, error: this.#summary.error }),
6308
+ engine: Object.freeze({ status: this.#summary.status, sceneId: this.#summary.sceneId, revision: this.#summary.revision, presenting: this.#summary.presenting, presenterId: this.#summary.presenterId ?? null, error: this.#summary.error }),
6259
6309
  access: Object.freeze({ canDraw: this.#summary.canDraw, policy: this.#summary.drawPolicy, permissions: this.#summary.drawPermissions })
6260
6310
  });
6261
6311
  const current = this.#store.getSnapshot().whiteboard;
@@ -6264,7 +6314,7 @@ var WhiteboardControllerRuntime = class {
6264
6314
  }
6265
6315
  };
6266
6316
  function sameWhiteboard(left, right) {
6267
- return left.open === right.open && left.engine.status === right.engine.status && left.engine.sceneId === right.engine.sceneId && left.engine.revision === right.engine.revision && left.engine.presenting === right.engine.presenting && sameError2(left.engine.error, right.engine.error) && sameAccess(left.access, right.access);
6317
+ return left.open === right.open && left.engine.status === right.engine.status && left.engine.sceneId === right.engine.sceneId && left.engine.revision === right.engine.revision && left.engine.presenting === right.engine.presenting && left.engine.presenterId === right.engine.presenterId && sameError2(left.engine.error, right.engine.error) && sameAccess(left.access, right.access);
6268
6318
  }
6269
6319
  function sameAccess(left, right) {
6270
6320
  return left.canDraw === right.canDraw && left.policy === right.policy && left.permissions.length === right.permissions.length && left.permissions.every((permission, index) => permission.participantId === right.permissions[index]?.participantId && permission.canDraw === right.permissions[index]?.canDraw);
@@ -6359,7 +6409,9 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
6359
6409
  diagnostics: {
6360
6410
  onEvent: (event) => {
6361
6411
  platform.onConnectionDiagnostic?.(event);
6362
- options.logger?.debug?.("SpaceClient connection event", { event: event.event, state: event.state, epoch: event.epoch, ...event.code ? { code: event.code } : {}, ...event.sync ? { sync: event.sync } : {} });
6412
+ const attributes = { event: event.event, state: event.state, epoch: event.epoch, ...event.code ? { code: event.code } : {}, ...event.sync ? { sync: event.sync } : {}, ...event.recovery ? { recovery: event.recovery } : {} };
6413
+ if (event.event === "recovery_exhausted") options.logger?.warn?.("SpaceClient connection event", attributes);
6414
+ else options.logger?.debug?.("SpaceClient connection event", attributes);
6363
6415
  }
6364
6416
  }
6365
6417
  });
@@ -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, reason: event.reason }));
123
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason, ...typeof event.wasClean === "boolean" ? { wasClean: event.wasClean } : {} }));
124
124
  this.#socket.addEventListener("error", () => this.onerror?.());
125
125
  }
126
126
  send(data) {
@@ -178,7 +178,7 @@ var ReactNativeSyncSocket = class {
178
178
  this.#socket = socket;
179
179
  this.#socket.onopen = () => this.onopen?.();
180
180
  this.#socket.onmessage = (event) => this.onmessage?.({ data: event.data });
181
- this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0 });
181
+ this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0, ...typeof event.wasClean === "boolean" ? { wasClean: event.wasClean } : {} });
182
182
  this.#socket.onerror = () => this.onerror?.();
183
183
  }
184
184
  send(data) {
@@ -198,7 +198,7 @@ var BrowserFallbackSyncSocket = class {
198
198
  this.#socket = socket;
199
199
  this.#socket.addEventListener("open", () => this.onopen?.());
200
200
  this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
201
- this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
201
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason, ...typeof event.wasClean === "boolean" ? { wasClean: event.wasClean } : {} }));
202
202
  this.#socket.addEventListener("error", () => this.onerror?.());
203
203
  }
204
204
  send(data) {
@@ -339,7 +339,7 @@ var SyncProtocolMetadata = {
339
339
  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"],
340
340
  heartbeat: {
341
341
  intervalMs: 2e4,
342
- missedDeadlinesBeforeClose: 2
342
+ missedDeadlinesBeforeClose: 5
343
343
  }
344
344
  },
345
345
  {
@@ -3028,6 +3028,9 @@ var V1SyncClient = class {
3028
3028
  #dialTimeoutCount = 0;
3029
3029
  #connectStartedAt = null;
3030
3030
  #connectAttemptCount = 0;
3031
+ #socketCloses = [];
3032
+ #omittedSocketCloses = 0;
3033
+ #lastHeartbeatReplyAt = null;
3031
3034
  #opened = false;
3032
3035
  #connectFailure;
3033
3036
  #connectDiagnostic() {
@@ -3095,6 +3098,9 @@ var V1SyncClient = class {
3095
3098
  if (this.#started) return;
3096
3099
  this.#started = true;
3097
3100
  this.#connectAttemptCount = 0;
3101
+ this.#socketCloses = [];
3102
+ this.#omittedSocketCloses = 0;
3103
+ this.#lastHeartbeatReplyAt = null;
3098
3104
  this.#connectFailure = void 0;
3099
3105
  const startupGeneration = ++this.#startupGeneration;
3100
3106
  let stored;
@@ -3150,6 +3156,7 @@ var V1SyncClient = class {
3150
3156
  return {
3151
3157
  connection: {
3152
3158
  ...this.#phase,
3159
+ recoveryDiagnostic: { socketAttempts: this.#connectAttemptCount, socketCloses: this.#socketCloses.slice(), omittedSocketCloses: this.#omittedSocketCloses, lastHeartbeatReplyAt: this.#lastHeartbeatReplyAt },
3153
3160
  ...this.#connectStartedAt !== null && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectAttempt: this.#connectDiagnostic() } : {},
3154
3161
  ...this.#connectFailure && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectFailure: this.#connectFailure } : {},
3155
3162
  ...this.#phase.phase === "live" ? { noticeUnresponsive: this.#health.health === "recovering", health: this.#health.health } : {},
@@ -3331,6 +3338,13 @@ var V1SyncClient = class {
3331
3338
  this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
3332
3339
  };
3333
3340
  socket.onclose = (event) => {
3341
+ if (this.#started) {
3342
+ this.#socketCloses.push({ code: event.code, wasClean: event.wasClean ?? null });
3343
+ if (this.#socketCloses.length > 50) {
3344
+ this.#socketCloses.shift();
3345
+ this.#omittedSocketCloses += 1;
3346
+ }
3347
+ }
3334
3348
  if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
3335
3349
  this.#connectFailure = { ...this.#connectDiagnostic(), closeCode: event.code, closeReason: safeSyncCloseReason(event.reason) };
3336
3350
  this.#emit();
@@ -3501,6 +3515,7 @@ var V1SyncClient = class {
3501
3515
  case "pong": {
3502
3516
  this.#requireLive();
3503
3517
  this.#missedHeartbeats = 0;
3518
+ this.#lastHeartbeatReplyAt = this.#now();
3504
3519
  const previous = this.#health.health;
3505
3520
  this.#health.observePong(this.#now());
3506
3521
  if (previous !== this.#health.health) this.#emit();
@@ -32,6 +32,13 @@ export type ConnectionDiagnostic = {
32
32
  readonly outcome?: ConnectionJoinTraceOutcome;
33
33
  readonly durationMs?: number;
34
34
  readonly sync?: import("../sync/connect-diagnostics").SyncConnectDiagnostic;
35
+ readonly recovery?: {
36
+ readonly reference: "connection.sync_recovery_exhausted" | "connection.media_recovery_exhausted";
37
+ readonly attempts: number;
38
+ readonly transport: import("../sync/connect-diagnostics").SyncRecoveryDiagnostic | null;
39
+ readonly lastHeartbeatReplyAgoMs: number | null;
40
+ readonly visibilityState: DocumentVisibilityState | null;
41
+ };
35
42
  };
36
43
  export declare class ConnectionDiagnostics {
37
44
  #private;
@@ -41,6 +48,13 @@ export declare class ConnectionDiagnostics {
41
48
  readonly onEvent?: (event: ConnectionDiagnostic) => void;
42
49
  });
43
50
  record(input: Omit<ConnectionDiagnostic, "timestamp">): void;
51
+ recordRecoveryExhausted(input: {
52
+ readonly state: ConnectionState;
53
+ readonly epoch: number;
54
+ readonly code: "sync_recovery_exhausted" | "media_recovery_exhausted";
55
+ readonly attempts: number;
56
+ readonly transport: import("../sync/connect-diagnostics").SyncRecoveryDiagnostic | null;
57
+ }): void;
44
58
  snapshot(): readonly ConnectionDiagnostic[];
45
59
  startSpan(input: {
46
60
  readonly step: ConnectionJoinTraceStep;
package/dist/effect.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  makeSpaceClientCoreLayer,
18
18
  makeSpaceClientCoreLayerFromServices,
19
19
  makeSpaceStoreLayer
20
- } from "./chunk-XPW3OCPO.js";
20
+ } from "./chunk-4ZUN55Z2.js";
21
21
  import "./chunk-DWKTGBHR.js";
22
22
  import {
23
23
  APIKeyListSchema,
@@ -829,7 +829,7 @@ import {
829
829
  WhiteboardStorageUnavailableErrorWireSchema,
830
830
  createChalkEffectClient
831
831
  } from "./chunk-GA3JDPFI.js";
832
- import "./chunk-AIJ5Z6HA.js";
832
+ import "./chunk-IMMT22US.js";
833
833
 
834
834
  // src/space-client/effect.ts
835
835
  import { Context, Effect, Exit, Layer, Scope } from "effect";
@@ -37,7 +37,7 @@ export declare const SyncProtocolMetadata: {
37
37
  readonly accepts: readonly ["entered", "command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"];
38
38
  readonly heartbeat: {
39
39
  readonly intervalMs: 20000;
40
- readonly missedDeadlinesBeforeClose: 2;
40
+ readonly missedDeadlinesBeforeClose: 5;
41
41
  };
42
42
  }, {
43
43
  readonly id: "terminal";
@@ -11,6 +11,7 @@ export declare const WhiteboardV1ProtocolMetadata: {
11
11
  readonly presentation: {
12
12
  readonly name: "presentation_v1";
13
13
  readonly exactFields: readonly ["name"];
14
+ readonly presenterExactFields: readonly ["name", "presenter"];
14
15
  };
15
16
  readonly view: {
16
17
  readonly name: "view_v1";
@@ -258,6 +259,7 @@ export type WhiteboardV1SharedAppState = {
258
259
  } | null;
259
260
  export type WhiteboardV1PresentationExtension = {
260
261
  readonly name: "presentation_v1";
262
+ readonly presenter?: true;
261
263
  };
262
264
  export type WhiteboardV1ViewExtension = {
263
265
  readonly name: "view_v1";
@@ -388,6 +390,7 @@ export type WhiteboardV1ServerFrame = {
388
390
  readonly revision: string;
389
391
  readonly can_draw: boolean;
390
392
  readonly presenting: boolean;
393
+ readonly presenter_id?: string | null;
391
394
  readonly view_owner_id?: string;
392
395
  } | {
393
396
  readonly type: "snapshot_page";
@@ -451,6 +454,7 @@ export type WhiteboardV1ServerFrame = {
451
454
  readonly scene_id: string;
452
455
  readonly revision: string;
453
456
  readonly presenting: boolean;
457
+ readonly presenter_id?: string | null;
454
458
  } | {
455
459
  readonly type: "reset_required";
456
460
  readonly scene_id: string;
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  createSpaceClient,
16
16
  decodeWhiteboardV1ServerFrame,
17
17
  encodeWhiteboardV1ClientFrame
18
- } from "./chunk-XPW3OCPO.js";
18
+ } from "./chunk-4ZUN55Z2.js";
19
19
  import {
20
20
  CloudflareSFUClient,
21
21
  CloudflareSFUError,
@@ -69,7 +69,7 @@ import {
69
69
  optimisticV1Control,
70
70
  reactionFromFrame,
71
71
  restoreV1Snapshot
72
- } from "./chunk-AIJ5Z6HA.js";
72
+ } from "./chunk-IMMT22US.js";
73
73
 
74
74
  // src/space-client/reload-rejoin.ts
75
75
  import { Schema } from "effect";
@@ -123,7 +123,7 @@ var FAILURE_DETAILS = {
123
123
  "access.unavailable": "Couldn\u2019t get access to the call. Try again.",
124
124
  "connection.sync_start_failed": "Couldn\u2019t reach the call service. Try again.",
125
125
  "connection.sync_hello_timeout": "The call service did not respond in time. Try again.",
126
- "connection.sync_recovery_exhausted": "The call connection could not recover. Try again.",
126
+ "connection.sync_recovery_exhausted": "You\u2019re disconnected from the call. Rejoin to continue.",
127
127
  "connection.media_start_failed": "Audio and video could not connect. Try again.",
128
128
  "connection.media_recovery_exhausted": "Audio and video could not reconnect. Try again.",
129
129
  "environment.unsupported": "This device cannot start the call. Use an up-to-date browser over HTTPS, or update the app."
@@ -203,6 +203,7 @@ export type WhiteboardSlice = {
203
203
  readonly sceneId: string | null;
204
204
  readonly revision: string | null;
205
205
  readonly presenting: boolean;
206
+ readonly presenterId?: string | null;
206
207
  readonly error: ChalkWhiteboardV1Failure | null;
207
208
  };
208
209
  /** Who can draw, as Sync last told this person. `canDraw` is false until the board subscription is live. */
@@ -1,3 +1,13 @@
1
+ /** Transport facts only. No URLs, close reasons, payloads, or identities. */
2
+ export type SyncRecoveryDiagnostic = {
3
+ readonly socketAttempts: number;
4
+ readonly socketCloses: readonly {
5
+ readonly code: number;
6
+ readonly wasClean: boolean | null;
7
+ }[];
8
+ readonly omittedSocketCloses: number;
9
+ readonly lastHeartbeatReplyAt: number | null;
10
+ };
1
11
  /** Browsers expose only upgrade/open and hello, never DNS/TCP/TLS progress. */
2
12
  export type SyncConnectDiagnostic = {
3
13
  readonly stage: "upgrade" | "hello";
@@ -22,7 +22,7 @@ import {
22
22
  encodeV1ClientFrame,
23
23
  optimisticV1Control,
24
24
  restoreV1Snapshot
25
- } from "../chunk-AIJ5Z6HA.js";
25
+ } from "../chunk-IMMT22US.js";
26
26
  export {
27
27
  AsyncStorageV1PendingTargetStore,
28
28
  InMemoryV1PendingTargetStore,
@@ -7,6 +7,7 @@ export type ReactNativeAsyncStorage = {
7
7
  export type ReactNativeWebSocketCloseEvent = {
8
8
  readonly code?: unknown;
9
9
  readonly reason?: unknown;
10
+ readonly wasClean?: unknown;
10
11
  };
11
12
  export type ReactNativeWebSocket = {
12
13
  onopen: ((event: unknown) => void) | null;
@@ -10,6 +10,7 @@ export type SyncSocket = {
10
10
  onclose: ((event: {
11
11
  readonly code: number;
12
12
  readonly reason?: string;
13
+ readonly wasClean?: boolean;
13
14
  }) => void) | null;
14
15
  onerror: (() => void) | null;
15
16
  send(data: string): void;
@@ -81,6 +81,7 @@ export type V1EpisodeSnapshot = {
81
81
  readonly dialTimeoutCount?: number;
82
82
  readonly connectAttempt?: import("./connect-diagnostics").SyncConnectDiagnostic;
83
83
  readonly connectFailure?: import("./connect-diagnostics").SyncConnectDiagnostic;
84
+ readonly recoveryDiagnostic?: import("./connect-diagnostics").SyncRecoveryDiagnostic;
84
85
  };
85
86
  readonly participantId: string | null;
86
87
  readonly participantGeneration: number | null;
@@ -179,6 +179,8 @@ export type ChalkWhiteboardSummary = {
179
179
  readonly drawPolicy: ChalkWhiteboardDrawPolicy | null;
180
180
  /** The host's per-person choices for people in the call. Empty unless this person is a host. */
181
181
  readonly drawPermissions: readonly ChalkWhiteboardDrawPermission[];
182
+ /** The person who opened the current presentation; absent on older servers. */
183
+ readonly presenterId?: string | null;
182
184
  readonly presenting: boolean;
183
185
  readonly error: ChalkWhiteboardV1Failure | null;
184
186
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q9labsai/chalk-client",
3
- "version": "5.2.2",
3
+ "version": "5.2.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",