@q9labsai/chalk-client 5.2.2 → 5.2.4

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.
@@ -2,11 +2,11 @@ import {
2
2
  CloudflareSFUClient,
3
3
  createCloudflareSFUHTTPTransport,
4
4
  describeMediaCaptureError
5
- } from "./chunk-DWKTGBHR.js";
5
+ } from "./chunk-3ASKGMD3.js";
6
6
  import {
7
7
  AccessGrantError,
8
8
  requireParsedAccessGrant
9
- } from "./chunk-GA3JDPFI.js";
9
+ } from "./chunk-AZONR3Q3.js";
10
10
  import {
11
11
  ChatAttachmentMimeTypeSchema,
12
12
  ChatReactionSetFrameSchema,
@@ -20,7 +20,7 @@ import {
20
20
  formatSyncConnectDiagnostic,
21
21
  scheduleSyncDialDeadline,
22
22
  syncConnectFailureCode
23
- } from "./chunk-AIJ5Z6HA.js";
23
+ } from "./chunk-5YPQNMJE.js";
24
24
 
25
25
  // src/access/manager.ts
26
26
  import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
@@ -59,7 +59,7 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
59
59
  automaticMediaReplacementListeners.add(listener);
60
60
  return () => automaticMediaReplacementListeners.delete(listener);
61
61
  }),
62
- requiresRefresh: millisecondsUntilRefresh.pipe(Effect.map((milliseconds) => milliseconds === null || milliseconds === 0)),
62
+ requiresRefresh: millisecondsUntilRefresh.pipe(Effect.map((milliseconds2) => milliseconds2 === null || milliseconds2 === 0)),
63
63
  millisecondsUntilRefresh,
64
64
  clear: SynchronizedRef.set(state, { access: null })
65
65
  };
@@ -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
  }
@@ -2134,7 +2156,7 @@ var ChalkWhiteboardV1Client = class {
2134
2156
  return this.#options.clock ?? {
2135
2157
  now: Date.now,
2136
2158
  monotonicNow: () => globalThis.performance.now(),
2137
- setTimeout: (callback, milliseconds) => globalThis.setTimeout(callback, milliseconds),
2159
+ setTimeout: (callback, milliseconds2) => globalThis.setTimeout(callback, milliseconds2),
2138
2160
  clearTimeout: (handle) => globalThis.clearTimeout(handle)
2139
2161
  };
2140
2162
  }
@@ -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 }] };
@@ -2265,6 +2288,124 @@ function createChalkWhiteboardV1Client(options) {
2265
2288
  });
2266
2289
  }
2267
2290
 
