@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.
- package/README.md +60 -42
- package/dist/access/grant.d.ts +0 -10
- package/dist/access/grant.d.ts.map +1 -1
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-ZRMGKMX4.js → chunk-2HJIJVE2.js} +552 -3651
- package/dist/{chunk-CZUJNAUI.js → chunk-AVXJ4HQU.js} +142 -49
- package/dist/{chunk-A7BVZEFM.js → chunk-CMVO7USN.js} +2 -8
- package/dist/{chunk-AUQTQOLR.js → chunk-IPAN4LUZ.js} +627 -2434
- package/dist/{chunk-2DZUHV6I.js → chunk-O2NOZKJP.js} +129 -721
- package/dist/client.d.ts +2559 -5303
- package/dist/client.d.ts.map +1 -1
- package/dist/collaboration/types.d.ts +21 -1
- package/dist/collaboration/types.d.ts.map +1 -1
- package/dist/collaboration/wire.d.ts +8 -1
- package/dist/collaboration/wire.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +7 -13
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/index.d.ts +1 -6
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.d.ts.map +1 -1
- package/dist/connection/media-devices.d.ts +11 -0
- package/dist/connection/media-devices.d.ts.map +1 -1
- package/dist/connection/production.d.ts +1 -1
- package/dist/connection/production.d.ts.map +1 -1
- package/dist/connection/types.d.ts +4 -4
- package/dist/connection/types.d.ts.map +1 -1
- package/dist/effect.d.ts +0 -4
- package/dist/effect.d.ts.map +1 -1
- package/dist/effect.js +53 -776
- package/dist/generated/http-api.d.ts +2603 -5293
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +1893 -6870
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/generated/sync.d.ts +191 -28
- package/dist/generated/sync.d.ts.map +1 -1
- package/dist/generated/whiteboard-v1.d.ts +0 -7
- package/dist/generated/whiteboard-v1.d.ts.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -119
- package/dist/media/client.d.ts +6 -5
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/index.d.ts +1 -2
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -7
- package/dist/media/plane.d.ts +12 -1
- package/dist/media/plane.d.ts.map +1 -1
- package/dist/media/target.d.ts +4 -2
- package/dist/media/target.d.ts.map +1 -1
- package/dist/media/tracks.d.ts +2 -2
- package/dist/media/tracks.d.ts.map +1 -1
- package/dist/media/transport.d.ts.map +1 -1
- package/dist/media/types.d.ts +4 -11
- package/dist/media/types.d.ts.map +1 -1
- package/dist/public-invites/client.d.ts.map +1 -1
- package/dist/public-invites/index.d.ts +2 -2
- package/dist/public-invites/index.d.ts.map +1 -1
- package/dist/public-invites/index.js +2 -5
- package/dist/public-invites/types.d.ts +2 -6
- package/dist/public-invites/types.d.ts.map +1 -1
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/index.cjs +12 -112
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -109
- package/dist/server/types.d.cts +0 -7
- package/dist/server/types.d.ts +0 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/space-client/chat-controller-helpers.d.ts.map +1 -1
- package/dist/space-client/chat-controller.d.ts +2 -3
- package/dist/space-client/chat-controller.d.ts.map +1 -1
- package/dist/space-client/controller-effects.d.ts +0 -3
- package/dist/space-client/controller-effects.d.ts.map +1 -1
- package/dist/space-client/core.d.ts +3 -18
- package/dist/space-client/core.d.ts.map +1 -1
- package/dist/space-client/core.test.helpers.d.ts.map +1 -1
- package/dist/space-client/effect.d.ts +0 -2
- package/dist/space-client/effect.d.ts.map +1 -1
- package/dist/space-client/index.d.ts +1 -2
- package/dist/space-client/index.d.ts.map +1 -1
- package/dist/space-client/media-controller.d.ts +4 -4
- package/dist/space-client/media-controller.d.ts.map +1 -1
- package/dist/space-client/offline-actions-storage.d.ts +18 -3
- package/dist/space-client/offline-actions-storage.d.ts.map +1 -1
- package/dist/space-client/offline-actions.d.ts +1 -0
- package/dist/space-client/offline-actions.d.ts.map +1 -1
- package/dist/space-client/participants-controller.d.ts +2 -3
- package/dist/space-client/participants-controller.d.ts.map +1 -1
- package/dist/space-client/reactions-controller.d.ts +2 -3
- package/dist/space-client/reactions-controller.d.ts.map +1 -1
- package/dist/space-client/recording-controller.d.ts +2 -3
- package/dist/space-client/recording-controller.d.ts.map +1 -1
- package/dist/space-client/space-client.d.ts.map +1 -1
- package/dist/space-client/store.d.ts.map +1 -1
- package/dist/space-client/terminal.test.helpers.d.ts +33 -12
- package/dist/space-client/terminal.test.helpers.d.ts.map +1 -1
- package/dist/space-client/types.d.ts +15 -8
- package/dist/space-client/types.d.ts.map +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -3
- package/dist/sync/v1-client.d.ts +4 -3
- package/dist/sync/v1-client.d.ts.map +1 -1
- package/dist/sync/v1-collaboration-state.d.ts +10 -3
- package/dist/sync/v1-collaboration-state.d.ts.map +1 -1
- package/dist/sync/v1-types.d.ts +10 -9
- package/dist/sync/v1-types.d.ts.map +1 -1
- package/dist/whiteboard/file-download-transport.d.ts +8 -0
- package/dist/whiteboard/file-download-transport.d.ts.map +1 -0
- package/dist/whiteboard/index.d.ts +1 -1
- package/dist/whiteboard/index.d.ts.map +1 -1
- package/dist/whiteboard/types.d.ts +12 -15
- package/dist/whiteboard/types.d.ts.map +1 -1
- package/dist/whiteboard/v1-client.d.ts.map +1 -1
- package/package.json +4 -16
- package/dist/access/episode-diagnostic-credential.d.ts +0 -11
- package/dist/access/episode-diagnostic-credential.d.ts.map +0 -1
- package/dist/chunk-EWTITNJ3.js +0 -1163
- package/dist/chunk-HW5TTZL2.js +0 -99
- package/dist/chunk-PBSYXPNJ.js +0 -23
- package/dist/chunk-RCDXIDCN.js +0 -33
- package/dist/connection/snapshot.d.ts +0 -23
- package/dist/connection/snapshot.d.ts.map +0 -1
- package/dist/feedback/collector.d.ts +0 -14
- package/dist/feedback/collector.d.ts.map +0 -1
- package/dist/feedback/context.d.ts +0 -38
- package/dist/feedback/context.d.ts.map +0 -1
- package/dist/feedback/controller.d.ts +0 -4
- package/dist/feedback/controller.d.ts.map +0 -1
- package/dist/feedback/index.d.ts +0 -7
- package/dist/feedback/index.d.ts.map +0 -1
- package/dist/feedback/transport.d.ts +0 -29
- package/dist/feedback/transport.d.ts.map +0 -1
- package/dist/feedback/types.d.ts +0 -202
- package/dist/feedback/types.d.ts.map +0 -1
- package/dist/feedback/validation.d.ts +0 -30
- package/dist/feedback/validation.d.ts.map +0 -1
- package/dist/media/rtk.d.ts +0 -101
- package/dist/media/rtk.d.ts.map +0 -1
- package/dist/space-client/episode-diagnostic-registry.d.ts +0 -13
- package/dist/space-client/episode-diagnostic-registry.d.ts.map +0 -1
- package/dist/space-client/episode-diagnostic-render-registry.d.ts +0 -4
- package/dist/space-client/episode-diagnostic-render-registry.d.ts.map +0 -1
- package/dist/space-client/episode-diagnostic-runtime.d.ts +0 -105
- package/dist/space-client/episode-diagnostic-runtime.d.ts.map +0 -1
- package/dist/telemetry/attributes.d.ts +0 -7
- package/dist/telemetry/attributes.d.ts.map +0 -1
- package/dist/telemetry/client.d.ts +0 -44
- package/dist/telemetry/client.d.ts.map +0 -1
- package/dist/telemetry/delivery.d.ts +0 -107
- package/dist/telemetry/delivery.d.ts.map +0 -1
- package/dist/telemetry/exporter.d.ts +0 -38
- package/dist/telemetry/exporter.d.ts.map +0 -1
- package/dist/telemetry/index.d.ts +0 -7
- package/dist/telemetry/index.d.ts.map +0 -1
- package/dist/telemetry/index.js +0 -50
- package/dist/telemetry/journey.d.ts +0 -52
- package/dist/telemetry/journey.d.ts.map +0 -1
- package/dist/telemetry/random.d.ts +0 -18
- package/dist/telemetry/random.d.ts.map +0 -1
- package/dist/telemetry/reconnect.d.ts +0 -7
- package/dist/telemetry/reconnect.d.ts.map +0 -1
- package/dist/telemetry/rtc.d.ts +0 -41
- package/dist/telemetry/rtc.d.ts.map +0 -1
- package/dist/telemetry/storage.d.ts +0 -22
- package/dist/telemetry/storage.d.ts.map +0 -1
- package/dist/telemetry/sync.d.ts +0 -10
- package/dist/telemetry/sync.d.ts.map +0 -1
- package/dist/telemetry/test-support.d.ts +0 -51
- package/dist/telemetry/test-support.d.ts.map +0 -1
- package/dist/telemetry/trace.d.ts +0 -18
- package/dist/telemetry/trace.d.ts.map +0 -1
- package/dist/telemetry/types.d.ts +0 -109
- package/dist/telemetry/types.d.ts.map +0 -1
- package/dist/whiteboard/file-http-transport.d.ts +0 -9
- package/dist/whiteboard/file-http-transport.d.ts.map +0 -1
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AccessGrantError,
|
|
3
|
-
requireParsedAccessGrant
|
|
4
|
-
|
|
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-
|
|
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-
|
|
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", (
|
|
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(
|
|
163
|
-
super(
|
|
150
|
+
constructor(failure2, options) {
|
|
151
|
+
super(failure2.message, options);
|
|
164
152
|
this.name = "ChalkWhiteboardV1Error";
|
|
165
|
-
this.operation =
|
|
166
|
-
this.code =
|
|
167
|
-
this.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(
|
|
201
|
-
super(
|
|
189
|
+
constructor(failure2, options) {
|
|
190
|
+
super(failure2.message, options);
|
|
202
191
|
this.name = "ChalkChatFileError";
|
|
203
|
-
this.operation =
|
|
204
|
-
this.code =
|
|
205
|
-
this.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-
|
|
319
|
-
function
|
|
307
|
+
// src/whiteboard/file-download-transport.ts
|
|
308
|
+
function createChalkWhiteboardV1FileDownloadTransport(options) {
|
|
320
309
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
321
|
-
|
|
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:
|
|
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 =
|
|
925
|
-
var
|
|
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
|
-
#
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
-
}
|
|
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(
|
|
1736
|
-
super(
|
|
1800
|
+
constructor(failure2, options) {
|
|
1801
|
+
super(failure2.message, options);
|
|
1737
1802
|
this.name = "ConnectionError";
|
|
1738
|
-
this.code =
|
|
1739
|
-
this.action =
|
|
1740
|
-
this.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() ?? [])
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
(
|
|
2225
|
+
(failure2) => enqueue(
|
|
2169
2226
|
Effect2.gen(function* () {
|
|
2170
2227
|
if (model.epoch !== epoch || !active(model)) return;
|
|
2171
2228
|
model.refreshInFlight = false;
|
|
2172
|
-
if (
|
|
2173
|
-
diagnostics.record({ event: "access_refresh_failed", state: model.state, epoch, code:
|
|
2174
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
2347
|
-
model.failure = toFailure(
|
|
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:
|
|
2353
|
-
const retry = !retried &&
|
|
2354
|
-
if (!retry) yield* recordFailure(
|
|
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(
|
|
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((
|
|
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: (
|
|
2510
|
+
onFailure: (failure2) => ({ attempt: null, failure: failure2 })
|
|
2469
2511
|
})
|
|
2470
2512
|
);
|
|
2471
2513
|
if (outcome.failure) {
|
|
2472
|
-
const
|
|
2473
|
-
enqueueBackground(Effect2.suspend(() => model.epoch === epoch && active(model) ? failForSnapshot(
|
|
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
|
|
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 (
|
|
2518
|
-
if (!
|
|
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*
|
|
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: (
|
|
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
|
|
2532
|
-
if (
|
|
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
|
-
|
|
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*
|
|
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
|
|
2618
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
2693
|
-
if (
|
|
2694
|
-
yield* failForSnapshot(
|
|
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 = (
|
|
2708
|
-
|
|
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 = (
|
|
2714
|
-
model.failure = toFailure(
|
|
2715
|
-
yield* recordFailure(
|
|
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
|
|
2909
|
-
if (
|
|
2910
|
-
return lifecycleFailure(
|
|
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:
|
|
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 =
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3112
|
-
|
|
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/
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
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/
|
|
3215
|
-
import {
|
|
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
|
|
3218
|
-
var
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
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
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
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
|
|
3460
|
-
|
|
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
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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
|
|
3480
|
-
|
|
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
|
|
3488
|
-
if (
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
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
|
|
3511
|
-
return
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
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
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
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
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
return
|
|
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
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
const
|
|
3576
|
-
const
|
|
3577
|
-
|
|
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
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
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
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4963
|
-
|
|
4964
|
-
Effect5.
|
|
4965
|
-
(
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
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.
|
|
4988
|
-
|
|
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
|
-
|
|
5058
|
-
|
|
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
|
|
5138
|
-
return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(
|
|
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.#
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
|
5379
|
-
return new ConnectionError({ ...
|
|
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
|
|
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 =
|
|
5409
|
-
var StoredOfflineAction =
|
|
5410
|
-
id:
|
|
5411
|
-
kind:
|
|
5412
|
-
text:
|
|
5413
|
-
queuedAt:
|
|
5414
|
-
clientMessageId:
|
|
5415
|
-
attachments:
|
|
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 =
|
|
5418
|
-
version:
|
|
5419
|
-
subject:
|
|
5420
|
-
actions:
|
|
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 =
|
|
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,
|
|
5671
|
-
Effect7.sync(() => new ParticipantsControllerRuntime(connection, store,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
5766
|
-
this.#
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6021
|
-
Effect9.sync(() => new RecordingControllerRuntime(connection, store
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
6198
|
-
const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch,
|
|
6199
|
-
const participants = yield* makeParticipantsController(input.connection, input.store,
|
|
6200
|
-
const reactions = yield* makeReactionsController(input.connection, input.store
|
|
6201
|
-
const recording = yield* makeRecordingController(input.connection, input.store
|
|
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 =
|
|
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
|
|
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) =>
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|