@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.
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
@@ -120,7 +120,7 @@ var BrowserSyncSocket = class {
120
120
  this.#socket = socket;
121
121
  this.#socket.addEventListener("open", () => this.onopen?.());
122
122
  this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
123
- this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
123
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason, ...typeof event.wasClean === "boolean" ? { wasClean: event.wasClean } : {} }));
124
124
  this.#socket.addEventListener("error", () => this.onerror?.());
125
125
  }
126
126
  send(data) {
@@ -178,7 +178,7 @@ var ReactNativeSyncSocket = class {
178
178
  this.#socket = socket;
179
179
  this.#socket.onopen = () => this.onopen?.();
180
180
  this.#socket.onmessage = (event) => this.onmessage?.({ data: event.data });
181
- this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0 });
181
+ this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0, ...typeof event.wasClean === "boolean" ? { wasClean: event.wasClean } : {} });
182
182
  this.#socket.onerror = () => this.onerror?.();
183
183
  }
184
184
  send(data) {
@@ -198,7 +198,7 @@ var BrowserFallbackSyncSocket = class {
198
198
  this.#socket = socket;
199
199
  this.#socket.addEventListener("open", () => this.onopen?.());
200
200
  this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
201
- this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
201
+ this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason, ...typeof event.wasClean === "boolean" ? { wasClean: event.wasClean } : {} }));
202
202
  this.#socket.addEventListener("error", () => this.onerror?.());
203
203
  }
204
204
  send(data) {
@@ -339,7 +339,7 @@ var SyncProtocolMetadata = {
339
339
  accepts: ["entered", "command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"],
340
340
  heartbeat: {
341
341
  intervalMs: 2e4,
342
- missedDeadlinesBeforeClose: 2
342
+ missedDeadlinesBeforeClose: 5
343
343
  }
344
344
  },
345
345
  {
@@ -3028,6 +3028,9 @@ var V1SyncClient = class {
3028
3028
  #dialTimeoutCount = 0;
3029
3029
  #connectStartedAt = null;
3030
3030
  #connectAttemptCount = 0;
3031
+ #socketCloses = [];
3032
+ #omittedSocketCloses = 0;
3033
+ #lastHeartbeatReplyAt = null;
3031
3034
  #opened = false;
3032
3035
  #connectFailure;
3033
3036
  #connectDiagnostic() {
@@ -3095,6 +3098,9 @@ var V1SyncClient = class {
3095
3098
  if (this.#started) return;
3096
3099
  this.#started = true;
3097
3100
  this.#connectAttemptCount = 0;
3101
+ this.#socketCloses = [];
3102
+ this.#omittedSocketCloses = 0;
3103
+ this.#lastHeartbeatReplyAt = null;
3098
3104
  this.#connectFailure = void 0;
3099
3105
  const startupGeneration = ++this.#startupGeneration;
3100
3106
  let stored;
@@ -3150,6 +3156,7 @@ var V1SyncClient = class {
3150
3156
  return {
3151
3157
  connection: {
3152
3158
  ...this.#phase,
3159
+ recoveryDiagnostic: { socketAttempts: this.#connectAttemptCount, socketCloses: this.#socketCloses.slice(), omittedSocketCloses: this.#omittedSocketCloses, lastHeartbeatReplyAt: this.#lastHeartbeatReplyAt },
3153
3160
  ...this.#connectStartedAt !== null && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectAttempt: this.#connectDiagnostic() } : {},
3154
3161
  ...this.#connectFailure && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectFailure: this.#connectFailure } : {},
3155
3162
  ...this.#phase.phase === "live" ? { noticeUnresponsive: this.#health.health === "recovering", health: this.#health.health } : {},
@@ -3296,6 +3303,7 @@ var V1SyncClient = class {
3296
3303
  this.#phase = { phase: "connecting" };
3297
3304
  this.#connectAttemptCount += 1;
3298
3305
  this.#connectStartedAt = this.#clock().now();
3306
+ const socketStartedAt = this.#connectStartedAt;
3299
3307
  this.#opened = false;
3300
3308
  if (this.#connectFailure?.opened) this.#connectFailure = void 0;
3301
3309
  this.#emit();
@@ -3324,6 +3332,7 @@ var V1SyncClient = class {
3324
3332
  if (socket !== this.#socket) return;
3325
3333
  this.#clearDial();
3326
3334
  this.#opened = true;
3335
+ this.#diagnostic({ event: "sync_socket_open", attempt: this.#connectAttemptCount, duration_ms: this.#clock().now() - socketStartedAt });
3327
3336
  this.#connectFailure = void 0;
3328
3337
  void this.#authenticate(socket);
3329
3338
  };
@@ -3331,6 +3340,14 @@ var V1SyncClient = class {
3331
3340
  this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
3332
3341
  };
3333
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 } });
3344
+ if (this.#started) {
3345
+ this.#socketCloses.push({ code: event.code, wasClean: event.wasClean ?? null });
3346
+ if (this.#socketCloses.length > 50) {
3347
+ this.#socketCloses.shift();
3348
+ this.#omittedSocketCloses += 1;
3349
+ }
3350
+ }
3334
3351
  if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
3335
3352
  this.#connectFailure = { ...this.#connectDiagnostic(), closeCode: event.code, closeReason: safeSyncCloseReason(event.reason) };
3336
3353
  this.#emit();
@@ -3362,6 +3379,7 @@ var V1SyncClient = class {
3362
3379
  const hello = {
3363
3380
  type: "hello",
3364
3381
  protocol: 1,
3382
+ ...this.#options.traceparent ? { traceparent: this.#options.traceparent } : {},
3365
3383
  token,
3366
3384
  streams: {
3367
3385
  control: { cursor: this.#control && { revision: this.#control.revision, state_schema_version: this.#control.stateSchemaVersion, state_digest: this.#control.stateDigest } },
@@ -3501,6 +3519,7 @@ var V1SyncClient = class {
3501
3519
  case "pong": {
3502
3520
  this.#requireLive();
3503
3521
  this.#missedHeartbeats = 0;
3522
+ this.#lastHeartbeatReplyAt = this.#now();
3504
3523
  const previous = this.#health.health;
3505
3524
  this.#health.observePong(this.#now());
3506
3525
  if (previous !== this.#health.health) this.#emit();
@@ -3814,12 +3833,19 @@ var V1SyncClient = class {
3814
3833
  #updateLocalMediaStates() {
3815
3834
  this.#liveTargets.updateFromPublications(this.#localPublications);
3816
3835
  }
3836
+ #diagnostic(event) {
3837
+ try {
3838
+ this.#options.onDiagnostic?.(event);
3839
+ } catch {
3840
+ }
3841
+ }
3817
3842
  #startHeartbeat() {
3818
3843
  this.#clearHeartbeat();
3819
3844
  this.#heartbeatTimer = this.#clock().setTimeout(() => {
3820
3845
  this.#heartbeatTimer = void 0;
3821
3846
  if (this.#phase.phase !== "live") return;
3822
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 } });
3823
3849
  if (this.#missedHeartbeats > 2) {
3824
3850
  this.#socket?.close(CLIENT_RESTART_CLOSE_CODE, "heartbeat timeout");
3825
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,