2291
+ // src/connection/report.ts
2292
+ var MAX_EVENTS = 64;
2293
+ var MAX_BYTES = 32768;
2294
+ var MAX_MS = 2147483647;
2295
+ var COOLDOWN_MS = 6e4;
2296
+ var participantCooldowns = /* @__PURE__ */ new Map();
2297
+ var milliseconds = (value) => Math.max(0, Math.min(MAX_MS, Math.round(Number.isFinite(value) ? value : 0)));
2298
+ var optionalMilliseconds = (value) => value == null ? void 0 : milliseconds(value);
2299
+ function reserveParticipantReport(access, now) {
2300
+ const key = `${access.subject.tenantId}:${access.subject.participantId}`;
2301
+ for (const [participant, until] of participantCooldowns) if (until <= now) participantCooldowns.delete(participant);
2302
+ if (participantCooldowns.has(key)) return false;
2303
+ if (participantCooldowns.size >= 128) participantCooldowns.delete(participantCooldowns.keys().next().value ?? "");
2304
+ participantCooldowns.set(key, now + COOLDOWN_MS);
2305
+ return true;
2306
+ }
2307
+ var ConnectionReporter = class {
2308
+ #events = [];
2309
+ #startedAt;
2310
+ #options;
2311
+ #pending = null;
2312
+ #unsubscribe;
2313
+ #controller = null;
2314
+ #retryOnReconnect = false;
2315
+ #disposed = false;
2316
+ constructor(options) {
2317
+ this.#options = options;
2318
+ this.#startedAt = options.now();
2319
+ }
2320
+ recordDiagnostic(diagnostic) {
2321
+ this.#record({
2322
+ event: diagnostic.event,
2323
+ elapsed_ms: this.#elapsed(),
2324
+ state: diagnostic.state,
2325
+ step: diagnostic.step,
2326
+ outcome: diagnostic.outcome,
2327
+ code: diagnostic.code,
2328
+ attempt: optionalMilliseconds(diagnostic.attempt),
2329
+ duration_ms: optionalMilliseconds(diagnostic.sync?.durationMs ?? diagnostic.durationMs),
2330
+ close_code: diagnostic.sync?.closeCode ?? void 0,
2331
+ heartbeat_gap_ms: optionalMilliseconds(diagnostic.recovery?.lastHeartbeatReplyAgoMs)
2332
+ });
2333
+ if (diagnostic.event === "recovery_exhausted") this.#report("recovery_exhausted");
2334
+ else if (diagnostic.event === "connection_failed") this.#report(diagnostic.state === "joining" ? "join_failed" : "connection_lost");
2335
+ }
2336
+ recordTransport(event) {
2337
+ this.#record({
2338
+ event: event.event,
2339
+ elapsed_ms: this.#elapsed(),
2340
+ ...event.phase ? { phase: event.phase } : {},
2341
+ ...event.attempt == null ? {} : { attempt: milliseconds(event.attempt) },
2342
+ ...event.duration_ms == null ? {} : { duration_ms: milliseconds(event.duration_ms) },
2343
+ ...event.heartbeat_gap_ms == null ? {} : { heartbeat_gap_ms: milliseconds(event.heartbeat_gap_ms) },
2344
+ ...event.close_code == null ? {} : { close_code: event.close_code },
2345
+ ...event.http_status == null ? {} : { http_status: event.http_status }
2346
+ });
2347
+ }
2348
+ dispose() {
2349
+ this.#disposed = true;
2350
+ this.#controller?.abort();
2351
+ this.#stopListening();
2352
+ this.#pending = null;
2353
+ this.#events.length = 0;
2354
+ }
2355
+ get traceparent() {
2356
+ return this.#options.traceparent;
2357
+ }
2358
+ #elapsed() {
2359
+ return milliseconds(this.#options.now() - this.#startedAt);
2360
+ }
2361
+ #record(event) {
2362
+ if (this.#disposed) return;
2363
+ this.#events.push(event);
2364
+ if (this.#events.length > MAX_EVENTS) this.#events.shift();
2365
+ }
2366
+ #report(trigger) {
2367
+ if (this.#disposed || this.#pending) return;
2368
+ const access = this.#options.access();
2369
+ if (!access) return;
2370
+ if (!reserveParticipantReport(access, this.#options.now())) return;
2371
+ const events = this.#events.slice();
2372
+ const report = { version: 1, trigger, events };
2373
+ while (events.length && new TextEncoder().encode(JSON.stringify(report)).byteLength > MAX_BYTES) events.shift();
2374
+ if (!events.length) return;
2375
+ this.#pending = { access, report, attempts: 0 };
2376
+ this.#unsubscribe = this.#options.subscribeOnline(() => {
2377
+ this.#retryOnReconnect = true;
2378
+ void this.#flush();
2379
+ });
2380
+ void this.#flush();
2381
+ }
2382
+ async #flush() {
2383
+ const pending = this.#pending;
2384
+ if (!pending || this.#controller || this.#disposed || !this.#options.online()) return;
2385
+ this.#retryOnReconnect = false;
2386
+ const controller = new AbortController();
2387
+ this.#controller = controller;
2388
+ pending.attempts += 1;
2389
+ const timer = globalThis.setTimeout(() => controller.abort(), 5e3);
2390
+ try {
2391
+ await this.#options.send(pending.access, pending.report, controller.signal, this.traceparent);
2392
+ this.#pending = null;
2393
+ } catch {
2394
+ if (pending.attempts >= 2) this.#pending = null;
2395
+ } finally {
2396
+ globalThis.clearTimeout(timer);
2397
+ this.#controller = null;
2398
+ if (!this.#pending) this.#stopListening();
2399
+ if (this.#retryOnReconnect) void this.#flush();
2400
+ }
2401
+ }
2402
+ #stopListening() {
2403
+ this.#unsubscribe?.();
2404
+ this.#unsubscribe = void 0;
2405
+ this.#retryOnReconnect = false;
2406
+ }
2407
+ };
2408
+
2268
2409
  // src/connection/types.ts
2269
2410
  var ConnectionError = class extends Error {
2270
2411
  code;
@@ -2336,9 +2477,34 @@ function createDefaultConnectionDependencies(options) {
2336
2477
  return {
2337
2478
  clock: {
2338
2479
  now: () => Date.now(),
2339
- setTimeout: (callback, milliseconds) => globalThis.setTimeout(callback, milliseconds),
2480
+ setTimeout: (callback, milliseconds2) => globalThis.setTimeout(callback, milliseconds2),
2340
2481
  clearTimeout: (handle) => globalThis.clearTimeout(handle)
2341
2482
  },
2483
+ createConnectionReporter: (access) => {
2484
+ const random = globalThis.crypto;
2485
+ if (!random?.randomUUID) return void 0;
2486
+ return new ConnectionReporter({
2487
+ traceparent: `00-${random.randomUUID().replaceAll("-", "")}-${random.randomUUID().replaceAll("-", "").slice(0, 16)}-01`,
2488
+ now: () => performance.now(),
2489
+ access,
2490
+ online: () => globalThis.navigator?.onLine !== false,
2491
+ subscribeOnline: (listener) => {
2492
+ globalThis.window?.addEventListener("online", listener);
2493
+ return () => globalThis.window?.removeEventListener("online", listener);
2494
+ },
2495
+ send: async (grant, report, signal, traceparent) => {
2496
+ const response = await fetch(`${options.apiBaseURL.replace(/\/$/, "")}/v1/tenants/${encodeURIComponent(grant.subject.tenantId)}/connection-reports`, {
2497
+ method: "POST",
2498
+ headers: { Authorization: `Bearer ${grant.media.token}`, "Content-Type": "application/json", traceparent },
2499
+ body: JSON.stringify(report),
2500
+ signal,
2501
+ keepalive: true,
2502
+ credentials: "omit"
2503
+ });
2504
+ await response.body?.cancel();
2505
+ }
2506
+ });
2507
+ },
2342
2508
  mediaDevices: createBrowserMediaDevices(),
2343
2509
  cancelPendingAdmission: async (access, signal) => {
2344
2510
  const { tenantId, spaceId, episodeId, participantId } = access.subject;
@@ -2403,6 +2569,8 @@ function createMediaClient(apiBaseURL, input) {
2403
2569
  transport: createCloudflareSFUHTTPTransport({
2404
2570
  apiBaseURL,
2405
2571
  credential: input.credential,
2572
+ onDiagnostic: input.onTransportDiagnostic,
2573
+ traceparent: input.traceparent,
2406
2574
  tenantId: subject.tenantId,
2407
2575
  spaceId: subject.spaceId,
2408
2576
  episodeId: subject.episodeId,
@@ -2444,6 +2612,8 @@ function bindConnectionMediaClient(client, restart) {
2444
2612
  function createSyncClient(syncURL, input) {
2445
2613
  return createV1SyncClient({
2446
2614
  url: syncURL,
2615
+ onDiagnostic: input.onTransportDiagnostic,
2616
+ traceparent: input.traceparent,
2447
2617
  token: input.token,
2448
2618
  mediaPlane: input.media,
2449
2619
  collaboration: { name: "collaboration_v2", chatCursor: { afterSequence: null, retainedFloorSequence: null } },
@@ -2475,7 +2645,7 @@ function whiteboardURL(syncURL) {
2475
2645
  }
2476
2646
 
2477
2647
  // src/foreground-deadline.ts
2478
- function scheduleForegroundDeadline(clock, milliseconds, expired) {
2648
+ function scheduleForegroundDeadline(clock, milliseconds2, expired) {
2479
2649
  let timer;
2480
2650
  let cancelled = false;
2481
2651
  const arm = () => {
@@ -2483,9 +2653,9 @@ function scheduleForegroundDeadline(clock, milliseconds, expired) {
2483
2653
  timer = clock.setTimeout(() => {
2484
2654
  if (cancelled) return;
2485
2655
  const elapsed = clock.now() - started;
2486
- if (typeof document !== "undefined" && document.visibilityState === "hidden" || elapsed > milliseconds + 2e3 || elapsed < 0) arm();
2656
+ if (typeof document !== "undefined" && document.visibilityState === "hidden" || elapsed > milliseconds2 + 2e3 || elapsed < 0) arm();
2487
2657
  else expired();
2488
- }, milliseconds);
2658
+ }, milliseconds2);
2489
2659
  };
2490
2660
  arm();
2491
2661
  return () => {
@@ -2518,6 +2688,22 @@ var ConnectionDiagnostics = class {
2518
2688
  } catch {
2519
2689
  }
2520
2690
  }
2691
+ recordRecoveryExhausted(input) {
2692
+ const { state, epoch, code, attempts, transport } = input;
2693
+ this.record({
2694
+ event: "recovery_exhausted",
2695
+ state,
2696
+ epoch,
2697
+ code,
2698
+ recovery: {
2699
+ reference: `connection.${code}`,
2700
+ attempts,
2701
+ transport,
2702
+ lastHeartbeatReplyAgoMs: transport?.lastHeartbeatReplyAt == null ? null : Math.max(0, this.#now() - transport.lastHeartbeatReplyAt),
2703
+ visibilityState: typeof document === "undefined" ? null : document.visibilityState
2704
+ }
2705
+ });
2706
+ }
2521
2707
  snapshot() {
2522
2708
  return Object.freeze([...this.#events]);
2523
2709
  }
@@ -2555,6 +2741,7 @@ var MEDIA_RECOVERY_TIMEOUT_MS = 6e4;
2555
2741
  var START_TIMEOUT_MS = 3e4;
2556
2742
  var LEAVE_TIMEOUT_MS = 5e3;
2557
2743
  var RECOVERY_BUDGET_MS = 1e4;
2744
+ var SYNC_RECOVERY_BUDGET_MS = MEDIA_RECOVERY_TIMEOUT_MS;
2558
2745
  var MAX_RECOVERY_ATTEMPTS = 3;
2559
2746
  var REFRESH_RETRY_MS = 5e3;
2560
2747
  var RECONNECT_GRACE_MS = 250;
@@ -2595,7 +2782,16 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2595
2782
  const clock = yield* Clock2.Clock;
2596
2783
  const context = yield* Effect2.context();
2597
2784
  const serviceScope = yield* Effect2.scope;
2598
- const diagnostics = new ConnectionDiagnostics({ now: () => clock.currentTimeMillisUnsafe(), ...options.diagnostics });
2785
+ const reporter = platform.createConnectionReporter?.(access.currentUnsafe);
2786
+ const diagnostics = new ConnectionDiagnostics({
2787
+ now: () => clock.currentTimeMillisUnsafe(),
2788
+ ...options.diagnostics,
2789
+ onEvent: (event) => {
2790
+ reporter?.recordDiagnostic(event);
2791
+ options.diagnostics?.onEvent?.(event);
2792
+ }
2793
+ });
2794
+ const onTransportDiagnostic = (event) => reporter?.recordTransport(event);
2599
2795
  const snapshotRef = yield* SubscriptionRef.make(idleSnapshot());
2600
2796
  const queue = yield* Queue.unbounded();
2601
2797
  const listeners = /* @__PURE__ */ new Set();
@@ -2638,10 +2834,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2638
2834
  };
2639
2835
  const mediaAttempt = { expiresAt: null };
2640
2836
  const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure(code, true, message, cause) });
2641
- const withinForegroundBudget = (effect, milliseconds, failure2) => Effect2.raceFirst(
2837
+ const withinForegroundBudget = (effect, milliseconds2, failure2) => Effect2.raceFirst(
2642
2838
  effect,
2643
2839
  Effect2.callback((resume) => {
2644
- const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(typeof failure2 === "function" ? failure2() : failure2)));
2840
+ const cancel = scheduleForegroundDeadline(platform.clock, milliseconds2, () => resume(Effect2.fail(typeof failure2 === "function" ? failure2() : failure2)));
2645
2841
  return Effect2.sync(cancel);
2646
2842
  })
2647
2843
  );
@@ -2702,10 +2898,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2702
2898
  const scheduleRefresh = (delay) => Effect2.gen(function* () {
2703
2899
  yield* stopRefresh();
2704
2900
  if (!active(model)) return;
2705
- const milliseconds = delay ?? (yield* access.millisecondsUntilRefresh) ?? 0;
2901
+ const milliseconds2 = delay ?? (yield* access.millisecondsUntilRefresh) ?? 0;
2706
2902
  const scope = model.activeScope;
2707
2903
  if (!scope) return;
2708
- model.refreshFiber = yield* Effect2.forkIn(Effect2.sleep(Math.max(0, milliseconds)).pipe(Effect2.andThen(enqueue(refreshAccess())), Effect2.ignore), scope);
2904
+ model.refreshFiber = yield* Effect2.forkIn(Effect2.sleep(Math.max(0, milliseconds2)).pipe(Effect2.andThen(enqueue(refreshAccess())), Effect2.ignore), scope);
2709
2905
  });
2710
2906
  const refreshAccess = () => Effect2.gen(function* () {
2711
2907
  const scope = model.activeScope;
@@ -2884,6 +3080,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2884
3080
  if (media === model.media) mediaAttempt.expiresAt = platform.clock.now() + MEDIA_RECOVERY_TIMEOUT_MS;
2885
3081
  return replacement.media;
2886
3082
  }),
3083
+ onTransportDiagnostic,
3084
+ traceparent: reporter?.traceparent,
2887
3085
  onFailure: () => enqueueBackground(handleMediaFailure()),
2888
3086
  onScreenEnded: () => enqueueBackground(notifyScreenEnded())
2889
3087
  }),
@@ -2894,7 +3092,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2894
3092
  const sync = yield* trace(
2895
3093
  "create_sync_client",
2896
3094
  Effect2.try({
2897
- try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media }),
3095
+ try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, onTransportDiagnostic, traceparent: reporter?.traceparent }),
2898
3096
  catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
2899
3097
  })
2900
3098
  );
@@ -3049,7 +3247,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3049
3247
  const syncAttempt = { client: null, expiresAt: null, started: true };
3050
3248
  while (model.epoch === epoch && active(model)) {
3051
3249
  const plan = yield* recoveryPlan(model.recoveryQueued ?? kind);
3052
- const outcome = yield* attemptRecovery(plan, mediaAttempt, syncAttempt).pipe(
3250
+ let attempts = 0;
3251
+ const outcome = yield* attemptRecovery(plan, mediaAttempt, syncAttempt, () => attempts++).pipe(
3053
3252
  Effect2.match({
3054
3253
  onSuccess: (attempt) => ({ attempt, failure: null }),
3055
3254
  onFailure: (failure2) => ({ attempt: null, failure: failure2 })
@@ -3067,9 +3266,18 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3067
3266
  yield* enqueue(
3068
3267
  Effect2.gen(function* () {
3069
3268
  if (model.epoch !== epoch || !active(model)) return;
3269
+ if (plan.kind === "sync" && model.sync?.getSnapshot().connection.phase === "live") {
3270
+ model.recoveryQueued = "media";
3271
+ return;
3272
+ }
3070
3273
  const code = plan.kind === "media" ? "media_recovery_exhausted" : "sync_recovery_exhausted";
3274
+ const transport = model.sync?.getSnapshot().connection.recoveryDiagnostic ?? null;
3275
+ diagnostics.recordRecoveryExhausted({ state: model.state, epoch, code, attempts, transport });
3276
+ if (plan.kind === "sync") {
3277
+ yield* failForSnapshot(lifecycleFailure(code, true, "Connection lost. Rejoin to continue."));
3278
+ return;
3279
+ }
3071
3280
  model.failure = toFailure(lifecycleFailure(code, true, "Connection interrupted. Reconnecting in the background."));
3072
- diagnostics.record({ event: "recovery_exhausted", state: model.state, epoch, code });
3073
3281
  yield* publish();
3074
3282
  })
3075
3283
  );
@@ -3085,19 +3293,19 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3085
3293
  const recoveryPlan = (kind) => Clock2.currentTimeMillis.pipe(
3086
3294
  Effect2.map((now) => ({
3087
3295
  kind,
3088
- deadline: now + boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4),
3296
+ deadline: now + boundedInteger(options.recovery?.budgetMs, kind === "sync" ? SYNC_RECOVERY_BUDGET_MS : RECOVERY_BUDGET_MS, 1, 6e4),
3089
3297
  attempts: boundedInteger(options.recovery?.maxAttempts, MAX_RECOVERY_ATTEMPTS, 1, 10),
3090
3298
  delays: options.recovery?.backoffMs?.length ? options.recovery.backoffMs : [100, 250, 500]
3091
3299
  }))
3092
3300
  );
3093
- const attemptRecovery = (plan, mediaAttempt2, syncAttempt) => Effect2.gen(function* () {
3301
+ const attemptRecovery = (plan, mediaAttempt2, syncAttempt, onAttempt) => Effect2.gen(function* () {
3094
3302
  for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
3095
3303
  const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
3096
3304
  if (remaining <= 0) return null;
3305
+ onAttempt();
3097
3306
  diagnostics.record({ event: "recovery_attempt", state: model.state, epoch: model.epoch, attempt });
3098
3307
  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"))),
3308
+ (effect) => withinForegroundBudget(effect, remaining, lifecycleFailure("sync_start_failed", true, "Connection recovery is still waiting for the transport")),
3101
3309
  Effect2.match({ onSuccess: () => null, onFailure: (cause) => cause })
3102
3310
  );
3103
3311
  if (failure2 === null) return attempt;
@@ -3210,7 +3418,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3210
3418
  attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
3211
3419
  }
3212
3420
  if ((attempt.started || snapshot.connection.phase !== "idle") && (yield* syncNeedsReplacement(snapshot))) return yield* restartSync(attempt);
3213
- yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
3421
+ yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, SYNC_RECOVERY_BUDGET_MS, 1, 6e4));
3214
3422
  });
3215
3423
  const restartSync = (attempt) => Effect2.gen(function* () {
3216
3424
  const epoch = model.epoch;
@@ -3224,7 +3432,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3224
3432
  model.syncBindingCleanup?.();
3225
3433
  model.sync = null;
3226
3434
  const sync2 = yield* Effect2.try({
3227
- try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media }),
3435
+ try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, onTransportDiagnostic, traceparent: reporter?.traceparent }),
3228
3436
  catch: (cause) => lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
3229
3437
  });
3230
3438
  model.sync = sync2;
@@ -3241,7 +3449,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3241
3449
  else if (attempt.client === sync) attempt.started = true;
3242
3450
  })
3243
3451
  ).pipe(
3244
- Effect2.andThen(waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4))),
3452
+ Effect2.andThen(waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, SYNC_RECOVERY_BUDGET_MS, 1, 6e4))),
3245
3453
  Effect2.andThen(Effect2.suspend(() => sync === model.sync && model.epoch === epoch && active(model) ? confirmSyncEntry(sync) : Effect2.fail(lifecycleFailure("invalid_state", false, "Sync recovery was superseded")))),
