@q9labsai/chalk-client 4.1.17 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +60 -42
  2. package/dist/access/grant.d.ts +0 -10
  3. package/dist/access/grant.d.ts.map +1 -1
  4. package/dist/access/manager.d.ts.map +1 -1
  5. package/dist/{chunk-ZRMGKMX4.js → chunk-2HJIJVE2.js} +552 -3651
  6. package/dist/{chunk-CZUJNAUI.js → chunk-AVXJ4HQU.js} +142 -49
  7. package/dist/{chunk-A7BVZEFM.js → chunk-CMVO7USN.js} +2 -8
  8. package/dist/{chunk-AUQTQOLR.js → chunk-IPAN4LUZ.js} +627 -2434
  9. package/dist/{chunk-2DZUHV6I.js → chunk-O2NOZKJP.js} +129 -721
  10. package/dist/client.d.ts +2559 -5303
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/collaboration/types.d.ts +21 -1
  13. package/dist/collaboration/types.d.ts.map +1 -1
  14. package/dist/collaboration/wire.d.ts +8 -1
  15. package/dist/collaboration/wire.d.ts.map +1 -1
  16. package/dist/connection/dependencies.d.ts +7 -13
  17. package/dist/connection/dependencies.d.ts.map +1 -1
  18. package/dist/connection/index.d.ts +1 -6
  19. package/dist/connection/index.d.ts.map +1 -1
  20. package/dist/connection/lifecycle.d.ts.map +1 -1
  21. package/dist/connection/media-devices.d.ts +11 -0
  22. package/dist/connection/media-devices.d.ts.map +1 -1
  23. package/dist/connection/production.d.ts +1 -1
  24. package/dist/connection/production.d.ts.map +1 -1
  25. package/dist/connection/types.d.ts +4 -4
  26. package/dist/connection/types.d.ts.map +1 -1
  27. package/dist/effect.d.ts +0 -4
  28. package/dist/effect.d.ts.map +1 -1
  29. package/dist/effect.js +53 -776
  30. package/dist/generated/http-api.d.ts +2603 -5293
  31. package/dist/generated/http-api.d.ts.map +1 -1
  32. package/dist/generated/schemas.d.ts +1893 -6870
  33. package/dist/generated/schemas.d.ts.map +1 -1
  34. package/dist/generated/sync.d.ts +191 -28
  35. package/dist/generated/sync.d.ts.map +1 -1
  36. package/dist/generated/whiteboard-v1.d.ts +0 -7
  37. package/dist/generated/whiteboard-v1.d.ts.map +1 -1
  38. package/dist/index.d.ts +0 -4
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +10 -119
  41. package/dist/media/client.d.ts +6 -5
  42. package/dist/media/client.d.ts.map +1 -1
  43. package/dist/media/index.d.ts +1 -2
  44. package/dist/media/index.d.ts.map +1 -1
  45. package/dist/media/index.js +1 -7
  46. package/dist/media/plane.d.ts +12 -1
  47. package/dist/media/plane.d.ts.map +1 -1
  48. package/dist/media/target.d.ts +4 -2
  49. package/dist/media/target.d.ts.map +1 -1
  50. package/dist/media/tracks.d.ts +2 -2
  51. package/dist/media/tracks.d.ts.map +1 -1
  52. package/dist/media/transport.d.ts.map +1 -1
  53. package/dist/media/types.d.ts +4 -11
  54. package/dist/media/types.d.ts.map +1 -1
  55. package/dist/public-invites/client.d.ts.map +1 -1
  56. package/dist/public-invites/index.d.ts +2 -2
  57. package/dist/public-invites/index.d.ts.map +1 -1
  58. package/dist/public-invites/index.js +2 -5
  59. package/dist/public-invites/types.d.ts +2 -6
  60. package/dist/public-invites/types.d.ts.map +1 -1
  61. package/dist/server/client.d.ts.map +1 -1
  62. package/dist/server/index.cjs +12 -112
  63. package/dist/server/index.d.cts +1 -1
  64. package/dist/server/index.d.ts +1 -1
  65. package/dist/server/index.d.ts.map +1 -1
  66. package/dist/server/index.js +9 -109
  67. package/dist/server/types.d.cts +0 -7
  68. package/dist/server/types.d.ts +0 -7
  69. package/dist/server/types.d.ts.map +1 -1
  70. package/dist/space-client/chat-controller-helpers.d.ts.map +1 -1
  71. package/dist/space-client/chat-controller.d.ts +2 -3
  72. package/dist/space-client/chat-controller.d.ts.map +1 -1
  73. package/dist/space-client/controller-effects.d.ts +0 -3
  74. package/dist/space-client/controller-effects.d.ts.map +1 -1
  75. package/dist/space-client/core.d.ts +3 -18
  76. package/dist/space-client/core.d.ts.map +1 -1
  77. package/dist/space-client/core.test.helpers.d.ts.map +1 -1
  78. package/dist/space-client/effect.d.ts +0 -2
  79. package/dist/space-client/effect.d.ts.map +1 -1
  80. package/dist/space-client/index.d.ts +1 -2
  81. package/dist/space-client/index.d.ts.map +1 -1
  82. package/dist/space-client/media-controller.d.ts +4 -4
  83. package/dist/space-client/media-controller.d.ts.map +1 -1
  84. package/dist/space-client/offline-actions-storage.d.ts +18 -3
  85. package/dist/space-client/offline-actions-storage.d.ts.map +1 -1
  86. package/dist/space-client/offline-actions.d.ts +1 -0
  87. package/dist/space-client/offline-actions.d.ts.map +1 -1
  88. package/dist/space-client/participants-controller.d.ts +2 -3
  89. package/dist/space-client/participants-controller.d.ts.map +1 -1
  90. package/dist/space-client/reactions-controller.d.ts +2 -3
  91. package/dist/space-client/reactions-controller.d.ts.map +1 -1
  92. package/dist/space-client/recording-controller.d.ts +2 -3
  93. package/dist/space-client/recording-controller.d.ts.map +1 -1
  94. package/dist/space-client/space-client.d.ts.map +1 -1
  95. package/dist/space-client/store.d.ts.map +1 -1
  96. package/dist/space-client/terminal.test.helpers.d.ts +33 -12
  97. package/dist/space-client/terminal.test.helpers.d.ts.map +1 -1
  98. package/dist/space-client/types.d.ts +15 -8
  99. package/dist/space-client/types.d.ts.map +1 -1
  100. package/dist/sync/index.d.ts +1 -1
  101. package/dist/sync/index.d.ts.map +1 -1
  102. package/dist/sync/index.js +1 -3
  103. package/dist/sync/v1-client.d.ts +4 -3
  104. package/dist/sync/v1-client.d.ts.map +1 -1
  105. package/dist/sync/v1-collaboration-state.d.ts +10 -3
  106. package/dist/sync/v1-collaboration-state.d.ts.map +1 -1
  107. package/dist/sync/v1-types.d.ts +10 -9
  108. package/dist/sync/v1-types.d.ts.map +1 -1
  109. package/dist/whiteboard/file-download-transport.d.ts +8 -0
  110. package/dist/whiteboard/file-download-transport.d.ts.map +1 -0
  111. package/dist/whiteboard/index.d.ts +1 -1
  112. package/dist/whiteboard/index.d.ts.map +1 -1
  113. package/dist/whiteboard/types.d.ts +12 -15
  114. package/dist/whiteboard/types.d.ts.map +1 -1
  115. package/dist/whiteboard/v1-client.d.ts.map +1 -1
  116. package/package.json +4 -16
  117. package/dist/access/episode-diagnostic-credential.d.ts +0 -11
  118. package/dist/access/episode-diagnostic-credential.d.ts.map +0 -1
  119. package/dist/chunk-EWTITNJ3.js +0 -1163
  120. package/dist/chunk-HW5TTZL2.js +0 -99
  121. package/dist/chunk-PBSYXPNJ.js +0 -23
  122. package/dist/chunk-RCDXIDCN.js +0 -33
  123. package/dist/connection/snapshot.d.ts +0 -23
  124. package/dist/connection/snapshot.d.ts.map +0 -1
  125. package/dist/feedback/collector.d.ts +0 -14
  126. package/dist/feedback/collector.d.ts.map +0 -1
  127. package/dist/feedback/context.d.ts +0 -38
  128. package/dist/feedback/context.d.ts.map +0 -1
  129. package/dist/feedback/controller.d.ts +0 -4
  130. package/dist/feedback/controller.d.ts.map +0 -1
  131. package/dist/feedback/index.d.ts +0 -7
  132. package/dist/feedback/index.d.ts.map +0 -1
  133. package/dist/feedback/transport.d.ts +0 -29
  134. package/dist/feedback/transport.d.ts.map +0 -1
  135. package/dist/feedback/types.d.ts +0 -202
  136. package/dist/feedback/types.d.ts.map +0 -1
  137. package/dist/feedback/validation.d.ts +0 -30
  138. package/dist/feedback/validation.d.ts.map +0 -1
  139. package/dist/media/rtk.d.ts +0 -101
  140. package/dist/media/rtk.d.ts.map +0 -1
  141. package/dist/space-client/episode-diagnostic-registry.d.ts +0 -13
  142. package/dist/space-client/episode-diagnostic-registry.d.ts.map +0 -1
  143. package/dist/space-client/episode-diagnostic-render-registry.d.ts +0 -4
  144. package/dist/space-client/episode-diagnostic-render-registry.d.ts.map +0 -1
  145. package/dist/space-client/episode-diagnostic-runtime.d.ts +0 -105
  146. package/dist/space-client/episode-diagnostic-runtime.d.ts.map +0 -1
  147. package/dist/telemetry/attributes.d.ts +0 -7
  148. package/dist/telemetry/attributes.d.ts.map +0 -1
  149. package/dist/telemetry/client.d.ts +0 -44
  150. package/dist/telemetry/client.d.ts.map +0 -1
  151. package/dist/telemetry/delivery.d.ts +0 -107
  152. package/dist/telemetry/delivery.d.ts.map +0 -1
  153. package/dist/telemetry/exporter.d.ts +0 -38
  154. package/dist/telemetry/exporter.d.ts.map +0 -1
  155. package/dist/telemetry/index.d.ts +0 -7
  156. package/dist/telemetry/index.d.ts.map +0 -1
  157. package/dist/telemetry/index.js +0 -50
  158. package/dist/telemetry/journey.d.ts +0 -52
  159. package/dist/telemetry/journey.d.ts.map +0 -1
  160. package/dist/telemetry/random.d.ts +0 -18
  161. package/dist/telemetry/random.d.ts.map +0 -1
  162. package/dist/telemetry/reconnect.d.ts +0 -7
  163. package/dist/telemetry/reconnect.d.ts.map +0 -1
  164. package/dist/telemetry/rtc.d.ts +0 -41
  165. package/dist/telemetry/rtc.d.ts.map +0 -1
  166. package/dist/telemetry/storage.d.ts +0 -22
  167. package/dist/telemetry/storage.d.ts.map +0 -1
  168. package/dist/telemetry/sync.d.ts +0 -10
  169. package/dist/telemetry/sync.d.ts.map +0 -1
  170. package/dist/telemetry/test-support.d.ts +0 -51
  171. package/dist/telemetry/test-support.d.ts.map +0 -1
  172. package/dist/telemetry/trace.d.ts +0 -18
  173. package/dist/telemetry/trace.d.ts.map +0 -1
  174. package/dist/telemetry/types.d.ts +0 -109
  175. package/dist/telemetry/types.d.ts.map +0 -1
  176. package/dist/whiteboard/file-http-transport.d.ts +0 -9
  177. package/dist/whiteboard/file-http-transport.d.ts.map +0 -1
@@ -1,33 +1,23 @@
1
1
  import {
2
2
  AccessGrantError,
3
- requireParsedAccessGrant,
4
- validEpisodeDiagnosticCredential
5
- } from "./chunk-ZRMGKMX4.js";
3
+ requireParsedAccessGrant
4
+ } from "./chunk-2HJIJVE2.js";
6
5
  import {
7
- CloudflareRTKClient,
8
6
  CloudflareSFUClient,
9
7
  createCloudflareSFUHTTPTransport,
10
8
  describeMediaCaptureError
11
- } from "./chunk-2DZUHV6I.js";
9
+ } from "./chunk-O2NOZKJP.js";
12
10
  import {
13
11
  ChatAttachmentMimeTypeSchema,
12
+ ChatReactionSetFrameSchema,
13
+ ReactionSchema,
14
+ ReactionValues,
15
+ UuidSchema,
14
16
  V1SyncError,
15
17
  createBrowserSyncLifecycle,
16
18
  createBrowserWebSocketFactory,
17
19
  createV1SyncClient
18
- } from "./chunk-CZUJNAUI.js";
19
- import {
20
- recordInitialConnection,
21
- recordReconnect
22
- } from "./chunk-RCDXIDCN.js";
23
- import {
24
- normalizeTelemetryAttributes
25
- } from "./chunk-EWTITNJ3.js";
26
- import {
27
- createTraceContext,
28
- journeyHeaders,
29
- parseTraceparent
30
- } from "./chunk-HW5TTZL2.js";
20
+ } from "./chunk-AVXJ4HQU.js";
31
21
 
32
22
  // src/access/manager.ts
33
23
  import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
@@ -46,7 +36,7 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
46
36
  Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
47
37
  Effect.map((result) => result.access)
48
38
  );
