@q9labsai/chalk-client 5.2.3 → 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.
package/README.md CHANGED
@@ -311,7 +311,7 @@ SpaceClient negotiates `collaboration_v2` for the 24-emoji set and per-message r
311
311
 
312
312
  `await client.media.setScreenShareEnabled(true, { surface: "browser", audio: true })` requests tab audio. The browser picker remains authoritative: selecting a window or declining audio still shares video normally. Audio is published separately as `screen_audio`; it starts, stops, and is revoked with the share, never with the microphone. `media.screenShare.hasAudio` reports local live audio; `participants.roster[].media.screenShareHasAudio` reports the remote publication. `media.remote` contains the received track.
313
313
 
314
- The sharer keeps local playback (`suppressLocalAudioPlayback: false`). Display audio disables echo cancellation, noise suppression and automatic gain control to preserve music; microphone capture and its processing are unchanged. System audio is excluded to avoid sending the call back to itself. Headphones avoid acoustic doubling through the microphone; local playback cannot be preserved while guaranteeing that loudspeakers never leak into it. Chrome documents [local playback control](https://developer.chrome.com/blog/screen-sharing-improvements-in-chrome-109) and [system-audio feedback](https://developer.chrome.com/docs/web-platform/screen-sharing-controls). [Meet documents the same “Also share tab audio” picker control](https://support.google.com/meet/answer/9308856?hl=en). No public X documentation establishing its echo default was found.
314
+ The sharer keeps local playback (`suppressLocalAudioPlayback: false`). Display audio disables echo cancellation, noise suppression and automatic gain control to preserve music; microphone capture and its processing are unchanged. System audio is excluded to avoid sending the call back to itself. Headphones avoid acoustic doubling through the microphone; local playback cannot be preserved while guaranteeing that loudspeakers never leak into it. Chrome documents [local playback control](https://developer.chrome.com/blog/screen-sharing-improvements-in-chrome-109) and [system-audio feedback](https://developer.chrome.com/docs/web-platform/screen-sharing-controls). [Meet documents the same “Also share tab audio” picker control](https://support.google.com/meet/answer/9308856?hl=en).
315
315
 
316
316
  Shared tab audio is excluded from recordings until the recording format supports it. React Native display capture behavior is unchanged.
317
317
 
@@ -1428,12 +1428,17 @@ function createCloudflareSFUHTTPTransport(options) {
1428
1428
  const token = await withDeadline(CREDENTIAL_TIMEOUT_MS, (signal) => credential(signal));
1429
1429
  if (!token.trim()) throw new CloudflareSFUError("The media credential provider returned an empty token", "signaling_failed");
1430
1430
  return withDeadline(timeoutMs, async (signal) => {
1431
+ const startedAt = performance.now();
1431
1432
  const response = await fetch(`${mediaPath}/${path}`, {
1432
1433
  ...init,
1433
1434
  signal,
1434
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init?.headers }
1435
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...options.traceparent ? { traceparent: options.traceparent } : {}, ...init?.headers }
1435
1436
  });
1436
1437
  if (!response.ok) {
1438
+ try {
1439
+ options.onDiagnostic?.({ event: "media_http_error", http_status: response.status, duration_ms: performance.now() - startedAt });
1440
+ } catch {
1441
+ }
1437
1442
  throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
1438
1443
  status: response.status,
1439
1444
  retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
@@ -3303,6 +3303,7 @@ var V1SyncClient = class {
3303
3303
  this.#phase = { phase: "connecting" };
3304
3304
  this.#connectAttemptCount += 1;
3305
3305
  this.#connectStartedAt = this.#clock().now();
3306
+ const socketStartedAt = this.#connectStartedAt;
3306
3307
  this.#opened = false;
3307
3308
  if (this.#connectFailure?.opened) this.#connectFailure = void 0;
3308
3309
  this.#emit();
@@ -3331,6 +3332,7 @@ var V1SyncClient = class {
3331
3332
  if (socket !== this.#socket) return;
3332
3333
  this.#clearDial();
3333
3334
  this.#opened = true;
3335
+ this.#diagnostic({ event: "sync_socket_open", attempt: this.#connectAttemptCount, duration_ms: this.#clock().now() - socketStartedAt });
3334
3336
  this.#connectFailure = void 0;
3335
3337
  void this.#authenticate(socket);
3336
3338
  };
@@ -3338,6 +3340,7 @@ var V1SyncClient = class {
3338
3340
  this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
3339
3341
  };
3340
3342
  socket.onclose = (event) => {
3343
+ this.#diagnostic({ event: "sync_socket_close", close_code: event.code, duration_ms: this.#clock().now() - socketStartedAt, ...this.#lastHeartbeatReplyAt === null ? {} : { heartbeat_gap_ms: this.#now() - this.#lastHeartbeatReplyAt } });
3341
3344
  if (this.#started) {
3342
3345
  this.#socketCloses.push({ code: event.code, wasClean: event.wasClean ?? null });
3343
3346
  if (this.#socketCloses.length > 50) {
@@ -3376,6 +3379,7 @@ var V1SyncClient = class {
3376
3379
  const hello = {
3377
3380
  type: "hello",
3378
3381
  protocol: 1,
3382
+ ...this.#options.traceparent ? { traceparent: this.#options.traceparent } : {},
3379
3383
  token,
3380
3384
  streams: {
3381
3385
  control: { cursor: this.#control && { revision: this.#control.revision, state_schema_version: this.#control.stateSchemaVersion, state_digest: this.#control.stateDigest } },
@@ -3829,12 +3833,19 @@ var V1SyncClient = class {
3829
3833
  #updateLocalMediaStates() {
3830
3834
  this.#liveTargets.updateFromPublications(this.#localPublications);
3831
3835
  }
3836
+ #diagnostic(event) {
3837
+ try {
3838
+ this.#options.onDiagnostic?.(event);
3839
+ } catch {
3840
+ }
3841
+ }
3832
3842
  #startHeartbeat() {
3833
3843
  this.#clearHeartbeat();
3834
3844
  this.#heartbeatTimer = this.#clock().setTimeout(() => {
3835
3845
  this.#heartbeatTimer = void 0;
3836
3846
  if (this.#phase.phase !== "live") return;
3837
3847
  this.#missedHeartbeats += 1;
3848
+ if (this.#missedHeartbeats > 1) this.#diagnostic({ event: "sync_heartbeat_gap", duration_ms: this.#missedHeartbeats * 2e4, ...this.#lastHeartbeatReplyAt === null ? {} : { heartbeat_gap_ms: this.#now() - this.#lastHeartbeatReplyAt } });
3838
3849
  if (this.#missedHeartbeats > 2) {
3839
3850
  this.#socket?.close(CLIENT_RESTART_CLOSE_CODE, "heartbeat timeout");
3840
3851
  return;
@@ -243,6 +243,71 @@ var CloudflareSFUTracksRequestSchema = Schema.Struct({
243
243
  })
244
244
  )
245
245
  });
246
+ var ConnectionReportRequestSchema = Schema.Struct({
247
+ events: Schema.Array(
248
+ Schema.Struct({
249
+ attempt: Schema.optional(Schema.NullOr(Schema.Number)),
250
+ close_code: Schema.optional(Schema.NullOr(Schema.Number)),
251
+ code: Schema.optional(
252
+ Schema.Literals([
253
+ "invalid_state",
254
+ "invalid_access",
255
+ "access_unavailable",
256
+ "permission_denied",
257
+ "device_not_found",
258
+ "device_busy",
259
+ "device_constraint_invalid",
260
+ "media_capture_failed",
261
+ "sync_start_failed",
262
+ "state_digest_recovery_failed",
263
+ "sync_hello_timeout",
264
+ "media_start_failed",
265
+ "join_cleanup_unconfirmed",
266
+ "sync_recovery_exhausted",
267
+ "media_recovery_exhausted",
268
+ "command_rejected",
269
+ "leave_unconfirmed",
270
+ "episode_ended",
271
+ "unsupported_environment",
272
+ "internal_error",
273
+ "collaboration_unavailable",
274
+ "chat_cursor_reset_required",
275
+ "rate_limited",
276
+ "invalid_payload"
277
+ ])
278
+ ),
279
+ duration_ms: Schema.optional(Schema.NullOr(Schema.Number)),
280
+ elapsed_ms: Schema.Number,
281
+ event: Schema.Literals([
282
+ "space_join_span",
283
+ "state_changed",
284
+ "connection_failed",
285
+ "sync_connect_failed",
286
+ "sync_dial_timeout",
287
+ "access_refreshed",
288
+ "access_refresh_failed",
289
+ "recovery_attempt",
290
+ "recovery_succeeded",
291
+ "recovery_exhausted",
292
+ "cleanup_completed",
293
+ "cleanup_unconfirmed",
294
+ "sync_socket_open",
295
+ "sync_socket_close",
296
+ "sync_heartbeat_gap",
297
+ "media_state",
298
+ "media_http_error"
299
+ ]),
300
+ heartbeat_gap_ms: Schema.optional(Schema.NullOr(Schema.Number)),
301
+ http_status: Schema.optional(Schema.NullOr(Schema.Number)),
302
+ outcome: Schema.optional(Schema.Literals(["started", "succeeded", "failed", "cancelled"])),
303
+ phase: Schema.optional(Schema.Literals(["idle", "connecting", "live", "recovering", "failed", "stopped", "new", "connected", "disconnected", "closed"])),
304
+ state: Schema.optional(Schema.Literals(["idle", "joining", "live", "reconnecting", "leaving", "left", "failed"])),
305
+ step: Schema.optional(Schema.Literals(["join", "acquire_initial_media", "access_initialize", "create_media_client", "create_sync_client", "start_media", "start_sync", "wait_for_sync_live", "confirm_entered"]))
306
+ })
307
+ ).check(Schema.isMinLength(1), Schema.isMaxLength(64)),
308
+ trigger: Schema.Literals(["join_failed", "connection_lost", "recovery_exhausted"]),
309
+ version: Schema.Literal(1)
310
+ });
246
311
  var CreateAPIKeyRequestSchema = Schema.Struct({
247
312
  expires_at: DateTimeStringSchema,
248
313
  name: Schema.String.check(Schema.isMinLength(1)),
@@ -1687,6 +1752,15 @@ var RenegotiateCloudflareSFU429ResponseHeadersSchema = Schema.Struct({
1687
1752
  "X-RateLimit-Limit": RateLimitLimitHeaderSchema,
1688
1753
  "X-RateLimit-Remaining": RateLimitRemainingHeaderSchema
1689
1754
  });
1755
+ var ReportConnectionFailurePathParamsSchema = Schema.Struct({
1756
+ tenant_id: TenantIdSchema
1757
+ });
1758
+ var ReportConnectionFailureRequestBodySchema = ConnectionReportRequestSchema;
1759
+ var ReportConnectionFailure429ResponseHeadersSchema = Schema.Struct({
1760
+ "Retry-After": RetryAfterHeaderSchema,
1761
+ "X-RateLimit-Limit": RateLimitLimitHeaderSchema,
1762
+ "X-RateLimit-Remaining": RateLimitRemainingHeaderSchema
1763
+ });
1690
1764
  var RequestRecordingExportPathParamsSchema = Schema.Struct({
1691
1765
  recording_id: RecordingIdSchema,
1692
1766
  tenant_id: TenantIdSchema
@@ -4794,6 +4868,16 @@ var RenegotiateCloudflareSFUErrorSchema = Schema.Union([
4794
4868
  SpaceInvalidIdErrorSchema,
4795
4869
  TenantInvalidIdErrorSchema
4796
4870
  ]);
4871
+ var ReportConnectionFailureErrorSchema = Schema.Union([
4872
+ AccessForbiddenErrorSchema,
4873
+ AccessUnauthenticatedErrorSchema,
4874
+ RequestInvalidErrorSchema,
4875
+ RequestPayloadTooLargeErrorSchema,
4876
+ RequestRateLimitedErrorSchema,
4877
+ ServiceInternalErrorSchema,
4878
+ ServiceUnavailableErrorSchema,
4879
+ TenantInvalidIdErrorSchema
4880
+ ]);
4797
4881
  var RequestRecordingExportErrorSchema = Schema.Union([
4798
4882
  AccessForbiddenErrorSchema,
4799
4883
  AccessUnauthenticatedErrorSchema,
@@ -5087,6 +5171,7 @@ var ChalkOperationPolicies = {
5087
5171
  refreshSpacePublicInviteAccess: { maxBodyBytes: 1048576, rateLimit: { limit: 60, policy: "v1.authenticated.write", windowSeconds: 60 } },
5088
5172
  removeEpisodeParticipant: { maxBodyBytes: 1048576, rateLimit: { limit: 60, policy: "v1.authenticated.write", windowSeconds: 60 } },
5089
5173
  renegotiateCloudflareSFU: { maxBodyBytes: 1048576, rateLimit: { limit: 60, policy: "v1.authenticated.write", windowSeconds: 60 } },
5174
+ reportConnectionFailure: { maxBodyBytes: 32768, rateLimit: { limit: 6, policy: "connection_reports.participant", windowSeconds: 600 } },
5090
5175
  requestRecordingExport: { maxBodyBytes: 1048576, rateLimit: { limit: 60, policy: "v1.authenticated.write", windowSeconds: 60 } },
5091
5176
  requestTranscript: { maxBodyBytes: 1048576, rateLimit: { limit: 60, policy: "v1.authenticated.write", windowSeconds: 60 } },
5092
5177
  restoreSpace: { rateLimit: { limit: 60, policy: "v1.authenticated.write", windowSeconds: 60 } },
@@ -5528,6 +5613,22 @@ var defaultGroup = HttpApiGroup.make("default").add(
5528
5613
  ServiceUnavailableErrorSchema.pipe(HttpApiSchema.status(503))
5529
5614
  ]
5530
5615
  })
5616
+ ).add(
5617
+ HttpApiEndpoint.post("reportConnectionFailure", "/v1/tenants/:tenant_id/connection-reports", {
5618
+ params: ReportConnectionFailurePathParamsSchema,
5619
+ payload: ReportConnectionFailureRequestBodySchema,
5620
+ success: HttpApiSchema.Empty(204).pipe(HttpApiSchema.status(204)),
5621
+ error: [
5622
+ RequestInvalidErrorSchema.pipe(HttpApiSchema.status(400)),
5623
+ TenantInvalidIdErrorSchema.pipe(HttpApiSchema.status(400)),
5624
+ AccessUnauthenticatedErrorSchema.pipe(HttpApiSchema.status(401)),
5625
+ AccessForbiddenErrorSchema.pipe(HttpApiSchema.status(403)),
5626
+ RequestPayloadTooLargeErrorSchema.pipe(HttpApiSchema.status(413)),
5627
+ RequestRateLimitedErrorSchema.pipe(HttpApiSchema.status(429)),
5628
+ ServiceInternalErrorSchema.pipe(HttpApiSchema.status(500)),
5629
+ ServiceUnavailableErrorSchema.pipe(HttpApiSchema.status(503))
5630
+ ]
5631
+ })
5531
5632
  ).add(
5532
5633
  HttpApiEndpoint.delete("revokeAPIKey", "/v1/tenants/:tenant_id/api-keys/:api_key_id", {
5533
5634
  params: RevokeAPIKeyPathParamsSchema,
@@ -6877,6 +6978,7 @@ export {
6877
6978
  CloudflareSFURenegotiateResponseSchema,
6878
6979
  CloudflareSFUTracksAPIResponseSchema,
6879
6980
  CloudflareSFUTracksRequestSchema,
6981
+ ConnectionReportRequestSchema,
6880
6982
  CreateAPIKeyRequestSchema,
6881
6983
  CreateEpisodeRequestSchema,
6882
6984
  CreateMembershipRequestSchema,
@@ -7174,6 +7276,9 @@ export {
7174
7276
  RenegotiateCloudflareSFURequestBodySchema,
7175
7277
  RenegotiateCloudflareSFUResponseSchema,
7176
7278
  RenegotiateCloudflareSFU429ResponseHeadersSchema,
7279
+ ReportConnectionFailurePathParamsSchema,
7280
+ ReportConnectionFailureRequestBodySchema,
7281
+ ReportConnectionFailure429ResponseHeadersSchema,
7177
7282
  RequestRecordingExportPathParamsSchema,
7178
7283
  RequestRecordingExportRequestBodySchema,
7179
7284
  RequestRecordingExportResponseSchema,
@@ -7642,6 +7747,7 @@ export {
7642
7747
  RefreshSpacePublicInviteAccessErrorSchema,
7643
7748
  RemoveEpisodeParticipantErrorSchema,
7644
7749
  RenegotiateCloudflareSFUErrorSchema,
7750
+ ReportConnectionFailureErrorSchema,
7645
7751
  RequestRecordingExportErrorSchema,
7646
7752
  RequestTranscriptErrorSchema,
7647
7753
  RestoreSpaceErrorSchema,
@@ -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-IMMT22US.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
  };
@@ -2156,7 +2156,7 @@ var ChalkWhiteboardV1Client = class {
2156
2156
  return this.#options.clock ?? {
2157
2157
  now: Date.now,
2158
2158
  monotonicNow: () => globalThis.performance.now(),
2159
- setTimeout: (callback, milliseconds) => globalThis.setTimeout(callback, milliseconds),
2159
+ setTimeout: (callback, milliseconds2) => globalThis.setTimeout(callback, milliseconds2),
2160
2160
  clearTimeout: (handle) => globalThis.clearTimeout(handle)
2161
2161
  };
2162
2162
  }
@@ -2288,6 +2288,124 @@ function createChalkWhiteboardV1Client(options) {
2288
2288
  });
2289
2289
  }
2290
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
+
2291
2409
  // src/connection/types.ts
2292
2410
  var ConnectionError = class extends Error {
2293
2411
  code;
@@ -2359,9 +2477,34 @@ function createDefaultConnectionDependencies(options) {
2359
2477
  return {
2360
2478
  clock: {
2361
2479
  now: () => Date.now(),
2362
- setTimeout: (callback, milliseconds) => globalThis.setTimeout(callback, milliseconds),
2480
+ setTimeout: (callback, milliseconds2) => globalThis.setTimeout(callback, milliseconds2),
2363
2481
  clearTimeout: (handle) => globalThis.clearTimeout(handle)
2364
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
+ },
2365
2508
  mediaDevices: createBrowserMediaDevices(),
2366
2509
  cancelPendingAdmission: async (access, signal) => {
2367
2510
  const { tenantId, spaceId, episodeId, participantId } = access.subject;
@@ -2426,6 +2569,8 @@ function createMediaClient(apiBaseURL, input) {
2426
2569
  transport: createCloudflareSFUHTTPTransport({
2427
2570
  apiBaseURL,
2428
2571
  credential: input.credential,
2572
+ onDiagnostic: input.onTransportDiagnostic,
2573
+ traceparent: input.traceparent,
2429
2574
  tenantId: subject.tenantId,
2430
2575
  spaceId: subject.spaceId,
2431
2576
  episodeId: subject.episodeId,
@@ -2467,6 +2612,8 @@ function bindConnectionMediaClient(client, restart) {
2467
2612
  function createSyncClient(syncURL, input) {
2468
2613
  return createV1SyncClient({
2469
2614
  url: syncURL,
2615
+ onDiagnostic: input.onTransportDiagnostic,
2616
+ traceparent: input.traceparent,
2470
2617
  token: input.token,
2471
2618
  mediaPlane: input.media,
2472
2619
  collaboration: { name: "collaboration_v2", chatCursor: { afterSequence: null, retainedFloorSequence: null } },
@@ -2498,7 +2645,7 @@ function whiteboardURL(syncURL) {
2498
2645
  }
2499
2646
 
2500
2647
  // src/foreground-deadline.ts
2501
- function scheduleForegroundDeadline(clock, milliseconds, expired) {
2648
+ function scheduleForegroundDeadline(clock, milliseconds2, expired) {
2502
2649
  let timer;
2503
2650
  let cancelled = false;
2504
2651
  const arm = () => {
@@ -2506,9 +2653,9 @@ function scheduleForegroundDeadline(clock, milliseconds, expired) {
2506
2653
  timer = clock.setTimeout(() => {
2507
2654
  if (cancelled) return;
2508
2655
  const elapsed = clock.now() - started;
2509
- 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();
2510
2657
  else expired();
2511
- }, milliseconds);
2658
+ }, milliseconds2);
2512
2659
  };
2513
2660
  arm();
2514
2661
  return () => {
@@ -2594,6 +2741,7 @@ var MEDIA_RECOVERY_TIMEOUT_MS = 6e4;
2594
2741
  var START_TIMEOUT_MS = 3e4;
2595
2742
  var LEAVE_TIMEOUT_MS = 5e3;
2596
2743
  var RECOVERY_BUDGET_MS = 1e4;
2744
+ var SYNC_RECOVERY_BUDGET_MS = MEDIA_RECOVERY_TIMEOUT_MS;
2597
2745
  var MAX_RECOVERY_ATTEMPTS = 3;
2598
2746
  var REFRESH_RETRY_MS = 5e3;
2599
2747
  var RECONNECT_GRACE_MS = 250;
@@ -2634,7 +2782,16 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2634
2782
  const clock = yield* Clock2.Clock;
2635
2783
  const context = yield* Effect2.context();
2636
2784
  const serviceScope = yield* Effect2.scope;
2637
- 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);
2638
2795
  const snapshotRef = yield* SubscriptionRef.make(idleSnapshot());
2639
2796
  const queue = yield* Queue.unbounded();
2640
2797
  const listeners = /* @__PURE__ */ new Set();
@@ -2677,10 +2834,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2677
2834
  };
2678
2835
  const mediaAttempt = { expiresAt: null };
2679
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) });
2680
- const withinForegroundBudget = (effect, milliseconds, failure2) => Effect2.raceFirst(
2837
+ const withinForegroundBudget = (effect, milliseconds2, failure2) => Effect2.raceFirst(
2681
2838
  effect,
2682
2839
  Effect2.callback((resume) => {
2683
- 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)));
2684
2841
  return Effect2.sync(cancel);
2685
2842
  })
2686
2843
  );
@@ -2741,10 +2898,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2741
2898
  const scheduleRefresh = (delay) => Effect2.gen(function* () {
2742
2899
  yield* stopRefresh();
2743
2900
  if (!active(model)) return;
2744
- const milliseconds = delay ?? (yield* access.millisecondsUntilRefresh) ?? 0;
2901
+ const milliseconds2 = delay ?? (yield* access.millisecondsUntilRefresh) ?? 0;
2745
2902
  const scope = model.activeScope;
2746
2903
  if (!scope) return;
2747
- 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);
2748
2905
  });
2749
2906
  const refreshAccess = () => Effect2.gen(function* () {
2750
2907
  const scope = model.activeScope;
@@ -2923,6 +3080,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2923
3080
  if (media === model.media) mediaAttempt.expiresAt = platform.clock.now() + MEDIA_RECOVERY_TIMEOUT_MS;
2924
3081
  return replacement.media;
2925
3082
  }),
3083
+ onTransportDiagnostic,
3084
+ traceparent: reporter?.traceparent,
2926
3085
  onFailure: () => enqueueBackground(handleMediaFailure()),
2927
3086
  onScreenEnded: () => enqueueBackground(notifyScreenEnded())
2928
3087
  }),
@@ -2933,7 +3092,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2933
3092
  const sync = yield* trace(
2934
3093
  "create_sync_client",
2935
3094
  Effect2.try({
2936
- 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 }),
2937
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)
2938
3097
  })
2939
3098
  );
@@ -3134,7 +3293,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3134
3293
  const recoveryPlan = (kind) => Clock2.currentTimeMillis.pipe(
3135
3294
  Effect2.map((now) => ({
3136
3295
  kind,
3137
- 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),
3138
3297
  attempts: boundedInteger(options.recovery?.maxAttempts, MAX_RECOVERY_ATTEMPTS, 1, 10),
3139
3298
  delays: options.recovery?.backoffMs?.length ? options.recovery.backoffMs : [100, 250, 500]
3140
3299
  }))
@@ -3259,7 +3418,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3259
3418
  attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
3260
3419
  }
3261
3420
  if ((attempt.started || snapshot.connection.phase !== "idle") && (yield* syncNeedsReplacement(snapshot))) return yield* restartSync(attempt);
3262
- 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));
3263
3422
  });
3264
3423
  const restartSync = (attempt) => Effect2.gen(function* () {
3265
3424
  const epoch = model.epoch;
@@ -3273,7 +3432,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3273
3432
  model.syncBindingCleanup?.();
3274
3433
  model.sync = null;
3275
3434
  const sync2 = yield* Effect2.try({
3276
- 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 }),
3277
3436
  catch: (cause) => lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
3278
3437
  });
3279
3438
  model.sync = sync2;
@@ -3290,7 +3449,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3290
3449
  else if (attempt.client === sync) attempt.started = true;
3291
3450
  })
3292
3451
  ).pipe(
3293
- 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))),
3294
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")))),
3295
3454
  Effect2.tapError(
3296
3455
  () => enqueue(
@@ -3341,6 +3500,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3341
3500
  yield* transition("left");
3342
3501
  });
3343
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);
3344
3504
  const recordFailure = (failure2) => Effect2.sync(() => {
3345
3505
  diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code, ...failure2.sync ? { sync: failure2.sync } : {} });
3346
3506
  });
@@ -3353,8 +3513,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3353
3513
  const handleMediaSnapshot = (media, snapshot) => Effect2.gen(function* () {
3354
3514
  if (media !== model.media || media.getSnapshot() !== snapshot) return;
3355
3515
  if (snapshot === model.mediaSnapshot) return;
3516
+ const phase = mediaDiagnosticPhase(snapshot);
3517
+ if (phase !== mediaDiagnosticPhase(model.mediaSnapshot)) onTransportDiagnostic({ event: "media_state", phase });
3356
3518
  model.mediaSnapshot = snapshot;
3357
- if (active(model) && (snapshot.connection.phase === "recovering" || snapshot.connection.phase === "failed" && snapshot.failure?.recoverable)) yield* recover("media");
3519
+ if (mediaNeedsRecovery(snapshot)) yield* recover("media");
3358
3520
  yield* publish();
3359
3521
  });
3360
3522
  const handleMediaFailure = () => model.media ? handleMediaSnapshot(model.media, model.media.getSnapshot()) : Effect2.void;
@@ -3418,6 +3580,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3418
3580
  yield* Effect2.addFinalizer(
3419
3581
  () => Effect2.gen(function* () {
3420
3582
  model.closed = true;
3583
+ reporter?.dispose();
3421
3584
  accessReplacementUnsubscribe();
3422
3585
  foregroundUnsubscribe?.();
3423
3586
  if (activeJoin) yield* Fiber.interrupt(activeJoin);
@@ -3491,7 +3654,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3491
3654
  ),
3492
3655
  runPortCommand: (operation) => withFreshAccess(operation),
3493
3656
  nowUnsafe: () => clock.currentTimeMillisUnsafe(),
3494
- scheduleUnsafe: (callback, milliseconds) => platform.clock.setTimeout(callback, milliseconds),
3657
+ scheduleUnsafe: (callback, milliseconds2) => platform.clock.setTimeout(callback, milliseconds2),
3495
3658
  cancelScheduleUnsafe: (handle) => platform.clock.clearTimeout(handle),
3496
3659
  createId: () => platform.createId?.() ?? `local-${++fallbackIdentifier}`
3497
3660
  };
@@ -3617,6 +3780,9 @@ function syncPhase(phase) {
3617
3780
  if (phase === "connecting") return "connecting";
3618
3781
  return "idle";
3619
3782
  }
3783
+ function mediaDiagnosticPhase(snapshot) {
3784
+ return snapshot?.connection.peerConnectionState ?? snapshot?.connection.phase;
3785
+ }
3620
3786
  function mediaPhase(phase) {
3621
3787
  if (phase === "live") return "healthy";
3622
3788
  if (phase === "recovering") return "recovering";
@@ -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";
package/dist/client.d.ts CHANGED
@@ -1761,6 +1761,66 @@ export declare const createChalkEffectClient: (options: ChalkEffectClientOptions
1761
1761
  readonly message: import("effect/Schema").String;
1762
1762
  }>;
1763
1763
  }>, never, never>>, never, never>, never, never>;
1764
+ readonly reportConnectionFailure: HttpApiClient.Client.Method<import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"reportConnectionFailure", "POST", "/v1/tenants/:tenant_id/connection-reports", import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
1765
+ readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
1766
+ }>>, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
1767
+ readonly events: import("effect/Schema").$Array<import("effect/Schema").Struct<{
1768
+ readonly attempt: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1769
+ readonly close_code: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1770
+ readonly code: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["invalid_state", "invalid_access", "access_unavailable", "permission_denied", "device_not_found", "device_busy", "device_constraint_invalid", "media_capture_failed", "sync_start_failed", "state_digest_recovery_failed", "sync_hello_timeout", "media_start_failed", "join_cleanup_unconfirmed", "sync_recovery_exhausted", "media_recovery_exhausted", "command_rejected", "leave_unconfirmed", "episode_ended", "unsupported_environment", "internal_error", "collaboration_unavailable", "chat_cursor_reset_required", "rate_limited", "invalid_payload"]>>;
1771
+ readonly duration_ms: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1772
+ readonly elapsed_ms: import("effect/Schema").Number;
1773
+ readonly event: import("effect/Schema").Literals<readonly ["space_join_span", "state_changed", "connection_failed", "sync_connect_failed", "sync_dial_timeout", "access_refreshed", "access_refresh_failed", "recovery_attempt", "recovery_succeeded", "recovery_exhausted", "cleanup_completed", "cleanup_unconfirmed", "sync_socket_open", "sync_socket_close", "sync_heartbeat_gap", "media_state", "media_http_error"]>;
1774
+ readonly heartbeat_gap_ms: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1775
+ readonly http_status: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1776
+ readonly outcome: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["started", "succeeded", "failed", "cancelled"]>>;
1777
+ readonly phase: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["idle", "connecting", "live", "recovering", "failed", "stopped", "new", "connected", "disconnected", "closed"]>>;
1778
+ readonly state: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["idle", "joining", "live", "reconnecting", "leaving", "left", "failed"]>>;
1779
+ readonly step: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["join", "acquire_initial_media", "access_initialize", "create_media_client", "create_sync_client", "start_media", "start_sync", "wait_for_sync_live", "confirm_entered"]>>;
1780
+ }>>;
1781
+ readonly trigger: import("effect/Schema").Literals<readonly ["join_failed", "connection_lost", "recovery_exhausted"]>;
1782
+ readonly version: import("effect/Schema").Literal<1>;
1783
+ }>>, never, import("effect/Schema").toCodecJson<import("effect/Schema").Void>, import("effect/Schema").toCodecJson<import("effect/Schema").decodeTo<typeof import("./effect").AccessForbiddenError, import("effect/Schema").Struct<{
1784
+ readonly error: import("effect/Schema").Struct<{
1785
+ readonly code: import("effect/Schema").Literal<"access.forbidden">;
1786
+ readonly message: import("effect/Schema").String;
1787
+ }>;
1788
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").AccessUnauthenticatedError, import("effect/Schema").Struct<{
1789
+ readonly error: import("effect/Schema").Struct<{
1790
+ readonly code: import("effect/Schema").Literal<"access.unauthenticated">;
1791
+ readonly message: import("effect/Schema").String;
1792
+ }>;
1793
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").RequestInvalidError, import("effect/Schema").Struct<{
1794
+ readonly error: import("effect/Schema").Struct<{
1795
+ readonly code: import("effect/Schema").Literal<"request.invalid">;
1796
+ readonly message: import("effect/Schema").String;
1797
+ }>;
1798
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").RequestPayloadTooLargeError, import("effect/Schema").Struct<{
1799
+ readonly error: import("effect/Schema").Struct<{
1800
+ readonly code: import("effect/Schema").Literal<"request.payload_too_large">;
1801
+ readonly message: import("effect/Schema").String;
1802
+ }>;
1803
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").RequestRateLimitedError, import("effect/Schema").Struct<{
1804
+ readonly error: import("effect/Schema").Struct<{
1805
+ readonly code: import("effect/Schema").Literal<"request.rate_limited">;
1806
+ readonly message: import("effect/Schema").String;
1807
+ }>;
1808
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").ServiceInternalError, import("effect/Schema").Struct<{
1809
+ readonly error: import("effect/Schema").Struct<{
1810
+ readonly code: import("effect/Schema").Literal<"service.internal_error">;
1811
+ readonly message: import("effect/Schema").String;
1812
+ }>;
1813
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").ServiceUnavailableError, import("effect/Schema").Struct<{
1814
+ readonly error: import("effect/Schema").Struct<{
1815
+ readonly code: import("effect/Schema").Literal<"service.unavailable">;
1816
+ readonly message: import("effect/Schema").String;
1817
+ }>;
1818
+ }>, never, never> | import("effect/Schema").decodeTo<typeof import("./effect").TenantInvalidIdError, import("effect/Schema").Struct<{
1819
+ readonly error: import("effect/Schema").Struct<{
1820
+ readonly code: import("effect/Schema").Literal<"tenant.invalid_id">;
1821
+ readonly message: import("effect/Schema").String;
1822
+ }>;
1823
+ }>, never, never>>, never, never>, never, never>;
1764
1824
  readonly revokeAPIKey: HttpApiClient.Client.Method<import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"revokeAPIKey", "DELETE", "/v1/tenants/:tenant_id/api-keys/:api_key_id", import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
1765
1825
  readonly api_key_id: import("effect/Schema").brand<import("effect/Schema").String, "UUID">;
1766
1826
  readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
@@ -1,3 +1,4 @@
1
+ import type { ConnectionReporter, ConnectionReportTransportEvent } from "./report";
1
2
  import type { ClientMediaPlane, CloudflareSFUBootstrap, ConnectionMediaSnapshot } from "../media";
2
3
  import type { ChalkChatFileTransport } from "../chat-files";
3
4
  import type { V1AdmissionTarget, V1AssignableRole, V1CommandResult, V1EpisodeSnapshot, V1SelfMediaTargetResult } from "../sync";
@@ -79,6 +80,8 @@ export type ConnectionMediaFactoryInput = {
79
80
  readonly access: ParsedAccessGrant;
80
81
  readonly credential: () => Promise<string>;
81
82
  readonly replaceMediaConnection?: () => Promise<ParticipantMediaAccess>;
83
+ readonly traceparent?: string;
84
+ readonly onTransportDiagnostic?: (event: ConnectionReportTransportEvent) => void;
82
85
  readonly onFailure: (error: unknown) => void;
83
86
  readonly onScreenEnded: () => void;
84
87
  };
@@ -86,6 +89,8 @@ export type ConnectionSyncFactoryInput = {
86
89
  readonly access: ParsedAccessGrant;
87
90
  readonly token: () => Promise<string>;
88
91
  readonly media: ConnectionMediaClient;
92
+ readonly traceparent?: string;
93
+ readonly onTransportDiagnostic?: (event: ConnectionReportTransportEvent) => void;
89
94
  };
90
95
  export type ConnectionWhiteboardFactoryInput = {
91
96
  readonly onWatchdogReplacement?: ChalkWhiteboardV1ClientOptions["onWatchdogReplacement"];
@@ -97,6 +102,7 @@ export type ConnectionChatFileFactoryInput = {
97
102
  };
98
103
  export type ConnectionDependencies = {
99
104
  readonly clock: ConnectionClock;
105
+ readonly createConnectionReporter?: (access: () => ParsedAccessGrant | null) => ConnectionReporter | undefined;
100
106
  readonly cancelPendingAdmission?: (access: ParsedAccessGrant, signal: AbortSignal) => Promise<boolean>;
101
107
  readonly mediaDevices: ConnectionMediaDevices;
102
108
  readonly createMediaClient: (input: ConnectionMediaFactoryInput) => ConnectionMediaClient;
@@ -0,0 +1,21 @@
1
+ import type { ParsedAccessGrant } from "../access/grant";
2
+ import type { ConnectionDiagnostic } from "./diagnostics";
3
+ import type { ConnectionReport, ConnectionReportTransportEvent } from "../transport-diagnostics";
4
+ export type { ConnectionReport, ConnectionReportTransportEvent } from "../transport-diagnostics";
5
+ type ReporterOptions = {
6
+ readonly traceparent: string;
7
+ readonly now: () => number;
8
+ readonly access: () => ParsedAccessGrant | null;
9
+ readonly send: (access: ParsedAccessGrant, report: ConnectionReport, signal: AbortSignal, traceparent: string) => Promise<void>;
10
+ readonly online: () => boolean;
11
+ readonly subscribeOnline: (listener: () => void) => () => void;
12
+ };
13
+ /** Only SDK-owned enums and numeric facts enter this buffer. No generic logging. */
14
+ export declare class ConnectionReporter {
15
+ #private;
16
+ constructor(options: ReporterOptions);
17
+ recordDiagnostic(diagnostic: ConnectionDiagnostic): void;
18
+ recordTransport(event: ConnectionReportTransportEvent): void;
19
+ dispose(): void;
20
+ get traceparent(): string;
21
+ }
package/dist/effect.js CHANGED
@@ -17,8 +17,8 @@ import {
17
17
  makeSpaceClientCoreLayer,
18
18
  makeSpaceClientCoreLayerFromServices,
19
19
  makeSpaceStoreLayer
20
- } from "./chunk-4ZUN55Z2.js";
21
- import "./chunk-DWKTGBHR.js";
20
+ } from "./chunk-LDPC2JZB.js";
21
+ import "./chunk-3ASKGMD3.js";
22
22
  import {
23
23
  APIKeyListSchema,
24
24
  APIKeyWithSecretSchema,
@@ -139,6 +139,7 @@ import {
139
139
  CloudflareSFURenegotiateResponseSchema,
140
140
  CloudflareSFUTracksAPIResponseSchema,
141
141
  CloudflareSFUTracksRequestSchema,
142
+ ConnectionReportRequestSchema,
142
143
  CreateAPIKey429ResponseHeadersSchema,
143
144
  CreateAPIKeyErrorSchema,
144
145
  CreateAPIKeyPathParamsSchema,
@@ -548,6 +549,10 @@ import {
548
549
  RenegotiateCloudflareSFUPathParamsSchema,
549
550
  RenegotiateCloudflareSFURequestBodySchema,
550
551
  RenegotiateCloudflareSFUResponseSchema,
552
+ ReportConnectionFailure429ResponseHeadersSchema,
553
+ ReportConnectionFailureErrorSchema,
554
+ ReportConnectionFailurePathParamsSchema,
555
+ ReportConnectionFailureRequestBodySchema,
551
556
  ReportsDeliveryFailedError,
552
557
  ReportsDeliveryFailedErrorSchema,
553
558
  ReportsDeliveryFailedErrorWireSchema,
@@ -828,8 +833,8 @@ import {
828
833
  WhiteboardStorageUnavailableErrorSchema,
829
834
  WhiteboardStorageUnavailableErrorWireSchema,
830
835
  createChalkEffectClient
831
- } from "./chunk-GA3JDPFI.js";
832
- import "./chunk-IMMT22US.js";
836
+ } from "./chunk-AZONR3Q3.js";
837
+ import "./chunk-5YPQNMJE.js";
833
838
 
834
839
  // src/space-client/effect.ts
835
840
  import { Context, Effect, Exit, Layer, Scope } from "effect";
@@ -990,6 +995,7 @@ export {
990
995
  ConnectionAccessService,
991
996
  ConnectionLifecycleService,
992
997
  ConnectionPlatformService,
998
+ ConnectionReportRequestSchema,
993
999
  CreateAPIKey429ResponseHeadersSchema,
994
1000
  CreateAPIKeyErrorSchema,
995
1001
  CreateAPIKeyPathParamsSchema,
@@ -1399,6 +1405,10 @@ export {
1399
1405
  RenegotiateCloudflareSFUPathParamsSchema,
1400
1406
  RenegotiateCloudflareSFURequestBodySchema,
1401
1407
  RenegotiateCloudflareSFUResponseSchema,
1408
+ ReportConnectionFailure429ResponseHeadersSchema,
1409
+ ReportConnectionFailureErrorSchema,
1410
+ ReportConnectionFailurePathParamsSchema,
1411
+ ReportConnectionFailureRequestBodySchema,
1402
1412
  ReportsDeliveryFailedError,
1403
1413
  ReportsDeliveryFailedErrorSchema,
1404
1414
  ReportsDeliveryFailedErrorWireSchema,
@@ -1726,6 +1726,65 @@ declare const ChalkApi_base: HttpApi.HttpApi<"ChalkApi", HttpApiGroup.HttpApiGro
1726
1726
  readonly code: import("effect/Schema").Literal<"tenant.invalid_id">;
1727
1727
  readonly message: import("effect/Schema").String;
1728
1728
  }>;
1729
+ }>, never, never>>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"reportConnectionFailure", "POST", "/v1/tenants/:tenant_id/connection-reports", import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
1730
+ readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
1731
+ }>>, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
1732
+ readonly events: import("effect/Schema").$Array<import("effect/Schema").Struct<{
1733
+ readonly attempt: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1734
+ readonly close_code: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1735
+ readonly code: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["invalid_state", "invalid_access", "access_unavailable", "permission_denied", "device_not_found", "device_busy", "device_constraint_invalid", "media_capture_failed", "sync_start_failed", "state_digest_recovery_failed", "sync_hello_timeout", "media_start_failed", "join_cleanup_unconfirmed", "sync_recovery_exhausted", "media_recovery_exhausted", "command_rejected", "leave_unconfirmed", "episode_ended", "unsupported_environment", "internal_error", "collaboration_unavailable", "chat_cursor_reset_required", "rate_limited", "invalid_payload"]>>;
1736
+ readonly duration_ms: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1737
+ readonly elapsed_ms: import("effect/Schema").Number;
1738
+ readonly event: import("effect/Schema").Literals<readonly ["space_join_span", "state_changed", "connection_failed", "sync_connect_failed", "sync_dial_timeout", "access_refreshed", "access_refresh_failed", "recovery_attempt", "recovery_succeeded", "recovery_exhausted", "cleanup_completed", "cleanup_unconfirmed", "sync_socket_open", "sync_socket_close", "sync_heartbeat_gap", "media_state", "media_http_error"]>;
1739
+ readonly heartbeat_gap_ms: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1740
+ readonly http_status: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Number>>;
1741
+ readonly outcome: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["started", "succeeded", "failed", "cancelled"]>>;
1742
+ readonly phase: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["idle", "connecting", "live", "recovering", "failed", "stopped", "new", "connected", "disconnected", "closed"]>>;
1743
+ readonly state: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["idle", "joining", "live", "reconnecting", "leaving", "left", "failed"]>>;
1744
+ readonly step: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["join", "acquire_initial_media", "access_initialize", "create_media_client", "create_sync_client", "start_media", "start_sync", "wait_for_sync_live", "confirm_entered"]>>;
1745
+ }>>;
1746
+ readonly trigger: import("effect/Schema").Literals<readonly ["join_failed", "connection_lost", "recovery_exhausted"]>;
1747
+ readonly version: import("effect/Schema").Literal<1>;
1748
+ }>>, never, import("effect/Schema").toCodecJson<import("effect/Schema").Void>, import("effect/Schema").toCodecJson<import("effect/Schema").decodeTo<typeof S.AccessForbiddenError, import("effect/Schema").Struct<{
1749
+ readonly error: import("effect/Schema").Struct<{
1750
+ readonly code: import("effect/Schema").Literal<"access.forbidden">;
1751
+ readonly message: import("effect/Schema").String;
1752
+ }>;
1753
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.AccessUnauthenticatedError, import("effect/Schema").Struct<{
1754
+ readonly error: import("effect/Schema").Struct<{
1755
+ readonly code: import("effect/Schema").Literal<"access.unauthenticated">;
1756
+ readonly message: import("effect/Schema").String;
1757
+ }>;
1758
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.RequestInvalidError, import("effect/Schema").Struct<{
1759
+ readonly error: import("effect/Schema").Struct<{
1760
+ readonly code: import("effect/Schema").Literal<"request.invalid">;
1761
+ readonly message: import("effect/Schema").String;
1762
+ }>;
1763
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.RequestPayloadTooLargeError, import("effect/Schema").Struct<{
1764
+ readonly error: import("effect/Schema").Struct<{
1765
+ readonly code: import("effect/Schema").Literal<"request.payload_too_large">;
1766
+ readonly message: import("effect/Schema").String;
1767
+ }>;
1768
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.RequestRateLimitedError, import("effect/Schema").Struct<{
1769
+ readonly error: import("effect/Schema").Struct<{
1770
+ readonly code: import("effect/Schema").Literal<"request.rate_limited">;
1771
+ readonly message: import("effect/Schema").String;
1772
+ }>;
1773
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.ServiceInternalError, import("effect/Schema").Struct<{
1774
+ readonly error: import("effect/Schema").Struct<{
1775
+ readonly code: import("effect/Schema").Literal<"service.internal_error">;
1776
+ readonly message: import("effect/Schema").String;
1777
+ }>;
1778
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.ServiceUnavailableError, import("effect/Schema").Struct<{
1779
+ readonly error: import("effect/Schema").Struct<{
1780
+ readonly code: import("effect/Schema").Literal<"service.unavailable">;
1781
+ readonly message: import("effect/Schema").String;
1782
+ }>;
1783
+ }>, never, never> | import("effect/Schema").decodeTo<typeof S.TenantInvalidIdError, import("effect/Schema").Struct<{
1784
+ readonly error: import("effect/Schema").Struct<{
1785
+ readonly code: import("effect/Schema").Literal<"tenant.invalid_id">;
1786
+ readonly message: import("effect/Schema").String;
1787
+ }>;
1729
1788
  }>, never, never>>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"revokeAPIKey", "DELETE", "/v1/tenants/:tenant_id/api-keys/:api_key_id", import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
1730
1789
  readonly api_key_id: import("effect/Schema").brand<import("effect/Schema").String, "UUID">;
1731
1790
  readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
@@ -219,6 +219,25 @@ export declare const CloudflareSFUTracksRequestSchema: Schema.Struct<{
219
219
  }>>;
220
220
  }>;
221
221
  export type CloudflareSFUTracksRequest = typeof CloudflareSFUTracksRequestSchema.Type;
222
+ export declare const ConnectionReportRequestSchema: Schema.Struct<{
223
+ readonly events: Schema.$Array<Schema.Struct<{
224
+ readonly attempt: Schema.optional<Schema.NullOr<Schema.Number>>;
225
+ readonly close_code: Schema.optional<Schema.NullOr<Schema.Number>>;
226
+ readonly code: Schema.optional<Schema.Literals<readonly ["invalid_state", "invalid_access", "access_unavailable", "permission_denied", "device_not_found", "device_busy", "device_constraint_invalid", "media_capture_failed", "sync_start_failed", "state_digest_recovery_failed", "sync_hello_timeout", "media_start_failed", "join_cleanup_unconfirmed", "sync_recovery_exhausted", "media_recovery_exhausted", "command_rejected", "leave_unconfirmed", "episode_ended", "unsupported_environment", "internal_error", "collaboration_unavailable", "chat_cursor_reset_required", "rate_limited", "invalid_payload"]>>;
227
+ readonly duration_ms: Schema.optional<Schema.NullOr<Schema.Number>>;
228
+ readonly elapsed_ms: Schema.Number;
229
+ readonly event: Schema.Literals<readonly ["space_join_span", "state_changed", "connection_failed", "sync_connect_failed", "sync_dial_timeout", "access_refreshed", "access_refresh_failed", "recovery_attempt", "recovery_succeeded", "recovery_exhausted", "cleanup_completed", "cleanup_unconfirmed", "sync_socket_open", "sync_socket_close", "sync_heartbeat_gap", "media_state", "media_http_error"]>;
230
+ readonly heartbeat_gap_ms: Schema.optional<Schema.NullOr<Schema.Number>>;
231
+ readonly http_status: Schema.optional<Schema.NullOr<Schema.Number>>;
232
+ readonly outcome: Schema.optional<Schema.Literals<readonly ["started", "succeeded", "failed", "cancelled"]>>;
233
+ readonly phase: Schema.optional<Schema.Literals<readonly ["idle", "connecting", "live", "recovering", "failed", "stopped", "new", "connected", "disconnected", "closed"]>>;
234
+ readonly state: Schema.optional<Schema.Literals<readonly ["idle", "joining", "live", "reconnecting", "leaving", "left", "failed"]>>;
235
+ readonly step: Schema.optional<Schema.Literals<readonly ["join", "acquire_initial_media", "access_initialize", "create_media_client", "create_sync_client", "start_media", "start_sync", "wait_for_sync_live", "confirm_entered"]>>;
236
+ }>>;
237
+ readonly trigger: Schema.Literals<readonly ["join_failed", "connection_lost", "recovery_exhausted"]>;
238
+ readonly version: Schema.Literal<1>;
239
+ }>;
240
+ export type ConnectionReportRequest = typeof ConnectionReportRequestSchema.Type;
222
241
  export declare const CreateAPIKeyRequestSchema: Schema.Struct<{
223
242
  readonly expires_at: Schema.brand<Schema.String, "DateTimeString">;
224
243
  readonly name: Schema.String;
@@ -3284,6 +3303,35 @@ export declare const RenegotiateCloudflareSFU429ResponseHeadersSchema: Schema.St
3284
3303
  readonly "X-RateLimit-Remaining": Schema.NumberFromString;
3285
3304
  }>;
3286
3305
  export type RenegotiateCloudflareSFU429ResponseHeaders = typeof RenegotiateCloudflareSFU429ResponseHeadersSchema.Type;
3306
+ export declare const ReportConnectionFailurePathParamsSchema: Schema.Struct<{
3307
+ readonly tenant_id: Schema.brand<Schema.String, "TenantId">;
3308
+ }>;
3309
+ export type ReportConnectionFailurePathParams = typeof ReportConnectionFailurePathParamsSchema.Type;
3310
+ export declare const ReportConnectionFailureRequestBodySchema: Schema.Struct<{
3311
+ readonly events: Schema.$Array<Schema.Struct<{
3312
+ readonly attempt: Schema.optional<Schema.NullOr<Schema.Number>>;
3313
+ readonly close_code: Schema.optional<Schema.NullOr<Schema.Number>>;
3314
+ readonly code: Schema.optional<Schema.Literals<readonly ["invalid_state", "invalid_access", "access_unavailable", "permission_denied", "device_not_found", "device_busy", "device_constraint_invalid", "media_capture_failed", "sync_start_failed", "state_digest_recovery_failed", "sync_hello_timeout", "media_start_failed", "join_cleanup_unconfirmed", "sync_recovery_exhausted", "media_recovery_exhausted", "command_rejected", "leave_unconfirmed", "episode_ended", "unsupported_environment", "internal_error", "collaboration_unavailable", "chat_cursor_reset_required", "rate_limited", "invalid_payload"]>>;
3315
+ readonly duration_ms: Schema.optional<Schema.NullOr<Schema.Number>>;
3316
+ readonly elapsed_ms: Schema.Number;
3317
+ readonly event: Schema.Literals<readonly ["space_join_span", "state_changed", "connection_failed", "sync_connect_failed", "sync_dial_timeout", "access_refreshed", "access_refresh_failed", "recovery_attempt", "recovery_succeeded", "recovery_exhausted", "cleanup_completed", "cleanup_unconfirmed", "sync_socket_open", "sync_socket_close", "sync_heartbeat_gap", "media_state", "media_http_error"]>;
3318
+ readonly heartbeat_gap_ms: Schema.optional<Schema.NullOr<Schema.Number>>;
3319
+ readonly http_status: Schema.optional<Schema.NullOr<Schema.Number>>;
3320
+ readonly outcome: Schema.optional<Schema.Literals<readonly ["started", "succeeded", "failed", "cancelled"]>>;
3321
+ readonly phase: Schema.optional<Schema.Literals<readonly ["idle", "connecting", "live", "recovering", "failed", "stopped", "new", "connected", "disconnected", "closed"]>>;
3322
+ readonly state: Schema.optional<Schema.Literals<readonly ["idle", "joining", "live", "reconnecting", "leaving", "left", "failed"]>>;
3323
+ readonly step: Schema.optional<Schema.Literals<readonly ["join", "acquire_initial_media", "access_initialize", "create_media_client", "create_sync_client", "start_media", "start_sync", "wait_for_sync_live", "confirm_entered"]>>;
3324
+ }>>;
3325
+ readonly trigger: Schema.Literals<readonly ["join_failed", "connection_lost", "recovery_exhausted"]>;
3326
+ readonly version: Schema.Literal<1>;
3327
+ }>;
3328
+ export type ReportConnectionFailureRequestBody = typeof ReportConnectionFailureRequestBodySchema.Type;
3329
+ export declare const ReportConnectionFailure429ResponseHeadersSchema: Schema.Struct<{
3330
+ readonly "Retry-After": Schema.NumberFromString;
3331
+ readonly "X-RateLimit-Limit": Schema.NumberFromString;
3332
+ readonly "X-RateLimit-Remaining": Schema.NumberFromString;
3333
+ }>;
3334
+ export type ReportConnectionFailure429ResponseHeaders = typeof ReportConnectionFailure429ResponseHeadersSchema.Type;
3287
3335
  export declare const RequestRecordingExportPathParamsSchema: Schema.Struct<{
3288
3336
  readonly recording_id: Schema.brand<Schema.String, "RecordingId">;
3289
3337
  readonly tenant_id: Schema.brand<Schema.String, "TenantId">;
@@ -9843,6 +9891,48 @@ export declare const RenegotiateCloudflareSFUErrorSchema: Schema.Union<readonly
9843
9891
  }>;
9844
9892
  }>, never, never>]>;
9845
9893
  export type RenegotiateCloudflareSFUError = typeof RenegotiateCloudflareSFUErrorSchema.Type;
9894
+ export declare const ReportConnectionFailureErrorSchema: Schema.Union<readonly [Schema.decodeTo<typeof AccessForbiddenError, Schema.Struct<{
9895
+ readonly error: Schema.Struct<{
9896
+ readonly code: Schema.Literal<"access.forbidden">;
9897
+ readonly message: Schema.String;
9898
+ }>;
9899
+ }>, never, never>, Schema.decodeTo<typeof AccessUnauthenticatedError, Schema.Struct<{
9900
+ readonly error: Schema.Struct<{
9901
+ readonly code: Schema.Literal<"access.unauthenticated">;
9902
+ readonly message: Schema.String;
9903
+ }>;
9904
+ }>, never, never>, Schema.decodeTo<typeof RequestInvalidError, Schema.Struct<{
9905
+ readonly error: Schema.Struct<{
9906
+ readonly code: Schema.Literal<"request.invalid">;
9907
+ readonly message: Schema.String;
9908
+ }>;
9909
+ }>, never, never>, Schema.decodeTo<typeof RequestPayloadTooLargeError, Schema.Struct<{
9910
+ readonly error: Schema.Struct<{
9911
+ readonly code: Schema.Literal<"request.payload_too_large">;
9912
+ readonly message: Schema.String;
9913
+ }>;
9914
+ }>, never, never>, Schema.decodeTo<typeof RequestRateLimitedError, Schema.Struct<{
9915
+ readonly error: Schema.Struct<{
9916
+ readonly code: Schema.Literal<"request.rate_limited">;
9917
+ readonly message: Schema.String;
9918
+ }>;
9919
+ }>, never, never>, Schema.decodeTo<typeof ServiceInternalError, Schema.Struct<{
9920
+ readonly error: Schema.Struct<{
9921
+ readonly code: Schema.Literal<"service.internal_error">;
9922
+ readonly message: Schema.String;
9923
+ }>;
9924
+ }>, never, never>, Schema.decodeTo<typeof ServiceUnavailableError, Schema.Struct<{
9925
+ readonly error: Schema.Struct<{
9926
+ readonly code: Schema.Literal<"service.unavailable">;
9927
+ readonly message: Schema.String;
9928
+ }>;
9929
+ }>, never, never>, Schema.decodeTo<typeof TenantInvalidIdError, Schema.Struct<{
9930
+ readonly error: Schema.Struct<{
9931
+ readonly code: Schema.Literal<"tenant.invalid_id">;
9932
+ readonly message: Schema.String;
9933
+ }>;
9934
+ }>, never, never>]>;
9935
+ export type ReportConnectionFailureError = typeof ReportConnectionFailureErrorSchema.Type;
9846
9936
  export declare const RequestRecordingExportErrorSchema: Schema.Union<readonly [Schema.decodeTo<typeof AccessForbiddenError, Schema.Struct<{
9847
9937
  readonly error: Schema.Struct<{
9848
9938
  readonly code: Schema.Literal<"access.forbidden">;
@@ -11346,6 +11436,14 @@ export declare const ChalkOperationPolicies: {
11346
11436
  readonly windowSeconds: 60;
11347
11437
  };
11348
11438
  };
11439
+ readonly reportConnectionFailure: {
11440
+ readonly maxBodyBytes: 32768;
11441
+ readonly rateLimit: {
11442
+ readonly limit: 6;
11443
+ readonly policy: "connection_reports.participant";
11444
+ readonly windowSeconds: 600;
11445
+ };
11446
+ };
11349
11447
  readonly requestRecordingExport: {
11350
11448
  readonly maxBodyBytes: 1048576;
11351
11449
  readonly rateLimit: {
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  createSpaceClient,
16
16
  decodeWhiteboardV1ServerFrame,
17
17
  encodeWhiteboardV1ClientFrame
18
- } from "./chunk-4ZUN55Z2.js";
18
+ } from "./chunk-LDPC2JZB.js";
19
19
  import {
20
20
  CloudflareSFUClient,
21
21
  CloudflareSFUError,
@@ -28,19 +28,19 @@ import {
28
28
  requireSFUDescription,
29
29
  validatePublicationSnapshot,
30
30
  waitFor
31
- } from "./chunk-DWKTGBHR.js";
31
+ } from "./chunk-3ASKGMD3.js";
32
32
  import {
33
33
  createChalkPublicClient,
34
34
  createChalkPublicInviteClient,
35
35
  parseInviteCode
36
- } from "./chunk-6LNRDKJF.js";
36
+ } from "./chunk-SYUGRHZ5.js";
37
37
  import {
38
38
  ArrivalInvalidHandleError,
39
39
  ArrivalUnavailableError,
40
40
  SpacePublicInviteUnavailableError,
41
41
  parseParsedAccessGrant,
42
42
  requireAccessGrant
43
- } from "./chunk-GA3JDPFI.js";
43
+ } from "./chunk-AZONR3Q3.js";
44
44
  import {
45
45
  AsyncStorageV1PendingTargetStore,
46
46
  InMemoryV1PendingTargetStore,
@@ -69,7 +69,7 @@ import {
69
69
  optimisticV1Control,
70
70
  reactionFromFrame,
71
71
  restoreV1Snapshot
72
- } from "./chunk-IMMT22US.js";
72
+ } from "./chunk-5YPQNMJE.js";
73
73
 
74
74
  // src/space-client/reload-rejoin.ts
75
75
  import { Schema } from "effect";
@@ -10,7 +10,7 @@ import {
10
10
  requireSFUDescription,
11
11
  validatePublicationSnapshot,
12
12
  waitFor
13
- } from "../chunk-DWKTGBHR.js";
13
+ } from "../chunk-3ASKGMD3.js";
14
14
  export {
15
15
  CloudflareSFUClient,
16
16
  CloudflareSFUError,
@@ -1,3 +1,4 @@
1
+ import type { ConnectionReportTransportEvent } from "../transport-diagnostics";
1
2
  import type { MediaPublication, MediaPublicationSource } from "./plane";
2
3
  export type CloudflareSFUBootstrap = {
3
4
  readonly connectionId: string;
@@ -108,6 +109,8 @@ export type CloudflareSFURestartOptions = {
108
109
  };
109
110
  export type CloudflareSFUCredentialProvider = (signal?: AbortSignal) => string | Promise<string>;
110
111
  export type CloudflareSFUHTTPTransportOptions = {
112
+ readonly traceparent?: string;
113
+ readonly onDiagnostic?: (event: ConnectionReportTransportEvent) => void;
111
114
  readonly apiBaseURL: string;
112
115
  readonly credential?: CloudflareSFUCredentialProvider;
113
116
  /** @deprecated Use credential so refreshed media access is read before every request. */
@@ -2,12 +2,12 @@ import {
2
2
  createChalkPublicClient,
3
3
  createChalkPublicInviteClient,
4
4
  parseInviteCode
5
- } from "../chunk-6LNRDKJF.js";
5
+ } from "../chunk-SYUGRHZ5.js";
6
6
  import {
7
7
  ArrivalInvalidHandleError,
8
8
  ArrivalUnavailableError,
9
9
  SpacePublicInviteUnavailableError
10
- } from "../chunk-GA3JDPFI.js";
10
+ } from "../chunk-AZONR3Q3.js";
11
11
  export {
12
12
  ArrivalInvalidHandleError,
13
13
  ArrivalUnavailableError,
@@ -22,7 +22,7 @@ import {
22
22
  encodeV1ClientFrame,
23
23
  optimisticV1Control,
24
24
  restoreV1Snapshot
25
- } from "../chunk-IMMT22US.js";
25
+ } from "../chunk-5YPQNMJE.js";
26
26
  export {
27
27
  AsyncStorageV1PendingTargetStore,
28
28
  InMemoryV1PendingTargetStore,
@@ -1,3 +1,4 @@
1
+ import type { ConnectionReportTransportEvent } from "../transport-diagnostics";
1
2
  import type { SetAdmissionPolicyCommandFrame, SyncV1ServerFrame } from "../generated/sync";
2
3
  import type { ClientMediaPlane, MediaPlaneOutcome, MediaPlaneResult, MediaPlaneTarget, MediaPublication, MediaSource } from "../media/plane";
3
4
  import type { ChalkChatReactionInput, ChalkChatReactions, ChalkChatMessage, ChalkChatPageResult, ChalkChatReadReceipt, ChalkReaction, ChalkReactionEvent, ChalkSendChatMessageInput, ChalkSyncV1CollaborationCapability } from "../collaboration/types";
@@ -189,6 +190,8 @@ export type V1CollaborationClient = {
189
190
  }) => Promise<ChalkChatPageResult>;
190
191
  };
191
192
  export type V1SyncClientOptions = {
193
+ readonly traceparent?: string;
194
+ readonly onDiagnostic?: (event: ConnectionReportTransportEvent) => void;
192
195
  readonly url: string;
193
196
  readonly token: () => Promise<string>;
194
197
  readonly webSocket: SyncWebSocketFactory;
@@ -0,0 +1,3 @@
1
+ import type { components } from "./generated/openapi-types";
2
+ export type ConnectionReport = components["schemas"]["ConnectionReportRequest"];
3
+ export type ConnectionReportTransportEvent = Pick<ConnectionReport["events"][number], "event" | "phase" | "attempt" | "duration_ms" | "heartbeat_gap_ms" | "close_code" | "http_status">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q9labsai/chalk-client",
3
- "version": "5.2.3",
3
+ "version": "5.2.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -218,7 +218,7 @@
218
218
  "dev": "tsup ./src/index.ts ./src/effect.ts ./src/media/index.ts ./src/sync/index.ts --format esm --out-dir ./dist --platform browser --external effect --watch",
219
219
  "generate:webhooks": "node ./scripts/generate-webhook-contract.mjs",
220
220
  "check:webhooks-generated": "node ./scripts/generate-webhook-contract.mjs --check",
221
- "test": "pnpm exec vitest run",
221
+ "test": "node ../../../scripts/test/run.mjs pnpm exec vitest run",
222
222
  "test:webhooks-package": "pnpm run build && node ./scripts/check-webhook-package.mjs",
223
223
  "test:server-package": "pnpm run build && node ./scripts/check-server-package.mjs",
224
224
  "test:sync-browser": "pnpm run build && node ./scripts/real-browser-sync.mjs",