3246
3454
  Effect2.tapError(
3247
3455
  () => enqueue(
@@ -3292,6 +3500,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3292
3500
  yield* transition("left");
3293
3501
  });
3294
3502
  const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "connecting" || snapshot.connection.phase === "recovering" || snapshot.connection.phase === "terminal");
3503
+ const mediaNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "recovering" || snapshot.connection.phase === "failed" && snapshot.failure?.recoverable === true);
3295
3504
  const recordFailure = (failure2) => Effect2.sync(() => {
3296
3505
  diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code, ...failure2.sync ? { sync: failure2.sync } : {} });
3297
3506
  });
@@ -3304,8 +3513,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3304
3513
  const handleMediaSnapshot = (media, snapshot) => Effect2.gen(function* () {
3305
3514
  if (media !== model.media || media.getSnapshot() !== snapshot) return;
3306
3515
  if (snapshot === model.mediaSnapshot) return;
3516
+ const phase = mediaDiagnosticPhase(snapshot);
3517
+ if (phase !== mediaDiagnosticPhase(model.mediaSnapshot)) onTransportDiagnostic({ event: "media_state", phase });
3307
3518
  model.mediaSnapshot = snapshot;
3308
- if (active(model) && (snapshot.connection.phase === "recovering" || snapshot.connection.phase === "failed" && snapshot.failure?.recoverable)) yield* recover("media");
3519
+ if (mediaNeedsRecovery(snapshot)) yield* recover("media");
3309
3520
  yield* publish();
3310
3521
  });