49
- const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure3) => failure3.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure3)));
39
+ const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure2) => failure2.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure2)));
50
40
  const millisecondsUntilRefresh = Effect.gen(function* () {
51
41
  const access = yield* SynchronizedRef.get(state);
52
42
  if (!access.access) return null;
@@ -137,8 +127,6 @@ function mediaBinding(access) {
137
127
  switch (access.media.provider) {
138
128
  case "cloudflare_sfu":
139
129
  return access.media.clientPayload.connectionId;
140
- case "cloudflare_rtk":
141
- return access.media.clientPayload.providerSubject;
142
130
  }
143
131
  }
144
132
  function expiresAt(value) {
@@ -159,16 +147,17 @@ var ChalkWhiteboardV1Error = class extends Error {
159
147
  operation;
160
148
  code;
161
149
  recoverable;
162
- constructor(failure3, options) {
163
- super(failure3.message, options);
150
+ constructor(failure2, options) {
151
+ super(failure2.message, options);
164
152
  this.name = "ChalkWhiteboardV1Error";
165
- this.operation = failure3.operation;
166
- this.code = failure3.code;
167
- this.recoverable = failure3.recoverable;
153
+ this.operation = failure2.operation;
154
+ this.code = failure2.code;
155
+ this.recoverable = failure2.recoverable;
168
156
  }
169
157
  };
170
158
 
171
159
  // src/collaboration/types.ts
160
+ var CHALK_REACTIONS = ReactionValues;
172
161
  var CHALK_CHAT_ATTACHMENT_LIMITS = {
173
162
  maximumPerMessage: 5,
174
163
  maximumByteLength: 25 * 1024 * 1024,
@@ -197,12 +186,12 @@ var ChalkChatFileError = class extends Error {
197
186
  operation;
198
187
  code;
199
188
  recoverable;
200
- constructor(failure3, options) {
201
- super(failure3.message, options);
189
+ constructor(failure2, options) {
190
+ super(failure2.message, options);
202
191
  this.name = "ChalkChatFileError";
203
- this.operation = failure3.operation;
204
- this.code = failure3.code;
205
- this.recoverable = failure3.recoverable;
192
+ this.operation = failure2.operation;
193
+ this.code = failure2.code;
194
+ this.recoverable = failure2.recoverable;
206
195
  }
207
196
  };
208
197
 
@@ -315,28 +304,13 @@ function isAllowedMimeType(value) {
315
304
  return typeof value === "string" && CHALK_CHAT_ATTACHMENT_MIME_TYPES.includes(value);
316
305
  }
317
306
 
318
- // src/whiteboard/file-http-transport.ts
319
- function createChalkWhiteboardV1FileHttpTransport(options) {
307
+ // src/whiteboard/file-download-transport.ts
308
+ function createChalkWhiteboardV1FileDownloadTransport(options) {
320
309
  const fetchImplementation = options.fetch ?? globalThis.fetch;
321
- const baseUrl = options.baseUrl.replace(/\/+$/u, "");
310
+ let baseUrlEnd = options.baseUrl.length;
311
+ while (baseUrlEnd > 0 && options.baseUrl[baseUrlEnd - 1] === "/") baseUrlEnd -= 1;
312
+ const baseUrl = options.baseUrl.slice(0, baseUrlEnd);
322
313
  return {
323
- initiateUpload: async (input) => {
324
- const response = await authorizedFetch(fetchImplementation, options.token, `${baseUrl}/v1/whiteboard/files/uploads`, {
325
- method: "POST",
326
- headers: { "content-type": "application/json" },
327
- body: JSON.stringify({ ...input, sceneId: options.sceneId() })
328
- });
329
- if (!response.ok) throw await responseError2(response, "initiate_file_upload");
330
- const value = await response.json();
331
- if (!isInitiateUploadResponse2(value)) {
332
- throw whiteboardError("initiate_file_upload", "file_transfer_failed", false, "Whiteboard upload service returned an invalid response.");
333
- }
334
- return value;
335
- },
336
- finalizeUpload: async (uploadId) => {
337
- const response = await authorizedFetch(fetchImplementation, options.token, `${baseUrl}/v1/whiteboard/files/uploads/${encodeURIComponent(uploadId)}/finalize`, { method: "POST" });
338
- if (!response.ok) throw await responseError2(response, "finalize_file_upload");
339
- },
340
314
  getDownloadUrl: async (fileId) => {
341
315
  const response = await authorizedFetch(fetchImplementation, options.token, `${baseUrl}/v1/whiteboard/files/${encodeURIComponent(fileId)}/download`, { method: "GET" });
342
316
  if (!response.ok) throw await responseError2(response, "get_file_download");
@@ -357,7 +331,7 @@ async function authorizedFetch(fetchImplementation, token, url, init) {
357
331
  } catch (cause) {
358
332
  throw new ChalkWhiteboardV1Error(
359
333
  {
360
- operation: operationFromUrl(url),
334
+ operation: "get_file_download",
361
335
  code: "storage_unavailable",
362
336
  recoverable: true,
363
337
  message: "Whiteboard file storage is unavailable."
@@ -370,26 +344,15 @@ async function responseError2(response, operation) {
370
344
  const code = response.status === 401 || response.status === 403 ? "permission_denied" : response.status === 400 ? "invalid_payload" : response.status >= 500 || response.status === 429 ? "storage_unavailable" : "file_transfer_failed";
371
345
  return whiteboardError(operation, code, response.status >= 500 || response.status === 429, "Whiteboard file request failed.");
372
346
  }
373
- function operationFromUrl(url) {
374
- if (url.endsWith("/finalize")) return "finalize_file_upload";
375
- if (url.endsWith("/download")) return "get_file_download";
376
- return "initiate_file_upload";
377
- }
378
347
  function whiteboardError(operation, code, recoverable, message) {
379
348
  return new ChalkWhiteboardV1Error({ operation, code, recoverable, message });
380
349
  }
381
- function isInitiateUploadResponse2(value) {
382
- return isExactObject(value, ["uploadId", "method", "uploadUrl", "headers", "expiresAt"]) && typeof value.uploadId === "string" && value.method === "PUT" && isHttpUrl2(value.uploadUrl) && isStringRecord2(value.headers) && isDate2(value.expiresAt);
383
- }
384
350
  function isDownloadResponse2(value) {
385
351
  return isExactObject(value, ["downloadUrl", "expiresAt"]) && isHttpUrl2(value.downloadUrl) && isDate2(value.expiresAt);
386
352
  }
387
353
  function isExactObject(value, keys) {
388
354
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
389
355
  }
390
- function isStringRecord2(value) {
391
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
392
- }
393
356
  function isHttpUrl2(value) {
394
357
  if (typeof value !== "string") return false;
395
358
  try {
@@ -558,13 +521,6 @@ var WhiteboardV1ProtocolMetadata = {
558
521
  durablePayloadContainsContent: false
559
522
  },
560
523
  fileTransport: {
561
- initiateUploadResult: {
562
- method: "PUT",
563
- uploadUrl: "string",
564
- headers: "exact-string-map",
565
- expiresAt: "rfc3339"
566
- },
567
- finalizeVerifies: ["provider_existence", "byte_length", "sha256", "mime_type", "immutable_object_identity", "active_scene", "file_id_uniqueness", "participant_authorization"],
568
524
  download: "participant-bound-short-lived-url"
569
525
  },
570
526
  persistence: {
@@ -921,8 +877,11 @@ function completeAssembly(assembly) {
921
877
  // src/whiteboard/v1-client.ts
922
878
  var CLIENT_RESTART_CLOSE_CODE = 4e3;
923
879
  var DEPENDENCY_UNAVAILABLE_CLOSE_CODE = 1012;
924
- var HEARTBEAT_INTERVAL_MS = 2e4;
925
- var MAX_MISSED_HEARTBEATS = 2;
880
+ var HEARTBEAT_INTERVAL_MS = 4e3;
881
+ var SLOW_HEARTBEAT_MS = 2e3;
882
+ var MAX_SLOW_HEARTBEATS = 2;
883
+ var MAX_TIMER_DRIFT_MS = 250;
884
+ var MAX_SLOW_CONNECTION_BACKOFF_MS = 4e3;
926
885
  var DEFAULT_RECONNECT_DELAY_MS = 250;
927
886
  var MAX_RECONNECT_DELAY_MS = 5e3;
928
887
  var encoder2 = new TextEncoder();
@@ -962,7 +921,20 @@ var ChalkWhiteboardV1Client = class {
962
921
  #reconnectTimer;
963
922
  #reconnectAttempt = 0;
964
923
  #heartbeatTimer;
965
- #missedHeartbeats = 0;
924
+ #heartbeats = [];
925
+ #ignoredHeartbeats = 0;
926
+ #slowHeartbeats = 0;
927
+ #nextSlowConnectionReplacementAt = 0;
928
+ #heartbeatDueAt = 0;
929
+ #lastProgressAt = Number.NEGATIVE_INFINITY;
930
+ #slowConnectionBackoffMs = 0;
931
+ #healthySince = null;
932
+ #roundTrips = [];
933
+ #resumedAt = Number.NEGATIVE_INFINITY;
934
+ #visibilityChanged = () => {
935
+ this.#resumedAt = this.#elapsedNow();
936
+ this.#resetProbeState();
937
+ };
966
938
  #lastCursorAt = Number.NEGATIVE_INFINITY;
967
939
  #unsubscribeLifecycle;
968
940
  #transportAvailable = true;
@@ -971,6 +943,9 @@ var ChalkWhiteboardV1Client = class {
971
943
  #inbound = Promise.resolve();
972
944
  constructor(options) {
973
945
  assertWhiteboardV1Url(options.url);
946
+ if (options.clock && typeof options.clock.monotonicNow !== "function") {
947
+ throw new TypeError("whiteboard clock.monotonicNow must use the same timeline as clock.setTimeout");
948
+ }
974
949
  this.#options = options;
975
950
  this.files = options.files;
976
951
  this.#store = options.pendingStore ?? new InMemoryChalkWhiteboardV1PendingOperationStore();
@@ -981,6 +956,7 @@ var ChalkWhiteboardV1Client = class {
981
956
  if (this.#stopPromise) return this.#stopPromise.then(() => this.startSceneSubscription());
982
957
  if (this.#started && this.#startPromise) return this.#startPromise;
983
958
  this.#started = true;
959
+ globalThis.document?.addEventListener("visibilitychange", this.#visibilityChanged);
984
960
  this.#useLegacyHello = false;
985
961
  this.#phase = "connecting";
986
962
  this.#publishSummary("loading", null);
@@ -1005,6 +981,7 @@ var ChalkWhiteboardV1Client = class {
1005
981
  }
1006
982
  #stopNow() {
1007
983
  this.#started = false;
984
+ this.#removeVisibilityListener();
1008
985
  this.#startupGeneration += 1;
1009
986
  this.#unsubscribeLifecycle?.();
1010
987
  this.#unsubscribeLifecycle = void 0;
@@ -1139,6 +1116,7 @@ var ChalkWhiteboardV1Client = class {
1139
1116
  }
1140
1117
  }
1141
1118
  #failStartup(cause) {
1119
+ this.#removeVisibilityListener();
1142
1120
  const startupFailure = failure("start_scene_subscription", "unavailable", true, "Unable to restore whiteboard retry state.");
1143
1121
  this.#started = false;
1144
1122
  this.#phase = "idle";
@@ -1175,7 +1153,12 @@ var ChalkWhiteboardV1Client = class {
1175
1153
  if (typeof data !== "string" || encoder2.encode(data).byteLength > WhiteboardV1ProtocolLimits.encodedOutboundFrameBytes) {
1176
1154
  throw new Error("invalid whiteboard frame size");
1177
1155
  }
1178
- this.#handleFrame(decodeWhiteboardV1ServerFrame(data));
1156
+ const frame = decodeWhiteboardV1ServerFrame(data);
1157
+ if (frame.type !== "pong") {
1158
+ this.#lastProgressAt = this.#elapsedNow();
1159
+ this.#resetProbeState();
1160
+ }
1161
+ this.#handleFrame(frame);
1179
1162
  } catch {
1180
1163
  socket.close(1009, "invalid whiteboard frame");
1181
1164
  }
@@ -1238,7 +1221,7 @@ var ChalkWhiteboardV1Client = class {
1238
1221
  return;
1239
1222
  case "pong":
1240
1223
  this.#requireLive();
1241
- this.#missedHeartbeats = 0;
1224
+ this.#receivePong();
1242
1225
  return;
1243
1226
  }
1244
1227
  }
@@ -1287,7 +1270,7 @@ var ChalkWhiteboardV1Client = class {
1287
1270
  this.#presentationSupported = "presenting" in frame;
1288
1271
  this.#presenting = "presenting" in frame ? frame.presenting : false;
1289
1272
  this.#publishSummary("loading", null);
1290
- this.#missedHeartbeats = 0;
1273
+ this.#clearHeartbeat();
1291
1274
  const pendingOperations = [...this.#operations.values()].sort((left, right) => compareChalkWhiteboardV1PendingOperations(left.pending, right.pending));
1292
1275
  this.#awaitingOperationIds.clear();
1293
1276
  this.#waitingForOperations = true;
@@ -1296,7 +1279,7 @@ var ChalkWhiteboardV1Client = class {
1296
1279
  this.#sendOperation(entry.pending.frame);
1297
1280
  }
1298
1281
  this.#requestSnapshotAfterOperations();
1299
- this.#startHeartbeat();
1282
+ this.#startHeartbeat(HEARTBEAT_INTERVAL_MS + Math.max(0, this.#nextSlowConnectionReplacementAt - this.#elapsedNow()));
1300
1283
  }
1301
1284
  #requestSnapshotAfterOperations() {
1302
1285
  if (this.#phase !== "live" || !this.#waitingForOperations || this.#awaitingOperationIds.size > 0) return;
@@ -1494,6 +1477,7 @@ var ChalkWhiteboardV1Client = class {
1494
1477
  this.#publishSummary("recovering", failure("start_scene_subscription", "unavailable", true, "Whiteboard connection interrupted."));
1495
1478
  }
1496
1479
  #failRejectedConnection(closeCode) {
1480
+ this.#removeVisibilityListener();
1497
1481
  const terminalFailure = failure("start_scene_subscription", closeCode === 1008 ? "permission_denied" : "invalid_payload", false, "Whiteboard connection was rejected.");
1498
1482
  this.#started = false;
1499
1483
  this.#publishSummary("failed", terminalFailure);
@@ -1573,19 +1557,88 @@ var ChalkWhiteboardV1Client = class {
1573
1557
  } catch {
1574
1558
  }
1575
1559
  }
1576
- #startHeartbeat() {
1577
- this.#clearHeartbeat();
1560
+ #startHeartbeat(delay = HEARTBEAT_INTERVAL_MS) {
1561
+ this.#heartbeatDueAt = this.#elapsedNow() + delay;
1578
1562
  this.#heartbeatTimer = this.#clock().setTimeout(() => {
1579
1563
  this.#heartbeatTimer = void 0;
1580
1564
  if (this.#phase !== "live") return;
1581
- this.#missedHeartbeats += 1;
1582
- if (this.#missedHeartbeats > MAX_MISSED_HEARTBEATS) {
1583
- this.#socket?.close(DEPENDENCY_UNAVAILABLE_CLOSE_CODE, "whiteboard heartbeat timeout");
1584
- return;
1585
- }
1565
+ const now = this.#elapsedNow();
1566
+ const qualified = this.#qualifyProbe(now);
1567
+ if (qualified && this.#checkSlowHeartbeats(now)) return;
1568
+ if (qualified) this.#heartbeats.push({ sentAt: now, slow: false });
1569
+ else this.#ignoredHeartbeats += 1;
1586
1570
  this.#send({ type: "ping" });
1571
+ const outstanding = this.#heartbeats.length > 0;
1572
+ this.#startHeartbeat(this.#options.watchdog !== false && outstanding ? SLOW_HEARTBEAT_MS : HEARTBEAT_INTERVAL_MS);
1573
+ }, delay);
1574
+ }
1575
+ #checkSlowHeartbeats(now) {
1576
+ for (const heartbeat of this.#heartbeats) {
1577
+ if (!heartbeat.slow && now - heartbeat.sentAt >= SLOW_HEARTBEAT_MS) {
1578
+ heartbeat.slow = true;
1579
+ this.#healthySince = null;
1580
+ this.#slowHeartbeats += 1;
1581
+ }
1582
+ }
1583
+ return this.#replaceSlowConnection();
1584
+ }
1585
+ #resetProbeState() {
1586
+ this.#ignoredHeartbeats += this.#heartbeats.length;
1587
+ this.#heartbeats.length = 0;
1588
+ this.#slowHeartbeats = 0;
1589
+ this.#roundTrips.length = 0;
1590
+ this.#healthySince = null;
1591
+ }
1592
+ #qualifyProbe(now) {
1593
+ if (this.#options.watchdog === false || globalThis.document?.hidden || now - this.#resumedAt < HEARTBEAT_INTERVAL_MS || now - this.#heartbeatDueAt > MAX_TIMER_DRIFT_MS || now - this.#lastProgressAt < SLOW_HEARTBEAT_MS) {
1594
+ this.#resetProbeState();
1595
+ return false;
1596
+ }
1597
+ return true;
1598
+ }
1599
+ #receivePong() {
1600
+ const now = this.#elapsedNow();
1601
+ const qualified = this.#qualifyProbe(now);
1602
+ if (this.#ignoredHeartbeats > 0) {
1603
+ this.#ignoredHeartbeats -= 1;
1604
+ return;
1605
+ }
1606
+ const heartbeat = this.#heartbeats.shift();
1607
+ if (!heartbeat) {
1608
+ this.#resetProbeState();
1609
+ return;
1610
+ }
1611
+ if (!qualified) return;
1612
+ const roundTrip = now - heartbeat.sentAt;
1613
+ this.#roundTrips.push(roundTrip);
1614
+ if (this.#roundTrips.length > MAX_SLOW_HEARTBEATS) this.#roundTrips.shift();
1615
+ if (roundTrip < SLOW_HEARTBEAT_MS) {
1616
+ this.#slowHeartbeats = 0;
1617
+ this.#healthySince ??= now;
1618
+ if (now - this.#healthySince >= 6e4) this.#slowConnectionBackoffMs = 0;
1619
+ this.#clock().clearTimeout(this.#heartbeatTimer);
1587
1620
  this.#startHeartbeat();
1588
- }, HEARTBEAT_INTERVAL_MS);
1621
+ } else {
1622
+ this.#healthySince = null;
1623
+ if (!heartbeat.slow) this.#slowHeartbeats += 1;
1624
+ }
1625
+ this.#replaceSlowConnection();
1626
+ }
1627
+ #replaceSlowConnection() {
1628
+ const now = this.#elapsedNow();
1629
+ if (this.#slowHeartbeats < MAX_SLOW_HEARTBEATS || now < this.#nextSlowConnectionReplacementAt) return false;
1630
+ const socket = this.#socket;
1631
+ if (!socket) return false;
1632
+ this.#slowConnectionBackoffMs = Math.min(MAX_SLOW_CONNECTION_BACKOFF_MS, this.#slowConnectionBackoffMs === 0 ? 1e3 : this.#slowConnectionBackoffMs * 2);
1633
+ this.#nextSlowConnectionReplacementAt = now + this.#slowConnectionBackoffMs;
1634
+ const event = { reason: "slow_heartbeat", roundTripsMs: [...this.#roundTrips], outstandingAgesMs: this.#heartbeats.slice(-2).map((heartbeat) => now - heartbeat.sentAt), backoffMs: this.#slowConnectionBackoffMs };
1635
+ this.#disconnected(socket, DEPENDENCY_UNAVAILABLE_CLOSE_CODE);
1636
+ socket.close(CLIENT_RESTART_CLOSE_CODE, "whiteboard connection slow");
1637
+ try {
1638
+ this.#options.onWatchdogReplacement?.(event);
1639
+ } catch {
1640
+ }
1641
+ return true;
1589
1642
  }
1590
1643
  #rejectSnapshots(value) {
1591
1644
  for (const assembly of this.#snapshots.values()) rejectDeferred(assembly.deferred, error(value));
@@ -1621,13 +1674,25 @@ var ChalkWhiteboardV1Client = class {
1621
1674
  this.#reconnectTimer = void 0;
1622
1675
  }
1623
1676
  #clearHeartbeat() {
1677
+ this.#heartbeats.length = 0;
1678
+ this.#ignoredHeartbeats = 0;
1679
+ this.#slowHeartbeats = 0;
1680
+ this.#roundTrips.length = 0;
1681
+ this.#healthySince = null;
1624
1682
  if (this.#heartbeatTimer === void 0) return;
1625
1683
  this.#clock().clearTimeout(this.#heartbeatTimer);
1626
1684
  this.#heartbeatTimer = void 0;
1627
1685
  }
1686
+ #removeVisibilityListener() {
1687
+ globalThis.document?.removeEventListener("visibilitychange", this.#visibilityChanged);
1688
+ }
1689
+ #elapsedNow() {
1690
+ return this.#clock().monotonicNow();
1691
+ }
1628
1692
  #clock() {
1629
1693
  return this.#options.clock ?? {
1630
1694
  now: Date.now,
1695
+ monotonicNow: () => globalThis.performance.now(),
1631
1696
  setTimeout: (callback, milliseconds) => globalThis.setTimeout(callback, milliseconds),
1632
1697
  clearTimeout: (handle) => globalThis.clearTimeout(handle)
1633
1698
  };
@@ -1732,12 +1797,12 @@ var ConnectionError = class extends Error {
1732
1797
  code;
1733
1798
  action;
1734
1799
  recoverable;
1735
- constructor(failure3, options) {
1736
- super(failure3.message, options);
1800
+ constructor(failure2, options) {
1801
+ super(failure2.message, options);
1737
1802
  this.name = "ConnectionError";
1738
- this.code = failure3.code;
1739
- this.action = failure3.action;
1740
- this.recoverable = failure3.recoverable;
1803
+ this.code = failure2.code;
1804
+ this.action = failure2.action;
1805
+ this.recoverable = failure2.recoverable;
1741
1806
  }
1742
1807
  };
1743
1808
 
@@ -1759,7 +1824,9 @@ function createBrowserMediaDevices(mediaDevices = globalThis.navigator?.mediaDev
1759
1824
  };
1760
1825
  }
1761
1826
  function stopStream(stream) {
1762
- for (const track of stream?.getTracks() ?? []) track.stop();
1827
+ for (const track of stream?.getTracks() ?? []) {
1828
+ if (track.readyState !== "ended") track.stop();
1829
+ }
1763
1830
  }
1764
1831
  function streamFromTracks(tracks) {
1765
1832
  return { getTracks: () => [...tracks] };
@@ -1771,10 +1838,23 @@ function requireDisplayVideoTrack(stream) {
1771
1838
  throw new TypeError("Display capture did not return a video track");
1772
1839
  }
1773
1840
  for (const track of stream.getTracks()) {
1774
- if (track !== video) track.stop();
1841
+ if (track !== video && track.kind !== "audio") track.stop();
1775
1842
  }
1776
1843
  return video;
1777
1844
  }
1845
+ function displayMediaConstraints(options = {}) {
1846
+ return {
1847
+ video: options.surface ? { displaySurface: options.surface } : true,
1848
+ audio: options.audio ? { suppressLocalAudioPlayback: false, echoCancellation: false, noiseSuppression: false, autoGainControl: false } : false,
1849
+ systemAudio: "exclude",
1850
+ selfBrowserSurface: "exclude",
1851
+ surfaceSwitching: "include"
1852
+ };
1853
+ }
1854
+ function screenShareCapture(track) {
1855
+ const surface = track.getSettings().displaySurface;
1856
+ return { surface: surface === "monitor" || surface === "window" || surface === "browser" ? surface : null, label: track.label || null };
1857
+ }
1778
1858
 
1779
1859
  // src/connection/production.ts
1780
1860
  function createDefaultConnectionDependencies(options) {
@@ -1820,16 +1900,6 @@ function createChatFileTransport(apiBaseURL, input) {
1820
1900
  });
1821
1901
  }
1822
1902
  function createMediaClient(apiBaseURL, input) {
1823
- if (input.access.media.provider === "cloudflare_rtk") {
1824
- return createRTKConnectionMediaClient(
1825
- new CloudflareRTKClient({
1826
- authToken: input.access.media.clientPayload.token,
1827
- participantId: input.access.subject.participantId,
1828
- onError: input.onFailure,
1829
- onScreenEnded: input.onScreenEnded
1830
- })
1831
- );
1832
- }
1833
1903
  const { subject } = input.access;
1834
1904
  const replaceMediaConnection = input.replaceMediaConnection;
1835
1905
  const client = new CloudflareSFUClient({
@@ -1842,26 +1912,17 @@ function createMediaClient(apiBaseURL, input) {
1842
1912
  } : void 0,
1843
1913
  transport: createCloudflareSFUHTTPTransport({
1844
1914
  apiBaseURL,
1845
- telemetry: input.telemetry,
1846
1915
  credential: input.credential,
1847
1916
  tenantId: subject.tenantId,
1848
1917
  spaceId: subject.spaceId,
1849
1918
  episodeId: subject.episodeId,
1850
1919
  participantId: subject.participantId
1851
1920
  }),
1852
- recordReconnect: input.recordReconnect,
1853
1921
  onError: input.onFailure,
1854
- ...input.recordRtcSummary ? { onRtcSummary: input.recordRtcSummary } : {},
1855
1922
  onScreenEnded: input.onScreenEnded
1856
1923
  });
1857
1924
  return createSFUConnectionMediaClient(client);
1858
1925
  }
1859
- function createRTKConnectionMediaClient(client) {
1860
- return bindConnectionMediaClient(client, (input) => {
1861
- if (!("provider" in input) || input.provider !== "cloudflare_rtk") throw new TypeError("The RealtimeKit adapter requires a Cloudflare RealtimeKit access grant");
1862
- return client.restart(input);
1863
- });
1864
- }
1865
1926
  function createSFUConnectionMediaClient(client) {
1866
1927
  return bindConnectionMediaClient(client, (input) => {
1867
1928
  if ("provider" in input) {
@@ -1878,6 +1939,7 @@ function bindConnectionMediaClient(client, restart) {
1878
1939
  ...client.closeForcedLocalPublication ? { closeForcedLocalPublication: client.closeForcedLocalPublication.bind(client) } : {},
1879
1940
  observeLocalPublications: client.observeLocalPublications.bind(client),
1880
1941
  observeRemotePublications: client.observeRemotePublications.bind(client),
1942
+ ...client.remoteParticipantsChanged ? { remoteParticipantsChanged: client.remoteParticipantsChanged.bind(client) } : {},
1881
1943
  ...client.remotePublicationsChanged ? { remotePublicationsChanged: client.remotePublicationsChanged.bind(client) } : {},
1882
1944
  ...client.remotePublicationResumed ? { remotePublicationResumed: client.remotePublicationResumed.bind(client) } : {},
1883
1945
  start: client.start.bind(client),
@@ -1894,27 +1956,21 @@ function createSyncClient(syncURL, input) {
1894
1956
  url: syncURL,
1895
1957
  token: input.token,
1896
1958
  mediaPlane: input.media,
1897
- telemetry: input.telemetry,
1898
- recordReconnect: input.recordReconnect,
1959
+ collaboration: { name: "collaboration_v2", chatCursor: { afterSequence: null, retainedFloorSequence: null } },
1899
1960
  persistenceScope: `${input.access.subject.tenantId}:${input.access.subject.episodeId}:${input.access.subject.participantId}`
1900
1961
  });
1901
1962
  }
1902
1963
  function createWhiteboardClient(apiBaseURL, url, input) {
1903
- let sceneId = null;
1904
- const files = createChalkWhiteboardV1FileHttpTransport({
1964
+ const files = createChalkWhiteboardV1FileDownloadTransport({
1905
1965
  baseUrl: apiBaseURL,
1906
- token: input.token,
1907
- sceneId: () => {
1908
- if (sceneId === null) throw new TypeError("Whiteboard scene is not ready");
1909
- return sceneId;
1910
- }
1966
+ token: input.token
1911
1967
  });
1912
1968
  return createChalkWhiteboardV1Client({
1913
1969
  url,
1914
1970
  token: input.token,
1915
1971
  files,
1972
+ onWatchdogReplacement: input.onWatchdogReplacement,
1916
1973
  onSummary: (summary) => {
1917
- sceneId = summary.sceneId;
1918
1974
  input.onSummary(summary);
1919
1975
  }
1920
1976
  });
@@ -2074,6 +2130,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2074
2130
  initialMediaIntent: null,
2075
2131
  intent: { microphone: options.initialMicrophoneEnabled ?? true, camera: options.initialCameraEnabled ?? true },
2076
2132
  episodeEndConfirmed: false,
2133
+ removedByHost: false,
2077
2134
  activeScope: null,
2078
2135
  syncBindingCleanup: null,
2079
2136
  mediaBindingCleanup: null,
@@ -2087,10 +2144,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2087
2144
  };
2088
2145
  const mediaAttempt = { expiresAt: null };
2089
2146
  const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
2090
- const withinForegroundBudget = (effect, milliseconds, failure3) => Effect2.raceFirst(
2147
+ const withinForegroundBudget = (effect, milliseconds, failure2) => Effect2.raceFirst(
2091
2148
  effect,
2092
2149
  Effect2.callback((resume) => {
2093
- const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(failure3)));
2150
+ const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(failure2)));
2094
2151
  return Effect2.sync(cancel);
2095
2152
  })
2096
2153
  );
@@ -2165,13 +2222,13 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2165
2222
  access.ensureFresh("scheduled_refresh").pipe(
2166
2223
  Effect2.match({ onSuccess: () => null, onFailure: (cause) => cause }),
2167
2224
  Effect2.flatMap(
2168
- (failure3) => enqueue(
2225
+ (failure2) => enqueue(
2169
2226
  Effect2.gen(function* () {
2170
2227
  if (model.epoch !== epoch || !active(model)) return;
2171
2228
  model.refreshInFlight = false;
2172
- if (failure3) {
2173
- diagnostics.record({ event: "access_refresh_failed", state: model.state, epoch, code: failure3.code === "access.invalid" ? "invalid_access" : "access_unavailable" });
2174
- if (failure3.code === "access.invalid") return yield* failForSnapshot(accessFailure(failure3));
2229
+ if (failure2) {
2230
+ diagnostics.record({ event: "access_refresh_failed", state: model.state, epoch, code: failure2.code === "access.invalid" ? "invalid_access" : "access_unavailable" });
2231
+ if (failure2.code === "access.invalid") return yield* failForSnapshot(accessFailure(failure2));
2175
2232
  model.refreshFailures = Math.min(model.refreshFailures + 1, 5);
2176
2233
  yield* scheduleRefresh(Math.min(BACKGROUND_RETRY_MAX_MS, REFRESH_RETRY_MS * 2 ** (model.refreshFailures - 1)));
2177
2234
  return;
@@ -2276,7 +2333,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2276
2333
  }).pipe((effect) => withinForegroundBudget(effect, timeoutMs, lifecycleFailure("sync_start_failed", true, "Sync did not become live before the startup deadline")));
2277
2334
  const trace = (step, effect) => {
2278
2335
  const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
2279
- return traceRecovery(step, effect, recordInitialConnection).pipe(
2336
+ return effect.pipe(
2280
2337
  Effect2.tap(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "succeeded" }))),
2281
2338
  Effect2.tapError(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "failed" })))
2282
2339
  );
@@ -2287,6 +2344,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2287
2344
  model.epoch += 1;
2288
2345
  model.failure = null;
2289
2346
  model.episodeEndConfirmed = false;
2347
+ model.removedByHost = false;
2290
2348
  yield* transition("joining");
2291
2349
  const span = diagnostics.startSpan({ step: "join", state: model.state, epoch: model.epoch });
2292
2350
  const scope = yield* Scope.make("sequential");
@@ -2310,9 +2368,6 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2310
2368
  if (media === model.media) mediaAttempt.expiresAt = platform.clock.now() + MEDIA_RECOVERY_TIMEOUT_MS;
2311
2369
  return replacement.media;
2312
2370
  }),
2313
- recordReconnect: options.recordReconnect,
2314
- telemetry: options.telemetry,
2315
- ...options.recordRtcSummary ? { recordRtcSummary: options.recordRtcSummary } : {},
2316
2371
  onFailure: () => enqueueBackground(handleMediaFailure()),
2317
2372
  onScreenEnded: () => enqueueBackground(notifyScreenEnded())
2318
2373
  }),
@@ -2323,7 +2378,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2323
2378
  const sync = yield* trace(
2324
2379
  "create_sync_client",
2325
2380
  Effect2.try({
2326
- try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
2381
+ try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media }),
2327
2382
  catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
2328
2383
  })
2329
2384
  );
@@ -2343,19 +2398,19 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2343
2398
  Effect2.matchEffect({
2344
2399
  onFailure: (cause) => Effect2.gen(function* () {
2345
2400
  stopStream(stream);
2346
- const failure3 = failureFromCause(cause, "sync_start_failed", "The Connection could not start");
2347
- model.failure = toFailure(failure3);
2401
+ const failure2 = failureFromCause(cause, "sync_start_failed", "The Connection could not start");
2402
+ model.failure = toFailure(failure2);
2348
2403
  if (model.joinCancelled) {
2349
2404
  span.end({ state: model.state, epoch: model.epoch, outcome: "cancelled", code: "invalid_state" });
2350
2405
  return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
2351
2406
  }
2352
- span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure3.code });
2353
- const retry = !retried && failure3.code === "invalid_access";
2354
- if (!retry) yield* recordFailure(failure3);
2407
+ span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure2.code });
2408
+ const retry = !retried && failure2.code === "invalid_access";
2409
+ if (!retry) yield* recordFailure(failure2);
2355
2410
  yield* stopPorts();
2356
2411
  if (retry) return yield* performJoin("access_retry", true);
2357
2412
  yield* transition("failed");
2358
- return yield* Effect2.fail(failure3);
2413
+ return yield* Effect2.fail(failure2);
2359
2414
  }),
2360
2415
  onSuccess: () => Effect2.gen(function* () {
2361
2416
  model.syncSnapshot = model.sync?.getSnapshot() ?? null;
@@ -2412,7 +2467,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2412
2467
  if (!accessRejected(cause)) return Effect2.fail(cause);
2413
2468
  return requireOpen.pipe(
2414
2469
  Effect2.andThen(access.refreshAfterRejection()),
2415
- Effect2.mapError((failure3) => failure3 instanceof ConnectionAccessFailure ? accessFailure(failure3) : failure3),
2470
+ Effect2.mapError((failure2) => failure2 instanceof ConnectionAccessFailure ? accessFailure(failure2) : failure2),
2416
2471
  Effect2.andThen(publishFreshAccess()),
2417
2472
  Effect2.andThen(run)
2418
2473
  );
@@ -2420,19 +2475,6 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2420
2475
  })
2421
2476
  );
2422
2477
  });
2423
- const traceRecovery = (step, operation, recordStep = recordReconnect) => Effect2.gen(function* () {
2424
- const started = yield* Clock2.currentTimeMillis;
2425
- recordStep(options.recordReconnect, step, { boundary: "start" });
2426
- return yield* operation.pipe(
2427
- Effect2.onExit(
2428
- (exit) => Clock2.currentTimeMillis.pipe(
2429
- Effect2.map((now) => {
2430
- recordStep(options.recordReconnect, step, { boundary: "end", duration_ms: Math.max(0, now - started) }, exit._tag === "Success" ? "succeeded" : "failed");
2431
- })
2432
- )
2433
- )
2434
- );
2435
- });
2436
2478
  const recover = (kind) => Effect2.gen(function* () {
2437
2479
  const scope = model.activeScope;
2438
2480
  if (!active(model) || !scope) return;
@@ -2465,12 +2507,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2465
2507
  const outcome = yield* attemptRecovery(plan, mediaAttempt, syncAttempt).pipe(
2466
2508
  Effect2.match({
2467
2509
  onSuccess: (attempt) => ({ attempt, failure: null }),
2468
- onFailure: (failure3) => ({ attempt: null, failure: failure3 })
2510
+ onFailure: (failure2) => ({ attempt: null, failure: failure2 })
2469
2511
  })
2470
2512
  );
2471
2513
  if (outcome.failure) {
2472
- const failure3 = outcome.failure;
2473
- enqueueBackground(Effect2.suspend(() => model.epoch === epoch && active(model) ? failForSnapshot(failure3) : Effect2.void));
2514
+ const failure2 = outcome.failure;
2515
+ enqueueBackground(Effect2.suspend(() => model.epoch === epoch && active(model) ? failForSnapshot(failure2) : Effect2.void));
2474
2516
  return;
2475
2517
  }
2476
2518
  if (outcome.attempt !== null) {
@@ -2507,29 +2549,28 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2507
2549
  for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
2508
2550
  const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
2509
2551
  if (remaining <= 0) return null;
2510
- recordReconnect(options.recordReconnect, "attempt", { recovery_kind: plan.kind, attempt });
2511
2552
  diagnostics.record({ event: "recovery_attempt", state: model.state, epoch: model.epoch, attempt });
2512
- const failure3 = yield* recoveryOperation(mediaAttempt2, syncAttempt).pipe(
2553
+ const failure2 = yield* recoveryOperation(mediaAttempt2, syncAttempt).pipe(
2513
2554
  Effect2.timeout(remaining),
2514
2555
  Effect2.catchTag("TimeoutError", () => Effect2.fail(lifecycleFailure("sync_start_failed", true, "Connection recovery is still waiting for the transport"))),
2515
2556
  Effect2.match({ onSuccess: () => null, onFailure: (cause) => cause })
2516
2557
  );
2517
- if (failure3 === null) return attempt;
2518
- if (!failure3.recoverable) return yield* Effect2.fail(failure3);
2558
+ if (failure2 === null) return attempt;
2559
+ if (!failure2.recoverable) return yield* Effect2.fail(failure2);
2519
2560
  if (attempt < plan.attempts) {
2520
2561
  const remaining2 = Math.max(0, plan.deadline - (yield* Clock2.currentTimeMillis));
2521
- yield* traceRecovery("lifecycle_backoff", Effect2.sleep(Math.min(recoveryDelay(plan, attempt), remaining2)));
2562
+ yield* Effect2.sleep(Math.min(recoveryDelay(plan, attempt), remaining2));
2522
2563
  }
2523
2564
  }
2524
2565
  return null;
2525
2566
  });
2526
2567
  const recoveryOperation = (mediaAttempt2, syncAttempt) => Effect2.gen(function* () {
2527
2568
  const results = yield* Effect2.all(
2528
- [recoverSync(syncAttempt), recoverMedia(mediaAttempt2)].map((effect) => effect.pipe(Effect2.matchEffect({ onSuccess: () => Effect2.succeed(null), onFailure: (failure4) => failure4.recoverable ? Effect2.succeed(failure4) : Effect2.fail(failure4) }))),
2569
+ [recoverSync(syncAttempt), recoverMedia(mediaAttempt2)].map((effect) => effect.pipe(Effect2.matchEffect({ onSuccess: () => Effect2.succeed(null), onFailure: (failure3) => failure3.recoverable ? Effect2.succeed(failure3) : Effect2.fail(failure3) }))),
2529
2570
  { concurrency: "unbounded" }
2530
2571
  );
2531
- const failure3 = results.find((failure4) => failure4 !== null);
2532
- if (failure3) return yield* Effect2.fail(failure3);
2572
+ const failure2 = results.find((failure3) => failure3 !== null);
2573
+ if (failure2) return yield* Effect2.fail(failure2);
2533
2574
  });
2534
2575
  const recoveryDelay = (plan, attempt) => {
2535
2576
  const delay = plan.delays[Math.min(attempt - 1, plan.delays.length - 1)] ?? 0;
@@ -2591,14 +2632,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2591
2632
  if (typeof document !== "undefined" && document.visibilityState === "hidden") attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
2592
2633
  if (now < attempt.expiresAt) return yield* waitForMediaLive(media);
2593
2634
  }
2594
- recordReconnect(options.recordReconnect, "media_decision", { strategy: "full_rebuild" });
2595
- const grant = yield* traceRecovery("access_refresh", access.refresh("media_recovery", true).pipe(Effect2.mapError(accessFailure)));
2635
+ const grant = yield* access.refresh("media_recovery", true).pipe(Effect2.mapError(accessFailure));
2596
2636
  const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
2597
2637
  attempt.expiresAt = (yield* Clock2.currentTimeMillis) + MEDIA_RECOVERY_TIMEOUT_MS;
2598
- yield* traceRecovery(
2599
- "media_rebuild",
2600
- foreign7(() => media.restart(restartInput), "media_start_failed", "Media transport could not restart")
2601
- );
2638
+ yield* foreign7(() => media.restart(restartInput), "media_start_failed", "Media transport could not restart");
2602
2639
  yield* waitForMediaLive(media);
2603
2640
  });
2604
2641
  const syncAttemptExpired = (attempt, sync, now) => {
@@ -2614,8 +2651,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2614
2651
  const syncNeedsReplacement = (snapshot) => {
2615
2652
  if (snapshot.connection.phase === "idle" || snapshot.connection.phase === "stopped") return Effect2.succeed(true);
2616
2653
  if (snapshot.connection.phase !== "terminal") return Effect2.succeed(false);
2617
- const failure3 = syncTerminalFailure(snapshot);
2618
- return failure3.recoverable ? Effect2.succeed(true) : Effect2.fail(failure3);
2654
+ const failure2 = syncTerminalFailure(snapshot);
2655
+ return failure2.recoverable ? Effect2.succeed(true) : Effect2.fail(failure2);
2619
2656
  };
2620
2657
  const recoverSync = (attempt) => Effect2.gen(function* () {
2621
2658
  const sync = model.sync;
@@ -2642,7 +2679,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2642
2679
  model.syncBindingCleanup?.();
2643
2680
  model.sync = null;
2644
2681
  const sync2 = yield* Effect2.try({
2645
- try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
2682
+ try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media }),
2646
2683
  catch: (cause) => lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
2647
2684
  });
2648
2685
  model.sync = sync2;
@@ -2683,15 +2720,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2683
2720
  });
2684
2721
  const settleTerminalSnapshot = (snapshot) => Effect2.gen(function* () {
2685
2722
  const ended = episodeEnded(snapshot);
2686
- if (ended || snapshot.connection.terminalReason === "participant_inactive") {
2723
+ const removed = snapshot.connection.terminalReason === "participant_removed";
2724
+ if (ended || removed || snapshot.connection.terminalReason === "participant_inactive") {
2687
2725
  if (ended) model.episodeEndConfirmed = true;
2726
+ if (removed) model.removedByHost = true;
2688
2727
  yield* finishTerminalSnapshot();
2689
2728
  return true;
2690
2729
  }
2691
2730
  if (snapshot.connection.phase !== "terminal") return false;
2692
- const failure3 = syncTerminalFailure(snapshot);
2693
- if (failure3.recoverable) return false;
2694
- yield* failForSnapshot(failure3);
2731
+ const failure2 = syncTerminalFailure(snapshot);
2732
+ if (failure2.recoverable) return false;
2733
+ yield* failForSnapshot(failure2);
2695
2734
  return true;
2696
2735
  });
2697
2736
  const isCurrentSyncSnapshot = (sync, snapshot) => sync === model.sync && snapshot !== model.syncSnapshot;
@@ -2704,15 +2743,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2704
2743
  yield* transition("left");
2705
2744
  });
2706
2745
  const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "connecting" || snapshot.connection.phase === "recovering" || snapshot.connection.phase === "terminal");
2707
- const recordFailure = (failure3) => Effect2.sync(() => {
2708
- const reason = model.sync?.getSnapshot().connection.terminalReason;
2709
- const terminalReason = reason === "participant_inactive" || reason === "stale_participant_generation" || reason === "episode_ended" ? reason : failure3.code;
2710
- diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure3.code });
2711
- recordReconnect(options.recordReconnect, "terminal_failure", { code: failure3.code, terminal_reason: terminalReason }, "failed");
2746
+ const recordFailure = (failure2) => Effect2.sync(() => {
2747
+ diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code });
2712
2748
  });
2713
- const failForSnapshot = (failure3) => Effect2.gen(function* failForSnapshotEffect() {
2714
- model.failure = toFailure(failure3);
2715
- yield* recordFailure(failure3);
2749
+ const failForSnapshot = (failure2) => Effect2.gen(function* failForSnapshotEffect() {
2750
+ model.failure = toFailure(failure2);
2751
+ yield* recordFailure(failure2);
2716
2752
  yield* stopPorts();
2717
2753
  yield* transition("failed");
2718
2754
  });
@@ -2873,7 +2909,8 @@ function snapshotFor(model, access) {
2873
2909
  episode: episodeFor(access, control),
2874
2910
  connection: Object.freeze({ sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
2875
2911
  failure: model.failure ? Object.freeze({ ...model.failure }) : null,
2876
- episodeEnded: model.episodeEndConfirmed
2912
+ episodeEnded: model.episodeEndConfirmed,
2913
+ removedByHost: model.removedByHost
2877
2914
  });
2878
2915
  }
2879
2916
  function subjectFor(access) {
@@ -2905,9 +2942,9 @@ function accessFailure(value) {
2905
2942
  }
2906
2943
  function captureFailure(cause) {
2907
2944
  if (cause instanceof ConnectionError && cause.cause === void 0) return lifecycleFailure(cause.code, cause.recoverable, cause.message, cause);
2908
- const failure3 = describeMediaCaptureError(cause instanceof ConnectionError ? cause.cause : cause);
2909
- if (failure3.code === "unsupported_environment") return lifecycleFailure("media_capture_failed", true, "Media capture failed. Try again or enter with devices off.", cause);
2910
- return lifecycleFailure(failure3.code, failure3.recoverable, failure3.message, cause);
2945
+ const failure2 = describeMediaCaptureError(cause instanceof ConnectionError ? cause.cause : cause);
2946
+ if (failure2.code === "unsupported_environment") return lifecycleFailure("media_capture_failed", true, "Media capture failed. Try again or enter with devices off.", cause);
2947
+ return lifecycleFailure(failure2.code, failure2.recoverable, failure2.message, cause);
2911
2948
  }
2912
2949
  function acquireInitialMedia(provider, intent) {
2913
2950
  return provider(intent).pipe(
@@ -2953,7 +2990,7 @@ function initialCaptureWarning(failures) {
2953
2990
  const first = failures[0];
2954
2991
  if (!first) return null;
2955
2992
  const names = failures.length === 2 ? "Microphone and camera" : first.source === "microphone" ? "Microphone" : "Camera";
2956
- const message = failures.length === 2 && first.failure.code !== failures[1]?.failure.code ? failures.map(({ source, failure: failure3 }) => `${source === "microphone" ? "Microphone" : "Camera"}: ${failure3.message}`).join(" ") : `${names}: ${first.failure.message}`;
2993
+ const message = failures.length === 2 && first.failure.code !== failures[1]?.failure.code ? failures.map(({ source, failure: failure2 }) => `${source === "microphone" ? "Microphone" : "Camera"}: ${failure2.message}`).join(" ") : `${names}: ${first.failure.message}`;
2957
2994
  return lifecycleFailure(first.failure.code, true, message, first.failure.cause);
2958
2995
  }
2959
2996
  function failureFromCause(cause, code, message) {
@@ -2991,7 +3028,7 @@ function mediaPhase(phase) {
2991
3028
  function syncTerminalFailure(snapshot) {
2992
3029
  const reason = snapshot.connection.terminalReason;
2993
3030
  if (reason === "episode_ended") return lifecycleFailure("episode_ended", false, "The Episode has ended");
2994
- if (reason === "participant_inactive" || reason === "stale_participant_generation") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
3031
+ if (reason === "participant_inactive" || reason === "participant_removed" || reason === "stale_participant_generation") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
2995
3032
  return lifecycleFailure("sync_start_failed", true, "The Sync transport needs recovery");
2996
3033
  }
2997
3034
 
@@ -3068,7 +3105,7 @@ import { Context as Context4, Effect as Effect3, Layer as Layer4, SubscriptionRe
3068
3105
  var INTERRUPTED_CONNECTION_PHASES = /* @__PURE__ */ new Set(["unresponsive", "connecting", "recovering"]);
3069
3106
  var empty = (value) => Object.freeze(value);
3070
3107
  var EMPTY = empty({
3071
- connection: empty({ status: "idle", episode: null, lastError: null }),
3108
+ connection: empty({ status: "idle", episode: null, lastError: null, episodeEnded: false, removedByHost: false }),
3072
3109
  self: empty({ participantId: null, displayName: null, role: null, capabilities: empty([]), handRaised: false, can: () => false }),
3073
3110
  participants: empty({ roster: empty([]), admissionQueue: empty([]) }),
3074
3111
  media: empty({
@@ -3076,7 +3113,7 @@ var EMPTY = empty({
3076
3113
  selection: empty({ microphone: null, camera: null, speaker: null }),
3077
3114
  local: empty({ microphone: empty({ source: "microphone", state: "unavailable", track: null }), camera: empty({ source: "camera", state: "unavailable", track: null }), screen: empty({ source: "screen", state: "unavailable", track: null }) }),
3078
3115
  remote: empty([]),
3079
- screenShare: empty({ source: "screen", state: "unavailable", track: null }),
3116
+ screenShare: empty({ source: "screen", state: "unavailable", track: null, hasAudio: false }),
3080
3117
  incomingRequests: empty([])
3081
3118
  }),
3082
3119
  chat: empty({ status: "idle", messages: empty([]), pendingSends: empty([]), readReceipts: empty([]), unreadCount: 0, pagination: empty({ cursor: null, hasOlder: false, historyTruncated: false }), lastError: null }),
@@ -3106,10 +3143,16 @@ var SpaceStore = class _SpaceStore {
3106
3143
  };
3107
3144
  updateConnection(snapshot) {
3108
3145
  const current = this.getSnapshot().connection;
3109
- const error2 = snapshot.failure ? empty({ code: publicCode(snapshot.failure.code), recoverable: snapshot.failure.recoverable, message: snapshot.failure.message }) : null;
3110
- const status = snapshot.state === "live" && (INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media)) ? "reconnecting" : snapshot.state;
3111
- if (current.status === status && sameEpisode(current.episode, snapshot.episode) && sameError(current.lastError, error2)) return;
3112
- this.#replace("connection", empty({ status, episode: snapshot.episode, lastError: sameError(current.lastError, error2) ? current.lastError : error2 }));
3146
+ const error2 = connectionError(snapshot.failure);
3147
+ const next = {
3148
+ status: connectionStatus(snapshot),
3149
+ episode: snapshot.episode,
3150
+ lastError: sameError(current.lastError, error2) ? current.lastError : error2,
3151
+ episodeEnded: snapshot.episodeEnded ?? false,
3152
+ removedByHost: snapshot.removedByHost ?? false
3153
+ };
3154
+ if (sameConnection(current, next)) return;
3155
+ this.#replace("connection", empty(next));
3113
3156
  }
3114
3157
  updateSelf(value) {
3115
3158
  this.#replace("self", value);
@@ -3168,6 +3211,16 @@ function mapCode(code) {
3168
3211
  command_rejected: "command.rejected"
3169
3212
  }[code] ?? "client.internal_error";
3170
3213
  }
3214
+ function connectionError(failure2) {
3215
+ return failure2 ? empty({ code: publicCode(failure2.code), recoverable: failure2.recoverable, message: failure2.message }) : null;
3216
+ }
3217
+ function connectionStatus(snapshot) {
3218
+ const interrupted = INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media);
3219
+ return snapshot.state === "live" && interrupted ? "reconnecting" : snapshot.state;
3220
+ }
3221
+ function sameConnection(left, right) {
3222
+ return left.status === right.status && sameEpisode(left.episode, right.episode) && left.lastError === right.lastError && left.episodeEnded === right.episodeEnded && left.removedByHost === right.removedByHost;
3223
+ }
3171
3224
  function sameEpisode(left, right) {
3172
3225
  if (left === right) return true;
3173
3226
  if (!left || !right) return false;
@@ -3193,1099 +3246,173 @@ function publicCode(code) {
3193
3246
  }
3194
3247
  }
3195
3248
 
3196
- // src/feedback/types.ts
3197
- var FEEDBACK_EVIDENCE_SCHEMA_VERSION = "FeedbackEvidence/v1";
3198
- var FEEDBACK_REQUEST_SCHEMA_VERSION = "FeedbackReportRequest/v1";
3199
- var FEEDBACK_RECEIPT_SCHEMA_VERSION = "FeedbackReportReceipt/v1";
3200
- var FEEDBACK_SCREENSHOT_SCHEMA_VERSION = "FeedbackScreenshot/v1";
3201
- var FEEDBACK_LOCAL_STATE_REGISTRY_VERSION = "FeedbackLocalState/v1";
3202
- var FEEDBACK_COOKIE_REGISTRY_VERSION = "FeedbackCookies/v1";
3203
- var FEEDBACK_MAX_MESSAGE_BYTES = 8e3;
3204
- var FEEDBACK_MAX_EVIDENCE_BYTES = 128 * 1024;
3205
- var FEEDBACK_MAX_REQUEST_BYTES = 1 << 20;
3206
- var FEEDBACK_MAX_SCREENSHOT_BYTES = 450 * 1024;
3207
- var FEEDBACK_MAX_SCREENSHOT_WIDTH = 1920;
3208
- var FEEDBACK_MAX_SCREENSHOT_HEIGHT = 1080;
3209
- var FEEDBACK_MAX_TELEMETRY_EVENTS = 50;
3210
- var FEEDBACK_MAX_DIAGNOSTIC_EVENTS = 50;
3211
- var FEEDBACK_MAX_LOCAL_STATE_ENTRIES = 32;
3212
- var FEEDBACK_MAX_COOKIE_ENTRIES = 16;
3249
+ // src/space-client/core.ts
3250
+ import { Clock as Clock5, Context as Context13, Duration as Duration2, Effect as Effect12, Layer as Layer13 } from "effect";
3251
+
3252
+ // src/space-client/controller-effects.ts
3253
+ import { Context as Context12, Effect as Effect11, Layer as Layer12 } from "effect";
3254
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
3255
+
3256
+ // src/space-client/chat-controller.ts
3257
+ import { Schema as Schema3 } from "effect";
3258
+ import { Context as Context5, Effect as Effect4, Layer as Layer5 } from "effect";
3213
3259
 
3214
- // src/feedback/validation.ts
3215
- import { isDiagnosticReference, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
3260
+ // src/space-client/chat-controller-helpers.ts
3261
+ import { sha256 } from "@noble/hashes/sha2.js";
3262
+ var MAX_CHAT_PAGE_SIZE = 100;
3263
+ var MAX_LOADED_CHAT_MESSAGES = 500;
3264
+ var MAX_CHAT_TEXT_BYTES = 16384;
3265
+ var MAX_CHAT_TEXT_SCALARS = 4e3;
3216
3266
  var encoder3 = new TextEncoder();
3217
- var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
3218
- var TRACE_ID_PATTERN = /^(?=.*[1-9a-f])[0-9a-f]{32}$/u;
3219
- var SPAN_ID_PATTERN = /^(?=.*[1-9a-f])[0-9a-f]{16}$/u;
3220
- var TRACEPARENT_PATTERN = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/iu;
3221
- var SAFE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@+/=-]{0,127}$/u;
3222
- var DASHBOARD_ACTION_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/u;
3223
- var UNSAFE_VALUE_PATTERN = /(?:https?:\/\/|wss?:\/\/|bearer\s+[a-z0-9._~+\/-]+|-----begin|candidate:|v=0\r?\n|\b(?:\d{1,3}\.){3}\d{1,3}\b|[\w.+-]+@[\w.-]+\.[a-z]{2,})/iu;
3224
- var TELEMETRY_KEYS = ["version", "event_id", "journey_id", "sequence", "occurred_at", "name", "phase", "state", "origin_kind", "first_observed_layer", "upstream_visibility", "parent_event_id", "trace_id", "span_id", "traceparent", "tracestate", "attributes"];
3225
- var TELEMETRY_NAMES = ["journey.started", "journey.phase", "journey.terminal", "journey.linked", "http.request", "sync.frame", "rtc.summary", "diagnostic.timeline"];
3226
- var TELEMETRY_PHASES = ["root", "authentication", "signaling", "media", "recovery", "terminal"];
3227
- var TELEMETRY_STATES = ["started", "in_progress", "succeeded", "failed", "cancelled", "observed"];
3228
- var TELEMETRY_ORIGINS = ["client", "diagnostic", "http", "rtc", "sync"];
3229
- var TELEMETRY_VISIBILITY = ["local", "propagated"];
3230
- var FeedbackValidationError = class extends Error {
3231
- issues;
3232
- constructor(message, issues = [{ path: "$", message }]) {
3233
- super(message);
3234
- this.name = "FeedbackValidationError";
3235
- this.issues = issues;
3236
- }
3237
- };
3238
- function validateFeedbackReportRequest(input) {
3239
- const issues = [];
3240
- if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a feedback request object");
3241
- checkKeys(input, ["schema_version", "category", "message", "source", "evidence", "screenshot"], issues, "$", false);
3242
- if (property(input, "schema_version") !== FEEDBACK_REQUEST_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported feedback request schema" });
3243
- const fields = validateFeedbackRequestFields(input, issues);
3244
- validateFeedbackScreenshotConsistency(fields, issues);
3245
- if (!fields.category || !fields.source || !fields.message || !fields.evidence || !fields.screenshot_valid || issues.length > 0) return { ok: false, issues };
3246
- const request = {
3247
- schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION,
3248
- category: fields.category,
3249
- message: fields.message,
3250
- source: fields.source,
3251
- evidence: fields.evidence,
3252
- ...fields.screenshot ? { screenshot: fields.screenshot } : {}
3253
- };
3254
- return validateFeedbackRequestSize(request, issues);
3255
- }
3256
- function validateFeedbackRequestFields(input, issues) {
3257
- const category = enumValue(property(input, "category"), ["bug", "feature_request", "other"], "$.category", issues);
3258
- const source = enumValue(property(input, "source"), ["embedded", "chalk_web", "chalk_mobile", "dashboard"], "$.source", issues);
3259
- const message = validateMessage(property(input, "message"), issues);
3260
- const evidenceResult = validateFeedbackEvidence(property(input, "evidence"));
3261
- appendIssues(issues, evidenceResult);
3262
- const screenshotResult = validateRequestScreenshot(property(input, "screenshot"));
3263
- appendIssues(issues, screenshotResult);
3264
- return {
3265
- category,
3266
- source,
3267
- message,
3268
- evidence: evidenceResult.ok ? evidenceResult.value : void 0,
3269
- screenshot: screenshotResult.ok ? screenshotResult.value : void 0,
3270
- screenshot_valid: screenshotResult.ok
3271
- };
3272
- }
3273
- function validateRequestScreenshot(input) {
3274
- return input === void 0 ? { ok: true, value: void 0 } : validateFeedbackScreenshot(input);
3275
- }
3276
- function validateFeedbackScreenshotConsistency(fields, issues) {
3277
- if (!fields.evidence) return;
3278
- if (!fields.screenshot && requiresScreenshot(fields.evidence)) {
3279
- issues.push({ path: "$.screenshot", message: "a captured screenshot payload is required" });
3280
- return;
3281
- }
3282
- if (!fields.screenshot) return;
3283
- if (!requiresScreenshot(fields.evidence)) issues.push({ path: "$.evidence.screenshot.state", message: "screenshot state must indicate a capture" });
3284
- if (fields.evidence.screenshot.captured_at !== fields.screenshot.captured_at) issues.push({ path: "$.screenshot.captured_at", message: "screenshot timestamps must match" });
3285
- }
3286
- function requiresScreenshot(evidence) {
3287
- return evidence.screenshot.state === "captured" || evidence.screenshot.state === "partial";
3288
- }
3289
- function validateFeedbackRequestSize(request, issues) {
3290
- if (encodedByteLength(request) > FEEDBACK_MAX_REQUEST_BYTES) issues.push({ path: "$", message: "feedback request exceeds 1 MiB" });
3291
- return issues.length === 0 ? { ok: true, value: request } : { ok: false, issues };
3292
- }
3293
- function appendIssues(issues, result) {
3294
- if (!result.ok) issues.push(...result.issues);
3295
- }
3296
- function parseFeedbackReportRequest(input) {
3297
- const result = validateFeedbackReportRequest(input);
3298
- if (result.ok) return result.value;
3299
- throw new FeedbackValidationError("Invalid feedback request", result.issues);
3300
- }
3301
- function validateFeedbackEvidence(input) {
3302
- const issues = [];
3303
- if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a feedback evidence object");
3304
- checkKeys(input, ["schema_version", "collected_at", "app", "sdk", "platform", "connection", "scope", "correlations", "diagnostics", "local_state", "cookies", "screenshot"], issues, "$", false);
3305
- if (property(input, "schema_version") !== FEEDBACK_EVIDENCE_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported feedback evidence schema" });
3306
- const app = validateApp(property(input, "app"), issues);
3307
- const sdk = validateSDK(property(input, "sdk"), issues);
3308
- const platform = validatePlatform(property(input, "platform"), issues);
3309
- const connection = validateConnection(property(input, "connection"), issues);
3310
- const scope = validateScope(property(input, "scope"), issues);
3311
- const correlations = validateCorrelations(property(input, "correlations"), issues);
3312
- const diagnostics = validateDiagnostics(property(input, "diagnostics"), issues);
3313
- const localState = validateLocalState(property(input, "local_state"), issues);
3314
- const cookies = validateCookies(property(input, "cookies"), issues);
3315
- const screenshot = validateScreenshotState(property(input, "screenshot"), issues);
3316
- if (!sdk || !platform || !correlations || !diagnostics || !localState || !cookies || !screenshot || issues.length > 0) return { ok: false, issues };
3317
- const collectedAt = checkDateTimeValue(property(input, "collected_at"), "$.collected_at", issues);
3318
- if (!collectedAt) return { ok: false, issues };
3319
- const evidence = {
3320
- schema_version: FEEDBACK_EVIDENCE_SCHEMA_VERSION,
3321
- collected_at: collectedAt,
3322
- ...app ? { app } : {},
3323
- sdk,
3324
- platform,
3325
- ...connection ? { connection } : {},
3326
- ...scope ? { scope } : {},
3327
- correlations,
3328
- diagnostics,
3329
- local_state: localState,
3330
- cookies,
3331
- screenshot
3332
- };
3333
- if (encodedByteLength(evidence) > FEEDBACK_MAX_EVIDENCE_BYTES) return failure2("$", "feedback evidence exceeds 128 KiB");
3334
- return { ok: true, value: evidence };
3335
- }
3336
- function parseFeedbackEvidence(input) {
3337
- const result = validateFeedbackEvidence(input);
3338
- if (result.ok) return result.value;
3339
- throw new FeedbackValidationError("Invalid feedback evidence", result.issues);
3340
- }
3341
- function validateFeedbackScreenshot(input) {
3342
- const issues = [];
3343
- if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a screenshot object");
3344
- checkKeys(input, ["schema_version", "mime_type", "width", "height", "captured_at", "data_base64"], issues, "$", false);
3345
- if (property(input, "schema_version") !== FEEDBACK_SCREENSHOT_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported screenshot schema" });
3346
- const mimeType = enumValue(property(input, "mime_type"), ["image/jpeg", "image/png", "image/webp"], "$.mime_type", issues);
3347
- const width = boundedInteger2(property(input, "width"), 1, FEEDBACK_MAX_SCREENSHOT_WIDTH, "$.width", issues);
3348
- const height = boundedInteger2(property(input, "height"), 1, FEEDBACK_MAX_SCREENSHOT_HEIGHT, "$.height", issues);
3349
- const capturedAt = property(input, "captured_at");
3350
- checkDateTime(capturedAt, "$.captured_at", issues);
3351
- const dataInput = property(input, "data_base64");
3352
- const data = typeof dataInput === "string" ? dataInput : void 0;
3353
- if (!data || !validBase64(data)) issues.push({ path: "$.data_base64", message: "screenshot data must be base64" });
3354
- else if (base64ByteLength(data) > FEEDBACK_MAX_SCREENSHOT_BYTES) issues.push({ path: "$.data_base64", message: "screenshot exceeds 450 KiB" });
3355
- if (!mimeType || width === void 0 || height === void 0 || typeof capturedAt !== "string" || !data || issues.length > 0) return { ok: false, issues };
3356
- return { ok: true, value: { schema_version: FEEDBACK_SCREENSHOT_SCHEMA_VERSION, mime_type: mimeType, width, height, captured_at: capturedAt, data_base64: data } };
3357
- }
3358
- function parseFeedbackScreenshot(input) {
3359
- const result = validateFeedbackScreenshot(input);
3360
- if (result.ok) return result.value;
3361
- throw new FeedbackValidationError("Invalid feedback screenshot", result.issues);
3362
- }
3363
- function assertFeedbackReceipt(input) {
3364
- const issues = [];
3365
- if (typeof input !== "object" || input === null || Array.isArray(input)) throw new FeedbackValidationError("Invalid feedback receipt");
3366
- checkKeys(input, ["schema_version", "id", "submitted_at"], issues, "$", false);
3367
- const schemaVersion = property(input, "schema_version");
3368
- const id = property(input, "id");
3369
- const submittedAt = property(input, "submitted_at");
3370
- if (schemaVersion !== FEEDBACK_RECEIPT_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported receipt schema" });
3371
- if (!isUUID(id)) issues.push({ path: "$.id", message: "receipt ID is not a valid ID" });
3372
- checkDateTime(submittedAt, "$.submitted_at", issues);
3373
- if (issues.length > 0 || typeof id !== "string" || typeof submittedAt !== "string") throw new FeedbackValidationError("Invalid feedback receipt", issues);
3374
- return { schema_version: FEEDBACK_RECEIPT_SCHEMA_VERSION, id, submitted_at: submittedAt };
3375
- }
3376
- function assertFeedbackMessage(message) {
3377
- const issues = [];
3378
- const normalized = validateMessage(message, issues);
3379
- if (!normalized) throw new FeedbackValidationError("Invalid feedback message", issues);
3380
- return normalized;
3381
- }
3382
- function validateMessage(value, issues) {
3383
- if (typeof value !== "string") {
3384
- issues.push({ path: "$.message", message: "message must be a string" });
3385
- return void 0;
3386
- }
3387
- const message = value.trim();
3388
- if (invalidMessageLength(message)) issues.push({ path: "$.message", message: "message must be between 1 and 8,000 UTF-8 bytes" });
3389
- if (hasForbiddenMessageControl(message)) issues.push({ path: "$.message", message: "message contains a forbidden control character" });
3390
- return hasMessageIssue(issues) ? void 0 : message;
3391
- }
3392
- function invalidMessageLength(message) {
3393
- const bytes = encoder3.encode(message).byteLength;
3394
- return bytes < 1 || bytes > FEEDBACK_MAX_MESSAGE_BYTES;
3395
- }
3396
- function hasForbiddenMessageControl(message) {
3397
- for (const character of message) if (isForbiddenMessageControl(character)) return true;
3398
- return false;
3399
- }
3400
- function isForbiddenMessageControl(character) {
3401
- return isControl(character) && character !== "\n" && character !== "\r" && character !== " ";
3402
- }
3403
- function hasMessageIssue(issues) {
3404
- return issues.some((issue) => issue.path === "$.message");
3405
- }
3406
- function validateApp(input, issues) {
3407
- if (input === void 0) return void 0;
3408
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3409
- issues.push({ path: "$.app", message: "app must be an object" });
3410
- return void 0;
3411
- }
3412
- checkKeys(input, ["name", "version", "build"], issues, "$.app", false);
3413
- const name = metadata(property(input, "name"), "$.app.name", issues, true);
3414
- const version = metadata(property(input, "version"), "$.app.version", issues, false);
3415
- const build = metadata(property(input, "build"), "$.app.build", issues, false);
3416
- return name ? { name, ...version ? { version } : {}, ...build ? { build } : {} } : void 0;
3417
- }
3418
- function validateSDK(input, issues) {
3419
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3420
- issues.push({ path: "$.sdk", message: "sdk must be an object" });
3421
- return void 0;
3422
- }
3423
- checkKeys(input, ["client", "react", "react_native"], issues, "$.sdk", false);
3424
- const client = metadata(property(input, "client"), "$.sdk.client", issues, true);
3425
- const react = metadata(property(input, "react"), "$.sdk.react", issues, false);
3426
- const reactNative = metadata(property(input, "react_native"), "$.sdk.react_native", issues, false);
3427
- return client ? { client, ...react ? { react } : {}, ...reactNative ? { react_native: reactNative } : {} } : void 0;
3428
- }
3429
- function validatePlatform(input, issues) {
3430
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3431
- issues.push({ path: "$.platform", message: "platform must be an object" });
3432
- return void 0;
3433
- }
3434
- checkKeys(input, ["kind", "os_name", "os_version", "browser_name", "browser_version", "device_class", "device_model"], issues, "$.platform", false);
3435
- const kind = enumValue(property(input, "kind"), ["web", "ios", "android", "macos"], "$.platform.kind", issues);
3436
- const deviceClassInput = property(input, "device_class");
3437
- const deviceClass = deviceClassInput === void 0 ? void 0 : enumValue(deviceClassInput, ["phone", "tablet", "desktop"], "$.platform.device_class", issues);
3438
- const values = {
3439
- os_name: metadata(property(input, "os_name"), "$.platform.os_name", issues, false),
3440
- os_version: metadata(property(input, "os_version"), "$.platform.os_version", issues, false),
3441
- browser_name: metadata(property(input, "browser_name"), "$.platform.browser_name", issues, false),
3442
- browser_version: metadata(property(input, "browser_version"), "$.platform.browser_version", issues, false),
3443
- device_model: metadata(property(input, "device_model"), "$.platform.device_model", issues, false)
3444
- };
3445
- if (!kind) return void 0;
3446
- return buildFeedbackPlatform(kind, values, deviceClass);
3267
+ var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
3268
+ var chatMessageFields = ["reactionRevision", "messageId", "clientMessageId", "sequence", "participantId", "displayName", "text", "createdAt"];
3269
+ function validateChatMessage(input) {
3270
+ const attachments = input.attachments ?? [];
3271
+ if (input.text.length === 0 && attachments.length === 0) return "A chat message requires text or an attachment";
3272
+ if (Array.from(input.text).length > MAX_CHAT_TEXT_SCALARS || encoder3.encode(input.text).byteLength > MAX_CHAT_TEXT_BYTES) return `Chat text must not exceed ${MAX_CHAT_TEXT_SCALARS} characters or ${MAX_CHAT_TEXT_BYTES} bytes`;
3273
+ if (attachments.length > CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `A chat message supports at most ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} attachments`;
3274
+ if (new Set(attachments.map((attachment2) => attachment2.attachmentId)).size !== attachments.length) return "Chat attachment IDs must be unique";
3275
+ return attachments.map(validateChatAttachment).find((value) => value !== null) ?? null;
3447
3276
  }
3448
- function buildFeedbackPlatform(kind, values, deviceClass) {
3449
- return {
3450
- kind,
3451
- ...values.os_name ? { os_name: values.os_name } : {},
3452
- ...values.os_version ? { os_version: values.os_version } : {},
3453
- ...values.browser_name ? { browser_name: values.browser_name } : {},
3454
- ...values.browser_version ? { browser_version: values.browser_version } : {},
3455
- ...deviceClass ? { device_class: deviceClass } : {},
3456
- ...values.device_model ? { device_model: values.device_model } : {}
3457
- };
3277
+ function validateChatUpload(file, bytes, clientAttachmentId) {
3278
+ const fileName = "fileName" in file ? file.fileName : file.name;
3279
+ const mimeType = "mimeType" in file ? file.mimeType : file.type;
3280
+ const error2 = validateChatAttachment({ attachmentId: clientAttachmentId, fileName, mimeType, byteLength: bytes.byteLength });
3281
+ return error2 ?? { fileName, mimeType };
3458
3282
  }
3459
- function validateConnection(input, issues) {
3460
- if (input === void 0) return void 0;
3461
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3462
- issues.push({ path: "$.connection", message: "connection must be an object" });
3463
- return void 0;
3464
- }
3465
- checkKeys(input, ["state", "error_code"], issues, "$.connection", false);
3466
- const state = safeToken(property(input, "state"), "$.connection.state", issues, true);
3467
- const errorCode = safeToken(property(input, "error_code"), "$.connection.error_code", issues, false);
3468
- return state ? { state, ...errorCode ? { error_code: errorCode } : {} } : void 0;
3283
+ function chatDigest(bytes) {
3284
+ return Array.from(sha256(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
3469
3285
  }
3470
- function validateScope(input, issues) {
3471
- if (input === void 0) return void 0;
3472
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3473
- issues.push({ path: "$.scope", message: "scope must be an object" });
3474
- return void 0;
3475
- }
3476
- checkKeys(input, ["space_id", "episode_id", "participant_id"], issues, "$.scope", false);
3477
- return buildFeedbackScope(optionalUUID(property(input, "space_id"), "$.scope.space_id", issues), optionalUUID(property(input, "episode_id"), "$.scope.episode_id", issues), optionalUUID(property(input, "participant_id"), "$.scope.participant_id", issues));
3286
+ function chatMessageFor(message) {
3287
+ return Object.freeze({
3288
+ ...message,
3289
+ reactionRevision: message.reactionRevision ?? "0",
3290
+ reactions: Object.freeze((message.reactions ?? []).map((reaction) => Object.freeze({ ...reaction, participantIds: Object.freeze([...reaction.participantIds]) }))),
3291
+ attachments: Object.freeze([...message.attachments])
3292
+ });
3478
3293
  }
3479
- function buildFeedbackScope(space, episode, participant) {
3480
- if (!space && !episode && !participant) return {};
3481
- return {
3482
- ...space ? { space_id: space } : {},
3483
- ...episode ? { episode_id: episode } : {},
3484
- ...participant ? { participant_id: participant } : {}
3485
- };
3294
+ function chatReceiptFor(receipt) {
3295
+ return Object.freeze({ ...receipt });
3486
3296
  }
3487
- function validateCorrelations(input, issues) {
3488
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3489
- issues.push({ path: "$.correlations", message: "correlations must be an object" });
3490
- return void 0;
3491
- }
3492
- checkKeys(input, ["journey_id", "root_journey_id", "trace_id", "span_id", "request_id", "command_id", "diagnostic_reference"], issues, "$.correlations", false);
3493
- const journey = optionalUUID(property(input, "journey_id"), "$.correlations.journey_id", issues);
3494
- const root = optionalUUID(property(input, "root_journey_id"), "$.correlations.root_journey_id", issues);
3495
- const trace = optionalHex(property(input, "trace_id"), TRACE_ID_PATTERN, "$.correlations.trace_id", issues);
3496
- const span = optionalHex(property(input, "span_id"), SPAN_ID_PATTERN, "$.correlations.span_id", issues);
3497
- const request = optionalUUID(property(input, "request_id"), "$.correlations.request_id", issues);
3498
- const command = optionalUUID(property(input, "command_id"), "$.correlations.command_id", issues);
3499
- const diagnostic = validateDiagnosticReference(property(input, "diagnostic_reference"), issues);
3500
- return buildFeedbackCorrelations(journey, root, trace, span, request, command, diagnostic);
3501
- }
3502
- function validateDiagnosticReference(input, issues) {
3503
- if (input === void 0) return void 0;
3504
- if (typeof input !== "string" || !isDiagnosticReference(input)) {
3505
- issues.push({ path: "$.correlations.diagnostic_reference", message: "diagnostic reference is malformed" });
3506
- return void 0;
3507
- }
3508
- return input;
3297
+ function mergeChatMessage(messages, existing, incoming) {
3298
+ if (compareChatSequence(existing.reactionRevision ?? "0", incoming.reactionRevision ?? "0") > 0) incoming = { ...incoming, reactions: existing.reactions, reactionRevision: existing.reactionRevision };
3299
+ if (sameChatMessage(existing, incoming)) return messages;
3300
+ return Object.freeze(
3301
+ messages.map((message) => message === existing ? incoming : message).sort((left, right) => compareChatSequence(left.sequence, right.sequence)).slice(-MAX_LOADED_CHAT_MESSAGES)
3302
+ );
3509
3303
  }
3510
- function buildFeedbackCorrelations(journey, root, trace, span, request, command, diagnostic) {
3511
- return {
3512
- ...journey ? { journey_id: journey } : {},
3513
- ...root ? { root_journey_id: root } : {},
3514
- ...trace ? { trace_id: trace } : {},
3515
- ...span ? { span_id: span } : {},
3516
- ...request ? { request_id: request } : {},
3517
- ...command ? { command_id: command } : {},
3518
- ...diagnostic ? { diagnostic_reference: diagnostic } : {}
3519
- };
3304
+ function compareChatSequence(left, right) {
3305
+ if (left === right) return 0;
3306
+ if (!/^\d+$/u.test(left) || !/^\d+$/u.test(right)) return left < right ? -1 : 1;
3307
+ const normalizedLeft = left.replace(/^0+/u, "") || "0";
3308
+ const normalizedRight = right.replace(/^0+/u, "") || "0";
3309
+ return normalizedLeft.length === normalizedRight.length ? normalizedLeft < normalizedRight ? -1 : 1 : normalizedLeft.length - normalizedRight.length;
3520
3310
  }
3521
- function validateDiagnostics(input, issues) {
3522
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3523
- issues.push({ path: "$.diagnostics", message: "diagnostics must be an object" });
3524
- return void 0;
3525
- }
3526
- checkKeys(input, ["availability", "dropped_count", "telemetry_events", "diagnostic_events"], issues, "$.diagnostics", false);
3527
- const availability = enumValue(property(input, "availability"), ["available", "disabled", "disposed", "unavailable"], "$.diagnostics.availability", issues);
3528
- const dropped = boundedInteger2(property(input, "dropped_count"), 0, Number.MAX_SAFE_INTEGER, "$.diagnostics.dropped_count", issues);
3529
- const telemetry = validateTelemetryEvents(property(input, "telemetry_events"), issues);
3530
- const diagnostic = validateDiagnosticEvents(property(input, "diagnostic_events"), issues);
3531
- if (!availability || dropped === void 0 || !telemetry || !diagnostic) return void 0;
3532
- return { availability, dropped_count: dropped, telemetry_events: telemetry, diagnostic_events: diagnostic };
3533
- }
3534
- function validateTelemetryEvents(input, issues) {
3535
- if (!Array.isArray(input)) {
3536
- issues.push({ path: "$.diagnostics.telemetry_events", message: "telemetry_events must be an array" });
3537
- return void 0;
3538
- }
3539
- if (input.length > FEEDBACK_MAX_TELEMETRY_EVENTS) issues.push({ path: "$.diagnostics.telemetry_events", message: "too many telemetry events" });
3540
- const events = [];
3541
- input.slice(0, FEEDBACK_MAX_TELEMETRY_EVENTS).forEach((event, index) => {
3542
- const parsed = sanitizeTelemetryEvent(event, `$.diagnostics.telemetry_events[${index}]`, issues);
3543
- if (parsed) events.push(parsed);
3544
- });
3545
- return events;
3311
+ function validateChatAttachment(attachment2) {
3312
+ const fileNameBytes = encoder3.encode(attachment2.fileName).byteLength;
3313
+ if (!attachment2.attachmentId || fileNameBytes < 1 || fileNameBytes > CHALK_CHAT_ATTACHMENT_LIMITS.maximumFileNameBytes) return "Chat attachment metadata is invalid";
3314
+ if (!Number.isSafeInteger(attachment2.byteLength) || attachment2.byteLength < 1 || attachment2.byteLength > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Chat attachment metadata is invalid";
3315
+ return allowedMimeTypes.has(attachment2.mimeType) ? null : "Chat attachment MIME type is not allowed";
3546
3316
  }
3547
- function validateDiagnosticEvents(input, issues) {
3548
- if (!Array.isArray(input)) {
3549
- issues.push({ path: "$.diagnostics.diagnostic_events", message: "diagnostic_events must be an array" });
3550
- return void 0;
3551
- }
3552
- if (input.length > FEEDBACK_MAX_DIAGNOSTIC_EVENTS) issues.push({ path: "$.diagnostics.diagnostic_events", message: "too many diagnostic events" });
3553
- const events = [];
3554
- input.slice(0, FEEDBACK_MAX_DIAGNOSTIC_EVENTS).forEach((event, index) => {
3555
- if (typeof event !== "object" || event === null || Array.isArray(event)) {
3556
- issues.push({ path: `$.diagnostics.diagnostic_events[${index}]`, message: "diagnostic event must be an object" });
3557
- return;
3558
- }
3559
- const attributes = redactDiagnosticAttributes(property(event, "attributes")).attributes;
3560
- const sanitized = { ...event, ...Object.keys(attributes).length > 0 ? { attributes } : {} };
3561
- try {
3562
- events.push(parseDiagnosticEventDraft(sanitized));
3563
- } catch {
3564
- issues.push({ path: `$.diagnostics.diagnostic_events[${index}]`, message: "diagnostic event failed the diagnostics contract" });
3565
- }
3317
+ function sameChatMessage(left, right) {
3318
+ return chatMessageFields.every((field) => left[field] === right[field]) && left.attachments.length === right.attachments.length && left.attachments.every((attachment2, index) => {
3319
+ const candidate = right.attachments[index];
3320
+ return candidate?.attachmentId === attachment2.attachmentId && candidate.fileName === attachment2.fileName && candidate.mimeType === attachment2.mimeType && candidate.byteLength === attachment2.byteLength;
3566
3321
  });
3567
- return events;
3568
3322
  }
3569
- function sanitizeTelemetryEvent(input, path, issues) {
3570
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3571
- issues.push({ path, message: "telemetry event must be an object" });
3572
- return void 0;
3573
- }
3574
- checkKeys(input, TELEMETRY_KEYS, issues, path, false);
3575
- const identity = validateTelemetryEventIdentity(input, path, issues);
3576
- const shape = validateTelemetryEventShape(input, path, issues);
3577
- if (!identity || !shape) return void 0;
3578
- const optional = validateTelemetryEventOptionalFields(input, path, issues);
3579
- return buildSanitizedTelemetryEvent(identity, shape, optional);
3580
- }
3581
- function buildSanitizedTelemetryEvent(identity, shape, optional) {
3582
- const event = {
3583
- version: 1,
3584
- event_id: identity.eventId,
3585
- journey_id: identity.journeyId,
3586
- sequence: identity.sequence,
3587
- occurred_at: identity.occurredAt,
3588
- name: shape.name,
3589
- phase: shape.phase,
3590
- state: shape.state,
3591
- origin_kind: shape.origin,
3592
- first_observed_layer: shape.firstObserved,
3593
- upstream_visibility: shape.visibility,
3594
- ...optional.parentEvent ? { parent_event_id: optional.parentEvent } : {},
3595
- ...optional.traceId ? { trace_id: optional.traceId } : {},
3596
- ...optional.spanId ? { span_id: optional.spanId } : {},
3597
- ...optional.traceparent ? { traceparent: optional.traceparent } : {},
3598
- ...optional.tracestate ? { tracestate: optional.tracestate } : {},
3599
- ...optional.attributes ? { attributes: optional.attributes } : {}
3600
- };
3601
- return isSanitizedTelemetryEvent(event) ? event : void 0;
3602
- }
3603
- function validateTelemetryEventIdentity(input, path, issues) {
3604
- const version = property(input, "version") === 1;
3605
- if (!version) issues.push({ path: `${path}.version`, message: "unsupported telemetry version" });
3606
- const eventId = requiredUUID(property(input, "event_id"), `${path}.event_id`, issues);
3607
- const journeyId = requiredUUID(property(input, "journey_id"), `${path}.journey_id`, issues);
3608
- const sequence = boundedInteger2(property(input, "sequence"), 0, Number.MAX_SAFE_INTEGER, `${path}.sequence`, issues);
3609
- const occurredAt = checkDateTimeValue(property(input, "occurred_at"), `${path}.occurred_at`, issues);
3610
- if (!version || !eventId || !journeyId || sequence === void 0 || !occurredAt) return void 0;
3611
- return { eventId, journeyId, sequence, occurredAt };
3612
- }
3613
- function validateTelemetryEventShape(input, path, issues) {
3614
- const name = enumValue(property(input, "name"), TELEMETRY_NAMES, `${path}.name`, issues);
3615
- const phase = enumValue(property(input, "phase"), TELEMETRY_PHASES, `${path}.phase`, issues);
3616
- const state = enumValue(property(input, "state"), TELEMETRY_STATES, `${path}.state`, issues);
3617
- const origin = enumValue(property(input, "origin_kind"), TELEMETRY_ORIGINS, `${path}.origin_kind`, issues);
3618
- const firstObserved = enumValue(property(input, "first_observed_layer"), TELEMETRY_ORIGINS, `${path}.first_observed_layer`, issues);
3619
- const visibility = enumValue(property(input, "upstream_visibility"), TELEMETRY_VISIBILITY, `${path}.upstream_visibility`, issues);
3620
- if (!name || !phase || !state || !origin || !firstObserved || !visibility) return void 0;
3621
- return { name, phase, state, origin, firstObserved, visibility };
3622
- }
3623
- function validateTelemetryEventOptionalFields(input, path, issues) {
3624
- return {
3625
- parentEvent: optionalUUID(property(input, "parent_event_id"), `${path}.parent_event_id`, issues),
3626
- traceId: optionalHex(property(input, "trace_id"), TRACE_ID_PATTERN, `${path}.trace_id`, issues),
3627
- spanId: optionalHex(property(input, "span_id"), SPAN_ID_PATTERN, `${path}.span_id`, issues),
3628
- traceparent: optionalTraceparent(property(input, "traceparent"), `${path}.traceparent`, issues),
3629
- tracestate: optionalSafeText(property(input, "tracestate"), `${path}.tracestate`, 512, issues),
3630
- attributes: sanitizeTelemetryAttributes(property(input, "attributes"), `${path}.attributes`, issues)
3323
+
3324
+ // src/space-client/chat-controller.ts
3325
+ var ChatControllerService = class extends Context5.Service()("@chalk/client/ChatController") {
3326
+ };
3327
+ var makeChatController = (input) => Effect4.gen(function* () {
3328
+ const scope = yield* Effect4.scope;
3329
+ const context = yield* Effect4.context();
3330
+ const fork = (effect) => {
3331
+ void Effect4.runForkWith(context)(Effect4.forkIn(effect, scope).pipe(Effect4.asVoid));
3631
3332
  };
3632
- }
3633
- function isSanitizedTelemetryEvent(value) {
3634
- switch (value.name) {
3635
- case "sync.frame":
3636
- return value.origin_kind === "sync";
3637
- case "http.request":
3638
- return value.origin_kind === "http";
3639
- case "rtc.summary":
3640
- return value.origin_kind === "rtc";
3641
- case "diagnostic.timeline":
3642
- return value.origin_kind === "diagnostic";
3643
- case "journey.started":
3644
- return isJourneyStartedEvent(value);
3645
- case "journey.phase":
3646
- return isJourneyPhaseEvent(value);
3647
- case "journey.terminal":
3648
- return isJourneyTerminalEvent(value);
3649
- case "journey.linked":
3650
- return isJourneyLinkedEvent(value);
3651
- }
3652
- }
3653
- function isJourneyStartedEvent(value) {
3654
- return value.origin_kind === "client" && value.phase === "root" && value.state === "started";
3655
- }
3656
- function isJourneyPhaseEvent(value) {
3657
- return value.origin_kind === "client" && value.phase !== "root" && value.phase !== "terminal" && (value.state === "in_progress" || value.state === "observed");
3658
- }
3659
- function isJourneyTerminalEvent(value) {
3660
- return value.origin_kind === "client" && value.phase === "terminal" && (value.state === "succeeded" || value.state === "failed" || value.state === "cancelled");
3661
- }
3662
- function isJourneyLinkedEvent(value) {
3663
- return value.origin_kind === "client" && value.phase === "root" && value.state === "observed" && typeof value.parent_event_id === "string";
3664
- }
3665
- function sanitizeTelemetryAttributes(input, path, issues) {
3666
- if (input === void 0) return void 0;
3667
- if (!isTelemetryAttributeObject(input)) {
3668
- issues.push({ path, message: "telemetry attributes are not safe" });
3669
- return void 0;
3670
- }
3671
- const redacted = redactDiagnosticAttributes(input).attributes;
3672
- const normalized = normalizeTelemetryAttributes(redacted);
3673
- if (!normalized) return void 0;
3674
- for (const [key, value] of Object.entries(normalized)) if (typeof value === "string" && (value.length > 256 || isControlText(value) || UNSAFE_VALUE_PATTERN.test(value))) issues.push({ path: `${path}.${key}`, message: "telemetry attribute value is unsafe" });
3675
- return normalized;
3676
- }
3677
- function isTelemetryAttributeObject(value) {
3678
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3679
- return Object.keys(value).every((key) => {
3680
- const entry = property(value, key);
3681
- return typeof entry === "boolean" || typeof entry === "string" || typeof entry === "number" && Number.isFinite(entry);
3682
- });
3683
- }
3684
- function validateLocalState(input, issues) {
3685
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3686
- issues.push({ path: "$.local_state", message: "local_state must be an object" });
3687
- return void 0;
3688
- }
3689
- checkKeys(input, ["registry_version", "entries"], issues, "$.local_state", false);
3690
- if (property(input, "registry_version") !== FEEDBACK_LOCAL_STATE_REGISTRY_VERSION) issues.push({ path: "$.local_state.registry_version", message: "unsupported local state registry" });
3691
- const entriesInput = property(input, "entries");
3692
- if (!Array.isArray(entriesInput)) {
3693
- issues.push({ path: "$.local_state.entries", message: "local state entries must be an array" });
3694
- return void 0;
3695
- }
3696
- if (entriesInput.length > FEEDBACK_MAX_LOCAL_STATE_ENTRIES) issues.push({ path: "$.local_state.entries", message: "too many local state entries" });
3697
- const entries = [];
3698
- const seen = /* @__PURE__ */ new Set();
3699
- entriesInput.slice(0, FEEDBACK_MAX_LOCAL_STATE_ENTRIES).forEach((entry, index) => {
3700
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
3701
- issues.push({ path: `$.local_state.entries[${index}]`, message: "local state entry must be an object" });
3702
- return;
3703
- }
3704
- checkKeys(entry, ["key", "value"], issues, `$.local_state.entries[${index}]`, false);
3705
- const key = property(entry, "key");
3706
- const entryValue = property(entry, "value");
3707
- if (typeof key !== "string" || key.length === 0 || seen.has(key)) {
3708
- issues.push({ path: `$.local_state.entries[${index}].key`, message: "local state key is invalid or duplicated" });
3709
- return;
3710
- }
3711
- seen.add(key);
3712
- if (!validLocalStateEntry(key, entryValue)) issues.push({ path: `$.local_state.entries[${index}]`, message: "local state entry is not allowlisted" });
3713
- else if (isFeedbackLocalStateValue(entryValue)) entries.push({ key, value: entryValue });
3333
+ const controller = yield* Effect4.sync(() => {
3334
+ let instance = null;
3335
+ const transport = input.createTransport?.({ token: () => Effect4.runPromiseWith(context)(input.connection.getSyncToken()) }) ?? null;
3336
+ instance = new ChatControllerRuntime(input.connection, input.store, transport, input.apiBaseUrl ?? "https://api.chalkmeet.com", input.fetch ?? globalThis.fetch, fork, input.offline);
3337
+ return instance;
3714
3338
  });
3715
- return { registry_version: FEEDBACK_LOCAL_STATE_REGISTRY_VERSION, entries };
3716
- }
3717
- function validateCookies(input, issues) {
3718
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3719
- issues.push({ path: "$.cookies", message: "cookies must be an object" });
3720
- return void 0;
3721
- }
3722
- checkKeys(input, ["registry_version", "entries"], issues, "$.cookies", false);
3723
- if (property(input, "registry_version") !== FEEDBACK_COOKIE_REGISTRY_VERSION) issues.push({ path: "$.cookies.registry_version", message: "unsupported cookie registry" });
3724
- const entriesInput = property(input, "entries");
3725
- if (!Array.isArray(entriesInput)) {
3726
- issues.push({ path: "$.cookies.entries", message: "cookie entries must be an array" });
3727
- return void 0;
3728
- }
3729
- if (entriesInput.length > FEEDBACK_MAX_COOKIE_ENTRIES) issues.push({ path: "$.cookies.entries", message: "too many cookie entries" });
3730
- const entries = [];
3731
- const seen = /* @__PURE__ */ new Set();
3732
- for (const [index, entry] of entriesInput.slice(0, FEEDBACK_MAX_COOKIE_ENTRIES).entries()) {
3733
- const parsed = validateCookieEntry(entry, index, seen, issues);
3734
- if (parsed) entries.push(parsed);
3735
- }
3736
- return { registry_version: FEEDBACK_COOKIE_REGISTRY_VERSION, entries };
3737
- }
3738
- function validateCookieEntry(input, index, seen, issues) {
3739
- const path = `$.cookies.entries[${index}]`;
3740
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3741
- issues.push({ path, message: "cookie entry must be an object" });
3742
- return void 0;
3743
- }
3744
- checkKeys(input, ["name", "present", "value"], issues, path, false);
3745
- const name = enumValue(property(input, "name"), ["chalk_theme", "chalk_sidebar_state", "account", "csrf"], `${path}.name`, issues);
3746
- const present = property(input, "present");
3747
- if (!name || typeof present !== "boolean" || seen.has(name)) {
3748
- issues.push({ path, message: "cookie entry is invalid or duplicated" });
3749
- return void 0;
3750
- }
3751
- seen.add(name);
3752
- return buildCookieEntry(name, present, property(input, "value"), path, issues);
3753
- }
3754
- function buildCookieEntry(name, present, input, path, issues) {
3755
- const value = cookieValue(name, input, `${path}.value`, issues);
3756
- if (!present && value !== void 0) issues.push({ path: `${path}.value`, message: "absent cookies cannot include values" });
3757
- return { name, present, ...value !== void 0 ? { value } : {} };
3758
- }
3759
- function cookieValue(name, input, path, issues) {
3760
- if (input === void 0) return void 0;
3761
- if (typeof input !== "string") {
3762
- issues.push({ path, message: "cookie value is invalid" });
3763
- return void 0;
3764
- }
3765
- if (name === "account" || name === "csrf") {
3766
- issues.push({ path, message: "account and CSRF cookie values are never collected" });
3767
- return void 0;
3768
- }
3769
- if (name === "chalk_theme") {
3770
- const value2 = ["light", "dark", "system"];
3771
- const match2 = value2.find((candidate) => candidate === input);
3772
- if (match2 !== void 0) return match2;
3773
- issues.push({ path, message: "theme cookie value is invalid" });
3774
- return void 0;
3775
- }
3776
- const value = ["true", "false"];
3777
- const match = value.find((candidate) => candidate === input);
3778
- if (match !== void 0) return match;
3779
- issues.push({ path, message: "sidebar cookie value is invalid" });
3780
- return void 0;
3781
- }
3782
- function validateScreenshotState(input, issues) {
3783
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
3784
- issues.push({ path: "$.screenshot", message: "screenshot state must be an object" });
3785
- return void 0;
3786
- }
3787
- checkKeys(input, ["state", "captured_at", "failure_code"], issues, "$.screenshot", false);
3788
- const state = enumValue(property(input, "state"), ["captured", "partial", "removed", "unavailable"], "$.screenshot.state", issues);
3789
- const failureInput = property(input, "failure_code");
3790
- const failure3 = failureInput === void 0 ? void 0 : enumValue(failureInput, ["capture_failed", "unsupported", "tainted", "secure_surface", "too_large"], "$.screenshot.failure_code", issues);
3791
- const capturedAtInput = property(input, "captured_at");
3792
- const capturedAt = capturedAtInput === void 0 ? void 0 : checkDateTimeValue(capturedAtInput, "$.screenshot.captured_at", issues);
3793
- validateScreenshotStateConsistency(state, capturedAt, failure3, issues);
3794
- if (!state) return void 0;
3795
- return { state, ...capturedAt ? { captured_at: capturedAt } : {}, ...failure3 ? { failure_code: failure3 } : {} };
3796
- }
3797
- function validateScreenshotStateConsistency(state, capturedAt, failure3, issues) {
3798
- const captured = state === "captured" || state === "partial";
3799
- if (captured && !capturedAt) issues.push({ path: "$.screenshot.captured_at", message: "captured screenshots require a timestamp" });
3800
- if (captured && failure3) issues.push({ path: "$.screenshot.failure_code", message: "captured screenshots cannot have a failure code" });
3801
- }
3802
- function validLocalStateEntry(key, value) {
3803
- if (key === "chalk.tenant-hint") return isUUID(value);
3804
- if (key === "chalk.web.telemetry.v1" || key === "chalk.mobile.telemetry.v1") return validTelemetrySummary(value);
3805
- if (key.startsWith("chalk.dashboard-request.")) return DASHBOARD_ACTION_PATTERN.test(key.slice("chalk.dashboard-request.".length)) && value === true;
3806
- return false;
3807
- }
3808
- function isFeedbackLocalStateValue(value) {
3809
- if (typeof value === "string" || typeof value === "boolean") return true;
3810
- if (typeof value === "number") return Number.isFinite(value);
3811
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).every((key) => {
3812
- const entry = property(value, key);
3813
- return typeof entry === "number" && Number.isFinite(entry);
3814
- });
3815
- }
3816
- function validTelemetrySummary(value) {
3817
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3818
- const keys = ["pending_count", "timeline_count", "dropped_count"];
3819
- if (!keys.some((key) => property(value, key) !== void 0)) return false;
3820
- return keys.every((key) => {
3821
- const entry = property(value, key);
3822
- return entry === void 0 || typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 0 && entry <= 500;
3823
- });
3824
- }
3825
- function metadata(value, path, issues, required) {
3826
- if (value === void 0 && !required) return void 0;
3827
- if (typeof value !== "string" || !isSafeMetadata(value)) {
3828
- issues.push({ path, message: required ? "metadata is required and unsafe" : "metadata is unsafe" });
3829
- return void 0;
3830
- }
3831
- return value;
3832
- }
3833
- function isSafeMetadata(value) {
3834
- return value.length > 0 && value.length <= 128 && value.trim() === value && !isControlText(value) && !UNSAFE_VALUE_PATTERN.test(value);
3835
- }
3836
- function safeToken(value, path, issues, required) {
3837
- if (value === void 0 && !required) return void 0;
3838
- if (typeof value !== "string" || value.length === 0 || value.length > 128 || !SAFE_TOKEN_PATTERN.test(value) || UNSAFE_VALUE_PATTERN.test(value)) {
3839
- issues.push({ path, message: required ? "token is required and unsafe" : "token is unsafe" });
3840
- return void 0;
3841
- }
3842
- return value;
3843
- }
3844
- function optionalUUID(value, path, issues) {
3845
- if (value === void 0) return void 0;
3846
- return requiredUUID(value, path, issues);
3847
- }
3848
- function requiredUUID(value, path, issues) {
3849
- if (typeof value !== "string" || !isUUID(value)) {
3850
- issues.push({ path, message: "expected a non-zero UUID" });
3851
- return void 0;
3852
- }
3853
- return value;
3854
- }
3855
- function optionalHex(value, pattern, path, issues) {
3856
- if (value === void 0) return void 0;
3857
- if (typeof value !== "string" || !pattern.test(value.toLowerCase()) || value !== value.toLowerCase()) {
3858
- issues.push({ path, message: "expected a lower-case non-zero hexadecimal ID" });
3859
- return void 0;
3860
- }
3861
- return value;
3862
- }
3863
- function optionalTraceparent(value, path, issues) {
3864
- if (value === void 0) return void 0;
3865
- if (typeof value !== "string" || !TRACEPARENT_PATTERN.test(value)) {
3866
- issues.push({ path, message: "traceparent is malformed" });
3867
- return void 0;
3868
- }
3869
- return value;
3870
- }
3871
- function optionalSafeText(value, path, length, issues) {
3872
- if (value === void 0) return void 0;
3873
- if (typeof value !== "string" || value.length > length || isControlText(value) || UNSAFE_VALUE_PATTERN.test(value)) {
3874
- issues.push({ path, message: "text is unsafe" });
3875
- return void 0;
3876
- }
3877
- return value;
3878
- }
3879
- function checkDateTime(value, path, issues) {
3880
- checkDateTimeValue(value, path, issues);
3881
- }
3882
- function checkDateTimeValue(value, path, issues) {
3883
- if (typeof value !== "string" || value.length > 64 || !Number.isFinite(Date.parse(value))) {
3884
- issues.push({ path, message: "expected an ISO date-time" });
3885
- return void 0;
3886
- }
3887
- return value;
3888
- }
3889
- function boundedInteger2(value, minimum, maximum, path, issues) {
3890
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
3891
- issues.push({ path, message: "expected a bounded integer" });
3892
- return void 0;
3893
- }
3894
- return value;
3895
- }
3896
- function enumValue(value, values, path, issues) {
3897
- const match = values.find((candidate) => candidate === value);
3898
- if (match !== void 0) return match;
3899
- issues.push({ path, message: `expected one of: ${values.join(", ")}` });
3900
- return void 0;
3901
- }
3902
- function checkKeys(value, allowed, issues, path, allowNullable) {
3903
- const allowedSet = new Set(allowed);
3904
- for (const key of Object.keys(value)) if (!allowedSet.has(key) && !(allowNullable && key === "value")) issues.push({ path: `${path}.${key}`, message: "unknown property" });
3905
- }
3906
- function failure2(path, message) {
3907
- return { ok: false, issues: [{ path, message }] };
3908
- }
3909
- function property(value, key) {
3910
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
3911
- return Object.prototype.hasOwnProperty.call(value, key) ? Reflect.get(value, key) : void 0;
3912
- }
3913
- function isUUID(value) {
3914
- return typeof value === "string" && UUID_PATTERN.test(value) && value.replaceAll("0", "").replaceAll("-", "") !== "";
3915
- }
3916
- function validBase64(value) {
3917
- return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value);
3918
- }
3919
- function base64ByteLength(value) {
3920
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
3921
- return Math.floor(value.length * 3 / 4) - padding;
3922
- }
3923
- function encodedByteLength(value) {
3924
- return encoder3.encode(JSON.stringify(value)).byteLength;
3925
- }
3926
- function isControl(character) {
3927
- return /\p{Cc}/u.test(character);
3928
- }
3929
- function isControlText(value) {
3930
- for (const character of value) if (isControl(character)) return true;
3931
- return false;
3932
- }
3933
-
3934
- // src/feedback/collector.ts
3935
- import { validateDiagnosticEventDraft } from "@q9labsai/diagnostics-contracts";
3936
-
3937
- // src/feedback/context.ts
3938
- function safeSubject(snapshot) {
3939
- const subject = snapshot.subject;
3940
- if (!subject) return void 0;
3941
- return {
3942
- tenant_id: subject.tenantId,
3943
- space_id: subject.spaceId,
3944
- episode_id: subject.episodeId,
3945
- participant_id: subject.participantId,
3946
- participant_generation: subject.participantGeneration
3947
- };
3948
- }
3949
-
3950
- // src/feedback/collector.ts
3951
- function collectFeedbackEvidence(input, context = {}) {
3952
- const collectedAt = new Date((context.now ?? Date.now)()).toISOString();
3953
- const evidence = {
3954
- schema_version: FEEDBACK_EVIDENCE_SCHEMA_VERSION,
3955
- collected_at: collectedAt,
3956
- ...input.app ? { app: input.app } : {},
3957
- sdk: input.sdk,
3958
- platform: input.platform,
3959
- ...context.connection ? { connection: context.connection } : {},
3960
- ...context.scope ? { scope: context.scope } : {},
3961
- correlations: context.correlations ?? {},
3962
- diagnostics: context.diagnostics ?? unavailableDiagnostics(),
3963
- local_state: { registry_version: FEEDBACK_LOCAL_STATE_REGISTRY_VERSION, entries: collectLocalState(input.local_state) },
3964
- cookies: { registry_version: FEEDBACK_COOKIE_REGISTRY_VERSION, entries: collectCookies(input.cookies) },
3965
- screenshot: screenshotState(input.screenshot)
3966
- };
3967
- return parseFeedbackEvidence(evidence);
3968
- }
3969
- function collectFeedbackEvidenceFromContext(input, context) {
3970
- const snapshot = context.connection();
3971
- const subject = safeSubject(snapshot);
3972
- const activeTelemetry = context.telemetry;
3973
- const activeDiagnostics = context.diagnosticContext();
3974
- const correlationContext = activeTelemetry ?? activeDiagnostics;
3975
- const trace = parseTraceparent(correlationContext?.traceparent);
3976
- const diagnostics = context.diagnosticSnapshot();
3977
- const availability = context.diagnosticAvailability();
3978
- return collectFeedbackEvidence(input, {
3979
- now: context.now,
3980
- connection: {
3981
- state: snapshot.state,
3982
- ...snapshot.failure?.code ? { error_code: snapshot.failure.code } : {}
3983
- },
3984
- ...subject ? {
3985
- scope: {
3986
- space_id: subject.space_id,
3987
- episode_id: subject.episode_id,
3988
- participant_id: subject.participant_id
3989
- }
3990
- } : {},
3991
- correlations: {
3992
- ...correlationContext ? { journey_id: correlationContext.journeyId } : {},
3993
- ...activeTelemetry ? { root_journey_id: activeTelemetry.rootJourneyId } : {},
3994
- ...trace ? { trace_id: trace.traceId, span_id: trace.spanId } : {}
3995
- },
3996
- diagnostics: {
3997
- availability,
3998
- dropped_count: diagnostics.dropped,
3999
- telemetry_events: input.local_state?.telemetry?.events ?? [],
4000
- diagnostic_events: diagnostics.events.filter(isDiagnosticEvent)
4001
- }
4002
- });
4003
- }
4004
- function collectLocalState(input) {
4005
- if (!input) return [];
4006
- const entries = [];
4007
- if (input.telemetry) {
4008
- const summary = telemetrySummary(input.telemetry);
4009
- if (summary) entries.push({ key: input.telemetry.storage_key ?? "chalk.web.telemetry.v1", value: summary });
4010
- }
4011
- if (input.tenant_hint && validUUID(input.tenant_hint)) entries.push({ key: "chalk.tenant-hint", value: input.tenant_hint });
4012
- for (const request of input.dashboard_requests ?? []) {
4013
- if (!request.pending || !/^[a-z][a-z0-9_-]{0,63}$/u.test(request.action)) continue;
4014
- entries.push({ key: `chalk.dashboard-request.${request.action}`, value: true });
4015
- }
4016
- return entries.slice(0, 32);
4017
- }
4018
- function collectCookies(input) {
4019
- if (!input) return [];
4020
- const entries = [];
4021
- if (input.theme !== void 0) entries.push({ name: "chalk_theme", present: true, value: input.theme });
4022
- if (input.sidebar_state !== void 0) entries.push({ name: "chalk_sidebar_state", present: true, value: input.sidebar_state ? "true" : "false" });
4023
- if (input.account_present !== void 0) entries.push({ name: "account", present: input.account_present });
4024
- if (input.csrf_present !== void 0) entries.push({ name: "csrf", present: input.csrf_present });
4025
- return entries;
4026
- }
4027
- function telemetrySummary(input) {
4028
- const summary = {
4029
- ...input.pending_count !== void 0 && boundedCount(input.pending_count) ? { pending_count: input.pending_count } : {},
4030
- ...input.timeline_count !== void 0 && boundedCount(input.timeline_count) ? { timeline_count: input.timeline_count } : {},
4031
- ...input.dropped_count !== void 0 && boundedCount(input.dropped_count) ? { dropped_count: input.dropped_count } : {}
4032
- };
4033
- return Object.keys(summary).length > 0 ? summary : void 0;
4034
- }
4035
- function boundedCount(value) {
4036
- return Number.isSafeInteger(value) && value >= 0 && value <= 500;
4037
- }
4038
- function screenshotState(input) {
4039
- if (!input) return { state: "unavailable", failure_code: "unsupported" };
4040
- if (input.state === "captured" || input.state === "partial") return { state: input.state, captured_at: input.captured_at };
4041
- return "failure_code" in input && input.failure_code ? { state: input.state, failure_code: input.failure_code } : { state: input.state };
4042
- }
4043
- function unavailableDiagnostics() {
4044
- return { availability: "unavailable", dropped_count: 0, telemetry_events: [], diagnostic_events: [] };
4045
- }
4046
- function isDiagnosticEvent(value) {
4047
- return validateDiagnosticEventDraft(value).ok;
4048
- }
4049
- function validUUID(value) {
4050
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value) && value.replaceAll("0", "").replaceAll("-", "") !== "";
4051
- }
4052
-
4053
- // src/feedback/transport.ts
4054
- var FeedbackTransportError = class extends Error {
4055
- code;
4056
- status;
4057
- recoverable;
4058
- constructor(input) {
4059
- super(input.message);
4060
- this.name = "FeedbackTransportError";
4061
- this.code = input.code;
4062
- this.recoverable = input.recoverable;
4063
- this.status = input.status;
4064
- }
4065
- };
4066
- function createFeedbackTransport(options) {
4067
- const fetchImplementation = options.fetch ?? globalThis.fetch;
4068
- const baseUrl = options.baseUrl.replace(/\/+$/u, "");
4069
- return {
4070
- send: async ({ request, idempotencyKey }) => {
4071
- const parsedRequest = parseFeedbackReportRequest(request);
4072
- validateTransportBounds(parsedRequest);
4073
- validateIdempotencyKey(idempotencyKey);
4074
- const credential = options.credential();
4075
- if (!credential) throw new FeedbackTransportError({ code: "unauthenticated", recoverable: false, message: "Feedback authentication is unavailable.", status: 401 });
4076
- const headers = feedbackHeaders(idempotencyKey, credential.token, options.telemetry?.());
4077
- const response = await sendFeedbackRequest(fetchImplementation, `${baseUrl}/v1/feedback-reports`, headers, parsedRequest);
4078
- return parseFeedbackResponse(response);
4079
- }
4080
- };
4081
- }
4082
- function validateIdempotencyKey(idempotencyKey) {
4083
- if (idempotencyKey.length < 16 || idempotencyKey.length > 128 || !/^[A-Za-z0-9_-]+$/u.test(idempotencyKey)) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback idempotency key is invalid.", status: 400 });
4084
- }
4085
- function feedbackHeaders(idempotencyKey, credentialToken, telemetry) {
4086
- const headers = new Headers({ "content-type": "application/json", authorization: `Bearer ${credentialToken}`, "idempotency-key": idempotencyKey });
4087
- if (!telemetry) return headers;
4088
- for (const [key, value] of Object.entries(journeyHeaders(telemetry))) headers.set(key, value);
4089
- return headers;
4090
- }
4091
- async function sendFeedbackRequest(fetchImplementation, url, headers, request) {
4092
- try {
4093
- return await fetchImplementation(url, { method: "POST", headers, body: JSON.stringify(request) });
4094
- } catch {
4095
- throw new FeedbackTransportError({ code: "unavailable", recoverable: true, message: "Feedback service is unavailable." });
4096
- }
4097
- }
4098
- async function parseFeedbackResponse(response) {
4099
- if (!response.ok) throw responseError3(response.status);
4100
- let body;
4101
- try {
4102
- body = await response.json();
4103
- } catch {
4104
- throw new FeedbackTransportError({ code: "invalid_response", recoverable: true, message: "Feedback service returned an invalid receipt.", status: response.status });
4105
- }
4106
- try {
4107
- return assertFeedbackReceipt(body);
4108
- } catch {
4109
- throw new FeedbackTransportError({ code: "invalid_response", recoverable: true, message: "Feedback service returned an invalid receipt.", status: response.status });
4110
- }
4111
- }
4112
- function validateTransportBounds(request) {
4113
- const evidenceBytes = new TextEncoder().encode(JSON.stringify(request.evidence)).byteLength;
4114
- const requestBytes = new TextEncoder().encode(JSON.stringify(request)).byteLength;
4115
- if (evidenceBytes > FEEDBACK_MAX_EVIDENCE_BYTES) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback evidence exceeds the size limit." });
4116
- if (requestBytes > FEEDBACK_MAX_REQUEST_BYTES) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request exceeds the size limit." });
4117
- }
4118
- function responseError3(status) {
4119
- if (status === 400) return new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request was rejected.", status });
4120
- if (status === 413) return new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request exceeds the size limit.", status });
4121
- if (status === 401) return new FeedbackTransportError({ code: "unauthenticated", recoverable: false, message: "Feedback authentication was rejected.", status });
4122
- if (status === 403) return new FeedbackTransportError({ code: "forbidden", recoverable: false, message: "Feedback submission is not allowed.", status });
4123
- if (status === 409) return new FeedbackTransportError({ code: "conflict", recoverable: false, message: "Feedback idempotency key conflicts with another submission.", status });
4124
- if (status === 429) return new FeedbackTransportError({ code: "rate_limited", recoverable: true, message: "Feedback submissions are temporarily limited.", status });
4125
- return new FeedbackTransportError({ code: "unavailable", recoverable: true, message: "Feedback service is unavailable.", status });
4126
- }
4127
-
4128
- // src/space-client/core.ts
4129
- import { Clock as Clock5, Context as Context13, Duration as Duration2, Effect as Effect12, Layer as Layer13 } from "effect";
4130
-
4131
- // src/space-client/controller-effects.ts
4132
- import { Context as Context12, Effect as Effect11, Layer as Layer12 } from "effect";
4133
- import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
4134
-
4135
- // src/space-client/chat-controller.ts
4136
- import { Context as Context5, Effect as Effect4, Layer as Layer5 } from "effect";
4137
-
4138
- // src/space-client/chat-controller-helpers.ts
4139
- import { sha256 } from "@noble/hashes/sha2.js";
4140
- var MAX_CHAT_PAGE_SIZE = 100;
4141
- var MAX_LOADED_CHAT_MESSAGES = 500;
4142
- var MAX_CHAT_TEXT_BYTES = 16384;
4143
- var MAX_CHAT_TEXT_SCALARS = 4e3;
4144
- var encoder4 = new TextEncoder();
4145
- var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
4146
- function validateChatMessage(input) {
4147
- const attachments = input.attachments ?? [];
4148
- if (input.text.length === 0 && attachments.length === 0) return "A chat message requires text or an attachment";
4149
- if (Array.from(input.text).length > MAX_CHAT_TEXT_SCALARS || encoder4.encode(input.text).byteLength > MAX_CHAT_TEXT_BYTES) return `Chat text must not exceed ${MAX_CHAT_TEXT_SCALARS} characters or ${MAX_CHAT_TEXT_BYTES} bytes`;
4150
- if (attachments.length > CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `A chat message supports at most ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} attachments`;
4151
- if (new Set(attachments.map((attachment2) => attachment2.attachmentId)).size !== attachments.length) return "Chat attachment IDs must be unique";
4152
- return attachments.map(validateChatAttachment).find((value) => value !== null) ?? null;
4153
- }
4154
- function validateChatUpload(file, bytes, clientAttachmentId) {
4155
- const fileName = "fileName" in file ? file.fileName : file.name;
4156
- const mimeType = "mimeType" in file ? file.mimeType : file.type;
4157
- const error2 = validateChatAttachment({ attachmentId: clientAttachmentId, fileName, mimeType, byteLength: bytes.byteLength });
4158
- return error2 ?? { fileName, mimeType };
4159
- }
4160
- function chatDigest(bytes) {
4161
- return Array.from(sha256(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
4162
- }
4163
- function chatMessageFor(message) {
4164
- return Object.freeze({ ...message, attachments: Object.freeze([...message.attachments]) });
4165
- }
4166
- function chatReceiptFor(receipt) {
4167
- return Object.freeze({ ...receipt });
4168
- }
4169
- function mergeChatMessage(messages, existing, incoming) {
4170
- if (sameChatMessage(existing, incoming)) return messages;
4171
- return Object.freeze(
4172
- messages.map((message) => message === existing ? incoming : message).sort((left, right) => compareChatSequence(left.sequence, right.sequence)).slice(-MAX_LOADED_CHAT_MESSAGES)
4173
- );
4174
- }
4175
- function compareChatSequence(left, right) {
4176
- if (left === right) return 0;
4177
- if (!/^\d+$/u.test(left) || !/^\d+$/u.test(right)) return left < right ? -1 : 1;
4178
- const normalizedLeft = left.replace(/^0+/u, "") || "0";
4179
- const normalizedRight = right.replace(/^0+/u, "") || "0";
4180
- return normalizedLeft.length === normalizedRight.length ? normalizedLeft < normalizedRight ? -1 : 1 : normalizedLeft.length - normalizedRight.length;
4181
- }
4182
- function validateChatAttachment(attachment2) {
4183
- const fileNameBytes = encoder4.encode(attachment2.fileName).byteLength;
4184
- if (!attachment2.attachmentId || fileNameBytes < 1 || fileNameBytes > CHALK_CHAT_ATTACHMENT_LIMITS.maximumFileNameBytes) return "Chat attachment metadata is invalid";
4185
- if (!Number.isSafeInteger(attachment2.byteLength) || attachment2.byteLength < 1 || attachment2.byteLength > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Chat attachment metadata is invalid";
4186
- return allowedMimeTypes.has(attachment2.mimeType) ? null : "Chat attachment MIME type is not allowed";
4187
- }
4188
- function sameChatMessage(left, right) {
4189
- return left.messageId === right.messageId && left.clientMessageId === right.clientMessageId && left.sequence === right.sequence && left.participantId === right.participantId && left.displayName === right.displayName && left.text === right.text && left.createdAt === right.createdAt && left.attachments.length === right.attachments.length && left.attachments.every((attachment2, index) => {
4190
- const candidate = right.attachments[index];
4191
- return candidate?.attachmentId === attachment2.attachmentId && candidate.fileName === attachment2.fileName && candidate.mimeType === attachment2.mimeType && candidate.byteLength === attachment2.byteLength;
4192
- });
4193
- }
4194
-
4195
- // src/space-client/chat-controller.ts
4196
- var ChatControllerService = class extends Context5.Service()("@chalk/client/ChatController") {
4197
- };
4198
- var makeChatController = (input) => Effect4.gen(function* () {
4199
- const scope = yield* Effect4.scope;
4200
- const context = yield* Effect4.context();
4201
- const fork = (effect) => {
4202
- void Effect4.runForkWith(context)(Effect4.forkIn(effect, scope).pipe(Effect4.asVoid));
4203
- };
4204
- const controller = yield* Effect4.sync(() => {
4205
- let instance = null;
4206
- const transport = input.createTransport?.({ token: () => Effect4.runPromiseWith(context)(input.connection.getSyncToken()) }) ?? null;
4207
- instance = new ChatControllerRuntime(input.connection, input.store, transport, input.apiBaseUrl ?? "https://api.chalkmeet.com", input.fetch ?? globalThis.fetch, fork, input.episodeDiagnostics, input.offline);
4208
- return instance;
4209
- });
4210
- yield* Effect4.addFinalizer(() => Effect4.sync(() => controller.dispose()));
4211
- return controller;
4212
- });
4213
- var ChatControllerRuntime = class {
4214
- #apiBaseUrl;
4215
- #connection;
4216
- #store;
4217
- #transport;
4218
- #fetch;
4219
- #fork;
4220
- #diagnostics;
4221
- #offline;
4222
- #catchUpRunning = false;
4223
- #catchUpRequested = false;
4224
- #contiguousSequence = null;
4225
- #hasOlder = false;
4226
- #historyTruncated = false;
4227
- #initialCatchUpComplete = true;
4228
- #lastError = null;
4229
- #localReadThroughSequence = null;
4230
- #messages = Object.freeze([]);
4231
- #pendingSends = Object.freeze([]);
4232
- #ports = null;
4233
- #readReceipts = Object.freeze([]);
4234
- #retainedFloorSequence = null;
4235
- #status = "idle";
4236
- #unreadCount = 0;
4237
- #unsubscribeConnection = null;
4238
- #unsubscribeEvents = null;
4239
- #unsubscribeSnapshot = null;
4240
- constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics, offline) {
4241
- this.#offline = offline;
4242
- this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
4243
- this.#connection = connection;
4244
- this.#store = store;
4245
- this.#transport = transport;
4246
- this.#fetch = fetch;
4247
- this.#fork = fork;
4248
- this.#diagnostics = diagnostics;
4249
- this.#unsubscribeConnection = connection.subscribePorts((ports) => this.#bind(ports));
3339
+ yield* Effect4.addFinalizer(() => Effect4.sync(() => controller.dispose()));
3340
+ return controller;
3341
+ });
3342
+ var ChatControllerRuntime = class {
3343
+ #apiBaseUrl;
3344
+ #connection;
3345
+ #store;
3346
+ #transport;
3347
+ #fetch;
3348
+ #fork;
3349
+ #offline;
3350
+ #reactionStates = /* @__PURE__ */ new Map();
3351
+ #catchUpRunning = false;
3352
+ #catchUpRequested = false;
3353
+ #contiguousSequence = null;
3354
+ #hasOlder = false;
3355
+ #historyTruncated = false;
3356
+ #initialCatchUpComplete = true;
3357
+ #lastError = null;
3358
+ #localReadThroughSequence = null;
3359
+ #messages = Object.freeze([]);
3360
+ #pendingSends = Object.freeze([]);
3361
+ #ports = null;
3362
+ #readReceipts = Object.freeze([]);
3363
+ #retainedFloorSequence = null;
3364
+ #status = "idle";
3365
+ #unreadCount = 0;
3366
+ #unsubscribeConnection = null;
3367
+ #unsubscribeEvents = null;
3368
+ #unsubscribeSnapshot = null;
3369
+ constructor(connection, store, transport, apiBaseUrl, fetch, fork, offline) {
3370
+ this.#offline = offline;
3371
+ this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
3372
+ this.#connection = connection;
3373
+ this.#store = store;
3374
+ this.#transport = transport;
3375
+ this.#fetch = fetch;
3376
+ this.#fork = fork;
3377
+ this.#unsubscribeConnection = connection.subscribePorts((ports) => this.#bind(ports));
4250
3378
  }
4251
3379
  send = (input) => {
4252
- const operation = this.#diagnostics?.startOperation("chat.send");
4253
3380
  return Effect4.try({ try: () => validate(input), catch: normalizeClientError }).pipe(
4254
- Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "validation"))),
4255
3381
  Effect4.map((attachments) => ({ clientMessageId: this.#connection.createId(), attachments })),
4256
3382
  Effect4.flatMap(({ clientMessageId, attachments }) => {
4257
3383
  const command = this.#connection.runCommand(({ sync }) => foreign(() => sync.sendChatMessage({ text: input.text, attachments, clientMessageId })));
4258
3384
  const sendNow = () => Effect4.sync(() => this.#upsertPending(clientMessageId, input.text, attachments, "sending", null)).pipe(Effect4.andThen(command));
4259
3385
  const held = this.#offline?.hold({ kind: "chat_message", text: input.text, clientMessageId, attachments }, sendNow);
4260
3386
  if (held) this.#upsertPending(clientMessageId, input.text, attachments, "queued", null);
4261
- operation?.observe("observed", "authorization");
4262
- return (held ?? sendNow()).pipe(Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "durable_commit")))).pipe(
3387
+ return (held ?? sendNow()).pipe(Effect4.tap(() => Effect4.void)).pipe(
4263
3388
  Effect4.map(chatMessageFor),
4264
3389
  Effect4.tap(() => Effect4.sync(() => this.#removePending(clientMessageId))),
4265
3390
  Effect4.map((message) => this.#observeMessage(message, false)),
4266
- Effect4.tap(
4267
- () => Effect4.sync(() => {
4268
- operation?.observe("observed", "paging_visibility");
4269
- operation?.notObservable("recipient_projection", "recipient_projection_is_conditional");
4270
- operation?.succeed();
4271
- })
4272
- ),
4273
3391
  Effect4.catch((cause) => {
4274
3392
  const error2 = normalizeClientError(cause, "chat.payload_invalid");
4275
3393
  return Effect4.sync(() => {
4276
3394
  if (error2.code === "offline.discarded") this.#removePending(clientMessageId);
4277
3395
  else this.#upsertPending(clientMessageId, input.text, attachments, "failed", failureFromError(error2));
4278
- operation?.fail("send_failed");
4279
3396
  }).pipe(Effect4.andThen(Effect4.fail(error2)));
4280
3397
  })
4281
3398
  );
4282
3399
  }),
4283
- Effect4.mapError(normalizeClientError),
4284
- Effect4.tapError(() => Effect4.sync(() => operation?.fail("send_failed")))
3400
+ Effect4.mapError(normalizeClientError)
4285
3401
  );
4286
3402
  };
3403
+ react = (input) => Effect4.try({
3404
+ try: () => Schema3.decodeUnknownSync(ChatReactionSetFrameSchema)({ type: "chat_reaction_set", request_id: this.#connection.createId(), message_id: input.messageId, emoji: input.emoji, on: input.on }),
3405
+ catch: () => chatError("The message reaction is invalid")
3406
+ }).pipe(
3407
+ Effect4.flatMap(() => {
3408
+ const sendNow = () => this.#connection.runCommand(({ sync }) => foreign(() => sync.reactToChatMessage(input)));
3409
+ return (this.#offline?.hold({ kind: "chat_reaction", reaction: input }, sendNow) ?? sendNow()).pipe(
3410
+ Effect4.tap((state) => Effect4.sync(() => this.#mergeReactions(state))),
3411
+ Effect4.mapError(normalizeClientError)
3412
+ );
3413
+ })
3414
+ );
4287
3415
  loadOlder = () => {
4288
- const operation = this.#diagnostics?.startOperation("chat.page");
4289
3416
  this.#status = "loading";
4290
3417
  this.#lastError = null;
4291
3418
  this.#publish();
@@ -4293,8 +3420,6 @@ var ChatControllerRuntime = class {
4293
3420
  Effect4.tap(
4294
3421
  (result) => Effect4.sync(() => {
4295
3422
  this.#applyPage(result);
4296
- operation?.observe("observed", "page_visibility");
4297
- operation?.succeed();
4298
3423
  })
4299
3424
  ),
4300
3425
  Effect4.mapError((cause) => {
@@ -4303,74 +3428,49 @@ var ChatControllerRuntime = class {
4303
3428
  this.#lastError = failureFromError(error2);
4304
3429
  this.#publish();
4305
3430
  return error2;
4306
- }),
4307
- Effect4.tapError(() => Effect4.sync(() => operation?.fail("page_failed")))
3431
+ })
4308
3432
  );
4309
3433
  };
4310
3434
  markRead = (messageId) => {
4311
- const operation = this.#diagnostics?.startOperation("chat.read");
4312
3435
  return Effect4.suspend(() => {
4313
3436
  const message = this.#messages.find((candidate) => candidate.messageId === messageId);
4314
3437
  if (!message) {
4315
- operation?.notObservable("read_commit", "message_not_found");
4316
- operation?.succeed();
4317
3438
  return Effect4.succeed(null);
4318
3439
  }
4319
3440
  this.#applyLocalReadThrough(message.sequence);
4320
3441
  const ports = this.#ports;
4321
- if (!ports || ports.sync.getCollaborationExtensionState().version !== 1) {
4322
- operation?.notObservable("read_commit", "collaboration_extension_unavailable");
4323
- operation?.succeed();
3442
+ if (!ports || ports.sync.getCollaborationExtensionState().version === null) {
4324
3443
  return Effect4.succeed(null);
4325
3444
  }
4326
- operation?.observe("observed", "read_commit");
4327
3445
  return this.#connection.runCommand(({ sync }) => foreign(() => sync.markChatRead(message.sequence))).pipe(
4328
3446
  Effect4.map(chatReceiptFor),
4329
3447
  Effect4.tap((receipt) => Effect4.sync(() => this.#mergeReceipt(receipt))),
4330
- Effect4.tap(() => Effect4.sync(() => operation?.succeed())),
4331
- Effect4.mapError(normalizeClientError),
4332
- Effect4.tapError(() => Effect4.sync(() => operation?.fail("read_failed")))
3448
+ Effect4.mapError(normalizeClientError)
4333
3449
  );
4334
3450
  });
4335
3451
  };
4336
3452
  upload = (file) => {
4337
3453
  const fetch = this.#fetch;
4338
- const prepareOperation = this.#diagnostics?.startOperation("chat.attachment.prepare");
4339
3454
  return Effect4.suspend(() => {
4340
3455
  if (!this.#transport) {
4341
- prepareOperation?.fail("transport_unavailable");
4342
3456
  return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file upload is unavailable" }));
4343
3457
  }
4344
3458
  return bytesFor(file).pipe(
4345
3459
  Effect4.flatMap((bytes) => {
4346
3460
  const clientAttachmentId = this.#connection.createId();
4347
3461
  return Effect4.try({ try: () => validateUpload(file, bytes, clientAttachmentId), catch: normalizeClientError }).pipe(
4348
- Effect4.tap(() => Effect4.sync(() => prepareOperation?.observe("observed", "validation"))),
4349
3462
  Effect4.flatMap(
4350
3463
  (uploadFile) => this.#connection.runPortCommand(
4351
3464
  () => foreign(() => this.#transport.initiateUpload({ clientAttachmentId, fileName: uploadFile.fileName, mimeType: uploadFile.mimeType, byteLength: bytes.byteLength, sha256: chatDigest(bytes) })).pipe(
4352
3465
  Effect4.flatMap(
4353
- (upload) => Effect4.sync(() => {
4354
- prepareOperation?.observe("observed", "storage_prepare");
4355
- prepareOperation?.succeed();
4356
- }).pipe(
3466
+ (upload) => Effect4.void.pipe(
4357
3467
  Effect4.andThen(
4358
3468
  foreign(() => fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes })).pipe(
4359
3469
  Effect4.flatMap((response) => {
4360
- const commitOperation = this.#diagnostics?.startOperation("chat.attachment.commit");
4361
3470
  if (!response.ok) {
4362
- commitOperation?.fail("storage_rejected");
4363
3471
  return Effect4.fail(new SpaceClientError({ code: "command.rejected", recoverable: response.status >= 500, message: `Attachment upload failed with HTTP ${response.status}` }));
4364
3472
  }
4365
- return foreign(() => this.#transport.finalizeUpload(upload.uploadId)).pipe(
4366
- Effect4.tap(
4367
- () => Effect4.sync(() => {
4368
- commitOperation?.observe("observed", "storage_commit");
4369
- commitOperation?.succeed();
4370
- })
4371
- ),
4372
- Effect4.tapError(() => Effect4.sync(() => commitOperation?.fail("storage_commit_failed")))
4373
- );
3473
+ return foreign(() => this.#transport.finalizeUpload(upload.uploadId)).pipe();
4374
3474
  })
4375
3475
  )
4376
3476
  )
@@ -4381,15 +3481,7 @@ var ChatControllerRuntime = class {
4381
3481
  )
4382
3482
  );
4383
3483
  }),
4384
- Effect4.mapError(normalizeClientError),
4385
- Effect4.tapError(
4386
- () => Effect4.sync(() => {
4387
- prepareOperation?.fail("upload_failed");
4388
- const failureOperation = this.#diagnostics?.startOperation("chat.attachment.fail");
4389
- failureOperation?.observe("observed", "failure");
4390
- failureOperation?.succeed();
4391
- })
4392
- )
3484
+ Effect4.mapError(normalizeClientError)
4393
3485
  );
4394
3486
  });
4395
3487
  };
@@ -4419,13 +3511,46 @@ var ChatControllerRuntime = class {
4419
3511
  this.#initialCatchUpComplete = this.#contiguousSequence !== null;
4420
3512
  this.#unsubscribeEvents = ports.sync.subscribeCollaboration((event) => {
4421
3513
  if (event.type === "chat_message") this.#observeMessage(chatMessageFor(event.message), this.#initialCatchUpComplete);
3514
+ else if (event.type === "chat_reactions") this.#mergeReactions(event.state);
4422
3515
  else if (event.type === "chat_read_receipt") this.#mergeReceipt(chatReceiptFor(event.receipt));
4423
3516
  else if (event.type === "chat_cursor_reset") this.#applyCursorReset(event.retainedFloorSequence);
4424
3517
  });
4425
- this.#unsubscribeSnapshot = ports.sync.subscribe(() => this.#scheduleCatchUp());
3518
+ let wasReady = ports.sync.getCollaborationExtensionState().negotiated && ports.sync.getSnapshot().connection.phase === "live";
3519
+ this.#unsubscribeSnapshot = ports.sync.subscribe(() => {
3520
+ const ready = ports.sync.getCollaborationExtensionState().negotiated && ports.sync.getSnapshot().connection.phase === "live";
3521
+ if (ready && !wasReady) this.#refreshLoaded(ports);
3522
+ wasReady = ready;
3523
+ this.#scheduleCatchUp();
3524
+ });
3525
+ this.#refreshLoaded(ports);
4426
3526
  this.#scheduleCatchUp();
4427
3527
  this.#publish();
4428
3528
  }
3529
+ #refreshLoaded(ports) {
3530
+ const first = this.#messages[0];
3531
+ const last = this.#messages.at(-1);
3532
+ if (!first || !last || ports.sync.getCollaborationExtensionState().version !== 2 || ports.sync.getSnapshot().connection.phase !== "live") return;
3533
+ const owner = this;
3534
+ this.#fork(
3535
+ Effect4.gen(function* () {
3536
+ let cursor = String(BigInt(first.sequence) - 1n);
3537
+ for (let page = 0; page < MAX_LOADED_CHAT_MESSAGES && owner.#ports === ports; page++) {
3538
+ const result = yield* foreign(() => ports.sync.readChatPage({ afterSequence: cursor, limit: MAX_CHAT_PAGE_SIZE }));
3539
+ if (result.status !== "loaded" || result.count === 0) break;
3540
+ const next = owner.#messages.filter((message) => compareChatSequence(message.sequence, cursor) > 0).at(Math.min(result.count, MAX_LOADED_CHAT_MESSAGES) - 1)?.sequence;
3541
+ if (!next || compareChatSequence(next, cursor) <= 0 || compareChatSequence(next, last.sequence) >= 0) break;
3542
+ cursor = next;
3543
+ }
3544
+ }).pipe(
3545
+ Effect4.catch(
3546
+ (cause) => Effect4.sync(() => {
3547
+ this.#lastError = failureFromError(normalizeClientError(cause));
3548
+ this.#publish();
3549
+ })
3550
+ )
3551
+ )
3552
+ );
3553
+ }
4429
3554
  #scheduleCatchUp() {
4430
3555
  const ports = this.#ports;
4431
3556
  const floor = ports?.sync.getCollaborationExtensionState().retainedFloorSequence;
@@ -4535,6 +3660,8 @@ var ChatControllerRuntime = class {
4535
3660
  this.#contiguousSequence = String(sequence);
4536
3661
  }
4537
3662
  #observeMessage(message, countUnread) {
3663
+ const cached = this.#reactionStates.get(message.messageId);
3664
+ if (cached && compareChatSequence(cached.revision, message.reactionRevision ?? "0") > 0) message = chatMessageFor({ ...message, reactions: cached.reactions, reactionRevision: cached.revision });
4538
3665
  const existing = this.#messages.find((candidate) => candidate.messageId === message.messageId || candidate.sequence === message.sequence);
4539
3666
  const next = this.#messagesWith(message, existing);
4540
3667
  const pendingSends = this.#pendingSends.filter((pending) => pending.clientMessageId !== message.clientMessageId);
@@ -4562,6 +3689,17 @@ var ChatControllerRuntime = class {
4562
3689
  #messageStateMatches(messages, pendingSends, unread) {
4563
3690
  return messages === this.#messages && pendingSends.length === this.#pendingSends.length && unread === this.#unreadCount && this.#status === "ready" && this.#lastError === null;
4564
3691
  }
3692
+ #mergeReactions(state) {
3693
+ const cached = this.#reactionStates.get(state.messageId);
3694
+ if (cached && compareChatSequence(cached.revision, state.revision) >= 0) return;
3695
+ this.#reactionStates.delete(state.messageId);
3696
+ this.#reactionStates.set(state.messageId, state);
3697
+ if (this.#reactionStates.size > MAX_LOADED_CHAT_MESSAGES) this.#reactionStates.delete(this.#reactionStates.keys().next().value);
3698
+ const existing = this.#messages.find((message) => message.messageId === state.messageId);
3699
+ if (!existing || compareChatSequence(state.revision, existing.reactionRevision ?? "0") <= 0) return;
3700
+ this.#messages = mergeChatMessage(this.#messages, existing, chatMessageFor({ ...existing, reactions: state.reactions, reactionRevision: state.revision }));
3701
+ this.#publish();
3702
+ }
4565
3703
  #mergeReceipt(receipt) {
4566
3704
  const existing = this.#readReceipts.find((candidate) => candidate.participantId === receipt.participantId && candidate.participantGeneration === receipt.participantGeneration);
4567
3705
  if (existing && compareChatSequence(existing.readThroughSequence, receipt.readThroughSequence) >= 0) return;
@@ -4591,6 +3729,7 @@ var ChatControllerRuntime = class {
4591
3729
  this.#publish();
4592
3730
  }
4593
3731
  #reset() {
3732
+ this.#reactionStates.clear();
4594
3733
  this.#catchUpRequested = false;
4595
3734
  this.#contiguousSequence = null;
4596
3735
  this.#hasOlder = false;
@@ -4647,11 +3786,11 @@ function chatError(message) {
4647
3786
  }
4648
3787
 
4649
3788
  // src/space-client/media-controller.ts
4650
- import { Clock as Clock3, Context as Context6, Effect as Effect5, Layer as Layer6, Semaphore } from "effect";
3789
+ import { Clock as Clock3, Context as Context6, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
4651
3790
  var MEDIA_SOURCES = ["microphone", "camera", "screen"];
4652
3791
  var MediaControllerService = class extends Context6.Service()("@chalk/client/MediaController") {
4653
3792
  };
4654
- var makeMediaController = (connection, store, selection, diagnostics) => Effect5.gen(function* () {
3793
+ var makeMediaController = (connection, store, selection) => Effect5.gen(function* () {
4655
3794
  const [microphone, camera, screen] = yield* Effect5.all([Semaphore.make(1), Semaphore.make(1), Semaphore.make(1)]);
4656
3795
  const clock = yield* Clock3.Clock;
4657
3796
  const scope = yield* Effect5.scope;
@@ -4669,8 +3808,7 @@ var makeMediaController = (connection, store, selection, diagnostics) => Effect5
4669
3808
  ["screen", screen]
4670
3809
  ]),
4671
3810
  clock.currentTimeMillisUnsafe,
4672
- fork,
4673
- diagnostics
3811
+ fork
4674
3812
  );
4675
3813
  yield* connection.setInitialMedia(
4676
3814
  (intent) => controller.captureInitial(intent),
@@ -4687,7 +3825,6 @@ var MediaControllerRuntime = class {
4687
3825
  #gates;
4688
3826
  #now;
4689
3827
  #fork;
4690
- #diagnostics;
4691
3828
  #tracks = /* @__PURE__ */ new Map();
4692
3829
  #observedPublicationIds = /* @__PURE__ */ new Map();
4693
3830
  #intentRevisions = /* @__PURE__ */ new Map();
@@ -4698,20 +3835,20 @@ var MediaControllerRuntime = class {
4698
3835
  #requests = Object.freeze([]);
4699
3836
  #ports = null;
4700
3837
  #screenEndedPending = false;
3838
+ #screenAudioTrack = null;
4701
3839
  #unsubscribeMedia = null;
4702
3840
  #unsubscribeRequests = null;
4703
3841
  #unsubscribeSync = null;
4704
3842
  #unsubscribeConnection = null;
4705
3843
  #unsubscribeScreenEnded = null;
4706
3844
  #unsubscribePorts = null;
4707
- constructor(connection, store, selection, gates, now, fork, diagnostics) {
3845
+ constructor(connection, store, selection, gates, now, fork) {
4708
3846
  this.#connection = connection;
4709
3847
  this.#store = store;
4710
3848
  this.#selection = selection;
4711
3849
  this.#gates = gates;
4712
3850
  this.#now = now;
4713
3851
  this.#fork = fork;
4714
- this.#diagnostics = diagnostics;
4715
3852
  this.#unsubscribeConnection = connection.subscribe(() => this.#handleConnectionChange());
4716
3853
  this.#unsubscribeScreenEnded = connection.subscribeScreenEnded(() => this.#handleScreenEnded());
4717
3854
  this.#unsubscribePorts = connection.subscribePorts((ports) => this.#bind(ports));
@@ -4722,7 +3859,7 @@ var MediaControllerRuntime = class {
4722
3859
  }
4723
3860
  setMicrophoneEnabled = (enabled) => this.#set("microphone", enabled);
4724
3861
  setCameraEnabled = (enabled) => this.#set("camera", enabled);
4725
- setScreenShareEnabled = (enabled) => enabled ? this.#startScreen() : this.#stopScreen();
3862
+ setScreenShareEnabled = (enabled, options) => enabled ? this.#startScreen(options) : this.#stopScreen();
4726
3863
  selectMicrophone = (deviceId) => Effect5.sync(() => {
4727
3864
  this.#selection.selectCapture("microphone", deviceId);
4728
3865
  this.#store.select("microphone", deviceId);
@@ -4736,39 +3873,22 @@ var MediaControllerRuntime = class {
4736
3873
  Effect5.mapError(normalizeClientError)
4737
3874
  );
4738
3875
  acceptRequest = (requestId) => {
4739
- const operation = this.#diagnostics?.startOperation("media_request.accept");
4740
3876
  return Effect5.suspend(() => {
4741
3877
  const request = this.#requests.find((candidate) => candidate.requestId === requestId);
4742
3878
  if (!request) return Effect5.fail(new SpaceClientError({ code: "media.request_invalid", recoverable: false, message: "The media request is no longer active" }));
4743
- operation?.observe("observed", "capability_decision");
4744
3879
  return (request.kind === "unmute" ? this.#set("microphone", true) : this.#set("camera", true)).pipe(
4745
3880
  Effect5.tap(
4746
3881
  () => Effect5.sync(() => {
4747
3882
  this.#removeRequest(requestId);
4748
- operation?.observe("observed", "command_commit");
4749
- operation?.observe("observed", "target_result");
4750
- operation?.succeed();
4751
3883
  })
4752
- ),
4753
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("accept_failed")))
3884
+ )
4754
3885
  );
4755
- }).pipe(
4756
- Effect5.mapError(normalizeClientError),
4757
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("accept_failed")))
4758
- );
3886
+ }).pipe(Effect5.mapError(normalizeClientError));
4759
3887
  };
4760
3888
  declineRequest = (requestId) => {
4761
- const operation = this.#diagnostics?.startOperation("media_request.decline");
4762
3889
  return Effect5.sync(() => {
4763
- operation?.observe("observed", "capability_decision");
4764
3890
  this.#removeRequest(requestId);
4765
- operation?.notObservable("command_commit", "decline_is_local_only");
4766
- operation?.observe("observed", "target_result");
4767
- operation?.succeed();
4768
- }).pipe(
4769
- Effect5.mapError(normalizeClientError),
4770
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("decline_failed")))
4771
- );
3891
+ }).pipe(Effect5.mapError(normalizeClientError));
4772
3892
  };
4773
3893
  initialIntent() {
4774
3894
  return { ...this.#intent };
@@ -4819,14 +3939,14 @@ var MediaControllerRuntime = class {
4819
3939
  this.#unsubscribeScreenEnded = null;
4820
3940
  this.#unsubscribePorts = null;
4821
3941
  this.#ports = null;
3942
+ this.#screenAudioTrack?.stop();
3943
+ this.#screenAudioTrack = null;
4822
3944
  for (const track of this.#tracks.values()) track.stop();
4823
3945
  this.#tracks.clear();
4824
3946
  this.#clearRequests();
4825
3947
  this.#publish();
4826
3948
  }
4827
3949
  #set(source, enabled) {
4828
- const operation = this.#diagnostics?.startOperation(`${source}.${enabled ? "publish" : "unpublish"}`);
4829
- operation?.observe("observed", "intent");
4830
3950
  let previousIntent = this.#intent[source];
4831
3951
  let intentRevision = 0;
4832
3952
  let earlyPause = null;
@@ -4857,10 +3977,7 @@ var MediaControllerRuntime = class {
4857
3977
  paused = true;
4858
3978
  })
4859
3979
  ),
4860
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
4861
3980
  Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(false)) : foreign2(() => ports.sync.setCameraEnabled(false))),
4862
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
4863
- Effect5.tap(() => Effect5.sync(() => operation?.succeed())),
4864
3981
  Effect5.asVoid,
4865
3982
  Effect5.catch((cause) => {
4866
3983
  if (paused && !isSyncRejection(cause) || this.#intentRevisions.get(source) !== intentRevision) return Effect5.fail(cause);
@@ -4886,23 +4003,14 @@ var MediaControllerRuntime = class {
4886
4003
  this.#tracks.set(source, track);
4887
4004
  ports.media.prepareLocalTrack(source, track);
4888
4005
  prepared = true;
4889
- operation?.observe("observed", "local_track_state");
4890
4006
  this.#publish();
4891
4007
  })
4892
4008
  )
4893
4009
  ) : Effect5.void;
4894
4010
  return acquire.pipe(
4895
4011
  Effect5.asVoid,
4896
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
4897
4012
  Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(enabled)) : foreign2(() => ports.sync.setCameraEnabled(enabled))),
4898
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
4899
4013
  Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, action))),
4900
- Effect5.tap(
4901
- () => Effect5.sync(() => {
4902
- operation?.observe("observed", "sfu_publication");
4903
- operation?.succeed();
4904
- })
4905
- ),
4906
4014
  Effect5.asVoid,
4907
4015
  Effect5.catch(
4908
4016
  (cause) => this.#rollbackCapture({
@@ -4926,7 +4034,6 @@ var MediaControllerRuntime = class {
4926
4034
  )
4927
4035
  ).pipe(
4928
4036
  Effect5.mapError(normalizeClientError),
4929
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("media_failed"))),
4930
4037
  Effect5.ensuring(
4931
4038
  Effect5.sync(() => {
4932
4039
  if (this.#pendingEnables.get(source) === intentRevision) this.#pendingEnables.delete(source);
@@ -4934,8 +4041,7 @@ var MediaControllerRuntime = class {
4934
4041
  )
4935
4042
  );
4936
4043
  }
4937
- #startScreen() {
4938
- const operation = this.#diagnostics?.startOperation("screen.start");
4044
+ #startScreen(options) {
4939
4045
  let intentRevision = 0;
4940
4046
  return Effect5.sync(() => {
4941
4047
  intentRevision = (this.#intentRevisions.get("screen") ?? 0) + 1;
@@ -4947,45 +4053,64 @@ var MediaControllerRuntime = class {
4947
4053
  this.#serialize(
4948
4054
  "screen",
4949
4055
  this.#connection.runCommand((ports) => {
4950
- let stream = null;
4951
4056
  let track = null;
4952
4057
  let prepared = false;
4953
4058
  return Effect5.suspend(() => {
4954
- if (this.#tracks.has("screen")) {
4955
- operation?.notObservable("permission", "already_active");
4956
- operation?.notObservable("track_acquisition", "already_active");
4957
- operation?.notObservable("sync_commit", "already_active");
4958
- operation?.notObservable("sfu_publication", "already_active");
4959
- operation?.succeed();
4059
+ if (this.#tracks.has("screen") && options === void 0) {
4960
4060
  return Effect5.void;
4961
4061
  }
4962
- return foreign2(() => this.#selection.getDisplayMedia({ video: true, audio: false })).pipe(
4963
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "permission"))),
4964
- Effect5.tap(
4965
- (captured) => Effect5.sync(() => {
4966
- stream = captured;
4967
- this.#assertActivePorts(ports, "startScreenShare");
4968
- track = requireDisplayVideoTrack(captured);
4969
- this.#tracks.set("screen", track);
4970
- ports.media.prepareLocalTrack("screen", track);
4971
- prepared = true;
4972
- operation?.observe("observed", "track_acquisition");
4973
- track.addEventListener("ended", () => this.#handleScreenEnded(track));
4974
- this.#screenEndedPending = false;
4975
- this.#publish();
4976
- })
4977
- ),
4978
- Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
4979
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
4980
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
4981
- Effect5.tap(
4982
- () => Effect5.sync(() => {
4983
- operation?.observe("observed", "sfu_publication");
4984
- operation?.succeed();
4985
- })
4062
+ return Effect5.acquireUseRelease(
4063
+ foreign2(() => this.#selection.getDisplayMedia(displayMediaConstraints(options))).pipe(Effect5.mapError((cause) => captureError(cause, "startScreenShare", "Screen sharing permission was denied"))),
4064
+ (stream) => Effect5.sync(() => {
4065
+ this.#assertActivePorts(ports, "startScreenShare");
4066
+ track = requireDisplayVideoTrack(stream);
4067
+ return { stream, track };
4068
+ }).pipe(
4069
+ Effect5.tap(() => {
4070
+ const previous = this.#tracks.get("screen");
4071
+ if (!previous) return Effect5.void;
4072
+ return foreign2(() => ports.sync.setScreenShareEnabled(false)).pipe(
4073
+ Effect5.andThen(this.#clearScreenAudio(ports.media)),
4074
+ Effect5.andThen(foreign2(() => ports.media.clearPreparedLocalTrack("screen"))),
4075
+ Effect5.tap(
4076
+ () => Effect5.sync(() => {
4077
+ this.#assertActivePorts(ports, "startScreenShare");
4078
+ this.#tracks.delete("screen");
4079
+ previous.stop();
4080
+ })
4081
+ )
4082
+ );
4083
+ }),
4084
+ Effect5.tap(
4085
+ ({ stream: captured, track: screenTrack }) => Effect5.sync(() => {
4086
+ const audio = captured.getAudioTracks()[0];
4087
+ for (const extra of captured.getAudioTracks().slice(1)) extra.stop();
4088
+ ports.media.prepareLocalTrack("screen", screenTrack);
4089
+ prepared = true;
4090
+ this.#tracks.set("screen", screenTrack);
4091
+ this.#screenAudioTrack = audio ?? null;
4092
+ if (audio) {
4093
+ ports.media.prepareLocalTrack("screen_audio", audio);
4094
+ audio.addEventListener("ended", () => this.#handleScreenAudioEnded(audio, ports));
4095
+ }
4096
+ screenTrack.addEventListener("ended", () => this.#handleScreenEnded(screenTrack));
4097
+ this.#screenEndedPending = false;
4098
+ this.#publish();
4099
+ })
4100
+ ),
4101
+ Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
4102
+ Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
4103
+ Effect5.asVoid,
4104
+ Effect5.catch(
4105
+ (cause) => (prepared ? this.#clearScreenAudio(ports.media) : Effect5.void).pipe(
4106
+ Effect5.ignore,
4107
+ Effect5.andThen(this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
4108
+ )
4109
+ )
4986
4110
  ),
4987
- Effect5.asVoid,
4988
- Effect5.catch((cause) => this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
4111
+ (stream, exit) => Effect5.sync(() => {
4112
+ if (Exit2.isFailure(exit)) stopStream(stream);
4113
+ })
4989
4114
  );
4990
4115
  });
4991
4116
  })
@@ -4993,7 +4118,6 @@ var MediaControllerRuntime = class {
4993
4118
  )
4994
4119
  ).pipe(
4995
4120
  Effect5.mapError(normalizeClientError),
4996
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_start_failed"))),
4997
4121
  Effect5.ensuring(
4998
4122
  Effect5.sync(() => {
4999
4123
  if (this.#pendingEnables.get("screen") === intentRevision) this.#pendingEnables.delete("screen");
@@ -5002,7 +4126,6 @@ var MediaControllerRuntime = class {
5002
4126
  );
5003
4127
  }
5004
4128
  #stopScreen() {
5005
- const operation = this.#diagnostics?.startOperation("screen.stop");
5006
4129
  let intentRevision = 0;
5007
4130
  let earlyPause = null;
5008
4131
  const localPause = Effect5.suspend(() => {
@@ -5028,6 +4151,7 @@ var MediaControllerRuntime = class {
5028
4151
  ),
5029
4152
  Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(false))),
5030
4153
  Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "stopScreenShare"))),
4154
+ Effect5.andThen(this.#clearScreenAudio(ports.media)),
5031
4155
  Effect5.andThen(foreign2(() => ports.media.clearPreparedLocalTrack("screen"))),
5032
4156
  Effect5.tap(
5033
4157
  () => Effect5.sync(() => {
@@ -5036,8 +4160,6 @@ var MediaControllerRuntime = class {
5036
4160
  track?.stop();
5037
4161
  this.#screenEndedPending = false;
5038
4162
  this.#publish();
5039
- operation?.observe("observed", "stop_confirmation");
5040
- operation?.succeed();
5041
4163
  })
5042
4164
  ),
5043
4165
  Effect5.asVoid,
@@ -5053,9 +4175,16 @@ var MediaControllerRuntime = class {
5053
4175
  )
5054
4176
  )
5055
4177
  )
5056
- ).pipe(
5057
- Effect5.mapError(normalizeClientError),
5058
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_stop_failed")))
4178
+ ).pipe(Effect5.mapError(normalizeClientError));
4179
+ }
4180
+ #clearScreenAudio(media) {
4181
+ return foreign2(() => media.clearPreparedLocalTrack("screen_audio")).pipe(
4182
+ Effect5.ensuring(
4183
+ Effect5.sync(() => {
4184
+ this.#screenAudioTrack?.stop();
4185
+ this.#screenAudioTrack = null;
4186
+ })
4187
+ )
5059
4188
  );
5060
4189
  }
5061
4190
  #beginLocalPause(ports, source) {
@@ -5134,8 +4263,8 @@ var MediaControllerRuntime = class {
5134
4263
  input.restoreIntent?.();
5135
4264
  const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
5136
4265
  const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
5137
- const failure3 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
5138
- return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure3)));
4266
+ const failure2 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
4267
+ return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure2)));
5139
4268
  }
5140
4269
  #assertActivePorts(ports, action) {
5141
4270
  if (this.#connection.getSnapshot().state === "live" && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
@@ -5151,6 +4280,8 @@ var MediaControllerRuntime = class {
5151
4280
  if (this.#ports?.media !== ports?.media) this.#observedPublicationIds.clear();
5152
4281
  this.#ports = ports;
5153
4282
  if (!ports) {
4283
+ this.#screenAudioTrack?.stop();
4284
+ this.#screenAudioTrack = null;
5154
4285
  for (const track of this.#tracks.values()) track.stop();
5155
4286
  this.#tracks.clear();
5156
4287
  this.#screenEndedPending = false;
@@ -5174,7 +4305,7 @@ var MediaControllerRuntime = class {
5174
4305
  }
5175
4306
  #applyRemoteMediaState(ports, snapshot) {
5176
4307
  for (const publication of ports.media.getSnapshot().remoteTracks) {
5177
- publication.track.enabled = snapshot.media?.items.some((item) => item.participantId === publication.participantId && item.source === publication.source && item.enabled) ?? false;
4308
+ publication.track.enabled = snapshot.media?.items.some((item) => item.participantId === publication.participantId && item.source === (publication.source === "screen_audio" ? "screen" : publication.source) && item.enabled) ?? false;
5178
4309
  }
5179
4310
  }
5180
4311
  #applyForcedMediaState(ports, snapshot) {
@@ -5195,8 +4326,7 @@ var MediaControllerRuntime = class {
5195
4326
  #pauseForcedSource(ports, source) {
5196
4327
  const pendingEnable = this.#hasPendingEnable(source);
5197
4328
  if (!pendingEnable) {
5198
- this.#intentRevisions.set(source, (this.#intentRevisions.get(source) ?? 0) + 1);
5199
- if (source !== "screen") this.#intent[source] = false;
4329
+ this.#clearForcedIntent(source);
5200
4330
  }
5201
4331
  const track = this.#tracks.get(source);
5202
4332
  if (track) track.enabled = false;
@@ -5205,6 +4335,11 @@ var MediaControllerRuntime = class {
5205
4335
  if (pendingEnable) ports.media.setLocalSourceIntent?.(source, true);
5206
4336
  this.#publish();
5207
4337
  }
4338
+ #clearForcedIntent(source) {
4339
+ this.#intentRevisions.set(source, (this.#intentRevisions.get(source) ?? 0) + 1);
4340
+ if (source !== "screen") this.#intent[source] = false;
4341
+ else if (this.#screenAudioTrack) this.#screenAudioTrack.enabled = false;
4342
+ }
5208
4343
  #hasPendingEnable(source) {
5209
4344
  const revision = this.#pendingEnables.get(source);
5210
4345
  return revision !== void 0 && revision === this.#intentRevisions.get(source);
@@ -5258,21 +4393,24 @@ var MediaControllerRuntime = class {
5258
4393
  }
5259
4394
  this.#publish();
5260
4395
  }
4396
+ #handleScreenAudioEnded(track, ports) {
4397
+ this.#fork(
4398
+ this.#serialize(
4399
+ "screen",
4400
+ Effect5.suspend(() => {
4401
+ if (this.#screenAudioTrack !== track || this.#ports !== ports) return Effect5.void;
4402
+ return this.#clearScreenAudio(ports.media).pipe(Effect5.ensuring(Effect5.sync(() => this.#publish())));
4403
+ })
4404
+ ).pipe(Effect5.ignore)
4405
+ );
4406
+ }
5261
4407
  #handleScreenEnded(track = this.#tracks.get("screen")) {
5262
4408
  if (!track || this.#tracks.get("screen") !== track) return;
5263
4409
  if (this.#connection.getSnapshot().state === "reconnecting") {
5264
4410
  this.#screenEndedPending = true;
5265
4411
  return;
5266
4412
  }
5267
- const operation = this.#diagnostics?.startOperation("screen.unexpected_end");
5268
- operation?.observe("observed", "track_end");
5269
- this.#fork(
5270
- this.#stopScreen().pipe(
5271
- Effect5.tap(() => Effect5.sync(() => operation?.succeed())),
5272
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_stop_failed"))),
5273
- Effect5.ignore
5274
- )
5275
- );
4413
+ this.#fork(this.#stopScreen().pipe(Effect5.ignore));
5276
4414
  }
5277
4415
  #publish() {
5278
4416
  const current = this.#store.getSnapshot();
@@ -5284,7 +4422,17 @@ var MediaControllerRuntime = class {
5284
4422
  screen: localMedia("screen", this.#tracks, snapshot, false, state, this.#pendingSources.has("screen"))
5285
4423
  });
5286
4424
  const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
5287
- const media = Object.freeze({ ...current.media, local, remote, screenShare: local.screen, incomingRequests: this.#requests });
4425
+ const media = Object.freeze({
4426
+ ...current.media,
4427
+ local,
4428
+ remote,
4429
+ screenShare: {
4430
+ ...local.screen,
4431
+ hasAudio: Boolean(this.#screenAudioTrack?.readyState === "live" && snapshot?.localTracks.some((track) => track.source === "screen_audio" && track.enabled && track.track.readyState === "live")),
4432
+ ...local.screen.track ? { capture: screenShareCapture(local.screen.track) } : {}
4433
+ },
4434
+ incomingRequests: this.#requests
4435
+ });
5288
4436
  if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
5289
4437
  }
5290
4438
  };
@@ -5348,7 +4496,10 @@ function activeLocalMediaState(connectionState) {
5348
4496
  function sameMediaSlice(left, right) {
5349
4497
  if (left.selection !== right.selection || !sameDevices(left.devices, right.devices)) return false;
5350
4498
  for (const source of MEDIA_SOURCES) if (!sameLocalMedia(left.local[source], right.local[source])) return false;
5351
- return sameLocalMedia(left.screenShare, right.screenShare) && sameRemoteMedia(left.remote, right.remote) && sameRequests(left.incomingRequests, right.incomingRequests);
4499
+ return sameScreenShare(left.screenShare, right.screenShare) && sameRemoteMedia(left.remote, right.remote) && sameRequests(left.incomingRequests, right.incomingRequests);
4500
+ }
4501
+ function sameScreenShare(left, right) {
4502
+ return sameLocalMedia(left, right) && left.hasAudio === right.hasAudio && left.capture?.surface === right.capture?.surface && left.capture?.label === right.capture?.label;
5352
4503
  }
5353
4504
  function sameLocalMedia(left, right) {
5354
4505
  return left.source === right.source && left.state === right.state && left.track === right.track;
@@ -5375,8 +4526,8 @@ function sameDeviceList(left, right) {
5375
4526
  function captureError(cause, action, permissionMessage) {
5376
4527
  if (cause instanceof ConnectionError) return cause;
5377
4528
  if (isPermissionDenied(cause)) return new ConnectionError({ code: "permission_denied", action, recoverable: true, message: permissionMessage }, { cause });
5378
- const failure3 = describeMediaCaptureError(cause);
5379
- return new ConnectionError({ ...failure3, action, message: action === "startScreenShare" ? "Screen sharing could not start. Check screen recording permission and try again in a supported browser or app." : failure3.message }, { cause });
4529
+ const failure2 = describeMediaCaptureError(cause);
4530
+ return new ConnectionError({ ...failure2, action, message: action === "startScreenShare" ? "Screen sharing could not start. Check screen recording permission and try again in a supported browser or app." : failure2.message }, { cause });
5380
4531
  }
5381
4532
  function isPermissionDenied(cause) {
5382
4533
  return describeMediaCaptureError(cause).code === "permission_denied";
@@ -5395,7 +4546,7 @@ function canApplyForcedMediaState(snapshot) {
5395
4546
  import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
5396
4547
 
5397
4548
  // src/space-client/offline-actions-storage.ts
5398
- import { Schema as Schema3 } from "effect";
4549
+ import { Schema as Schema4 } from "effect";
5399
4550
  var OFFLINE_ACTION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5400
4551
  function browserOfflineActionsStorage() {
5401
4552
  if (typeof window === "undefined") return void 0;
@@ -5405,28 +4556,29 @@ function browserOfflineActionsStorage() {
5405
4556
  return void 0;
5406
4557
  }
5407
4558
  }
5408
- var attachment = Schema3.Struct({ attachmentId: Schema3.String, fileName: Schema3.String, mimeType: ChatAttachmentMimeTypeSchema, byteLength: Schema3.Number });
5409
- var StoredOfflineAction = Schema3.Struct({
5410
- id: Schema3.String,
5411
- kind: Schema3.Literals(["chat_message", "hand_raise", "hand_lower"]),
5412
- text: Schema3.NullOr(Schema3.String),
5413
- queuedAt: Schema3.Number,
5414
- clientMessageId: Schema3.optional(Schema3.String),
5415
- attachments: Schema3.optional(Schema3.Array(attachment))
4559
+ var attachment = Schema4.Struct({ attachmentId: Schema4.String, fileName: Schema4.String, mimeType: ChatAttachmentMimeTypeSchema, byteLength: Schema4.Number });
4560
+ var StoredOfflineAction = Schema4.Struct({
4561
+ id: Schema4.String,
4562
+ kind: Schema4.Literals(["chat_message", "chat_reaction", "hand_raise", "hand_lower"]),
4563
+ text: Schema4.NullOr(Schema4.String),
4564
+ queuedAt: Schema4.Number,
4565
+ clientMessageId: Schema4.optional(Schema4.String),
4566
+ attachments: Schema4.optional(Schema4.Array(attachment)),
4567
+ reaction: Schema4.optional(Schema4.Struct({ messageId: UuidSchema, emoji: ReactionSchema, on: Schema4.Boolean }))
5416
4568
  });
5417
- var StoredOfflineActions = Schema3.Struct({
5418
- version: Schema3.Literal(1),
5419
- subject: Schema3.Struct({ tenantId: Schema3.String, spaceId: Schema3.String, episodeId: Schema3.String, participantId: Schema3.String, participantGeneration: Schema3.Number }),
5420
- actions: Schema3.Array(StoredOfflineAction)
4569
+ var StoredOfflineActions = Schema4.Struct({
4570
+ version: Schema4.Literal(1),
4571
+ subject: Schema4.Struct({ tenantId: Schema4.String, spaceId: Schema4.String, episodeId: Schema4.String, participantId: Schema4.String, participantGeneration: Schema4.Number }),
4572
+ actions: Schema4.Array(StoredOfflineAction)
5421
4573
  });
5422
- var decodeOfflineActions = Schema3.decodeUnknownSync(StoredOfflineActions);
4574
+ var decodeOfflineActions = Schema4.decodeUnknownSync(StoredOfflineActions);
5423
4575
 
5424
4576
  // src/space-client/offline-actions.ts
5425
4577
  var OfflineActionsService = class extends Context7.Service()("@chalk/client/OfflineActions") {
5426
4578
  };
5427
4579
  var sameSubject = (current, saved) => !!current && !!saved && current.tenantId === saved.tenantId && current.spaceId === saved.spaceId && current.episodeId === saved.episodeId && current.participantId === saved.participantId && current.participantGeneration === saved.participantGeneration;
5428
4580
  var terminalConnection = (snapshot) => snapshot.state === "left" || snapshot.state === "leaving" || snapshot.state === "failed" && !snapshot.failure?.recoverable;
5429
- var validStoredAction = (action, now) => Number.isFinite(action.queuedAt) && action.queuedAt <= now && (action.kind !== "chat_message" || !!action.clientMessageId && action.text !== null && validateChatMessage({ text: action.text, attachments: action.attachments }) === null);
4581
+ var validStoredAction = (action, now) => (action.kind !== "chat_reaction" || action.reaction !== void 0) && Number.isFinite(action.queuedAt) && action.queuedAt <= now && (action.kind !== "chat_message" || !!action.clientMessageId && action.text !== null && validateChatMessage({ text: action.text, attachments: action.attachments }) === null);
5430
4582
  var discardedError = () => new SpaceClientError({ code: "offline.discarded", recoverable: false, message: "This action was not sent." });
5431
4583
  var makeOfflineActions = (input) => Effect6.gen(function* () {
5432
4584
  const runtime = new OfflineActionsRuntime(input.connection, input.store, input.storage ?? browserOfflineActionsStorage(), input.storageKey);
@@ -5576,7 +4728,7 @@ var OfflineActionsRuntime = class {
5576
4728
  resume: (permission) => {
5577
4729
  const command = this.#connection.runCommand(
5578
4730
  ({ sync }) => Effect6.tryPromise({
5579
- try: () => action.kind === "chat_message" ? sync.sendChatMessage({ text: action.text ?? "", clientMessageId: action.clientMessageId ?? action.id, attachments: action.attachments ?? [] }).then(() => void 0) : sync.setHandRaised(action.kind === "hand_raise"),
4731
+ try: () => action.kind === "chat_reaction" && action.reaction ? sync.reactToChatMessage(action.reaction).then(() => void 0) : action.kind === "chat_message" ? sync.sendChatMessage({ text: action.text ?? "", clientMessageId: action.clientMessageId ?? action.id, attachments: action.attachments ?? [] }).then(() => void 0) : sync.setHandRaised(action.kind === "hand_raise"),
5580
4732
  catch: () => discardedError()
5581
4733
  })
5582
4734
  );
@@ -5667,55 +4819,54 @@ var CAPABILITIES = new Set(V1_CAPABILITIES);
5667
4819
  var EMPTY_PARTICIPANTS = Object.freeze({ roster: Object.freeze([]), admissionQueue: Object.freeze([]) });
5668
4820
  var ParticipantsControllerService = class extends Context8.Service()("@chalk/client/ParticipantsController") {
5669
4821
  };
5670
- var makeParticipantsController = (connection, store, diagnostics, offline) => Effect7.acquireRelease(
5671
- Effect7.sync(() => new ParticipantsControllerRuntime(connection, store, diagnostics, offline)),
4822
+ var makeParticipantsController = (connection, store, offline) => Effect7.acquireRelease(
4823
+ Effect7.sync(() => new ParticipantsControllerRuntime(connection, store, offline)),
5672
4824
  (controller) => Effect7.sync(() => controller.dispose())
5673
4825
  );
5674
4826
  var ParticipantsControllerRuntime = class {
5675
4827
  #connection;
5676
4828
  #store;
5677
- #diagnostics;
5678
4829
  #offline;
5679
4830
  #unsubscribePorts = null;
5680
4831
  #unsubscribe = null;
5681
- constructor(connection, store, diagnostics, offline) {
4832
+ #unsubscribeMedia = null;
4833
+ constructor(connection, store, offline) {
5682
4834
  this.#offline = offline;
5683
4835
  this.#connection = connection;
5684
4836
  this.#store = store;
5685
- this.#diagnostics = diagnostics;
5686
4837
  this.#unsubscribePorts = connection.subscribePorts((ports) => this.#bind(ports));
5687
4838
  }
5688
- assignRole = (participantId, roleName) => this.#command("moderation.role.change", ["capability_decision", "command_commit"], () => {
4839
+ assignRole = (participantId, roleName) => this.#command(() => {
5689
4840
  assertIdentifier(participantId, "participant ID");
5690
4841
  assertName(roleName, "role");
5691
4842
  return ({ sync }) => foreign3(() => sync.assignRole(participantId, roleName));
5692
4843
  });
5693
- mute = (participantId) => this.#command("moderation.microphone.disable", ["capability_decision", "command_commit", "target_delivery"], () => {
4844
+ mute = (participantId) => this.#command(() => {
5694
4845
  assertIdentifier(participantId, "participant ID");
5695
4846
  return ({ sync }) => foreign3(() => sync.muteParticipant(participantId));
5696
4847
  });
5697
- stopVideo = (participantId) => this.#command("moderation.camera.disable", ["capability_decision", "command_commit", "target_delivery"], () => {
4848
+ stopVideo = (participantId) => this.#command(() => {
5698
4849
  assertIdentifier(participantId, "participant ID");
5699
4850
  return ({ sync }) => foreign3(() => sync.stopParticipantCamera(participantId));
5700
4851
  });
5701
- stopScreenShare = (participantId) => this.#command("moderation.screen.disable", ["capability_decision", "command_commit", "target_delivery"], () => {
4852
+ stopScreenShare = (participantId) => this.#command(() => {
5702
4853
  assertIdentifier(participantId, "participant ID");
5703
4854
  return ({ sync }) => foreign3(() => sync.stopParticipantScreenShare(participantId));
5704
4855
  });
5705
- requestMedia = (participantId, kind) => this.#command("media_request.request", ["capability_decision", "command_commit", "target_result"], () => {
4856
+ requestMedia = (participantId, kind) => this.#command(() => {
5706
4857
  assertIdentifier(participantId, "participant ID");
5707
4858
  if (kind !== "microphone" && kind !== "camera") throw invalid("Media requests must target a microphone or camera");
5708
4859
  return ({ sync }) => (kind === "microphone" ? foreign3(() => sync.requestUnmute(participantId)) : foreign3(() => sync.requestStartCamera(participantId))).pipe(Effect7.map(directed));
5709
4860
  });
5710
- remove = (participantId) => this.#command("moderation.remove", ["capability_decision", "command_commit", "target_delivery"], () => {
4861
+ remove = (participantId) => this.#command(() => {
5711
4862
  assertIdentifier(participantId, "participant ID");
5712
4863
  return ({ sync }) => foreign3(() => sync.removeParticipant(participantId));
5713
4864
  });
5714
- admit = (requestId) => this.#command("admission.admit", ["policy_decision", "authoritative_commit", "participant_result"], () => {
4865
+ admit = (requestId) => this.#command(() => {
5715
4866
  assertIdentifier(requestId, "admission request ID");
5716
4867
  return ({ sync }) => foreign3(() => sync.admit(requestId));
5717
4868
  });
5718
- deny = (requestId) => this.#command("admission.deny", ["policy_decision", "authoritative_commit", "participant_result"], () => {
4869
+ deny = (requestId) => this.#command(() => {
5719
4870
  assertIdentifier(requestId, "admission request ID");
5720
4871
  return ({ sync }) => foreign3(() => sync.deny(requestId));
5721
4872
  });
@@ -5723,50 +4874,50 @@ var ParticipantsControllerRuntime = class {
5723
4874
  lowerHand = () => this.#setHandRaised(false);
5724
4875
  #setHandRaised(raised) {
5725
4876
  const send = () => this.#command(
5726
- "participant.raised_hand.set",
5727
- ["membership_transition", "participant_result"],
5728
4877
  () => ({ sync }) => foreign3(() => sync.setHandRaised(raised))
5729
4878
  );
5730
4879
  return this.#offline?.hold({ kind: raised ? "hand_raise" : "hand_lower" }, send) ?? send();
5731
4880
  }
5732
- renameSelf = (displayName) => this.#command("participant.rename", ["membership_transition", "participant_result"], () => {
4881
+ renameSelf = (displayName) => this.#command(() => {
5733
4882
  assertName(displayName, "display name");
5734
4883
  return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
5735
4884
  });
5736
4885
  dispose() {
5737
4886
  this.#unsubscribePorts?.();
5738
4887
  this.#unsubscribe?.();
4888
+ this.#unsubscribeMedia?.();
4889
+ this.#unsubscribeMedia = null;
5739
4890
  this.#unsubscribePorts = null;
5740
4891
  this.#unsubscribe = null;
5741
4892
  }
5742
- #command(name, checkpoints, operation) {
5743
- const diagnostic = this.#diagnostics?.startOperation(name);
4893
+ #command(operation) {
5744
4894
  return Effect7.try({ try: operation, catch: normalizeClientError }).pipe(
5745
- Effect7.tap(() => Effect7.sync(() => diagnostic?.observe("observed", checkpoints[0]))),
5746
4895
  Effect7.flatMap((run) => this.#connection.runCommand(run)),
5747
- Effect7.mapError(normalizeClientError),
5748
- Effect7.tap(
5749
- () => Effect7.sync(() => {
5750
- for (const checkpoint of checkpoints.slice(1)) diagnostic?.observe("observed", checkpoint);
5751
- if (name.startsWith("moderation.")) diagnostic?.notObservable("target_application", "target_projection_is_conditional");
5752
- diagnostic?.succeed();
5753
- })
5754
- ),
5755
- Effect7.tapError(() => Effect7.sync(() => diagnostic?.fail("command_failed")))
4896
+ Effect7.mapError(normalizeClientError)
5756
4897
  );
5757
4898
  }
5758
4899
  #bind(ports) {
5759
4900
  this.#unsubscribe?.();
4901
+ this.#unsubscribeMedia?.();
5760
4902
  this.#unsubscribe = null;
4903
+ this.#unsubscribeMedia = null;
5761
4904
  if (!ports) {
5762
4905
  this.#publish(null, {});
5763
4906
  return;
5764
4907
  }
5765
- this.#unsubscribe = ports.sync.subscribe((snapshot) => this.#publish(snapshot, ports.sync.getParticipantCollaborationCapabilities()));
5766
- this.#publish(ports.sync.getSnapshot(), ports.sync.getParticipantCollaborationCapabilities());
5767
- }
5768
- #publish(snapshot, collaborationCapabilities) {
5769
- const participants = participantsFor(snapshot, collaborationCapabilities);
4908
+ const publish = () => this.#publish(ports.sync.getSnapshot(), ports.sync.getParticipantCollaborationCapabilities(), ports.media.getSnapshot());
4909
+ this.#unsubscribe = ports.sync.subscribe(publish);
4910
+ this.#unsubscribeMedia = ports.media.subscribe(publish);
4911
+ publish();
4912
+ }
4913
+ #publish(snapshot, collaborationCapabilities, media) {
4914
+ const audioParticipants = new Set(media?.remoteTracks.filter((track) => track.source === "screen_audio" && track.track.readyState === "live").map((track) => track.participantId));
4915
+ if (snapshot?.participantId && media?.localTracks.some((track) => track.source === "screen_audio" && track.enabled && track.track.readyState === "live")) audioParticipants.add(snapshot.participantId);
4916
+ const projected = participantsFor(snapshot, collaborationCapabilities);
4917
+ const participants = Object.freeze({
4918
+ ...projected,
4919
+ roster: Object.freeze(projected.roster.map((participant) => Object.freeze({ ...participant, media: Object.freeze({ ...participant.media, screenShareHasAudio: participant.media.screenShare === "active" && audioParticipants.has(participant.participantId) }) })))
4920
+ });
5770
4921
  const self = selfFor(snapshot, participants.roster);
5771
4922
  const current = this.#store.getSnapshot();
5772
4923
  if (!sameParticipants(current.participants, participants)) this.#store.updateParticipants(participants);
@@ -5868,7 +5019,7 @@ function sameParticipantProfile(left, right) {
5868
5019
  return left.participantId === right.participantId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.eligibleRoles, right.eligibleRoles) && sameValues(left.capabilities, right.capabilities);
5869
5020
  }
5870
5021
  function sameParticipantMedia(left, right) {
5871
- return left.media.microphone === right.media.microphone && left.media.camera === right.media.camera && left.media.screenShare === right.media.screenShare;
5022
+ return left.media.microphone === right.media.microphone && left.media.camera === right.media.camera && left.media.screenShare === right.media.screenShare && left.media.screenShareHasAudio === right.media.screenShareHasAudio;
5872
5023
  }
5873
5024
  function sameParticipantPresence(left, right) {
5874
5025
  return left.presence.state === right.presence.state && left.presence.speaking === right.presence.speaking && left.presence.activeSpeaker === right.presence.activeSpeaker;
@@ -5886,17 +5037,17 @@ function sameValues(left, right) {
5886
5037
  // src/space-client/reactions-controller.ts
5887
5038
  import { Clock as Clock4, Context as Context9, Effect as Effect8, Layer as Layer9 } from "effect";
5888
5039
  var MAX_VISIBLE_REACTIONS = 24;
5889
- var REACTIONS = /* @__PURE__ */ new Set(["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F389}"]);
5040
+ var REACTIONS = new Set(CHALK_REACTIONS);
5890
5041
  var ReactionsControllerService = class extends Context9.Service()("@chalk/client/ReactionsController") {
5891
5042
  };
5892
- var makeReactionsController = (connection, store, diagnostics) => Effect8.gen(function* () {
5043
+ var makeReactionsController = (connection, store) => Effect8.gen(function* () {
5893
5044
  const clock = yield* Clock4.Clock;
5894
5045
  const scope = yield* Effect8.scope;
5895
5046
  const context = yield* Effect8.context();
5896
5047
  const fork = (effect) => {
5897
5048
  void Effect8.runForkWith(context)(Effect8.forkIn(effect, scope).pipe(Effect8.asVoid));
5898
5049
  };
5899
- const controller = new ReactionsControllerRuntime(connection, store, clock.currentTimeMillisUnsafe, fork, diagnostics);
5050
+ const controller = new ReactionsControllerRuntime(connection, store, clock.currentTimeMillisUnsafe, fork);
5900
5051
  yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
5901
5052
  return controller;
5902
5053
  });
@@ -5905,41 +5056,32 @@ var ReactionsControllerRuntime = class {
5905
5056
  #store;
5906
5057
  #now;
5907
5058
  #fork;
5908
- #diagnostics;
5909
5059
  #active = /* @__PURE__ */ new Map();
5910
5060
  #expiryGenerations = /* @__PURE__ */ new Map();
5911
5061
  #unsubscribePorts = null;
5912
5062
  #unsubscribe = null;
5913
- constructor(connection, store, now, fork, diagnostics) {
5063
+ constructor(connection, store, now, fork) {
5914
5064
  this.#connection = connection;
5915
5065
  this.#store = store;
5916
5066
  this.#now = now;
5917
5067
  this.#fork = fork;
5918
- this.#diagnostics = diagnostics;
5919
5068
  this.#unsubscribePorts = connection.subscribePorts((ports) => this.#bind(ports));
5920
5069
  }
5921
5070
  send = (emoji) => {
5922
- const operation = this.#diagnostics?.startOperation("reaction.send");
5923
5071
  return Effect8.try({
5924
5072
  try: () => {
5925
5073
  if (!REACTIONS.has(emoji)) throw new SpaceClientError({ code: "reaction.invalid", recoverable: false, message: "The reaction is not supported" });
5926
5074
  },
5927
5075
  catch: normalizeClientError
5928
5076
  }).pipe(
5929
- Effect8.tap(() => Effect8.sync(() => operation?.observe("observed", "authorization"))),
5930
5077
  Effect8.andThen(this.#connection.runCommand(({ sync }) => foreign4(() => sync.sendReaction(emoji)))),
5931
- Effect8.tap(() => Effect8.sync(() => operation?.observe("observed", "accepted_commit"))),
5932
5078
  Effect8.map(reactionFor),
5933
5079
  Effect8.tap(
5934
5080
  (reaction) => Effect8.sync(() => {
5935
5081
  this.#observe(reaction);
5936
- operation?.observe("observed", "sender_result");
5937
- operation?.notObservable("recipient_projection", "recipient_projection_is_conditional");
5938
- operation?.succeed();
5939
5082
  })
5940
5083
  ),
5941
- Effect8.mapError(normalizeClientError),
5942
- Effect8.tapError(() => Effect8.sync(() => operation?.fail("send_failed")))
5084
+ Effect8.mapError(normalizeClientError)
5943
5085
  );
5944
5086
  };
5945
5087
  dispose() {
@@ -6017,30 +5159,24 @@ function sameReactions(left, right) {
6017
5159
  import { Context as Context10, Effect as Effect9, Layer as Layer10 } from "effect";
6018
5160
  var RecordingControllerService = class extends Context10.Service()("@chalk/client/RecordingController") {
6019
5161
  };
6020
- var makeRecordingController = (connection, store, diagnostics) => Effect9.acquireRelease(
6021
- Effect9.sync(() => new RecordingControllerRuntime(connection, store, diagnostics)),
5162
+ var makeRecordingController = (connection, store) => Effect9.acquireRelease(
5163
+ Effect9.sync(() => new RecordingControllerRuntime(connection, store)),
6022
5164
  (controller) => Effect9.sync(() => controller.dispose())
6023
5165
  );
6024
5166
  var RecordingControllerRuntime = class {
6025
5167
  #connection;
6026
5168
  #store;
6027
- #diagnostics;
6028
5169
  #subscriptions = { ports: null, recording: null };
6029
- constructor(connection, store, diagnostics) {
5170
+ constructor(connection, store) {
6030
5171
  this.#connection = connection;
6031
5172
  this.#store = store;
6032
- this.#diagnostics = diagnostics;
6033
5173
  this.#subscriptions.ports = connection.subscribePorts((ports) => this.#bind(ports));
6034
5174
  }
6035
5175
  start = () => this.#command(
6036
- "recording.start",
6037
- "start_failed",
6038
5176
  () => this.#authorizeStart(),
6039
5177
  (_, { sync }) => foreign5(() => sync.startRecording()).pipe(Effect9.map(({ recordingId }) => Object.freeze({ recordingId })))
6040
5178
  );
6041
5179
  stop = () => this.#command(
6042
- "recording.stop",
6043
- "stop_failed",
6044
5180
  () => this.#authorizeStop(),
6045
5181
  (recordingId, { sync }) => foreign5(() => sync.stopRecording(recordingId)).pipe(Effect9.asVoid)
6046
5182
  );
@@ -6064,16 +5200,11 @@ var RecordingControllerRuntime = class {
6064
5200
  if (!current || current.status !== "recording") throw invalidState("No active Recording can be stopped");
6065
5201
  return current.recordingId;
6066
5202
  }
6067
- #command(name, failureCode, authorize, execute) {
6068
- const operation = this.#diagnostics?.startOperation(name);
5203
+ #command(authorize, execute) {
6069
5204
  const authorized = Effect9.try({ try: authorize, catch: normalizeClientError });
6070
5205
  return authorized.pipe(
6071
- Effect9.tap(() => Effect9.sync(() => operation?.observe("observed", "authorization"))),
6072
5206
  Effect9.flatMap((authorization) => this.#connection.runCommand((ports) => execute(authorization, ports))),
6073
- Effect9.tap(() => Effect9.sync(() => operation?.observe("observed", "accepted_commit"))),
6074
- Effect9.tap(() => Effect9.sync(() => operation?.succeed())),
6075
- Effect9.mapError(normalizeClientError),
6076
- Effect9.tapError(() => Effect9.sync(() => operation?.fail(failureCode)))
5207
+ Effect9.mapError(normalizeClientError)
6077
5208
  );
6078
5209
  }
6079
5210
  #bind(ports) {
@@ -6194,11 +5325,11 @@ var ControllerEffectsService = class extends Context12.Service()("@chalk/client/
6194
5325
  };
6195
5326
  var makeControllerEffects = (input) => Effect11.gen(function* () {
6196
5327
  const offlineActions = yield* makeOfflineActions({ connection: input.connection, store: input.store, storage: input.offlineActionsStorage, storageKey: input.offlineActionsStorageKey });
6197
- const media = yield* makeMediaController(input.connection, input.store, input.mediaDeviceSelection, input.episodeDiagnostics);
6198
- const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics, offline: offlineActions });
6199
- const participants = yield* makeParticipantsController(input.connection, input.store, input.episodeDiagnostics, offlineActions);
6200
- const reactions = yield* makeReactionsController(input.connection, input.store, input.episodeDiagnostics);
6201
- const recording = yield* makeRecordingController(input.connection, input.store, input.episodeDiagnostics);
5328
+ const media = yield* makeMediaController(input.connection, input.store, input.mediaDeviceSelection);
5329
+ const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, offline: offlineActions });
5330
+ const participants = yield* makeParticipantsController(input.connection, input.store, offlineActions);
5331
+ const reactions = yield* makeReactionsController(input.connection, input.store);
5332
+ const recording = yield* makeRecordingController(input.connection, input.store);
6202
5333
  const whiteboard = yield* makeWhiteboardController(input.connection, input.store, input.featureFactories?.createWhiteboardClient);
6203
5334
  return { media, offline: offlineActions, chat, participants, reactions, recording, whiteboard };
6204
5335
  });
@@ -6254,775 +5385,21 @@ function requireDeviceId(deviceId) {
6254
5385
  throw new SpaceClientError({ code: "media.request_invalid", recoverable: false, message: "A media device ID is required" });
6255
5386
  }
6256
5387
 
6257
- // src/space-client/episode-diagnostic-runtime.ts
6258
- import { encodedEventSize, getActionContract, parseDiagnosticEventDraft as parseDiagnosticEventDraft2, redactDiagnosticAttributes as redactDiagnosticAttributes2 } from "@q9labsai/diagnostics-contracts";
6259
- var DEFAULT_RING_EVENTS = 256;
6260
- var DEFAULT_RING_BYTES = 256 * 1024;
6261
- var DEFAULT_RING_AGE_MS = 2 * 60 * 1e3;
6262
- var DEFAULT_QUEUE_EVENTS = 128;
6263
- var DEFAULT_QUEUE_BYTES = 128 * 1024;
6264
- var DEFAULT_QUEUE_AGE_MS = 60 * 1e3;
6265
- var DEFAULT_BATCH_EVENTS = 25;
6266
- var DEFAULT_BATCH_BYTES = 32 * 1024;
6267
- var DEFAULT_RETRY_ATTEMPTS = 3;
6268
- var DEFAULT_RETRY_DELAY_MS = 500;
6269
- var TERMINAL_CHECKPOINT_OVERRIDES = { "chat.send": "sender_receipt", "chat.retry": "sender_receipt", "reaction.send": "sender_result" };
6270
- var EpisodeDiagnosticRuntime = class {
6271
- #apiBaseUrl;
6272
- #instanceId;
6273
- #createId;
6274
- #fetch;
6275
- #now;
6276
- #setTimeout;
6277
- #clearTimeout;
6278
- #exporter;
6279
- #journeyId;
6280
- #traceparent;
6281
- #tracestate;
6282
- #maxRingEvents;
6283
- #maxRingBytes;
6284
- #maxRingAgeMs;
6285
- #maxQueueEvents;
6286
- #maxQueueBytes;
6287
- #maxQueueAgeMs;
6288
- #maxBatchEvents;
6289
- #maxBatchBytes;
6290
- #maxRetryAttempts;
6291
- #retryDelayMs;
6292
- #release;
6293
- #ring = [];
6294
- #queue = [];
6295
- #quarantine = [];
6296
- #ringBytes = 0;
6297
- #queueBytes = 0;
6298
- #quarantineBytes = 0;
6299
- #sequence = 0;
6300
- #credential = null;
6301
- #credentialEpoch = 0;
6302
- #captureEnabled = false;
6303
- #timer;
6304
- #retryTimer;
6305
- #retryResolve = null;
6306
- #activeExportController = null;
6307
- #flushing = false;
6308
- #disposed = false;
6309
- #pendingGapCount = 0;
6310
- #pendingGapReason = "client_queue_overflow";
6311
- constructor(options) {
6312
- this.#apiBaseUrl = normalizedBaseUrl(options.apiBaseUrl);
6313
- this.#createId = options.createId;
6314
- this.#instanceId = safeId(options.createId());
6315
- this.#fetch = options.fetch ?? globalThis.fetch;
6316
- this.#now = options.now ?? Date.now;
6317
- this.#setTimeout = options.setTimeout ?? ((callback, milliseconds) => globalThis.setTimeout(callback, milliseconds));
6318
- this.#clearTimeout = options.clearTimeout ?? ((handle) => globalThis.clearTimeout(handle));
6319
- this.#exporter = options.exporter ?? ((request) => this.#export(request));
6320
- this.#maxRingEvents = positive(options.maxRingEvents, DEFAULT_RING_EVENTS);
6321
- this.#maxRingBytes = positive(options.maxRingBytes, DEFAULT_RING_BYTES);
6322
- this.#maxRingAgeMs = positive(options.maxRingAgeMs, DEFAULT_RING_AGE_MS);
6323
- this.#maxQueueEvents = positive(options.maxQueueEvents, DEFAULT_QUEUE_EVENTS);
6324
- this.#maxQueueBytes = positive(options.maxQueueBytes, DEFAULT_QUEUE_BYTES);
6325
- this.#maxQueueAgeMs = positive(options.maxQueueAgeMs, DEFAULT_QUEUE_AGE_MS);
6326
- this.#maxBatchEvents = positive(options.maxBatchEvents, DEFAULT_BATCH_EVENTS);
6327
- this.#maxBatchBytes = positive(options.maxBatchBytes, DEFAULT_BATCH_BYTES);
6328
- this.#maxRetryAttempts = positive(options.maxRetryAttempts, DEFAULT_RETRY_ATTEMPTS);
6329
- this.#retryDelayMs = positive(options.retryDelayMs, DEFAULT_RETRY_DELAY_MS);
6330
- this.#release = safeRelease(options.release);
6331
- this.#journeyId = safeCorrelation(options.journeyId) ?? safeId(options.createId());
6332
- const trace = createTraceContext(options.traceparent, options.tracestate);
6333
- this.#traceparent = trace.traceparent;
6334
- this.#tracestate = trace.tracestate;
6335
- }
6336
- rotateCredential(credential) {
6337
- try {
6338
- if (this.#disposed) return;
6339
- if (credential === null) {
6340
- this.#disableCapture();
6341
- return;
6342
- }
6343
- if (!validCredential(credential, this.#now())) {
6344
- this.#disableCapture();
6345
- return;
6346
- }
6347
- if (this.#credential && credential.generation < this.#credential.generation) return;
6348
- this.#credential = Object.freeze({ ...credential });
6349
- this.#captureEnabled = true;
6350
- this.#emitPendingGap();
6351
- this.#scheduleFlush(0);
6352
- } catch {
6353
- }
6354
- }
6355
- context(operationRef) {
6356
- return {
6357
- journeyId: this.#journeyId,
6358
- traceparent: this.#traceparent,
6359
- ...this.#tracestate ? { tracestate: this.#tracestate } : {},
6360
- ...operationRef ? { producerOperationRef: operationRef } : {}
6361
- };
6362
- }
6363
- activeContext() {
6364
- return this.#enabled() ? this.context() : void 0;
6365
- }
6366
- /** Internal Feedback seam. It returns only a short-lived verified credential to the private transport. */
6367
- feedbackCredentialUnsafe() {
6368
- if (!this.#enabled() || !this.#credential) return null;
6369
- return Object.freeze({ ...this.#credential });
6370
- }
6371
- feedbackAvailabilityUnsafe() {
6372
- if (this.#disposed) return "disposed";
6373
- return this.#enabled() ? "available" : "disabled";
6374
- }
6375
- startOperation(name, attributes, parentOperationRef, producerOperationRef, correlationInput) {
6376
- if (!this.#enabled()) return void 0;
6377
- const credentialGeneration = this.#credential?.generation;
6378
- if (credentialGeneration === void 0) return void 0;
6379
- const credentialEpoch = this.#credentialEpoch;
6380
- const ref = safeId(producerOperationRef ?? this.#createId());
6381
- const trace = createTraceContext(reconnectBoundary(name) ? void 0 : this.#traceparent, this.#tracestate);
6382
- const correlation = { journeyId: this.#journeyId, traceId: trace.traceId, spanId: trace.spanId, ...correlationInput };
6383
- this.observe({ name, phase: "intent", state: "started", operationRef: ref, parentOperationRef, checkpoint: firstCheckpoint(name), attributes, correlation });
6384
- let ended = false;
6385
- const active2 = () => this.#enabled() && this.#credentialEpoch === credentialEpoch && this.#credential?.generation === credentialGeneration;
6386
- const terminal = (state, terminalAttributes) => {
6387
- if (ended || !active2()) return;
6388
- ended = true;
6389
- this.observe({ name, phase: state, state, operationRef: ref, parentOperationRef, checkpoint: terminalCheckpoint(name), attributes: terminalAttributes, correlation });
6390
- };
6391
- return Object.freeze({
6392
- ref,
6393
- observe: (phase, checkpoint, observedAttributes) => {
6394
- if (active2()) this.observe({ name, phase, state: "observed", operationRef: ref, parentOperationRef, checkpoint, attributes: observedAttributes, correlation });
6395
- },
6396
- succeed: (terminalAttributes) => terminal("succeeded", terminalAttributes),
6397
- fail: (reason) => terminal("failed", reason ? { reason: safeReason(reason) } : void 0),
6398
- notObservable: (checkpoint, reason) => {
6399
- if (active2()) this.observe({ name, phase: "not_observable", state: "not_observable", operationRef: ref, parentOperationRef, checkpoint, attributes: reason ? { reason: safeReason(reason) } : void 0, correlation });
6400
- }
6401
- });
6402
- }
6403
- observe(input) {
6404
- try {
6405
- if (!this.#enabled()) return;
6406
- this.#pruneExpired();
6407
- this.#emitPendingGap();
6408
- this.#enqueue(this.#draft(input));
6409
- this.#scheduleFlush(0);
6410
- } catch {
6411
- this.#pendingGapCount = Math.min(Number.MAX_SAFE_INTEGER, this.#pendingGapCount + 1);
6412
- this.#pendingGapReason = "client_event_rejected";
6413
- }
6414
- }
6415
- observePromise(operation, promise) {
6416
- if (!operation) return promise;
6417
- void promise.then(
6418
- () => operation.succeed(),
6419
- () => operation.fail("operation_failed")
6420
- );
6421
- return promise;
6422
- }
6423
- dispose() {
6424
- this.#disposed = true;
6425
- this.#disableCapture();
6426
- this.#flushing = false;
6427
- this.#clearBuffers();
6428
- }
6429
- /** Internal deterministic proof seam; it is not exported by the package. */
6430
- inspect() {
6431
- return Object.freeze({
6432
- ring: Object.freeze(this.#ring.map(({ event }) => event)),
6433
- queue: Object.freeze(this.#queue.map(({ event }) => event)),
6434
- quarantine: Object.freeze(this.#quarantine.map(({ event }) => event)),
6435
- dropped: this.#pendingGapCount,
6436
- credentialGeneration: this.#credential?.generation ?? null
6437
- });
6438
- }
6439
- #draft(input) {
6440
- return parseDiagnosticEventDraft2(
6441
- buildDraftInput(input, {
6442
- eventId: safeId(this.#createId()),
6443
- producerSequence: ++this.#sequence,
6444
- occurredAt: new Date(this.#now()).toISOString(),
6445
- journeyId: this.#journeyId,
6446
- traceparent: this.#traceparent,
6447
- release: this.#release
6448
- })
6449
- );
6450
- }
6451
- #enqueue(event) {
6452
- const buffered = { event, bytes: encodedEventSize(event), observedAt: this.#now(), credentialGeneration: this.#credential?.generation ?? 0 };
6453
- this.#ring.push(buffered);
6454
- this.#ringBytes += buffered.bytes;
6455
- while (this.#ring.length > this.#maxRingEvents || this.#ringBytes > this.#maxRingBytes) {
6456
- const removed = this.#ring.shift();
6457
- if (removed) this.#ringBytes -= removed.bytes;
6458
- }
6459
- this.#queue.push(buffered);
6460
- this.#queueBytes += buffered.bytes;
6461
- while (this.#queue.length > this.#maxQueueEvents || this.#queueBytes > this.#maxQueueBytes) {
6462
- const removed = this.#queue.shift();
6463
- if (removed) {
6464
- this.#queueBytes -= removed.bytes;
6465
- this.#pendingGapCount += 1;
6466
- this.#pendingGapReason = "client_queue_overflow";
6467
- }
6468
- }
6469
- }
6470
- #emitPendingGap() {
6471
- if (this.#pendingGapCount === 0 || !this.#enabled()) return;
6472
- const dropped = this.#pendingGapCount;
6473
- const reason = this.#pendingGapReason;
6474
- this.#pendingGapCount = 0;
6475
- try {
6476
- this.#enqueue(this.#draft({ name: "coverage.gap", phase: "observed", state: "observed", attributes: { reason, count: dropped } }));
6477
- } catch {
6478
- this.#pendingGapCount = dropped;
6479
- }
6480
- }
6481
- #pruneExpired() {
6482
- const now = this.#now();
6483
- while (this.#ring[0] && now - this.#ring[0].observedAt > this.#maxRingAgeMs) {
6484
- this.#ringBytes -= this.#ring.shift().bytes;
6485
- }
6486
- while (this.#queue[0] && now - this.#queue[0].observedAt > this.#maxQueueAgeMs) {
6487
- this.#queueBytes -= this.#queue.shift().bytes;
6488
- this.#pendingGapCount += 1;
6489
- this.#pendingGapReason = "client_queue_expired";
6490
- }
6491
- }
6492
- #scheduleFlush(delay) {
6493
- if (this.#timer !== void 0 || this.#flushing || !this.#deliveryCredential() || this.#queue.length === 0) return;
6494
- this.#timer = this.#setTimeout(() => {
6495
- this.#timer = void 0;
6496
- void this.#flush();
6497
- }, delay);
6498
- }
6499
- async #flush() {
6500
- if (this.#flushing || !this.#enabled()) return;
6501
- this.#pruneExpired();
6502
- this.#emitPendingGap();
6503
- if (!this.#deliveryCredential() || this.#queue.length === 0) return;
6504
- const batch = takeBatch(this.#queue, this.#maxBatchEvents, this.#maxBatchBytes);
6505
- if (batch.length === 0) return;
6506
- this.#flushing = true;
6507
- await this.#deliverBatch(batch);
6508
- this.#flushing = false;
6509
- this.#scheduleFlush(0);
6510
- }
6511
- async #deliverBatch(batch) {
6512
- let result;
6513
- let attempted = false;
6514
- let cancelled = false;
6515
- for (let attempt = 1; attempt <= this.#maxRetryAttempts; attempt += 1) {
6516
- const delivery = await this.#deliverAttempt(batch, attempt);
6517
- if (delivery.attempted) attempted = true;
6518
- if (delivery.result) {
6519
- result = delivery.result;
6520
- break;
6521
- }
6522
- if (delivery.cancelled) {
6523
- cancelled = true;
6524
- break;
6525
- }
6526
- }
6527
- this.#finishDelivery(batch, { result, attempted, cancelled });
6528
- }
6529
- async #deliverAttempt(batch, attempt) {
6530
- const credential = this.#deliveryCredential();
6531
- if (!credential) return { attempted: false, cancelled: true };
6532
- const controller = new AbortController();
6533
- this.#activeExportController = controller;
6534
- try {
6535
- return { result: await this.#exportBatch(batch, credential, controller.signal), attempted: true, cancelled: false };
6536
- } catch {
6537
- return { attempted: true, cancelled: await this.#cancelledAfterFailure(attempt) };
6538
- } finally {
6539
- if (this.#activeExportController === controller) this.#activeExportController = null;
6540
- }
6541
- }
6542
- async #cancelledAfterFailure(attempt) {
6543
- if (this.#captureEnabled && this.#credential && attempt < this.#maxRetryAttempts) return !await this.#waitRetry(this.#retryDelayMs * attempt);
6544
- return !this.#captureEnabled || !this.#credential || this.#disposed;
6545
- }
6546
- async #exportBatch(batch, credential, signal) {
6547
- return await this.#exporter({
6548
- endpoint: new URL(credential.intakePath, this.#apiBaseUrl).toString(),
6549
- token: credential.token,
6550
- signal,
6551
- body: { version: 1, producer: { id: "sdk", instanceId: this.#instanceId, generation: credential.generation }, events: batch.map(({ event }) => event) }
6552
- }) ?? { accepted: batch.map(({ event }) => event.eventId), duplicates: [], conflicts: [] };
6553
- }
6554
- #finishDelivery(batch, delivery) {
6555
- if (this.#disposed || delivery.cancelled) return;
6556
- if (delivery.result) {
6557
- this.#quarantineBatch(batch, new Set(delivery.result.conflicts));
6558
- this.#removeBatch(batch);
6559
- return;
6560
- }
6561
- if (!delivery.attempted) return;
6562
- this.#removeBatch(batch);
6563
- this.#pendingGapCount += batch.length;
6564
- this.#pendingGapReason = "delivery_retry_exhausted";
6565
- this.#emitPendingGap();
6566
- }
6567
- async #export(request) {
6568
- if (!this.#fetch) throw new TypeError("Fetch is unavailable");
6569
- const response = await this.#fetch(request.endpoint, {
6570
- method: "POST",
6571
- headers: { authorization: `Bearer ${request.token}`, "content-type": "application/json" },
6572
- body: JSON.stringify(request.body),
6573
- keepalive: true,
6574
- signal: request.signal
6575
- });
6576
- if (!response.ok) throw new TypeError(`Diagnostic intake rejected the batch with HTTP ${response.status}`);
6577
- const result = await response.json();
6578
- const delivery = parseIntakeResponse(result, new Set(request.body.events.map((event) => event.eventId)));
6579
- if (!delivery) throw new TypeError("Diagnostic intake returned an invalid durable acknowledgement");
6580
- return delivery;
6581
- }
6582
- #removeBatch(batch) {
6583
- const ids = new Set(batch.map(({ event }) => event.eventId));
6584
- for (let index = this.#queue.length - 1; index >= 0; index -= 1) {
6585
- const queued = this.#queue[index];
6586
- if (!queued || !ids.has(queued.event.eventId)) continue;
6587
- this.#queue.splice(index, 1);
6588
- this.#queueBytes -= queued.bytes;
6589
- }
6590
- }
6591
- #quarantineBatch(batch, conflicts) {
6592
- for (const event of batch) {
6593
- if (!conflicts.has(event.event.eventId)) continue;
6594
- this.#quarantine.push(event);
6595
- this.#quarantineBytes += event.bytes;
6596
- }
6597
- while (this.#quarantine.length > this.#maxRingEvents || this.#quarantineBytes > this.#maxRingBytes) {
6598
- const removed = this.#quarantine.shift();
6599
- if (removed) this.#quarantineBytes -= removed.bytes;
6600
- }
6601
- }
6602
- #enabled() {
6603
- if (this.#disposed || !this.#captureEnabled || !this.#credential) return false;
6604
- if (validCredential(this.#credential, this.#now())) return true;
6605
- this.#disableCapture();
6606
- return false;
6607
- }
6608
- #deliveryCredential() {
6609
- return this.#enabled() ? this.#credential : null;
6610
- }
6611
- #cancelTimer() {
6612
- if (this.#timer !== void 0) this.#clearTimeout(this.#timer);
6613
- this.#timer = void 0;
6614
- }
6615
- #disableCapture() {
6616
- this.#credentialEpoch += 1;
6617
- this.#credential = null;
6618
- this.#captureEnabled = false;
6619
- this.#cancelTimer();
6620
- this.#activeExportController?.abort();
6621
- this.#activeExportController = null;
6622
- if (this.#retryTimer !== void 0) this.#clearTimeout(this.#retryTimer);
6623
- this.#retryTimer = void 0;
6624
- this.#retryResolve?.();
6625
- this.#retryResolve = null;
6626
- }
6627
- #waitRetry(milliseconds) {
6628
- if (this.#disposed || !this.#captureEnabled || !this.#credential) return Promise.resolve(false);
6629
- return new Promise((resolve) => {
6630
- let settled = false;
6631
- const finish = (value) => {
6632
- if (settled) return;
6633
- settled = true;
6634
- this.#retryTimer = void 0;
6635
- this.#retryResolve = null;
6636
- resolve(value);
6637
- };
6638
- this.#retryResolve = () => finish(false);
6639
- this.#retryTimer = this.#setTimeout(() => finish(true), milliseconds);
6640
- });
6641
- }
6642
- #clearBuffers() {
6643
- this.#ring.length = 0;
6644
- this.#queue.length = 0;
6645
- this.#quarantine.length = 0;
6646
- this.#ringBytes = 0;
6647
- this.#queueBytes = 0;
6648
- this.#quarantineBytes = 0;
6649
- this.#pendingGapCount = 0;
6650
- this.#pendingGapReason = "client_queue_overflow";
6651
- }
6652
- };
6653
- function takeBatch(queue, maxEvents, maxBytes) {
6654
- const batch = [];
6655
- let bytes = 0;
6656
- for (const event of queue) {
6657
- if (batch.length >= maxEvents || batch.length > 0 && bytes + event.bytes > maxBytes) break;
6658
- batch.push(event);
6659
- bytes += event.bytes;
6660
- }
6661
- return batch;
6662
- }
6663
- function firstCheckpoint(name) {
6664
- return getActionContract(name)?.checkpoints[0]?.key;
6665
- }
6666
- function terminalCheckpoint(name) {
6667
- return TERMINAL_CHECKPOINT_OVERRIDES[name] ?? requiredTerminalCheckpoint(getActionContract(name)?.checkpoints);
6668
- }
6669
- function requiredTerminalCheckpoint(checkpoints) {
6670
- if (!checkpoints) return void 0;
6671
- for (let index = checkpoints.length - 1; index >= 0; index -= 1) {
6672
- const checkpoint = checkpoints[index];
6673
- if (checkpoint?.class === "required") return checkpoint.key;
6674
- }
6675
- return checkpoints[checkpoints.length - 1]?.key;
6676
- }
6677
- function reconnectBoundary(name) {
6678
- return name === "participant.reconnect" || name === "sync.reconnect" || name === "recovery.sync.retry" || name === "recovery.media.retry";
6679
- }
6680
- function compactCorrelation(correlation) {
6681
- return Object.fromEntries(Object.entries(correlation).filter(([, value]) => value !== void 0));
6682
- }
6683
- function correlationFromInput(input) {
6684
- if (!input) return {};
6685
- return compactCorrelation({
6686
- journeyId: safeCorrelation(input.journeyId),
6687
- traceId: safeCorrelation(input.traceId),
6688
- spanId: safeCorrelation(input.spanId),
6689
- requestId: safeCorrelation(input.requestId),
6690
- commandId: safeCorrelation(input.commandId),
6691
- retryGroupRef: safeCorrelation(input.retryGroupRef),
6692
- attempt: typeof input.attempt === "number" && Number.isSafeInteger(input.attempt) && input.attempt >= 0 ? input.attempt : void 0,
6693
- providerId: input.provider?.storage === "hmac" ? safeCorrelation(input.provider.value) : void 0
6694
- });
6695
- }
6696
- function validCredential(credential, now) {
6697
- return validEpisodeDiagnosticCredential(credential, now);
6698
- }
6699
- function safeId(value) {
6700
- const bounded = value.replace(/[^A-Za-z0-9._:@+/=-]/gu, "_").slice(0, 128);
6701
- return bounded.length > 0 && /^[A-Za-z0-9]/u.test(bounded) ? bounded : `sdk_${Date.now()}`;
6702
- }
6703
- function safeCorrelation(value) {
6704
- return value && /^[A-Za-z0-9][A-Za-z0-9._:@+/=-]{0,127}$/u.test(value) ? value : void 0;
6705
- }
6706
- function safeReason(value) {
6707
- return value.replace(/[^a-z0-9_.-]/giu, "_").slice(0, 64) || "unknown";
6708
- }
6709
- function safeRelease(value) {
6710
- const id = safeCorrelation(value?.id);
6711
- const sourceCommit = safeCorrelation(value?.sourceCommit);
6712
- return id ? Object.freeze({ id, ...sourceCommit ? { sourceCommit } : {} }) : void 0;
6713
- }
6714
- function positive(value, fallback) {
6715
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : fallback;
6716
- }
6717
- function normalizedBaseUrl(value) {
6718
- const parsed = new URL(value);
6719
- parsed.pathname = trimTrailingPathSlashes(parsed.pathname);
6720
- parsed.search = "";
6721
- parsed.hash = "";
6722
- return stripTrailingUrlSlash(parsed.toString());
6723
- }
6724
- function parseIntakeResponse(value, expected) {
6725
- const response = intakeResponse(value);
6726
- if (!response) return null;
6727
- const accepted = intakeIds(response.accepted, expected, true);
6728
- const duplicates = intakeIds(response.duplicates, expected, true);
6729
- const conflicts = intakeIds(response.conflicts, expected, false);
6730
- if (!accepted || !duplicates || !conflicts) return null;
6731
- return completeDelivery({ accepted, duplicates, conflicts }, expected);
6732
- }
6733
- function buildDraftInput(input, context) {
6734
- const contract = getActionContract(input.name);
6735
- const checkpoint = input.checkpoint ?? contract?.checkpoints[0]?.key;
6736
- const trace = parseTraceparent(context.traceparent);
6737
- const correlation = compactCorrelation({ journeyId: context.journeyId, traceId: trace?.traceId, spanId: trace?.spanId, ...correlationFromInput(input.correlation) });
6738
- return {
6739
- version: 1,
6740
- eventId: context.eventId,
6741
- ...eventReferences(input),
6742
- producerSequence: context.producerSequence,
6743
- occurredAt: context.occurredAt,
6744
- source: "sdk",
6745
- name: input.name,
6746
- phase: input.phase,
6747
- state: input.state,
6748
- expectation: expectationFor(contract, checkpoint),
6749
- correlation: optionalCorrelation(correlation),
6750
- release: context.release,
6751
- attributes: redactDiagnosticAttributes2(input.attributes).attributes
6752
- };
6753
- }
6754
- function eventReferences(input) {
6755
- return { producerOperationRef: optionalSafeId(input.operationRef), parentProducerOperationRef: optionalSafeId(input.parentOperationRef) };
6756
- }
6757
- function optionalSafeId(value) {
6758
- return value === void 0 ? void 0 : safeId(value);
6759
- }
6760
- function expectationFor(contract, checkpoint) {
6761
- const checkpointContract = contract?.checkpoints.find((candidate) => candidate.key === checkpoint);
6762
- return contract && checkpoint && checkpointContract ? { name: contract.operation, version: contract.expectationVersion, checkpoint, checkpointClass: checkpointContract.class } : void 0;
6763
- }
6764
- function optionalCorrelation(correlation) {
6765
- return Object.keys(correlation).length > 0 ? correlation : void 0;
6766
- }
6767
- function trimTrailingPathSlashes(value) {
6768
- return value.replace(/\/+$/u, "");
6769
- }
6770
- function stripTrailingUrlSlash(value) {
6771
- return value.endsWith("/") ? value.slice(0, -1) : value;
6772
- }
6773
- function intakeResponse(value) {
6774
- if (!isRecord3(value) || typeof value.diagnosticReference !== "string" || !Number.isSafeInteger(value.committedCursor)) return null;
6775
- const accepted = legacyEmptyAcknowledgement(value.accepted);
6776
- const duplicates = legacyEmptyAcknowledgement(value.duplicates);
6777
- const conflicts = legacyEmptyAcknowledgement(value.conflicts);
6778
- if (accepted === null || duplicates === null || conflicts === null) return null;
6779
- return { accepted, duplicates, conflicts };
6780
- }
6781
- function legacyEmptyAcknowledgement(value) {
6782
- if (value === null) return [];
6783
- return Array.isArray(value) ? value : null;
6784
- }
6785
- function completeDelivery(delivery, expected) {
6786
- const all = [...delivery.accepted, ...delivery.duplicates, ...delivery.conflicts];
6787
- return all.length === expected.size && new Set(all).size === expected.size ? Object.freeze(delivery) : null;
6788
- }
6789
- function intakeIds(items, expected, cursorRequired) {
6790
- const ids = [];
6791
- for (const item of items) {
6792
- if (!isRecord3(item) || typeof item.eventId !== "string" || !expected.has(item.eventId)) return null;
6793
- if (cursorRequired && !Number.isSafeInteger(item.cursor)) return null;
6794
- if (!cursorRequired && item.code !== "fingerprint_mismatch") return null;
6795
- ids.push(item.eventId);
6796
- }
6797
- return ids;
6798
- }
6799
- function isRecord3(value) {
6800
- return typeof value === "object" && value !== null && !Array.isArray(value);
6801
- }
6802
-
6803
- // src/space-client/episode-diagnostic-registry.ts
6804
- var connectionDiagnostics = /* @__PURE__ */ new WeakMap();
6805
- var dependencyDiagnostics = /* @__PURE__ */ new WeakMap();
6806
- var syncDiagnostics = /* @__PURE__ */ new WeakMap();
6807
- function registerEpisodeDiagnosticConnection(connection, diagnostics) {
6808
- connectionDiagnostics.set(connection, diagnostics);
6809
- }
6810
- function unregisterEpisodeDiagnosticConnection(connection, diagnostics) {
6811
- if (diagnostics === void 0 || connectionDiagnostics.get(connection) === diagnostics) connectionDiagnostics.delete(connection);
6812
- }
6813
- function registerEpisodeDiagnosticDependencies(dependencies, diagnostics) {
6814
- dependencyDiagnostics.set(dependencies, diagnostics);
6815
- }
6816
- function episodeDiagnosticsForDependencies(dependencies) {
6817
- return dependencyDiagnostics.get(dependencies);
6818
- }
6819
- function unregisterEpisodeDiagnosticDependencies(dependencies, diagnostics) {
6820
- if (diagnostics === void 0 || dependencyDiagnostics.get(dependencies) === diagnostics) dependencyDiagnostics.delete(dependencies);
6821
- }
6822
- function registerEpisodeDiagnosticSyncClient(client, diagnostics) {
6823
- syncDiagnostics.set(client, diagnostics);
6824
- return client;
6825
- }
6826
- function unregisterEpisodeDiagnosticSyncClient(client, diagnostics) {
6827
- if (diagnostics === void 0 || syncDiagnostics.get(client) === diagnostics) syncDiagnostics.delete(client);
6828
- }
6829
-
6830
- // src/space-client/episode-diagnostic-render-registry.ts
6831
- var REGISTRY = /* @__PURE__ */ Symbol.for("@chalk/private/episode-diagnostic-render-registry/v1");
6832
- var renderOwners = /* @__PURE__ */ new WeakMap();
6833
- var completedTracksByRuntime = /* @__PURE__ */ new WeakMap();
6834
- function registerEpisodeDiagnosticTrack(track, source, diagnostics) {
6835
- try {
6836
- if (completedTracksByRuntime.get(track)?.has(diagnostics) || renderOwners.get(track) === diagnostics) return;
6837
- const registry = renderRegistry();
6838
- let observed = false;
6839
- renderOwners.set(track, diagnostics);
6840
- registry.set(track, (observation) => {
6841
- if (observed) return;
6842
- observed = true;
6843
- const completed = completedTracksByRuntime.get(track) ?? /* @__PURE__ */ new WeakSet();
6844
- completed.add(diagnostics);
6845
- completedTracksByRuntime.set(track, completed);
6846
- const name = source === "screen" ? "screen.start" : "camera.publish";
6847
- if (observation === "first_frame") {
6848
- diagnostics.observe({ name, phase: "first_frame", state: "observed", checkpoint: source === "screen" ? "remote_first_frame" : "terminal", attributes: { direction: "remote", media_kind: source } });
6849
- } else {
6850
- diagnostics.observe({ name, phase: "not_observable", state: "not_observable", checkpoint: source === "screen" ? "remote_first_frame" : "terminal", attributes: { direction: "remote", media_kind: source, reason: "platform_render_unobservable" } });
6851
- }
6852
- registry.delete(track);
6853
- renderOwners.delete(track);
6854
- });
6855
- } catch {
6856
- }
6857
- }
6858
- function unregisterEpisodeDiagnosticTrack(track, diagnostics) {
6859
- try {
6860
- if (diagnostics !== void 0 && renderOwners.get(track) !== diagnostics) return;
6861
- renderRegistry().delete(track);
6862
- renderOwners.delete(track);
6863
- } catch {
6864
- }
6865
- }
6866
- function renderRegistry() {
6867
- const target = globalThis;
6868
- target[REGISTRY] ??= /* @__PURE__ */ new WeakMap();
6869
- return target[REGISTRY];
6870
- }
6871
-
6872
- // src/feedback/controller.ts
6873
- var DEFAULT_SDK = Object.freeze({ client: "@q9labsai/chalk-client" });
6874
- var DEFAULT_PLATFORM = Object.freeze({ kind: "web" });
6875
- function createFeedbackController(context) {
6876
- const transport = createFeedbackTransport({
6877
- baseUrl: context.apiBaseUrl,
6878
- fetch: context.fetch,
6879
- credential: context.diagnosticCredential,
6880
- telemetry: () => context.telemetry ?? context.diagnosticContext()
6881
- });
6882
- let disposed = false;
6883
- const ensureActive = () => {
6884
- if (disposed) throw new FeedbackTransportError({ code: "unavailable", recoverable: false, message: "Feedback has been disposed." });
6885
- };
6886
- const prepare = async (input = {}) => {
6887
- ensureActive();
6888
- const collectionInput = evidenceInput(input.evidence);
6889
- const screenshot = await resolveScreenshot(input.screenshot_provider ?? input.screenshotProvider);
6890
- ensureActive();
6891
- const initialInput = { ...collectionInput, screenshot };
6892
- const idempotencyKey = idempotencyKeyFor(context.createId);
6893
- const preparedEvidence = collectFeedbackEvidenceFromContext(initialInput, context);
6894
- const preparedScreenshot = capturedScreenshot(screenshot) ? screenshotPayload(screenshot).screenshot : void 0;
6895
- let cachedRequest;
6896
- const prepared = {
6897
- idempotency_key: idempotencyKey,
6898
- evidence: preparedEvidence,
6899
- ...preparedScreenshot ? { screenshot: preparedScreenshot } : {},
6900
- collect: (nextInput) => {
6901
- ensureActive();
6902
- return collectFeedbackEvidenceFromContext(nextInput ?? initialInput, context);
6903
- },
6904
- send: async ({ category, message }) => {
6905
- ensureActive();
6906
- const normalizedMessage = assertFeedbackMessage(message);
6907
- const request = cachedRequest ?? parseFeedbackReportRequest({
6908
- schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION,
6909
- category,
6910
- message: normalizedMessage,
6911
- source: input.source ?? context.source,
6912
- evidence: preparedEvidence,
6913
- ...preparedScreenshot ? { screenshot: preparedScreenshot } : {}
6914
- });
6915
- cachedRequest ??= request;
6916
- return transport.send({ request, idempotencyKey });
6917
- }
6918
- };
6919
- return prepared;
6920
- };
6921
- const send = async (input) => {
6922
- ensureActive();
6923
- const source = input.source ?? context.source;
6924
- const evidenceInput2 = input.evidence ?? defaultEvidenceInput();
6925
- const screenshot = input.screenshot ?? evidenceInput2.screenshot;
6926
- return sendRequest({ category: input.category, message: input.message, source, evidence: { ...evidenceInput2, ...screenshot ? { screenshot } : {} }, idempotencyKey: input.idempotency_key ?? input.idempotencyKey ?? idempotencyKeyFor(context.createId) });
6927
- };
6928
- const sendRequest = async (input) => {
6929
- ensureActive();
6930
- const evidence = collectFeedbackEvidenceFromContext(input.evidence, context);
6931
- const request = parseFeedbackReportRequest({ schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION, category: input.category, message: assertFeedbackMessage(input.message), source: input.source ?? context.source, evidence, ...screenshotPayload(input.evidence.screenshot) });
6932
- return transport.send({ request, idempotencyKey: input.idempotencyKey });
6933
- };
6934
- return {
6935
- prepare,
6936
- send,
6937
- dispose: () => {
6938
- disposed = true;
6939
- }
6940
- };
6941
- }
6942
- function evidenceInput(input) {
6943
- const base = { sdk: input?.sdk ?? DEFAULT_SDK, platform: input?.platform ?? DEFAULT_PLATFORM };
6944
- if (!input) return base;
6945
- return { ...base, ...optionalEvidenceInput(input) };
6946
- }
6947
- function optionalEvidenceInput(input) {
6948
- return {
6949
- ...input.app ? { app: input.app } : {},
6950
- ...input.local_state ? { local_state: input.local_state } : {},
6951
- ...input.cookies ? { cookies: input.cookies } : {}
6952
- };
6953
- }
6954
- function defaultEvidenceInput() {
6955
- return { sdk: DEFAULT_SDK, platform: DEFAULT_PLATFORM };
6956
- }
6957
- async function resolveScreenshot(provider) {
6958
- if (!provider) return { state: "unavailable", failure_code: "unsupported" };
6959
- let timeout;
6960
- try {
6961
- return await Promise.race([
6962
- provider(),
6963
- new Promise((resolve) => {
6964
- timeout = globalThis.setTimeout(() => resolve({ state: "unavailable", failure_code: "capture_failed" }), 5e3);
6965
- })
6966
- ]);
6967
- } catch {
6968
- return { state: "unavailable", failure_code: "capture_failed" };
6969
- } finally {
6970
- globalThis.clearTimeout(timeout);
6971
- }
6972
- }
6973
- function capturedScreenshot(input) {
6974
- return input.state === "captured" || input.state === "partial" ? input : void 0;
6975
- }
6976
- function screenshotPayload(input) {
6977
- if (!input) return {};
6978
- const screenshot = capturedScreenshot(input);
6979
- return screenshot ? { screenshot: { schema_version: "FeedbackScreenshot/v1", mime_type: screenshot.mime_type, width: screenshot.width, height: screenshot.height, captured_at: screenshot.captured_at, data_base64: screenshot.data_base64 } } : {};
6980
- }
6981
- function idempotencyKeyFor(createId) {
6982
- const generated = createId();
6983
- if (/^[A-Za-z0-9_-]{16,128}$/u.test(generated)) return generated;
6984
- if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
6985
- return `feedback-${Date.now()}-${Math.random().toString(36).slice(2, 18)}`;
6986
- }
6987
-
6988
5388
  // src/space-client/core.ts
6989
- var syncClientsByDependencies = /* @__PURE__ */ new WeakMap();
6990
5389
  var SpaceClientCoreService = class extends Context13.Service()("@chalk/client/SpaceClientCore") {
6991
5390
  };
6992
5391
  var makeSpaceClientCoreLayer = (options, platform = {}) => {
6993
- const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
5392
+ const apiBaseUrl = normalizedBaseUrl(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
6994
5393
  const syncUrl = platform.syncUrl ?? defaultSyncUrl(apiBaseUrl);
6995
5394
  const baseDependencies = { ...createDefaultConnectionDependencies({ apiBaseURL: apiBaseUrl, syncURL: syncUrl, whiteboardURL: platform.whiteboardUrl }), ...platform.dependencies };
6996
- const episodeDiagnostics = new EpisodeDiagnosticRuntime({
6997
- apiBaseUrl,
6998
- createId: platform.randomUUID ?? baseDependencies.createId ?? (() => globalThis.crypto.randomUUID()),
6999
- fetch: platform.fetch,
7000
- now: baseDependencies.clock.now,
7001
- setTimeout: baseDependencies.clock.setTimeout,
7002
- clearTimeout: baseDependencies.clock.clearTimeout,
7003
- release: { id: "chalk-client@4.1.17" }
7004
- });
7005
- const diagnosticSyncClients = /* @__PURE__ */ new Set();
7006
- const dependencies = {
7007
- ...baseDependencies,
7008
- createSyncClient: (input) => {
7009
- const syncClient = baseDependencies.createSyncClient(input);
7010
- diagnosticSyncClients.add(syncClient);
7011
- return registerEpisodeDiagnosticSyncClient(syncClient, episodeDiagnostics);
7012
- }
7013
- };
7014
- registerEpisodeDiagnosticDependencies(dependencies, episodeDiagnostics);
7015
- syncClientsByDependencies.set(dependencies, diagnosticSyncClients);
5395
+ const dependencies = baseDependencies;
7016
5396
  const lifecycle = makeConnectionLifecycleLayer({
7017
- access: (request) => resolveAccessGrant({ request, diagnostics: episodeDiagnostics, options, platform }),
5397
+ access: async (request) => requireParsedAccessGrant(await fetchAccessGrant2({ request, options, platform })),
7018
5398
  apiBaseURL: apiBaseUrl,
7019
5399
  syncURL: syncUrl,
7020
5400
  syncStartupTimeoutMs: platform.syncStartupTimeoutMs,
7021
5401
  initialMicrophoneEnabled: platform.initialMicrophoneEnabled,
7022
5402
  initialCameraEnabled: platform.initialCameraEnabled,
7023
- telemetry: platform.telemetry,
7024
- recordRtcSummary: platform.recordRtcSummary,
7025
- recordReconnect: platform.recordReconnect,
7026
5403
  dependencies,
7027
5404
  diagnostics: {
7028
5405
  onEvent: (event) => {
@@ -7042,10 +5419,8 @@ var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer13.e
7042
5419
  const space = options.space.trim();
7043
5420
  if (space.length === 0) return yield* Effect12.die(new TypeError("A Space slug is required"));
7044
5421
  if (typeof options.getAccess !== "function") return yield* Effect12.die(new TypeError("getAccess is required"));
7045
- const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
5422
+ const apiBaseUrl = normalizedBaseUrl(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
7046
5423
  const selection = new MediaDeviceSelection(dependencies.mediaDevices);
7047
- const episodeDiagnostics = episodeDiagnosticsForDependencies(dependencies);
7048
- if (episodeDiagnostics) registerEpisodeDiagnosticConnection(lifecycle, episodeDiagnostics);
7049
5424
  const controllers = yield* makeControllerEffects({
7050
5425
  offlineActionsStorage: options.offlineActionsStorage,
7051
5426
  offlineActionsStorageKey: `chalk:offline-actions:${JSON.stringify([apiBaseUrl, space])}`,
@@ -7054,64 +5429,28 @@ var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer13.e
7054
5429
  store,
7055
5430
  mediaDeviceSelection: selection,
7056
5431
  featureFactories: dependencies,
7057
- fetch: platform.fetch,
7058
- episodeDiagnostics
5432
+ fetch: platform.fetch
7059
5433
  });
7060
5434
  return yield* Effect12.acquireRelease(
7061
- Effect12.sync(
7062
- () => new SpaceClientCore(lifecycle, selection, store, controllers, episodeDiagnostics, dependencies, syncClientsByDependencies.get(dependencies), {
7063
- apiBaseUrl,
7064
- fetch: platform.fetch,
7065
- createId: platform.randomUUID ?? dependencies.createId ?? lifecycle.createId,
7066
- source: platform.feedbackSource ?? "embedded",
7067
- telemetry: platform.telemetry
7068
- })
7069
- ),
5435
+ Effect12.sync(() => new SpaceClientCore(lifecycle, selection, store, controllers)),
7070
5436
  (core) => Effect12.sync(() => core.dispose())
7071
5437
  );
7072
5438
  })
7073
5439
  );
7074
5440
  var SpaceClientCore = class {
7075
5441
  controllers;
7076
- feedback;
7077
5442
  #connection;
7078
5443
  #store;
7079
- #episodeDiagnostics;
7080
- #episodeDiagnosticDependencies;
7081
- #episodeDiagnosticSyncClients;
7082
- #episodeDiagnosticTracks = /* @__PURE__ */ new Map();
7083
5444
  #handlers = { participantJoined: /* @__PURE__ */ new Set(), participantLeft: /* @__PURE__ */ new Set(), episodeEnded: /* @__PURE__ */ new Set(), screenShareStarted: /* @__PURE__ */ new Set(), screenShareStopped: /* @__PURE__ */ new Set(), error: /* @__PURE__ */ new Set() };
7084
5445
  #disposed = false;
7085
5446
  #episodeEnded = false;
7086
5447
  #previous;
7087
5448
  #unsubscribeConnection = null;
7088
5449
  #unsubscribeStore = null;
7089
- #leaveOperation;
7090
- #participantJoinOperation;
7091
- #syncConnectOperation;
7092
- constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients, feedback) {
5450
+ constructor(connection, _selection, store, controllers) {
7093
5451
  this.#connection = connection;
7094
5452
  this.#store = store;
7095
5453
  this.controllers = controllers;
7096
- this.#episodeDiagnostics = episodeDiagnostics;
7097
- this.#episodeDiagnosticDependencies = episodeDiagnosticDependencies;
7098
- this.#episodeDiagnosticSyncClients = episodeDiagnosticSyncClients;
7099
- this.feedback = createFeedbackController({
7100
- apiBaseUrl: feedback?.apiBaseUrl ?? "https://api.chalkmeet.com",
7101
- fetch: feedback?.fetch,
7102
- createId: feedback?.createId ?? connection.createId,
7103
- now: connection.nowUnsafe,
7104
- source: feedback?.source ?? "embedded",
7105
- telemetry: feedback?.telemetry,
7106
- diagnosticContext: () => episodeDiagnostics?.activeContext(),
7107
- connection: connection.getSnapshot,
7108
- diagnosticCredential: () => episodeDiagnostics?.feedbackCredentialUnsafe() ?? null,
7109
- diagnosticAvailability: () => episodeDiagnostics?.feedbackAvailabilityUnsafe() ?? "unavailable",
7110
- diagnosticSnapshot: () => {
7111
- const inspected = episodeDiagnostics?.inspect();
7112
- return { dropped: inspected?.dropped ?? 0, events: inspected?.ring ?? [] };
7113
- }
7114
- });
7115
5454
  this.#store.updateConnection(connection.getSnapshot());
7116
5455
  this.#previous = this.#store.getSnapshot();
7117
5456
  this.#unsubscribeStore = this.#store.subscribe(() => this.#publishStore());
@@ -7122,9 +5461,7 @@ var SpaceClientCore = class {
7122
5461
  const newlyEnded = snapshot.episodeEnded && !this.#episodeEnded;
7123
5462
  this.#episodeEnded = snapshot.episodeEnded ?? false;
7124
5463
  if (newlyEnded) this.#emit("episodeEnded", { episode });
7125
- this.#publishConnectionDiagnostics();
7126
5464
  });
7127
- this.#publishConnectionDiagnostics();
7128
5465
  }
7129
5466
  getSnapshot = () => this.#store.getSnapshot();
7130
5467
  get changes() {
@@ -7132,49 +5469,23 @@ var SpaceClientCore = class {
7132
5469
  }
7133
5470
  subscribe = (listener) => this.#store.subscribe(listener);
7134
5471
  join(options = {}) {
7135
- this.#participantJoinOperation = this.#episodeDiagnostics?.startOperation("participant.join");
7136
- this.#syncConnectOperation = this.#episodeDiagnostics?.startOperation("sync.connect");
7137
5472
  return Effect12.sync(() => this.#requireActive()).pipe(
7138
5473
  Effect12.tap(() => Effect12.sync(() => this.controllers.media.configure(options))),
7139
5474
  Effect12.andThen(this.#connection.configureJoin(options)),
7140
5475
  Effect12.andThen(this.#connection.join()),
7141
5476
  Effect12.andThen(options.displayName === void 0 ? Effect12.void : this.controllers.participants.renameSelf(options.displayName)),
7142
- Effect12.tap(
7143
- () => Effect12.sync(() => {
7144
- this.#participantJoinOperation?.succeed();
7145
- this.#syncConnectOperation?.succeed();
7146
- })
7147
- ),
7148
- Effect12.tapError(
7149
- () => Effect12.sync(() => {
7150
- this.#participantJoinOperation?.fail("join_failed");
7151
- this.#syncConnectOperation?.fail("connect_failed");
7152
- })
7153
- ),
7154
- Effect12.catchTag("ConnectionLifecycleFailure", (failure3) => Effect12.fail(failure3.code === "episode_ended" ? new SpaceClientError({ code: "episode.ended", recoverable: failure3.recoverable, message: failure3.message }) : failure3)),
5477
+ Effect12.catchTag("ConnectionLifecycleFailure", (failure2) => Effect12.fail(failure2.code === "episode_ended" ? new SpaceClientError({ code: "episode.ended", recoverable: failure2.recoverable, message: failure2.message }) : failure2)),
7155
5478
  Effect12.mapError(normalizeClientError)
7156
5479
  );
7157
5480
  }
7158
5481
  leave() {
7159
- this.#leaveOperation = this.#episodeDiagnostics?.startOperation("participant.leave");
7160
- return this.#connection.leave().pipe(
7161
- Effect12.tap(() => Effect12.sync(() => this.#leaveOperation?.succeed())),
7162
- Effect12.tapError(() => Effect12.sync(() => this.#leaveOperation?.fail("leave_failed"))),
7163
- Effect12.mapError(normalizeClientError)
7164
- );
5482
+ return this.#connection.leave().pipe(Effect12.mapError(normalizeClientError));
7165
5483
  }
7166
5484
  endEpisode() {
7167
- return trackedEffect(this.#episodeDiagnostics, "episode.end.authorized", this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode()).pipe(Effect12.tap(() => this.#connection.confirmEpisodeEnded))).pipe(Effect12.tap(() => this.#connection.leave()))).pipe(
7168
- Effect12.asVoid,
7169
- Effect12.mapError(normalizeClientError)
7170
- );
5485
+ return this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode()).pipe(Effect12.tap(() => this.#connection.confirmEpisodeEnded))).pipe(Effect12.tap(() => this.#connection.leave())).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
7171
5486
  }
7172
5487
  extendEpisode(minutes) {
7173
- return trackedEffect(
7174
- this.#episodeDiagnostics,
7175
- "episode.deadline.extend",
7176
- this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes)))
7177
- ).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
5488
+ return this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes))).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
7178
5489
  }
7179
5490
  on(event, handler) {
7180
5491
  this.#handlers[event].add(handler);
@@ -7187,32 +5498,8 @@ var SpaceClientCore = class {
7187
5498
  this.#unsubscribeStore?.();
7188
5499
  this.#unsubscribeConnection = null;
7189
5500
  this.#unsubscribeStore = null;
7190
- this.feedback.dispose();
7191
- this.#disposeDiagnostics();
7192
5501
  this.#clearHandlers();
7193
5502
  }
7194
- #disposeDiagnostics() {
7195
- const diagnostics = this.#episodeDiagnostics;
7196
- if (!diagnostics) return;
7197
- this.#unregisterDiagnosticSyncClients(diagnostics);
7198
- this.#unregisterDiagnosticTracks(diagnostics);
7199
- this.#unregisterDiagnosticDependencies(diagnostics);
7200
- unregisterEpisodeDiagnosticConnection(this.#connection, diagnostics);
7201
- diagnostics.dispose();
7202
- }
7203
- #unregisterDiagnosticSyncClients(diagnostics) {
7204
- for (const syncClient of this.#episodeDiagnosticSyncClients ?? []) unregisterEpisodeDiagnosticSyncClient(syncClient, diagnostics);
7205
- }
7206
- #unregisterDiagnosticTracks(diagnostics) {
7207
- for (const { track } of this.#episodeDiagnosticTracks.values()) unregisterEpisodeDiagnosticTrack(track, diagnostics);
7208
- this.#episodeDiagnosticTracks.clear();
7209
- }
7210
- #unregisterDiagnosticDependencies(diagnostics) {
7211
- const dependencies = this.#episodeDiagnosticDependencies;
7212
- if (!dependencies) return;
7213
- unregisterEpisodeDiagnosticDependencies(dependencies, diagnostics);
7214
- syncClientsByDependencies.delete(dependencies);
7215
- }
7216
5503
  #clearHandlers() {
7217
5504
  for (const handlers of Object.values(this.#handlers)) handlers.clear();
7218
5505
  }
@@ -7221,38 +5508,8 @@ var SpaceClientCore = class {
7221
5508
  const next = this.#store.getSnapshot();
7222
5509
  if (previous === next) return;
7223
5510
  this.#previous = next;
7224
- if (this.#episodeDiagnostics) this.#syncDiagnosticTracks(next.media.remote, this.#episodeDiagnostics);
7225
5511
  this.#emitChanges(previous, next);
7226
5512
  }
7227
- #syncDiagnosticTracks(remote, diagnostics) {
7228
- const current = diagnosticTracksFor(remote);
7229
- for (const [key, previousTrack] of this.#episodeDiagnosticTracks) {
7230
- if (sameDiagnosticTrack(current.get(key), previousTrack)) continue;
7231
- this.#episodeDiagnosticTracks.delete(key);
7232
- this.#unregisterDiagnosticTrackIfUnused(previousTrack.track, diagnostics);
7233
- }
7234
- for (const [key, currentTrack] of current) {
7235
- if (sameDiagnosticTrack(this.#episodeDiagnosticTracks.get(key), currentTrack)) continue;
7236
- registerEpisodeDiagnosticTrack(currentTrack.track, currentTrack.source, diagnostics);
7237
- this.#episodeDiagnosticTracks.set(key, currentTrack);
7238
- }
7239
- }
7240
- #unregisterDiagnosticTrackIfUnused(track, diagnostics) {
7241
- for (const retained of this.#episodeDiagnosticTracks.values()) if (retained.track === track) return;
7242
- unregisterEpisodeDiagnosticTrack(track, diagnostics);
7243
- }
7244
- #publishConnectionDiagnostics() {
7245
- const snapshot = this.#connection.getSnapshot();
7246
- if (!this.#episodeDiagnostics || snapshot.state !== "live") return;
7247
- if (!this.#participantJoinOperation) {
7248
- this.#participantJoinOperation = this.#episodeDiagnostics.startOperation("participant.join");
7249
- this.#participantJoinOperation?.succeed({ status: "live" });
7250
- }
7251
- if (!this.#syncConnectOperation) {
7252
- this.#syncConnectOperation = this.#episodeDiagnostics.startOperation("sync.connect");
7253
- this.#syncConnectOperation?.succeed({ status: "live" });
7254
- }
7255
- }
7256
5513
  #emitChanges(previous, next) {
7257
5514
  this.#emitParticipantChanges(previous, next);
7258
5515
  this.#emitScreenShareChanges(previous, next);
@@ -7299,30 +5556,6 @@ function liveClock(dependencies) {
7299
5556
  function foreign6(operation) {
7300
5557
  return Effect12.tryPromise({ try: operation, catch: (cause) => cause });
7301
5558
  }
7302
- function trackedEffect(diagnostics, name, effect) {
7303
- const operation = diagnostics?.startOperation(name);
7304
- return effect.pipe(
7305
- Effect12.tap(() => Effect12.sync(() => operation?.succeed())),
7306
- Effect12.tapError(() => Effect12.sync(() => operation?.fail("operation_failed")))
7307
- );
7308
- }
7309
- async function resolveAccessGrant(input) {
7310
- const { operationName: operationName2, reason } = diagnosticAccessOperation(input.request);
7311
- const operationBeforeRotation = input.diagnostics.activeContext() ? input.diagnostics.startOperation(operationName2, { reason }) : void 0;
7312
- try {
7313
- const grant = await requireParsedAccessGrant(await fetchAccessGrant2(input));
7314
- input.diagnostics.rotateCredential(grant.diagnostics ?? null);
7315
- (operationBeforeRotation ?? input.diagnostics.startOperation(operationName2, { reason }))?.succeed({ result: "bound" });
7316
- return grant;
7317
- } catch (cause) {
7318
- operationBeforeRotation?.fail("access_failed");
7319
- throw cause;
7320
- }
7321
- }
7322
- function diagnosticAccessOperation(request) {
7323
- const reason = request?.reason ?? "join";
7324
- return { operationName: reason === "join" ? "access.request" : "access.refresh", reason };
7325
- }
7326
5559
  async function fetchAccessGrant2(input) {
7327
5560
  if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
7328
5561
  return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason), replaceMediaConnection: input.request?.replaceMediaConnection ?? false, signal: input.request?.signal });
@@ -7332,7 +5565,7 @@ function accessReason(reason) {
7332
5565
  if (reason === "scheduled_refresh") return "refresh";
7333
5566
  return "retry";
7334
5567
  }
7335
- function normalizedBaseUrl2(value) {
5568
+ function normalizedBaseUrl(value) {
7336
5569
  const url = new URL(value);
7337
5570
  url.pathname = url.pathname.replace(/\/+$/u, "");
7338
5571
  url.search = "";
@@ -7343,6 +5576,7 @@ function defaultSyncUrl(apiBaseUrl) {
7343
5576
  const url = new URL(apiBaseUrl);
7344
5577
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
7345
5578
  if (url.hostname.startsWith("api.")) url.hostname = `sync.${url.hostname.slice(4)}`;
5579
+ else if (url.hostname.endsWith(".chalkmeet.com")) url.hostname = url.hostname.replace(/^([^.]+-)api\./u, "$1sync.");
7346
5580
  url.pathname = "/v1/sync";
7347
5581
  return url.toString();
7348
5582
  }
@@ -7351,17 +5585,6 @@ function screenShares(snapshot) {
7351
5585
  if (snapshot.media.screenShare.state === "enabled" && snapshot.self.participantId) participants.add(snapshot.self.participantId);
7352
5586
  return participants;
7353
5587
  }
7354
- function diagnosticTracksFor(remote) {
7355
- const tracks = /* @__PURE__ */ new Map();
7356
- for (const publication of remote) {
7357
- if (publication.source !== "camera" && publication.source !== "screen") continue;
7358
- tracks.set(`${publication.participantId}:${publication.source}:${publication.publicationId}`, { track: publication.track, source: publication.source });
7359
- }
7360
- return tracks;
7361
- }
7362
- function sameDiagnosticTrack(left, right) {
7363
- return left?.track === right.track && left?.source === right.source;
7364
- }
7365
5588
 
7366
5589
  // src/space-client/space-client.ts
7367
5590
  import { Effect as Effect14, ManagedRuntime } from "effect";
@@ -7382,7 +5605,6 @@ function toPromiseController(runtime, controller) {
7382
5605
 
7383
5606
  // src/space-client/space-client.ts
7384
5607
  var PromiseSpaceClient = class {
7385
- feedback;
7386
5608
  media;
7387
5609
  chat;
7388
5610
  participants;
@@ -7397,7 +5619,6 @@ var PromiseSpaceClient = class {
7397
5619
  this.#runtime = ManagedRuntime.make(makeSpaceClientCoreLayer(options, platform));
7398
5620
  this.#core = this.#runtime.runSync(Effect14.service(SpaceClientCoreService));
7399
5621
  const controllers = this.#core.controllers;
7400
- this.feedback = this.#core.feedback;
7401
5622
  this.media = toPromiseController(this.#runtime, controllers.media);
7402
5623
  const chat = toPromiseController(this.#runtime, controllers.chat);
7403
5624
  const { upload, url, resolveUrl, ...chatCommands } = chat;
@@ -7439,11 +5660,12 @@ export {
7439
5660
  makeConnectionPlatformLayer,
7440
5661
  makeFakeConnectionPlatformLayer,
7441
5662
  ChalkWhiteboardV1Error,
5663
+ CHALK_REACTIONS,
7442
5664
  CHALK_CHAT_ATTACHMENT_LIMITS,
7443
5665
  CHALK_CHAT_ATTACHMENT_MIME_TYPES,
7444
5666
  ChalkChatFileError,
7445
5667
  createChalkChatFileHttpTransport,
7446
- createChalkWhiteboardV1FileHttpTransport,
5668
+ createChalkWhiteboardV1FileDownloadTransport,
7447
5669
  encodeWhiteboardV1ClientFrame,
7448
5670
  decodeWhiteboardV1ServerFrame,
7449
5671
  InMemoryChalkWhiteboardV1PendingOperationStore,
@@ -7457,35 +5679,6 @@ export {
7457
5679
  SpaceStoreService,
7458
5680
  makeSpaceStoreLayer,
7459
5681
  makeFakeSpaceStoreLayer,
7460
- FEEDBACK_EVIDENCE_SCHEMA_VERSION,
7461
- FEEDBACK_REQUEST_SCHEMA_VERSION,
7462
- FEEDBACK_RECEIPT_SCHEMA_VERSION,
7463
- FEEDBACK_SCREENSHOT_SCHEMA_VERSION,
7464
- FEEDBACK_LOCAL_STATE_REGISTRY_VERSION,
7465
- FEEDBACK_COOKIE_REGISTRY_VERSION,
7466
- FEEDBACK_MAX_MESSAGE_BYTES,
7467
- FEEDBACK_MAX_EVIDENCE_BYTES,
7468
- FEEDBACK_MAX_REQUEST_BYTES,
7469
- FEEDBACK_MAX_SCREENSHOT_BYTES,
7470
- FEEDBACK_MAX_SCREENSHOT_WIDTH,
7471
- FEEDBACK_MAX_SCREENSHOT_HEIGHT,
7472
- FEEDBACK_MAX_TELEMETRY_EVENTS,
7473
- FEEDBACK_MAX_DIAGNOSTIC_EVENTS,
7474
- FEEDBACK_MAX_LOCAL_STATE_ENTRIES,
7475
- FEEDBACK_MAX_COOKIE_ENTRIES,
7476
- FeedbackValidationError,
7477
- validateFeedbackReportRequest,
7478
- parseFeedbackReportRequest,
7479
- validateFeedbackEvidence,
7480
- parseFeedbackEvidence,
7481
- validateFeedbackScreenshot,
7482
- parseFeedbackScreenshot,
7483
- assertFeedbackReceipt,
7484
- assertFeedbackMessage,
7485
- collectFeedbackEvidence,
7486
- collectLocalState,
7487
- collectCookies,
7488
- FeedbackTransportError,
7489
5682
  SpaceClientCoreService,
7490
5683
  makeSpaceClientCoreLayer,
7491
5684
  makeSpaceClientCoreLayerFromServices,