3311
3522
  const handleMediaFailure = () => model.media ? handleMediaSnapshot(model.media, model.media.getSnapshot()) : Effect2.void;
@@ -3369,6 +3580,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3369
3580
  yield* Effect2.addFinalizer(
3370
3581
  () => Effect2.gen(function* () {
3371
3582
  model.closed = true;
3583
+ reporter?.dispose();
3372
3584
  accessReplacementUnsubscribe();
3373
3585
  foregroundUnsubscribe?.();
3374
3586
  if (activeJoin) yield* Fiber.interrupt(activeJoin);
@@ -3442,7 +3654,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3442
3654
  ),
3443
3655
  runPortCommand: (operation) => withFreshAccess(operation),
3444
3656
  nowUnsafe: () => clock.currentTimeMillisUnsafe(),
3445
- scheduleUnsafe: (callback, milliseconds) => platform.clock.setTimeout(callback, milliseconds),
3657
+ scheduleUnsafe: (callback, milliseconds2) => platform.clock.setTimeout(callback, milliseconds2),
3446
3658
  cancelScheduleUnsafe: (handle) => platform.clock.clearTimeout(handle),
3447
3659
  createId: () => platform.createId?.() ?? `local-${++fallbackIdentifier}`
3448
3660
  };
@@ -3568,6 +3780,9 @@ function syncPhase(phase) {
3568
3780
  if (phase === "connecting") return "connecting";
3569
3781
  return "idle";
3570
3782
  }
3783
+ function mediaDiagnosticPhase(snapshot) {
3784
+ return snapshot?.connection.peerConnectionState ?? snapshot?.connection.phase;
3785
+ }
3571
3786
  function mediaPhase(phase) {
3572
3787
  if (phase === "live") return "healthy";
3573
3788
  if (phase === "recovering") return "recovering";
@@ -6237,6 +6452,7 @@ var WhiteboardControllerRuntime = class {
6237
6452
  drawPolicy: summary.drawPolicy,
6238
6453
  drawPermissions: Object.freeze(summary.drawPermissions.map((permission) => Object.freeze({ ...permission }))),
6239
6454
  presenting: summary.presenting,
6455
+ presenterId: summary.presenterId ?? null,
6240
6456
  error: summary.error ? Object.freeze({ ...summary.error }) : null
6241
6457
  });
6242
6458
  this.#publish();
@@ -6255,7 +6471,7 @@ var WhiteboardControllerRuntime = class {
6255
6471
  #publish() {
6256
6472
  const slice = Object.freeze({
6257
6473
  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 }),
6474
+ 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
6475
  access: Object.freeze({ canDraw: this.#summary.canDraw, policy: this.#summary.drawPolicy, permissions: this.#summary.drawPermissions })
6260
6476
  });
6261
6477
  const current = this.#store.getSnapshot().whiteboard;
@@ -6264,7 +6480,7 @@ var WhiteboardControllerRuntime = class {
6264
6480
  }
6265
6481
  };
6266
6482
  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);
6483
+ 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
6484
  }
6269
6485
  function sameAccess(left, right) {
6270
6486
  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 +6575,9 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
6359
6575
  diagnostics: {
6360
6576
  onEvent: (event) => {
6361
6577
  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 } : {} });
6578
+ 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 } : {} };
6579
+ if (event.event === "recovery_exhausted") options.logger?.warn?.("SpaceClient connection event", attributes);
6580
+ else options.logger?.debug?.("SpaceClient connection event", attributes);
6363
6581
  }
6364
6582
  }
6365
6583
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChalkEffectClient,
3
3
  parseAccessGrant
4
- } from "./chunk-GA3JDPFI.js";
4
+ } from "./chunk-AZONR3Q3.js";
5
5
 
6
6
  // src/public-invites/client.ts
7
7
  import { Effect } from "effect";