@q9labsai/chalk-client 4.1.15 → 4.1.16
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 +56 -2
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-LQSXEWLL.js → chunk-BD7FRQ67.js} +2061 -326
- package/dist/{chunk-6ONSFCIY.js → chunk-E4OK5K6N.js} +7 -3
- package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
- package/dist/chunk-HW5TTZL2.js +99 -0
- package/dist/{chunk-LSDDADCN.js → chunk-M3S7HNQF.js} +1656 -517
- package/dist/{chunk-PQNSA7ZN.js → chunk-O7WTAXAY.js} +610 -73
- package/dist/{chunk-PWUU6T3R.js → chunk-OKIL2ELF.js} +1185 -874
- package/dist/chunk-RCDXIDCN.js +33 -0
- package/dist/client.d.ts +1412 -463
- package/dist/client.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +10 -0
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/index.d.ts +2 -0
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.d.ts.map +1 -1
- package/dist/connection/production.d.ts +4 -1
- package/dist/connection/production.d.ts.map +1 -1
- package/dist/connection/types.d.ts +2 -2
- package/dist/connection/types.d.ts.map +1 -1
- package/dist/effect.js +343 -130
- package/dist/feedback/collector.d.ts +14 -0
- package/dist/feedback/collector.d.ts.map +1 -0
- package/dist/feedback/context.d.ts +38 -0
- package/dist/feedback/context.d.ts.map +1 -0
- package/dist/feedback/controller.d.ts +4 -0
- package/dist/feedback/controller.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/transport.d.ts +29 -0
- package/dist/feedback/transport.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +202 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/feedback/validation.d.ts +30 -0
- package/dist/feedback/validation.d.ts.map +1 -0
- package/dist/generated/http-api.d.ts +1401 -462
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +2601 -858
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +150 -35
- package/dist/media/camera-uplink.d.ts +13 -0
- package/dist/media/camera-uplink.d.ts.map +1 -0
- package/dist/media/capture-error.d.ts +27 -0
- package/dist/media/capture-error.d.ts.map +1 -0
- package/dist/media/client.d.ts +4 -0
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +5 -1
- package/dist/media/plane.d.ts +4 -0
- package/dist/media/plane.d.ts.map +1 -1
- package/dist/media/remote-track-response.d.ts +6 -0
- package/dist/media/remote-track-response.d.ts.map +1 -0
- package/dist/media/transport.d.ts.map +1 -1
- package/dist/media/types.d.ts +20 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/public-invites/client.d.ts.map +1 -1
- package/dist/public-invites/index.js +4 -3
- package/dist/public-invites/types.d.ts +4 -2
- package/dist/public-invites/types.d.ts.map +1 -1
- package/dist/server/access-refresh.d.cts +8 -0
- package/dist/server/access-refresh.d.ts +9 -0
- package/dist/server/access-refresh.d.ts.map +1 -0
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/index.cjs +181 -20
- package/dist/server/index.d.cts +2 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +179 -19
- package/dist/server/types.d.cts +74 -0
- package/dist/server/types.d.ts +74 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/space-client/chat-controller.d.ts +3 -0
- package/dist/space-client/chat-controller.d.ts.map +1 -1
- package/dist/space-client/controller-effects.d.ts +6 -0
- package/dist/space-client/controller-effects.d.ts.map +1 -1
- package/dist/space-client/core.d.ts +12 -1
- package/dist/space-client/core.d.ts.map +1 -1
- package/dist/space-client/core.test.helpers.d.ts +1 -0
- package/dist/space-client/core.test.helpers.d.ts.map +1 -1
- package/dist/space-client/effect.d.ts +5 -0
- package/dist/space-client/effect.d.ts.map +1 -1
- package/dist/space-client/episode-diagnostic-runtime.d.ts +3 -0
- package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -1
- package/dist/space-client/errors.d.ts.map +1 -1
- package/dist/space-client/index.d.ts +4 -1
- package/dist/space-client/index.d.ts.map +1 -1
- package/dist/space-client/media-controller.d.ts.map +1 -1
- package/dist/space-client/offline-actions-storage.d.ts +66 -0
- package/dist/space-client/offline-actions-storage.d.ts.map +1 -0
- package/dist/space-client/offline-actions.d.ts +36 -0
- package/dist/space-client/offline-actions.d.ts.map +1 -0
- package/dist/space-client/participants-controller.d.ts +2 -1
- package/dist/space-client/participants-controller.d.ts.map +1 -1
- package/dist/space-client/recording-controller.d.ts +20 -0
- package/dist/space-client/recording-controller.d.ts.map +1 -0
- package/dist/space-client/reload-rejoin.d.ts +24 -0
- package/dist/space-client/reload-rejoin.d.ts.map +1 -0
- package/dist/space-client/space-client.d.ts.map +1 -1
- package/dist/space-client/store.d.ts +3 -1
- package/dist/space-client/store.d.ts.map +1 -1
- package/dist/space-client/types.d.ts +40 -4
- package/dist/space-client/types.d.ts.map +1 -1
- package/dist/sync/index.js +2 -1
- package/dist/sync/v1-client.d.ts +1 -5
- package/dist/sync/v1-client.d.ts.map +1 -1
- package/dist/sync/v1-collaboration-state.d.ts +65 -0
- package/dist/sync/v1-collaboration-state.d.ts.map +1 -0
- package/dist/sync/v1-command-scheduler.d.ts +54 -0
- package/dist/sync/v1-command-scheduler.d.ts.map +1 -0
- package/dist/sync/v1-deferred.d.ts +8 -0
- package/dist/sync/v1-deferred.d.ts.map +1 -0
- package/dist/sync/v1-error.d.ts +5 -0
- package/dist/sync/v1-error.d.ts.map +1 -0
- package/dist/sync/v1-frame-signature.d.ts +2 -0
- package/dist/sync/v1-frame-signature.d.ts.map +1 -0
- package/dist/sync/v1-live-target-coordinator.d.ts +32 -0
- package/dist/sync/v1-live-target-coordinator.d.ts.map +1 -0
- package/dist/sync/v1-types.d.ts +3 -0
- package/dist/sync/v1-types.d.ts.map +1 -1
- package/dist/telemetry/index.js +9 -7
- package/dist/telemetry/reconnect.d.ts +7 -0
- package/dist/telemetry/reconnect.d.ts.map +1 -0
- package/dist/telemetry/trace.d.ts +1 -1
- package/dist/telemetry/trace.d.ts.map +1 -1
- package/dist/whiteboard/v1-client.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CloudflareRTKClient,
|
|
3
3
|
CloudflareSFUClient,
|
|
4
|
-
createCloudflareSFUHTTPTransport
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
createBrowserSyncLifecycle,
|
|
8
|
-
createBrowserWebSocketFactory,
|
|
9
|
-
createV1SyncClient
|
|
10
|
-
} from "./chunk-PWUU6T3R.js";
|
|
4
|
+
createCloudflareSFUHTTPTransport,
|
|
5
|
+
describeMediaCaptureError
|
|
6
|
+
} from "./chunk-O7WTAXAY.js";
|
|
11
7
|
import {
|
|
12
8
|
AccessGrantError,
|
|
13
9
|
requireParsedAccessGrant,
|
|
14
10
|
validEpisodeDiagnosticCredential
|
|
15
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-M3S7HNQF.js";
|
|
12
|
+
import {
|
|
13
|
+
normalizeTelemetryAttributes
|
|
14
|
+
} from "./chunk-EWTITNJ3.js";
|
|
16
15
|
import {
|
|
17
16
|
createTraceContext,
|
|
17
|
+
journeyHeaders,
|
|
18
18
|
parseTraceparent
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-HW5TTZL2.js";
|
|
20
|
+
import {
|
|
21
|
+
ChatAttachmentMimeTypeSchema,
|
|
22
|
+
V1SyncError,
|
|
23
|
+
createBrowserSyncLifecycle,
|
|
24
|
+
createBrowserWebSocketFactory,
|
|
25
|
+
createV1SyncClient
|
|
26
|
+
} from "./chunk-OKIL2ELF.js";
|
|
27
|
+
import {
|
|
28
|
+
recordInitialConnection,
|
|
29
|
+
recordReconnect
|
|
30
|
+
} from "./chunk-RCDXIDCN.js";
|
|
20
31
|
|
|
21
32
|
// src/access/manager.ts
|
|
22
33
|
import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
|
|
23
34
|
var DEFAULT_REFRESH_WINDOW_MS = 6e4;
|
|
35
|
+
var ACCESS_PROVIDER_TIMEOUT_MS = 7e3;
|
|
24
36
|
var ConnectionAccessFailure = class extends Data.TaggedError("ConnectionAccessFailure") {
|
|
25
37
|
};
|
|
26
38
|
var ConnectionAccessService = class extends Context.Service()("@chalk/client/ConnectionAccess") {
|
|
@@ -34,7 +46,7 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
|
|
|
34
46
|
Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
|
|
35
47
|
Effect.map((result) => result.access)
|
|
36
48
|
);
|
|
37
|
-
const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (
|
|
49
|
+
const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure3) => failure3.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure3)));
|
|
38
50
|
const millisecondsUntilRefresh = Effect.gen(function* () {
|
|
39
51
|
const access = yield* SynchronizedRef.get(state);
|
|
40
52
|
if (!access.access) return null;
|
|
@@ -70,7 +82,10 @@ function fetchAccessGrant(provider, input) {
|
|
|
70
82
|
}
|
|
71
83
|
const mediaExpired = currentAccess !== null && expiresAt(currentAccess.media.expiresAt) <= now;
|
|
72
84
|
const shouldReplaceMediaConnection = input.replaceMediaConnection || mediaExpired;
|
|
73
|
-
const next = yield* provider(connectionAccessRequest(input, shouldReplaceMediaConnection))
|
|
85
|
+
const next = yield* provider(connectionAccessRequest(input, shouldReplaceMediaConnection)).pipe(
|
|
86
|
+
Effect.timeout(ACCESS_PROVIDER_TIMEOUT_MS),
|
|
87
|
+
Effect.mapError((cause) => cause instanceof ConnectionAccessFailure ? cause : new ConnectionAccessFailure({ code: "access.unavailable", cause }))
|
|
88
|
+
);
|
|
74
89
|
yield* Effect.try({
|
|
75
90
|
try: () => validateFetchedAccess(currentAccess, next, now, shouldReplaceMediaConnection),
|
|
76
91
|
catch: (cause) => new ConnectionAccessFailure({ code: "access.invalid", cause })
|
|
@@ -168,12 +183,12 @@ var ChalkChatFileError = class extends Error {
|
|
|
168
183
|
operation;
|
|
169
184
|
code;
|
|
170
185
|
recoverable;
|
|
171
|
-
constructor(
|
|
172
|
-
super(
|
|
186
|
+
constructor(failure3, options) {
|
|
187
|
+
super(failure3.message, options);
|
|
173
188
|
this.name = "ChalkChatFileError";
|
|
174
|
-
this.operation =
|
|
175
|
-
this.code =
|
|
176
|
-
this.recoverable =
|
|
189
|
+
this.operation = failure3.operation;
|
|
190
|
+
this.code = failure3.code;
|
|
191
|
+
this.recoverable = failure3.recoverable;
|
|
177
192
|
}
|
|
178
193
|
};
|
|
179
194
|
|
|
@@ -291,12 +306,12 @@ var ChalkWhiteboardV1Error = class extends Error {
|
|
|
291
306
|
operation;
|
|
292
307
|
code;
|
|
293
308
|
recoverable;
|
|
294
|
-
constructor(
|
|
295
|
-
super(
|
|
309
|
+
constructor(failure3, options) {
|
|
310
|
+
super(failure3.message, options);
|
|
296
311
|
this.name = "ChalkWhiteboardV1Error";
|
|
297
|
-
this.operation =
|
|
298
|
-
this.code =
|
|
299
|
-
this.recoverable =
|
|
312
|
+
this.operation = failure3.operation;
|
|
313
|
+
this.code = failure3.code;
|
|
314
|
+
this.recoverable = failure3.recoverable;
|
|
300
315
|
}
|
|
301
316
|
};
|
|
302
317
|
|
|
@@ -1028,7 +1043,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1028
1043
|
return () => this.#summaryListeners.delete(listener);
|
|
1029
1044
|
}
|
|
1030
1045
|
submitUpdate(input) {
|
|
1031
|
-
this.#assertLive("submit_update");
|
|
1046
|
+
if (!this.#started || !this.#sceneId) this.#assertLive("submit_update");
|
|
1032
1047
|
if (!this.#canDraw || !this.#capabilities.includes("drawWhiteboard")) {
|
|
1033
1048
|
throw error(failure("submit_update", "permission_denied", false, "Whiteboard draw permission is required."));
|
|
1034
1049
|
}
|
|
@@ -1042,7 +1057,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1042
1057
|
});
|
|
1043
1058
|
}
|
|
1044
1059
|
sendCursor(input) {
|
|
1045
|
-
if (this.#phase !== "live" || !this.#canDraw) return;
|
|
1060
|
+
if (this.#phase !== "live" || this.#summaryStatus !== "ready" || this.#snapshots.size > 0 || !this.#canDraw) return;
|
|
1046
1061
|
const now = this.#now();
|
|
1047
1062
|
if (now - this.#lastCursorAt < 1e3 / WhiteboardV1ProtocolLimits.cursorRatePerSecond) return;
|
|
1048
1063
|
const frame = { type: "cursor", x: input.x, y: input.y };
|
|
@@ -1328,6 +1343,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1328
1343
|
this.#sceneId = frame.scene_id;
|
|
1329
1344
|
this.#revision = frame.revision;
|
|
1330
1345
|
this.#publishSummary("ready", null);
|
|
1346
|
+
this.#flushQueuedOperations();
|
|
1331
1347
|
this.#emit({
|
|
1332
1348
|
type: "snapshot",
|
|
1333
1349
|
sceneId: frame.scene_id,
|
|
@@ -1375,7 +1391,10 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1375
1391
|
deferred = { resolve, reject, settled: false };
|
|
1376
1392
|
});
|
|
1377
1393
|
this.#operations.set(frame.operation_id, { pending, deferred, retries: 0 });
|
|
1378
|
-
this.#
|
|
1394
|
+
if (this.#phase === "live" && this.#snapshots.size === 0) {
|
|
1395
|
+
if (this.#waitingForOperations) this.#awaitingOperationIds.add(frame.operation_id);
|
|
1396
|
+
this.#sendOperation(frame);
|
|
1397
|
+
}
|
|
1379
1398
|
return promise;
|
|
1380
1399
|
}
|
|
1381
1400
|
#commit(frame) {
|
|
@@ -1423,6 +1442,13 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1423
1442
|
rejectDeferred(this.#initialSnapshot, snapshotError);
|
|
1424
1443
|
this.#initialSnapshot = null;
|
|
1425
1444
|
}
|
|
1445
|
+
if (frame.recoverable) this.#flushQueuedOperations();
|
|
1446
|
+
}
|
|
1447
|
+
#flushQueuedOperations() {
|
|
1448
|
+
if (this.#phase !== "live" || this.#snapshots.size > 0) return;
|
|
1449
|
+
for (const entry of this.#operations.values()) {
|
|
1450
|
+
if (!this.#awaitingOperationIds.has(entry.pending.operationId)) this.#sendOperation(entry.pending.frame);
|
|
1451
|
+
}
|
|
1426
1452
|
}
|
|
1427
1453
|
#scheduleOperationRetry(operationId, entry) {
|
|
1428
1454
|
if (this.#operationRetryTimers.has(operationId)) return;
|
|
@@ -1462,8 +1488,6 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1462
1488
|
this.#phase = "connecting";
|
|
1463
1489
|
this.#waitingForOperations = false;
|
|
1464
1490
|
this.#participantId = null;
|
|
1465
|
-
this.#capabilities = [];
|
|
1466
|
-
this.#canDraw = false;
|
|
1467
1491
|
this.#rejectSnapshots(failure("request_snapshot", "unavailable", true, "Whiteboard connection interrupted."));
|
|
1468
1492
|
this.#publishSummary("recovering", failure("start_scene_subscription", "unavailable", true, "Whiteboard connection interrupted."));
|
|
1469
1493
|
}
|
|
@@ -1705,11 +1729,11 @@ function createChalkWhiteboardV1Client(options) {
|
|
|
1705
1729
|
function createBrowserMediaDevices(mediaDevices = globalThis.navigator?.mediaDevices) {
|
|
1706
1730
|
return {
|
|
1707
1731
|
async getUserMedia(constraints) {
|
|
1708
|
-
if (!mediaDevices?.getUserMedia) throw new
|
|
1732
|
+
if (!mediaDevices?.getUserMedia) throw new DOMException("Browser media capture is unavailable", "NotSupportedError");
|
|
1709
1733
|
return mediaDevices.getUserMedia(constraints);
|
|
1710
1734
|
},
|
|
1711
1735
|
async getDisplayMedia(constraints) {
|
|
1712
|
-
if (!mediaDevices?.getDisplayMedia) throw new
|
|
1736
|
+
if (!mediaDevices?.getDisplayMedia) throw new DOMException("Browser display capture is unavailable", "NotSupportedError");
|
|
1713
1737
|
return mediaDevices.getDisplayMedia(constraints);
|
|
1714
1738
|
},
|
|
1715
1739
|
async enumerateDevices() {
|
|
@@ -1802,12 +1826,14 @@ function createMediaClient(apiBaseURL, input) {
|
|
|
1802
1826
|
} : void 0,
|
|
1803
1827
|
transport: createCloudflareSFUHTTPTransport({
|
|
1804
1828
|
apiBaseURL,
|
|
1829
|
+
telemetry: input.telemetry,
|
|
1805
1830
|
credential: input.credential,
|
|
1806
1831
|
tenantId: subject.tenantId,
|
|
1807
1832
|
spaceId: subject.spaceId,
|
|
1808
1833
|
episodeId: subject.episodeId,
|
|
1809
1834
|
participantId: subject.participantId
|
|
1810
1835
|
}),
|
|
1836
|
+
recordReconnect: input.recordReconnect,
|
|
1811
1837
|
onError: input.onFailure,
|
|
1812
1838
|
...input.recordRtcSummary ? { onRtcSummary: input.recordRtcSummary } : {},
|
|
1813
1839
|
onScreenEnded: input.onScreenEnded
|
|
@@ -1832,8 +1858,12 @@ function createSFUConnectionMediaClient(client) {
|
|
|
1832
1858
|
function bindConnectionMediaClient(client, restart) {
|
|
1833
1859
|
return {
|
|
1834
1860
|
setLocalPublicationTarget: client.setLocalPublicationTarget.bind(client),
|
|
1861
|
+
...client.setLocalSourceIntent ? { setLocalSourceIntent: client.setLocalSourceIntent.bind(client) } : {},
|
|
1862
|
+
...client.closeForcedLocalPublication ? { closeForcedLocalPublication: client.closeForcedLocalPublication.bind(client) } : {},
|
|
1835
1863
|
observeLocalPublications: client.observeLocalPublications.bind(client),
|
|
1836
1864
|
observeRemotePublications: client.observeRemotePublications.bind(client),
|
|
1865
|
+
...client.remotePublicationsChanged ? { remotePublicationsChanged: client.remotePublicationsChanged.bind(client) } : {},
|
|
1866
|
+
...client.remotePublicationResumed ? { remotePublicationResumed: client.remotePublicationResumed.bind(client) } : {},
|
|
1837
1867
|
start: client.start.bind(client),
|
|
1838
1868
|
stop: client.stop.bind(client),
|
|
1839
1869
|
restart,
|
|
@@ -1849,6 +1879,7 @@ function createSyncClient(syncURL, input) {
|
|
|
1849
1879
|
token: input.token,
|
|
1850
1880
|
mediaPlane: input.media,
|
|
1851
1881
|
telemetry: input.telemetry,
|
|
1882
|
+
recordReconnect: input.recordReconnect,
|
|
1852
1883
|
persistenceScope: `${input.access.subject.tenantId}:${input.access.subject.episodeId}:${input.access.subject.participantId}`
|
|
1853
1884
|
});
|
|
1854
1885
|
}
|
|
@@ -1941,12 +1972,12 @@ var ConnectionError = class extends Error {
|
|
|
1941
1972
|
code;
|
|
1942
1973
|
action;
|
|
1943
1974
|
recoverable;
|
|
1944
|
-
constructor(
|
|
1945
|
-
super(
|
|
1975
|
+
constructor(failure3, options) {
|
|
1976
|
+
super(failure3.message, options);
|
|
1946
1977
|
this.name = "ConnectionError";
|
|
1947
|
-
this.code =
|
|
1948
|
-
this.action =
|
|
1949
|
-
this.recoverable =
|
|
1978
|
+
this.code = failure3.code;
|
|
1979
|
+
this.action = failure3.action;
|
|
1980
|
+
this.recoverable = failure3.recoverable;
|
|
1950
1981
|
}
|
|
1951
1982
|
};
|
|
1952
1983
|
|
|
@@ -1965,7 +1996,7 @@ var makeConnectionLifecycleLayer = (options) => {
|
|
|
1965
1996
|
const platform = { ...defaults, ...options.dependencies };
|
|
1966
1997
|
const access = makeConnectionAccessLayer(
|
|
1967
1998
|
(request) => Effect2.tryPromise({
|
|
1968
|
-
try: () => Promise.resolve(options.access(request)),
|
|
1999
|
+
try: (signal) => Promise.resolve(options.access(request ? { ...request, signal } : void 0)),
|
|
1969
2000
|
catch: (cause) => new ConnectionAccessFailure({ code: "access.unavailable", cause })
|
|
1970
2001
|
}),
|
|
1971
2002
|
options.accessRefreshWindowMs
|
|
@@ -2006,6 +2037,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2006
2037
|
sync: null,
|
|
2007
2038
|
media: null,
|
|
2008
2039
|
syncSnapshot: null,
|
|
2040
|
+
syncNoticeUnresponsive: false,
|
|
2009
2041
|
mediaSnapshot: null,
|
|
2010
2042
|
failure: null,
|
|
2011
2043
|
initialMedia: (intent) => Effect2.tryPromise({
|
|
@@ -2023,7 +2055,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2023
2055
|
closed: false,
|
|
2024
2056
|
epoch: 0
|
|
2025
2057
|
};
|
|
2026
|
-
const
|
|
2058
|
+
const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
|
|
2027
2059
|
const toPromise = (effect) => Effect2.runPromiseWith(context)(effect);
|
|
2028
2060
|
const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe())).pipe(
|
|
2029
2061
|
Effect2.tap((snapshot) => SubscriptionRef.set(snapshotRef, snapshot)),
|
|
@@ -2096,6 +2128,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2096
2128
|
model.sync = null;
|
|
2097
2129
|
model.media = null;
|
|
2098
2130
|
model.syncSnapshot = null;
|
|
2131
|
+
model.syncNoticeUnresponsive = false;
|
|
2099
2132
|
model.mediaSnapshot = null;
|
|
2100
2133
|
yield* emitPorts();
|
|
2101
2134
|
if (scope) yield* Scope.close(scope, Exit.void);
|
|
@@ -2104,9 +2137,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2104
2137
|
});
|
|
2105
2138
|
const bindSync = (scope, sync) => Effect2.gen(function* () {
|
|
2106
2139
|
model.syncBindingCleanup?.();
|
|
2140
|
+
model.syncNoticeUnresponsive = false;
|
|
2107
2141
|
let latestSnapshot = null;
|
|
2108
2142
|
let snapshotQueued = false;
|
|
2109
2143
|
const unsubscribe = sync.subscribe((snapshot) => {
|
|
2144
|
+
if (sync === model.sync && snapshot.connection.phase === "live") {
|
|
2145
|
+
const notice = snapshot.connection.noticeUnresponsive === true;
|
|
2146
|
+
if (notice !== model.syncNoticeUnresponsive) {
|
|
2147
|
+
model.syncNoticeUnresponsive = notice;
|
|
2148
|
+
void Effect2.runForkWith(context)(publish());
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2110
2151
|
latestSnapshot = snapshot;
|
|
2111
2152
|
if (snapshotQueued) return;
|
|
2112
2153
|
snapshotQueued = true;
|
|
@@ -2167,7 +2208,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2167
2208
|
);
|
|
2168
2209
|
const trace = (step, effect) => {
|
|
2169
2210
|
const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
|
|
2170
|
-
return effect.pipe(
|
|
2211
|
+
return traceRecovery(step, effect, recordInitialConnection).pipe(
|
|
2171
2212
|
Effect2.tap(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "succeeded" }))),
|
|
2172
2213
|
Effect2.tapError(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "failed" })))
|
|
2173
2214
|
);
|
|
@@ -2195,6 +2236,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2195
2236
|
access: grant,
|
|
2196
2237
|
credential: () => toPromise(access.getMediaToken()),
|
|
2197
2238
|
replaceMediaConnection: () => toPromise(access.refresh("media_recovery", true)).then((replacement) => replacement.media),
|
|
2239
|
+
recordReconnect: options.recordReconnect,
|
|
2240
|
+
telemetry: options.telemetry,
|
|
2198
2241
|
...options.recordRtcSummary ? { recordRtcSummary: options.recordRtcSummary } : {},
|
|
2199
2242
|
onFailure: () => enqueueBackground(handleMediaFailure()),
|
|
2200
2243
|
onScreenEnded: () => enqueueBackground(notifyScreenEnded())
|
|
@@ -2206,7 +2249,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2206
2249
|
const sync = yield* trace(
|
|
2207
2250
|
"create_sync_client",
|
|
2208
2251
|
Effect2.try({
|
|
2209
|
-
try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry }),
|
|
2252
|
+
try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
|
|
2210
2253
|
catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
|
|
2211
2254
|
})
|
|
2212
2255
|
);
|
|
@@ -2216,9 +2259,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2216
2259
|
[
|
|
2217
2260
|
trace(
|
|
2218
2261
|
"start_media",
|
|
2219
|
-
|
|
2262
|
+
foreign7(() => media.start(stream), "media_start_failed", "The media layer could not start")
|
|
2220
2263
|
),
|
|
2221
|
-
trace("start_sync",
|
|
2264
|
+
trace("start_sync", foreign7(() => sync.start(), "sync_start_failed", "The sync layer could not start").pipe(Effect2.andThen(trace("wait_for_sync_live", waitForSyncLive(sync, boundedInteger(options.syncStartupTimeoutMs, START_TIMEOUT_MS, 1, 6e4))))))
|
|
2222
2265
|
],
|
|
2223
2266
|
{ concurrency: "unbounded", discard: true }
|
|
2224
2267
|
);
|
|
@@ -2226,17 +2269,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2226
2269
|
Effect2.matchEffect({
|
|
2227
2270
|
onFailure: (cause) => Effect2.gen(function* () {
|
|
2228
2271
|
stopStream(stream);
|
|
2229
|
-
const
|
|
2230
|
-
model.failure = toFailure(
|
|
2272
|
+
const failure3 = failureFromCause(cause, "sync_start_failed", "The Connection could not start");
|
|
2273
|
+
model.failure = toFailure(failure3);
|
|
2231
2274
|
if (model.joinCancelled) {
|
|
2232
2275
|
span.end({ state: model.state, epoch: model.epoch, outcome: "cancelled", code: "invalid_state" });
|
|
2233
2276
|
return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2234
2277
|
}
|
|
2235
2278
|
yield* stopPorts();
|
|
2236
|
-
if (!retried &&
|
|
2279
|
+
if (!retried && failure3.code === "invalid_access") return yield* performJoin("access_retry", true);
|
|
2237
2280
|
yield* transition("failed");
|
|
2238
|
-
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code:
|
|
2239
|
-
return yield* Effect2.fail(
|
|
2281
|
+
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure3.code });
|
|
2282
|
+
return yield* Effect2.fail(failure3);
|
|
2240
2283
|
}),
|
|
2241
2284
|
onSuccess: () => Effect2.gen(function* () {
|
|
2242
2285
|
model.syncSnapshot = model.sync?.getSnapshot() ?? null;
|
|
@@ -2262,7 +2305,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2262
2305
|
const leaveIsConfirmed = () => Effect2.gen(function* leaveIsConfirmedEffect() {
|
|
2263
2306
|
const sync = model.sync;
|
|
2264
2307
|
if (!sync || model.episodeEndConfirmed || access.currentUnsafe() === null) return true;
|
|
2265
|
-
const leave =
|
|
2308
|
+
const leave = foreign7(() => sync.leave()).pipe(Effect2.timeout(LEAVE_TIMEOUT_MS));
|
|
2266
2309
|
return (yield* Effect2.exit(leave))._tag === "Success";
|
|
2267
2310
|
});
|
|
2268
2311
|
const withFreshAccess = (operation) => Effect2.gen(function* () {
|
|
@@ -2290,6 +2333,19 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2290
2333
|
})
|
|
2291
2334
|
);
|
|
2292
2335
|
});
|
|
2336
|
+
const traceRecovery = (step, operation, recordStep = recordReconnect) => Effect2.gen(function* () {
|
|
2337
|
+
const started = yield* Clock2.currentTimeMillis;
|
|
2338
|
+
recordStep(options.recordReconnect, step, { boundary: "start" });
|
|
2339
|
+
return yield* operation.pipe(
|
|
2340
|
+
Effect2.onExit(
|
|
2341
|
+
(exit) => Clock2.currentTimeMillis.pipe(
|
|
2342
|
+
Effect2.map((now) => {
|
|
2343
|
+
recordStep(options.recordReconnect, step, { boundary: "end", duration_ms: Math.max(0, now - started) }, exit._tag === "Success" ? "succeeded" : "failed");
|
|
2344
|
+
})
|
|
2345
|
+
)
|
|
2346
|
+
)
|
|
2347
|
+
);
|
|
2348
|
+
});
|
|
2293
2349
|
const recover = (kind) => Effect2.gen(function* recoverEffect() {
|
|
2294
2350
|
if (!active(model)) return;
|
|
2295
2351
|
if (model.recoveryQueued === kind) return;
|
|
@@ -2312,10 +2368,11 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2312
2368
|
for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
|
|
2313
2369
|
const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
|
|
2314
2370
|
if (remaining <= 0) return null;
|
|
2371
|
+
recordReconnect(options.recordReconnect, "attempt", { recovery_kind: plan.kind, attempt });
|
|
2315
2372
|
diagnostics.record({ event: "recovery_attempt", state: model.state, epoch: model.epoch, attempt });
|
|
2316
2373
|
const outcome = yield* Effect2.exit(recoveryOperation(plan.kind).pipe(Effect2.timeout(remaining)));
|
|
2317
2374
|
if (outcome._tag === "Success") return attempt;
|
|
2318
|
-
if (attempt < plan.attempts) yield* Effect2.sleep(recoveryDelay(plan, attempt));
|
|
2375
|
+
if (attempt < plan.attempts) yield* traceRecovery("lifecycle_backoff", Effect2.sleep(recoveryDelay(plan, attempt)));
|
|
2319
2376
|
}
|
|
2320
2377
|
return null;
|
|
2321
2378
|
});
|
|
@@ -2339,9 +2396,13 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2339
2396
|
const media = model.media;
|
|
2340
2397
|
const sync = model.sync;
|
|
2341
2398
|
if (!media || !sync) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Media recovery requires active ports"));
|
|
2342
|
-
|
|
2399
|
+
recordReconnect(options.recordReconnect, "media_decision", { strategy: "full_rebuild" });
|
|
2400
|
+
const grant = yield* traceRecovery("access_refresh", access.refresh("media_recovery", true).pipe(Effect2.mapError(accessFailure)));
|
|
2343
2401
|
const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
|
|
2344
|
-
yield*
|
|
2402
|
+
yield* traceRecovery(
|
|
2403
|
+
"media_rebuild",
|
|
2404
|
+
foreign7(() => media.restart(restartInput))
|
|
2405
|
+
);
|
|
2345
2406
|
model.mediaSnapshot = media.getSnapshot();
|
|
2346
2407
|
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
2347
2408
|
});
|
|
@@ -2354,12 +2415,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2354
2415
|
model.syncBindingCleanup?.();
|
|
2355
2416
|
model.sync = null;
|
|
2356
2417
|
const sync = yield* Effect2.try({
|
|
2357
|
-
try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry }),
|
|
2418
|
+
try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
|
|
2358
2419
|
catch: (cause) => lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
|
|
2359
2420
|
});
|
|
2360
2421
|
model.sync = sync;
|
|
2361
2422
|
yield* bindSync(scope, sync);
|
|
2362
|
-
yield*
|
|
2423
|
+
yield* foreign7(() => sync.start());
|
|
2363
2424
|
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
2364
2425
|
yield* emitPorts();
|
|
2365
2426
|
});
|
|
@@ -2374,9 +2435,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2374
2435
|
const isCurrentSyncSnapshot = (sync, snapshot) => sync === model.sync && snapshot !== model.syncSnapshot;
|
|
2375
2436
|
const syncSubjectMismatched = (subject, snapshot) => subject !== null && snapshot.participantId !== null && (subject.participantId !== snapshot.participantId || subject.participantGeneration !== snapshot.participantGeneration);
|
|
2376
2437
|
const episodeEnded = (snapshot) => snapshot.control?.status === "ended" || snapshot.optimisticControl?.status === "ended";
|
|
2377
|
-
const syncNeedsRecovery = (snapshot) => active(model) &&
|
|
2378
|
-
const failForSnapshot = (
|
|
2379
|
-
model.failure = toFailure(
|
|
2438
|
+
const syncNeedsRecovery = (snapshot) => active(model) && snapshot.connection.phase === "terminal";
|
|
2439
|
+
const failForSnapshot = (failure3) => Effect2.gen(function* failForSnapshotEffect() {
|
|
2440
|
+
model.failure = toFailure(failure3);
|
|
2380
2441
|
yield* stopPorts();
|
|
2381
2442
|
yield* transition("failed");
|
|
2382
2443
|
});
|
|
@@ -2392,7 +2453,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2392
2453
|
const media = model.media;
|
|
2393
2454
|
if (!media || !active(model)) return;
|
|
2394
2455
|
const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
|
|
2395
|
-
const restarted = yield* Effect2.exit(
|
|
2456
|
+
const restarted = yield* Effect2.exit(foreign7(() => media.restart(restartInput), "media_start_failed", "The media client could not apply a replacement access grant"));
|
|
2396
2457
|
if (restarted._tag === "Failure") {
|
|
2397
2458
|
yield* recover("media");
|
|
2398
2459
|
return;
|
|
@@ -2502,7 +2563,7 @@ function snapshotFor(model, access) {
|
|
|
2502
2563
|
state: model.state,
|
|
2503
2564
|
subject: subjectFor(access),
|
|
2504
2565
|
episode: episodeFor(access, control),
|
|
2505
|
-
connection: Object.freeze({ sync: syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
2566
|
+
connection: Object.freeze({ sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
2506
2567
|
failure: model.failure ? Object.freeze({ ...model.failure }) : null
|
|
2507
2568
|
});
|
|
2508
2569
|
}
|
|
@@ -2578,6 +2639,10 @@ var CONNECTION_ERROR_CODE_MAP = {
|
|
|
2578
2639
|
invalid_access: "access.invalid",
|
|
2579
2640
|
access_unavailable: "access.unavailable",
|
|
2580
2641
|
permission_denied: "media.permission_denied",
|
|
2642
|
+
device_not_found: "media.device_not_found",
|
|
2643
|
+
device_busy: "media.device_busy",
|
|
2644
|
+
device_constraint_invalid: "media.device_constraint_invalid",
|
|
2645
|
+
media_capture_failed: "media.capture_failed",
|
|
2581
2646
|
sync_start_failed: "connection.sync_start_failed",
|
|
2582
2647
|
media_start_failed: "connection.media_start_failed",
|
|
2583
2648
|
join_cleanup_unconfirmed: "connection.join_cleanup_unconfirmed",
|
|
@@ -2610,6 +2675,9 @@ function normalizeClientError(cause, fallback = "client.internal_error") {
|
|
|
2610
2675
|
message: cause.message
|
|
2611
2676
|
});
|
|
2612
2677
|
}
|
|
2678
|
+
if (cause instanceof V1SyncError && ["signaling_timeout", "negotiation_timeout", "media_timeout"].includes(cause.code)) {
|
|
2679
|
+
return new SpaceClientError({ code: "media.timeout", recoverable: true, message: "Media did not respond in time. Try again." });
|
|
2680
|
+
}
|
|
2613
2681
|
if (isAccessInvalid(cause)) {
|
|
2614
2682
|
return new SpaceClientError({ code: "access.invalid", recoverable: true, message: "The access grant was rejected" });
|
|
2615
2683
|
}
|
|
@@ -2633,6 +2701,7 @@ function connectionErrorCode(code, action) {
|
|
|
2633
2701
|
|
|
2634
2702
|
// src/space-client/store.ts
|
|
2635
2703
|
import { Context as Context4, Effect as Effect3, Layer as Layer4, SubscriptionRef as SubscriptionRef2 } from "effect";
|
|
2704
|
+
var INTERRUPTED_CONNECTION_PHASES = /* @__PURE__ */ new Set(["unresponsive", "connecting", "recovering"]);
|
|
2636
2705
|
var empty = (value) => Object.freeze(value);
|
|
2637
2706
|
var EMPTY = empty({
|
|
2638
2707
|
connection: empty({ status: "idle", episode: null, lastError: null }),
|
|
@@ -2648,6 +2717,8 @@ var EMPTY = empty({
|
|
|
2648
2717
|
}),
|
|
2649
2718
|
chat: empty({ status: "idle", messages: empty([]), pendingSends: empty([]), readReceipts: empty([]), unreadCount: 0, pagination: empty({ cursor: null, hasOlder: false, historyTruncated: false }), lastError: null }),
|
|
2650
2719
|
reactions: empty({ active: empty([]) }),
|
|
2720
|
+
recording: empty({ current: null }),
|
|
2721
|
+
offline: empty({ pending: empty([]), decisionNeeded: false, policy: "send" }),
|
|
2651
2722
|
whiteboard: empty({ open: false, engine: empty({ status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null }) })
|
|
2652
2723
|
});
|
|
2653
2724
|
var SpaceStore = class _SpaceStore {
|
|
@@ -2672,8 +2743,9 @@ var SpaceStore = class _SpaceStore {
|
|
|
2672
2743
|
updateConnection(snapshot) {
|
|
2673
2744
|
const current = this.getSnapshot().connection;
|
|
2674
2745
|
const error2 = snapshot.failure ? empty({ code: mapCode(snapshot.failure.code), recoverable: snapshot.failure.recoverable, message: snapshot.failure.message }) : null;
|
|
2675
|
-
|
|
2676
|
-
|
|
2746
|
+
const status = snapshot.state === "live" && (INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media)) ? "reconnecting" : snapshot.state;
|
|
2747
|
+
if (current.status === status && sameEpisode(current.episode, snapshot.episode) && sameError(current.lastError, error2)) return;
|
|
2748
|
+
this.#replace("connection", empty({ status, episode: snapshot.episode, lastError: error2 }));
|
|
2677
2749
|
}
|
|
2678
2750
|
updateSelf(value) {
|
|
2679
2751
|
this.#replace("self", value);
|
|
@@ -2690,6 +2762,12 @@ var SpaceStore = class _SpaceStore {
|
|
|
2690
2762
|
updateReactions(value) {
|
|
2691
2763
|
this.#replace("reactions", value);
|
|
2692
2764
|
}
|
|
2765
|
+
updateRecording(value) {
|
|
2766
|
+
this.#replace("recording", value);
|
|
2767
|
+
}
|
|
2768
|
+
updateOffline(value) {
|
|
2769
|
+
this.#replace("offline", value);
|
|
2770
|
+
}
|
|
2693
2771
|
updateWhiteboard(value) {
|
|
2694
2772
|
this.#replace("whiteboard", value);
|
|
2695
2773
|
}
|
|
@@ -2728,11 +2806,943 @@ function sameError(left, right) {
|
|
|
2728
2806
|
return left.code === right.code && left.recoverable === right.recoverable && left.message === right.message;
|
|
2729
2807
|
}
|
|
2730
2808
|
|
|
2809
|
+
// src/feedback/types.ts
|
|
2810
|
+
var FEEDBACK_EVIDENCE_SCHEMA_VERSION = "FeedbackEvidence/v1";
|
|
2811
|
+
var FEEDBACK_REQUEST_SCHEMA_VERSION = "FeedbackReportRequest/v1";
|
|
2812
|
+
var FEEDBACK_RECEIPT_SCHEMA_VERSION = "FeedbackReportReceipt/v1";
|
|
2813
|
+
var FEEDBACK_SCREENSHOT_SCHEMA_VERSION = "FeedbackScreenshot/v1";
|
|
2814
|
+
var FEEDBACK_LOCAL_STATE_REGISTRY_VERSION = "FeedbackLocalState/v1";
|
|
2815
|
+
var FEEDBACK_COOKIE_REGISTRY_VERSION = "FeedbackCookies/v1";
|
|
2816
|
+
var FEEDBACK_MAX_MESSAGE_BYTES = 8e3;
|
|
2817
|
+
var FEEDBACK_MAX_EVIDENCE_BYTES = 128 * 1024;
|
|
2818
|
+
var FEEDBACK_MAX_REQUEST_BYTES = 1 << 20;
|
|
2819
|
+
var FEEDBACK_MAX_SCREENSHOT_BYTES = 450 * 1024;
|
|
2820
|
+
var FEEDBACK_MAX_SCREENSHOT_WIDTH = 1920;
|
|
2821
|
+
var FEEDBACK_MAX_SCREENSHOT_HEIGHT = 1080;
|
|
2822
|
+
var FEEDBACK_MAX_TELEMETRY_EVENTS = 50;
|
|
2823
|
+
var FEEDBACK_MAX_DIAGNOSTIC_EVENTS = 50;
|
|
2824
|
+
var FEEDBACK_MAX_LOCAL_STATE_ENTRIES = 32;
|
|
2825
|
+
var FEEDBACK_MAX_COOKIE_ENTRIES = 16;
|
|
2826
|
+
|
|
2827
|
+
// src/feedback/validation.ts
|
|
2828
|
+
import { isDiagnosticReference, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
|
|
2829
|
+
var encoder3 = new TextEncoder();
|
|
2830
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
2831
|
+
var TRACE_ID_PATTERN = /^(?=.*[1-9a-f])[0-9a-f]{32}$/u;
|
|
2832
|
+
var SPAN_ID_PATTERN = /^(?=.*[1-9a-f])[0-9a-f]{16}$/u;
|
|
2833
|
+
var TRACEPARENT_PATTERN = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/iu;
|
|
2834
|
+
var SAFE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@+/=-]{0,127}$/u;
|
|
2835
|
+
var DASHBOARD_ACTION_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
2836
|
+
var UNSAFE_VALUE_PATTERN = /(?:https?:\/\/|wss?:\/\/|bearer\s+[a-z0-9._~+\/-]+|-----begin|candidate:|v=0\r?\n|\b(?:\d{1,3}\.){3}\d{1,3}\b|[\w.+-]+@[\w.-]+\.[a-z]{2,})/iu;
|
|
2837
|
+
var TELEMETRY_KEYS = ["version", "event_id", "journey_id", "sequence", "occurred_at", "name", "phase", "state", "origin_kind", "first_observed_layer", "upstream_visibility", "parent_event_id", "trace_id", "span_id", "traceparent", "tracestate", "attributes"];
|
|
2838
|
+
var TELEMETRY_NAMES = ["journey.started", "journey.phase", "journey.terminal", "journey.linked", "http.request", "sync.frame", "rtc.summary", "diagnostic.timeline"];
|
|
2839
|
+
var TELEMETRY_PHASES = ["root", "authentication", "signaling", "media", "recovery", "terminal"];
|
|
2840
|
+
var TELEMETRY_STATES = ["started", "in_progress", "succeeded", "failed", "cancelled", "observed"];
|
|
2841
|
+
var TELEMETRY_ORIGINS = ["client", "diagnostic", "http", "rtc", "sync"];
|
|
2842
|
+
var TELEMETRY_VISIBILITY = ["local", "propagated"];
|
|
2843
|
+
var FeedbackValidationError = class extends Error {
|
|
2844
|
+
issues;
|
|
2845
|
+
constructor(message, issues = [{ path: "$", message }]) {
|
|
2846
|
+
super(message);
|
|
2847
|
+
this.name = "FeedbackValidationError";
|
|
2848
|
+
this.issues = issues;
|
|
2849
|
+
}
|
|
2850
|
+
};
|
|
2851
|
+
function validateFeedbackReportRequest(input) {
|
|
2852
|
+
const issues = [];
|
|
2853
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a feedback request object");
|
|
2854
|
+
checkKeys(input, ["schema_version", "category", "message", "source", "evidence", "screenshot"], issues, "$", false);
|
|
2855
|
+
if (property(input, "schema_version") !== FEEDBACK_REQUEST_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported feedback request schema" });
|
|
2856
|
+
const fields = validateFeedbackRequestFields(input, issues);
|
|
2857
|
+
validateFeedbackScreenshotConsistency(fields, issues);
|
|
2858
|
+
if (!fields.category || !fields.source || !fields.message || !fields.evidence || !fields.screenshot_valid || issues.length > 0) return { ok: false, issues };
|
|
2859
|
+
const request = {
|
|
2860
|
+
schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION,
|
|
2861
|
+
category: fields.category,
|
|
2862
|
+
message: fields.message,
|
|
2863
|
+
source: fields.source,
|
|
2864
|
+
evidence: fields.evidence,
|
|
2865
|
+
...fields.screenshot ? { screenshot: fields.screenshot } : {}
|
|
2866
|
+
};
|
|
2867
|
+
return validateFeedbackRequestSize(request, issues);
|
|
2868
|
+
}
|
|
2869
|
+
function validateFeedbackRequestFields(input, issues) {
|
|
2870
|
+
const category = enumValue(property(input, "category"), ["bug", "feature_request", "other"], "$.category", issues);
|
|
2871
|
+
const source = enumValue(property(input, "source"), ["embedded", "chalk_web", "chalk_mobile", "dashboard"], "$.source", issues);
|
|
2872
|
+
const message = validateMessage(property(input, "message"), issues);
|
|
2873
|
+
const evidenceResult = validateFeedbackEvidence(property(input, "evidence"));
|
|
2874
|
+
appendIssues(issues, evidenceResult);
|
|
2875
|
+
const screenshotResult = validateRequestScreenshot(property(input, "screenshot"));
|
|
2876
|
+
appendIssues(issues, screenshotResult);
|
|
2877
|
+
return {
|
|
2878
|
+
category,
|
|
2879
|
+
source,
|
|
2880
|
+
message,
|
|
2881
|
+
evidence: evidenceResult.ok ? evidenceResult.value : void 0,
|
|
2882
|
+
screenshot: screenshotResult.ok ? screenshotResult.value : void 0,
|
|
2883
|
+
screenshot_valid: screenshotResult.ok
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
function validateRequestScreenshot(input) {
|
|
2887
|
+
return input === void 0 ? { ok: true, value: void 0 } : validateFeedbackScreenshot(input);
|
|
2888
|
+
}
|
|
2889
|
+
function validateFeedbackScreenshotConsistency(fields, issues) {
|
|
2890
|
+
if (!fields.evidence) return;
|
|
2891
|
+
if (!fields.screenshot && requiresScreenshot(fields.evidence)) {
|
|
2892
|
+
issues.push({ path: "$.screenshot", message: "a captured screenshot payload is required" });
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (!fields.screenshot) return;
|
|
2896
|
+
if (!requiresScreenshot(fields.evidence)) issues.push({ path: "$.evidence.screenshot.state", message: "screenshot state must indicate a capture" });
|
|
2897
|
+
if (fields.evidence.screenshot.captured_at !== fields.screenshot.captured_at) issues.push({ path: "$.screenshot.captured_at", message: "screenshot timestamps must match" });
|
|
2898
|
+
}
|
|
2899
|
+
function requiresScreenshot(evidence) {
|
|
2900
|
+
return evidence.screenshot.state === "captured" || evidence.screenshot.state === "partial";
|
|
2901
|
+
}
|
|
2902
|
+
function validateFeedbackRequestSize(request, issues) {
|
|
2903
|
+
if (encodedByteLength(request) > FEEDBACK_MAX_REQUEST_BYTES) issues.push({ path: "$", message: "feedback request exceeds 1 MiB" });
|
|
2904
|
+
return issues.length === 0 ? { ok: true, value: request } : { ok: false, issues };
|
|
2905
|
+
}
|
|
2906
|
+
function appendIssues(issues, result) {
|
|
2907
|
+
if (!result.ok) issues.push(...result.issues);
|
|
2908
|
+
}
|
|
2909
|
+
function parseFeedbackReportRequest(input) {
|
|
2910
|
+
const result = validateFeedbackReportRequest(input);
|
|
2911
|
+
if (result.ok) return result.value;
|
|
2912
|
+
throw new FeedbackValidationError("Invalid feedback request", result.issues);
|
|
2913
|
+
}
|
|
2914
|
+
function validateFeedbackEvidence(input) {
|
|
2915
|
+
const issues = [];
|
|
2916
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a feedback evidence object");
|
|
2917
|
+
checkKeys(input, ["schema_version", "collected_at", "app", "sdk", "platform", "connection", "scope", "correlations", "diagnostics", "local_state", "cookies", "screenshot"], issues, "$", false);
|
|
2918
|
+
if (property(input, "schema_version") !== FEEDBACK_EVIDENCE_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported feedback evidence schema" });
|
|
2919
|
+
const app = validateApp(property(input, "app"), issues);
|
|
2920
|
+
const sdk = validateSDK(property(input, "sdk"), issues);
|
|
2921
|
+
const platform = validatePlatform(property(input, "platform"), issues);
|
|
2922
|
+
const connection = validateConnection(property(input, "connection"), issues);
|
|
2923
|
+
const scope = validateScope(property(input, "scope"), issues);
|
|
2924
|
+
const correlations = validateCorrelations(property(input, "correlations"), issues);
|
|
2925
|
+
const diagnostics = validateDiagnostics(property(input, "diagnostics"), issues);
|
|
2926
|
+
const localState = validateLocalState(property(input, "local_state"), issues);
|
|
2927
|
+
const cookies = validateCookies(property(input, "cookies"), issues);
|
|
2928
|
+
const screenshot = validateScreenshotState(property(input, "screenshot"), issues);
|
|
2929
|
+
if (!sdk || !platform || !correlations || !diagnostics || !localState || !cookies || !screenshot || issues.length > 0) return { ok: false, issues };
|
|
2930
|
+
const collectedAt = checkDateTimeValue(property(input, "collected_at"), "$.collected_at", issues);
|
|
2931
|
+
if (!collectedAt) return { ok: false, issues };
|
|
2932
|
+
const evidence = {
|
|
2933
|
+
schema_version: FEEDBACK_EVIDENCE_SCHEMA_VERSION,
|
|
2934
|
+
collected_at: collectedAt,
|
|
2935
|
+
...app ? { app } : {},
|
|
2936
|
+
sdk,
|
|
2937
|
+
platform,
|
|
2938
|
+
...connection ? { connection } : {},
|
|
2939
|
+
...scope ? { scope } : {},
|
|
2940
|
+
correlations,
|
|
2941
|
+
diagnostics,
|
|
2942
|
+
local_state: localState,
|
|
2943
|
+
cookies,
|
|
2944
|
+
screenshot
|
|
2945
|
+
};
|
|
2946
|
+
if (encodedByteLength(evidence) > FEEDBACK_MAX_EVIDENCE_BYTES) return failure2("$", "feedback evidence exceeds 128 KiB");
|
|
2947
|
+
return { ok: true, value: evidence };
|
|
2948
|
+
}
|
|
2949
|
+
function parseFeedbackEvidence(input) {
|
|
2950
|
+
const result = validateFeedbackEvidence(input);
|
|
2951
|
+
if (result.ok) return result.value;
|
|
2952
|
+
throw new FeedbackValidationError("Invalid feedback evidence", result.issues);
|
|
2953
|
+
}
|
|
2954
|
+
function validateFeedbackScreenshot(input) {
|
|
2955
|
+
const issues = [];
|
|
2956
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a screenshot object");
|
|
2957
|
+
checkKeys(input, ["schema_version", "mime_type", "width", "height", "captured_at", "data_base64"], issues, "$", false);
|
|
2958
|
+
if (property(input, "schema_version") !== FEEDBACK_SCREENSHOT_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported screenshot schema" });
|
|
2959
|
+
const mimeType = enumValue(property(input, "mime_type"), ["image/jpeg", "image/png", "image/webp"], "$.mime_type", issues);
|
|
2960
|
+
const width = boundedInteger2(property(input, "width"), 1, FEEDBACK_MAX_SCREENSHOT_WIDTH, "$.width", issues);
|
|
2961
|
+
const height = boundedInteger2(property(input, "height"), 1, FEEDBACK_MAX_SCREENSHOT_HEIGHT, "$.height", issues);
|
|
2962
|
+
const capturedAt = property(input, "captured_at");
|
|
2963
|
+
checkDateTime(capturedAt, "$.captured_at", issues);
|
|
2964
|
+
const dataInput = property(input, "data_base64");
|
|
2965
|
+
const data = typeof dataInput === "string" ? dataInput : void 0;
|
|
2966
|
+
if (!data || !validBase64(data)) issues.push({ path: "$.data_base64", message: "screenshot data must be base64" });
|
|
2967
|
+
else if (base64ByteLength(data) > FEEDBACK_MAX_SCREENSHOT_BYTES) issues.push({ path: "$.data_base64", message: "screenshot exceeds 450 KiB" });
|
|
2968
|
+
if (!mimeType || width === void 0 || height === void 0 || typeof capturedAt !== "string" || !data || issues.length > 0) return { ok: false, issues };
|
|
2969
|
+
return { ok: true, value: { schema_version: FEEDBACK_SCREENSHOT_SCHEMA_VERSION, mime_type: mimeType, width, height, captured_at: capturedAt, data_base64: data } };
|
|
2970
|
+
}
|
|
2971
|
+
function parseFeedbackScreenshot(input) {
|
|
2972
|
+
const result = validateFeedbackScreenshot(input);
|
|
2973
|
+
if (result.ok) return result.value;
|
|
2974
|
+
throw new FeedbackValidationError("Invalid feedback screenshot", result.issues);
|
|
2975
|
+
}
|
|
2976
|
+
function assertFeedbackReceipt(input) {
|
|
2977
|
+
const issues = [];
|
|
2978
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new FeedbackValidationError("Invalid feedback receipt");
|
|
2979
|
+
checkKeys(input, ["schema_version", "id", "submitted_at"], issues, "$", false);
|
|
2980
|
+
const schemaVersion = property(input, "schema_version");
|
|
2981
|
+
const id = property(input, "id");
|
|
2982
|
+
const submittedAt = property(input, "submitted_at");
|
|
2983
|
+
if (schemaVersion !== FEEDBACK_RECEIPT_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported receipt schema" });
|
|
2984
|
+
if (!isUUID(id)) issues.push({ path: "$.id", message: "receipt ID is not a valid ID" });
|
|
2985
|
+
checkDateTime(submittedAt, "$.submitted_at", issues);
|
|
2986
|
+
if (issues.length > 0 || typeof id !== "string" || typeof submittedAt !== "string") throw new FeedbackValidationError("Invalid feedback receipt", issues);
|
|
2987
|
+
return { schema_version: FEEDBACK_RECEIPT_SCHEMA_VERSION, id, submitted_at: submittedAt };
|
|
2988
|
+
}
|
|
2989
|
+
function assertFeedbackMessage(message) {
|
|
2990
|
+
const issues = [];
|
|
2991
|
+
const normalized = validateMessage(message, issues);
|
|
2992
|
+
if (!normalized) throw new FeedbackValidationError("Invalid feedback message", issues);
|
|
2993
|
+
return normalized;
|
|
2994
|
+
}
|
|
2995
|
+
function validateMessage(value, issues) {
|
|
2996
|
+
if (typeof value !== "string") {
|
|
2997
|
+
issues.push({ path: "$.message", message: "message must be a string" });
|
|
2998
|
+
return void 0;
|
|
2999
|
+
}
|
|
3000
|
+
const message = value.trim();
|
|
3001
|
+
if (invalidMessageLength(message)) issues.push({ path: "$.message", message: "message must be between 1 and 8,000 UTF-8 bytes" });
|
|
3002
|
+
if (hasForbiddenMessageControl(message)) issues.push({ path: "$.message", message: "message contains a forbidden control character" });
|
|
3003
|
+
return hasMessageIssue(issues) ? void 0 : message;
|
|
3004
|
+
}
|
|
3005
|
+
function invalidMessageLength(message) {
|
|
3006
|
+
const bytes = encoder3.encode(message).byteLength;
|
|
3007
|
+
return bytes < 1 || bytes > FEEDBACK_MAX_MESSAGE_BYTES;
|
|
3008
|
+
}
|
|
3009
|
+
function hasForbiddenMessageControl(message) {
|
|
3010
|
+
for (const character of message) if (isForbiddenMessageControl(character)) return true;
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
function isForbiddenMessageControl(character) {
|
|
3014
|
+
return isControl(character) && character !== "\n" && character !== "\r" && character !== " ";
|
|
3015
|
+
}
|
|
3016
|
+
function hasMessageIssue(issues) {
|
|
3017
|
+
return issues.some((issue) => issue.path === "$.message");
|
|
3018
|
+
}
|
|
3019
|
+
function validateApp(input, issues) {
|
|
3020
|
+
if (input === void 0) return void 0;
|
|
3021
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3022
|
+
issues.push({ path: "$.app", message: "app must be an object" });
|
|
3023
|
+
return void 0;
|
|
3024
|
+
}
|
|
3025
|
+
checkKeys(input, ["name", "version", "build"], issues, "$.app", false);
|
|
3026
|
+
const name = metadata(property(input, "name"), "$.app.name", issues, true);
|
|
3027
|
+
const version = metadata(property(input, "version"), "$.app.version", issues, false);
|
|
3028
|
+
const build = metadata(property(input, "build"), "$.app.build", issues, false);
|
|
3029
|
+
return name ? { name, ...version ? { version } : {}, ...build ? { build } : {} } : void 0;
|
|
3030
|
+
}
|
|
3031
|
+
function validateSDK(input, issues) {
|
|
3032
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3033
|
+
issues.push({ path: "$.sdk", message: "sdk must be an object" });
|
|
3034
|
+
return void 0;
|
|
3035
|
+
}
|
|
3036
|
+
checkKeys(input, ["client", "react", "react_native"], issues, "$.sdk", false);
|
|
3037
|
+
const client = metadata(property(input, "client"), "$.sdk.client", issues, true);
|
|
3038
|
+
const react = metadata(property(input, "react"), "$.sdk.react", issues, false);
|
|
3039
|
+
const reactNative = metadata(property(input, "react_native"), "$.sdk.react_native", issues, false);
|
|
3040
|
+
return client ? { client, ...react ? { react } : {}, ...reactNative ? { react_native: reactNative } : {} } : void 0;
|
|
3041
|
+
}
|
|
3042
|
+
function validatePlatform(input, issues) {
|
|
3043
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3044
|
+
issues.push({ path: "$.platform", message: "platform must be an object" });
|
|
3045
|
+
return void 0;
|
|
3046
|
+
}
|
|
3047
|
+
checkKeys(input, ["kind", "os_name", "os_version", "browser_name", "browser_version", "device_class", "device_model"], issues, "$.platform", false);
|
|
3048
|
+
const kind = enumValue(property(input, "kind"), ["web", "ios", "android", "macos"], "$.platform.kind", issues);
|
|
3049
|
+
const deviceClassInput = property(input, "device_class");
|
|
3050
|
+
const deviceClass = deviceClassInput === void 0 ? void 0 : enumValue(deviceClassInput, ["phone", "tablet", "desktop"], "$.platform.device_class", issues);
|
|
3051
|
+
const values = {
|
|
3052
|
+
os_name: metadata(property(input, "os_name"), "$.platform.os_name", issues, false),
|
|
3053
|
+
os_version: metadata(property(input, "os_version"), "$.platform.os_version", issues, false),
|
|
3054
|
+
browser_name: metadata(property(input, "browser_name"), "$.platform.browser_name", issues, false),
|
|
3055
|
+
browser_version: metadata(property(input, "browser_version"), "$.platform.browser_version", issues, false),
|
|
3056
|
+
device_model: metadata(property(input, "device_model"), "$.platform.device_model", issues, false)
|
|
3057
|
+
};
|
|
3058
|
+
if (!kind) return void 0;
|
|
3059
|
+
return buildFeedbackPlatform(kind, values, deviceClass);
|
|
3060
|
+
}
|
|
3061
|
+
function buildFeedbackPlatform(kind, values, deviceClass) {
|
|
3062
|
+
return {
|
|
3063
|
+
kind,
|
|
3064
|
+
...values.os_name ? { os_name: values.os_name } : {},
|
|
3065
|
+
...values.os_version ? { os_version: values.os_version } : {},
|
|
3066
|
+
...values.browser_name ? { browser_name: values.browser_name } : {},
|
|
3067
|
+
...values.browser_version ? { browser_version: values.browser_version } : {},
|
|
3068
|
+
...deviceClass ? { device_class: deviceClass } : {},
|
|
3069
|
+
...values.device_model ? { device_model: values.device_model } : {}
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
function validateConnection(input, issues) {
|
|
3073
|
+
if (input === void 0) return void 0;
|
|
3074
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3075
|
+
issues.push({ path: "$.connection", message: "connection must be an object" });
|
|
3076
|
+
return void 0;
|
|
3077
|
+
}
|
|
3078
|
+
checkKeys(input, ["state", "error_code"], issues, "$.connection", false);
|
|
3079
|
+
const state = safeToken(property(input, "state"), "$.connection.state", issues, true);
|
|
3080
|
+
const errorCode = safeToken(property(input, "error_code"), "$.connection.error_code", issues, false);
|
|
3081
|
+
return state ? { state, ...errorCode ? { error_code: errorCode } : {} } : void 0;
|
|
3082
|
+
}
|
|
3083
|
+
function validateScope(input, issues) {
|
|
3084
|
+
if (input === void 0) return void 0;
|
|
3085
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3086
|
+
issues.push({ path: "$.scope", message: "scope must be an object" });
|
|
3087
|
+
return void 0;
|
|
3088
|
+
}
|
|
3089
|
+
checkKeys(input, ["space_id", "episode_id", "participant_id"], issues, "$.scope", false);
|
|
3090
|
+
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));
|
|
3091
|
+
}
|
|
3092
|
+
function buildFeedbackScope(space, episode, participant) {
|
|
3093
|
+
if (!space && !episode && !participant) return {};
|
|
3094
|
+
return {
|
|
3095
|
+
...space ? { space_id: space } : {},
|
|
3096
|
+
...episode ? { episode_id: episode } : {},
|
|
3097
|
+
...participant ? { participant_id: participant } : {}
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
function validateCorrelations(input, issues) {
|
|
3101
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3102
|
+
issues.push({ path: "$.correlations", message: "correlations must be an object" });
|
|
3103
|
+
return void 0;
|
|
3104
|
+
}
|
|
3105
|
+
checkKeys(input, ["journey_id", "root_journey_id", "trace_id", "span_id", "request_id", "command_id", "diagnostic_reference"], issues, "$.correlations", false);
|
|
3106
|
+
const journey = optionalUUID(property(input, "journey_id"), "$.correlations.journey_id", issues);
|
|
3107
|
+
const root = optionalUUID(property(input, "root_journey_id"), "$.correlations.root_journey_id", issues);
|
|
3108
|
+
const trace = optionalHex(property(input, "trace_id"), TRACE_ID_PATTERN, "$.correlations.trace_id", issues);
|
|
3109
|
+
const span = optionalHex(property(input, "span_id"), SPAN_ID_PATTERN, "$.correlations.span_id", issues);
|
|
3110
|
+
const request = optionalUUID(property(input, "request_id"), "$.correlations.request_id", issues);
|
|
3111
|
+
const command = optionalUUID(property(input, "command_id"), "$.correlations.command_id", issues);
|
|
3112
|
+
const diagnostic = validateDiagnosticReference(property(input, "diagnostic_reference"), issues);
|
|
3113
|
+
return buildFeedbackCorrelations(journey, root, trace, span, request, command, diagnostic);
|
|
3114
|
+
}
|
|
3115
|
+
function validateDiagnosticReference(input, issues) {
|
|
3116
|
+
if (input === void 0) return void 0;
|
|
3117
|
+
if (typeof input !== "string" || !isDiagnosticReference(input)) {
|
|
3118
|
+
issues.push({ path: "$.correlations.diagnostic_reference", message: "diagnostic reference is malformed" });
|
|
3119
|
+
return void 0;
|
|
3120
|
+
}
|
|
3121
|
+
return input;
|
|
3122
|
+
}
|
|
3123
|
+
function buildFeedbackCorrelations(journey, root, trace, span, request, command, diagnostic) {
|
|
3124
|
+
return {
|
|
3125
|
+
...journey ? { journey_id: journey } : {},
|
|
3126
|
+
...root ? { root_journey_id: root } : {},
|
|
3127
|
+
...trace ? { trace_id: trace } : {},
|
|
3128
|
+
...span ? { span_id: span } : {},
|
|
3129
|
+
...request ? { request_id: request } : {},
|
|
3130
|
+
...command ? { command_id: command } : {},
|
|
3131
|
+
...diagnostic ? { diagnostic_reference: diagnostic } : {}
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
function validateDiagnostics(input, issues) {
|
|
3135
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3136
|
+
issues.push({ path: "$.diagnostics", message: "diagnostics must be an object" });
|
|
3137
|
+
return void 0;
|
|
3138
|
+
}
|
|
3139
|
+
checkKeys(input, ["availability", "dropped_count", "telemetry_events", "diagnostic_events"], issues, "$.diagnostics", false);
|
|
3140
|
+
const availability = enumValue(property(input, "availability"), ["available", "disabled", "disposed", "unavailable"], "$.diagnostics.availability", issues);
|
|
3141
|
+
const dropped = boundedInteger2(property(input, "dropped_count"), 0, Number.MAX_SAFE_INTEGER, "$.diagnostics.dropped_count", issues);
|
|
3142
|
+
const telemetry = validateTelemetryEvents(property(input, "telemetry_events"), issues);
|
|
3143
|
+
const diagnostic = validateDiagnosticEvents(property(input, "diagnostic_events"), issues);
|
|
3144
|
+
if (!availability || dropped === void 0 || !telemetry || !diagnostic) return void 0;
|
|
3145
|
+
return { availability, dropped_count: dropped, telemetry_events: telemetry, diagnostic_events: diagnostic };
|
|
3146
|
+
}
|
|
3147
|
+
function validateTelemetryEvents(input, issues) {
|
|
3148
|
+
if (!Array.isArray(input)) {
|
|
3149
|
+
issues.push({ path: "$.diagnostics.telemetry_events", message: "telemetry_events must be an array" });
|
|
3150
|
+
return void 0;
|
|
3151
|
+
}
|
|
3152
|
+
if (input.length > FEEDBACK_MAX_TELEMETRY_EVENTS) issues.push({ path: "$.diagnostics.telemetry_events", message: "too many telemetry events" });
|
|
3153
|
+
const events = [];
|
|
3154
|
+
input.slice(0, FEEDBACK_MAX_TELEMETRY_EVENTS).forEach((event, index) => {
|
|
3155
|
+
const parsed = sanitizeTelemetryEvent(event, `$.diagnostics.telemetry_events[${index}]`, issues);
|
|
3156
|
+
if (parsed) events.push(parsed);
|
|
3157
|
+
});
|
|
3158
|
+
return events;
|
|
3159
|
+
}
|
|
3160
|
+
function validateDiagnosticEvents(input, issues) {
|
|
3161
|
+
if (!Array.isArray(input)) {
|
|
3162
|
+
issues.push({ path: "$.diagnostics.diagnostic_events", message: "diagnostic_events must be an array" });
|
|
3163
|
+
return void 0;
|
|
3164
|
+
}
|
|
3165
|
+
if (input.length > FEEDBACK_MAX_DIAGNOSTIC_EVENTS) issues.push({ path: "$.diagnostics.diagnostic_events", message: "too many diagnostic events" });
|
|
3166
|
+
const events = [];
|
|
3167
|
+
input.slice(0, FEEDBACK_MAX_DIAGNOSTIC_EVENTS).forEach((event, index) => {
|
|
3168
|
+
if (typeof event !== "object" || event === null || Array.isArray(event)) {
|
|
3169
|
+
issues.push({ path: `$.diagnostics.diagnostic_events[${index}]`, message: "diagnostic event must be an object" });
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
const attributes = redactDiagnosticAttributes(property(event, "attributes")).attributes;
|
|
3173
|
+
const sanitized = { ...event, ...Object.keys(attributes).length > 0 ? { attributes } : {} };
|
|
3174
|
+
try {
|
|
3175
|
+
events.push(parseDiagnosticEventDraft(sanitized));
|
|
3176
|
+
} catch {
|
|
3177
|
+
issues.push({ path: `$.diagnostics.diagnostic_events[${index}]`, message: "diagnostic event failed the diagnostics contract" });
|
|
3178
|
+
}
|
|
3179
|
+
});
|
|
3180
|
+
return events;
|
|
3181
|
+
}
|
|
3182
|
+
function sanitizeTelemetryEvent(input, path, issues) {
|
|
3183
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3184
|
+
issues.push({ path, message: "telemetry event must be an object" });
|
|
3185
|
+
return void 0;
|
|
3186
|
+
}
|
|
3187
|
+
checkKeys(input, TELEMETRY_KEYS, issues, path, false);
|
|
3188
|
+
const identity = validateTelemetryEventIdentity(input, path, issues);
|
|
3189
|
+
const shape = validateTelemetryEventShape(input, path, issues);
|
|
3190
|
+
if (!identity || !shape) return void 0;
|
|
3191
|
+
const optional = validateTelemetryEventOptionalFields(input, path, issues);
|
|
3192
|
+
return buildSanitizedTelemetryEvent(identity, shape, optional);
|
|
3193
|
+
}
|
|
3194
|
+
function buildSanitizedTelemetryEvent(identity, shape, optional) {
|
|
3195
|
+
const event = {
|
|
3196
|
+
version: 1,
|
|
3197
|
+
event_id: identity.eventId,
|
|
3198
|
+
journey_id: identity.journeyId,
|
|
3199
|
+
sequence: identity.sequence,
|
|
3200
|
+
occurred_at: identity.occurredAt,
|
|
3201
|
+
name: shape.name,
|
|
3202
|
+
phase: shape.phase,
|
|
3203
|
+
state: shape.state,
|
|
3204
|
+
origin_kind: shape.origin,
|
|
3205
|
+
first_observed_layer: shape.firstObserved,
|
|
3206
|
+
upstream_visibility: shape.visibility,
|
|
3207
|
+
...optional.parentEvent ? { parent_event_id: optional.parentEvent } : {},
|
|
3208
|
+
...optional.traceId ? { trace_id: optional.traceId } : {},
|
|
3209
|
+
...optional.spanId ? { span_id: optional.spanId } : {},
|
|
3210
|
+
...optional.traceparent ? { traceparent: optional.traceparent } : {},
|
|
3211
|
+
...optional.tracestate ? { tracestate: optional.tracestate } : {},
|
|
3212
|
+
...optional.attributes ? { attributes: optional.attributes } : {}
|
|
3213
|
+
};
|
|
3214
|
+
return isSanitizedTelemetryEvent(event) ? event : void 0;
|
|
3215
|
+
}
|
|
3216
|
+
function validateTelemetryEventIdentity(input, path, issues) {
|
|
3217
|
+
const version = property(input, "version") === 1;
|
|
3218
|
+
if (!version) issues.push({ path: `${path}.version`, message: "unsupported telemetry version" });
|
|
3219
|
+
const eventId = requiredUUID(property(input, "event_id"), `${path}.event_id`, issues);
|
|
3220
|
+
const journeyId = requiredUUID(property(input, "journey_id"), `${path}.journey_id`, issues);
|
|
3221
|
+
const sequence = boundedInteger2(property(input, "sequence"), 0, Number.MAX_SAFE_INTEGER, `${path}.sequence`, issues);
|
|
3222
|
+
const occurredAt = checkDateTimeValue(property(input, "occurred_at"), `${path}.occurred_at`, issues);
|
|
3223
|
+
if (!version || !eventId || !journeyId || sequence === void 0 || !occurredAt) return void 0;
|
|
3224
|
+
return { eventId, journeyId, sequence, occurredAt };
|
|
3225
|
+
}
|
|
3226
|
+
function validateTelemetryEventShape(input, path, issues) {
|
|
3227
|
+
const name = enumValue(property(input, "name"), TELEMETRY_NAMES, `${path}.name`, issues);
|
|
3228
|
+
const phase = enumValue(property(input, "phase"), TELEMETRY_PHASES, `${path}.phase`, issues);
|
|
3229
|
+
const state = enumValue(property(input, "state"), TELEMETRY_STATES, `${path}.state`, issues);
|
|
3230
|
+
const origin = enumValue(property(input, "origin_kind"), TELEMETRY_ORIGINS, `${path}.origin_kind`, issues);
|
|
3231
|
+
const firstObserved = enumValue(property(input, "first_observed_layer"), TELEMETRY_ORIGINS, `${path}.first_observed_layer`, issues);
|
|
3232
|
+
const visibility = enumValue(property(input, "upstream_visibility"), TELEMETRY_VISIBILITY, `${path}.upstream_visibility`, issues);
|
|
3233
|
+
if (!name || !phase || !state || !origin || !firstObserved || !visibility) return void 0;
|
|
3234
|
+
return { name, phase, state, origin, firstObserved, visibility };
|
|
3235
|
+
}
|
|
3236
|
+
function validateTelemetryEventOptionalFields(input, path, issues) {
|
|
3237
|
+
return {
|
|
3238
|
+
parentEvent: optionalUUID(property(input, "parent_event_id"), `${path}.parent_event_id`, issues),
|
|
3239
|
+
traceId: optionalHex(property(input, "trace_id"), TRACE_ID_PATTERN, `${path}.trace_id`, issues),
|
|
3240
|
+
spanId: optionalHex(property(input, "span_id"), SPAN_ID_PATTERN, `${path}.span_id`, issues),
|
|
3241
|
+
traceparent: optionalTraceparent(property(input, "traceparent"), `${path}.traceparent`, issues),
|
|
3242
|
+
tracestate: optionalSafeText(property(input, "tracestate"), `${path}.tracestate`, 512, issues),
|
|
3243
|
+
attributes: sanitizeTelemetryAttributes(property(input, "attributes"), `${path}.attributes`, issues)
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
function isSanitizedTelemetryEvent(value) {
|
|
3247
|
+
switch (value.name) {
|
|
3248
|
+
case "sync.frame":
|
|
3249
|
+
return value.origin_kind === "sync";
|
|
3250
|
+
case "http.request":
|
|
3251
|
+
return value.origin_kind === "http";
|
|
3252
|
+
case "rtc.summary":
|
|
3253
|
+
return value.origin_kind === "rtc";
|
|
3254
|
+
case "diagnostic.timeline":
|
|
3255
|
+
return value.origin_kind === "diagnostic";
|
|
3256
|
+
case "journey.started":
|
|
3257
|
+
return isJourneyStartedEvent(value);
|
|
3258
|
+
case "journey.phase":
|
|
3259
|
+
return isJourneyPhaseEvent(value);
|
|
3260
|
+
case "journey.terminal":
|
|
3261
|
+
return isJourneyTerminalEvent(value);
|
|
3262
|
+
case "journey.linked":
|
|
3263
|
+
return isJourneyLinkedEvent(value);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
function isJourneyStartedEvent(value) {
|
|
3267
|
+
return value.origin_kind === "client" && value.phase === "root" && value.state === "started";
|
|
3268
|
+
}
|
|
3269
|
+
function isJourneyPhaseEvent(value) {
|
|
3270
|
+
return value.origin_kind === "client" && value.phase !== "root" && value.phase !== "terminal" && (value.state === "in_progress" || value.state === "observed");
|
|
3271
|
+
}
|
|
3272
|
+
function isJourneyTerminalEvent(value) {
|
|
3273
|
+
return value.origin_kind === "client" && value.phase === "terminal" && (value.state === "succeeded" || value.state === "failed" || value.state === "cancelled");
|
|
3274
|
+
}
|
|
3275
|
+
function isJourneyLinkedEvent(value) {
|
|
3276
|
+
return value.origin_kind === "client" && value.phase === "root" && value.state === "observed" && typeof value.parent_event_id === "string";
|
|
3277
|
+
}
|
|
3278
|
+
function sanitizeTelemetryAttributes(input, path, issues) {
|
|
3279
|
+
if (input === void 0) return void 0;
|
|
3280
|
+
if (!isTelemetryAttributeObject(input)) {
|
|
3281
|
+
issues.push({ path, message: "telemetry attributes are not safe" });
|
|
3282
|
+
return void 0;
|
|
3283
|
+
}
|
|
3284
|
+
const redacted = redactDiagnosticAttributes(input).attributes;
|
|
3285
|
+
const normalized = normalizeTelemetryAttributes(redacted);
|
|
3286
|
+
if (!normalized) return void 0;
|
|
3287
|
+
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" });
|
|
3288
|
+
return normalized;
|
|
3289
|
+
}
|
|
3290
|
+
function isTelemetryAttributeObject(value) {
|
|
3291
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3292
|
+
return Object.keys(value).every((key) => {
|
|
3293
|
+
const entry = property(value, key);
|
|
3294
|
+
return typeof entry === "boolean" || typeof entry === "string" || typeof entry === "number" && Number.isFinite(entry);
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
function validateLocalState(input, issues) {
|
|
3298
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3299
|
+
issues.push({ path: "$.local_state", message: "local_state must be an object" });
|
|
3300
|
+
return void 0;
|
|
3301
|
+
}
|
|
3302
|
+
checkKeys(input, ["registry_version", "entries"], issues, "$.local_state", false);
|
|
3303
|
+
if (property(input, "registry_version") !== FEEDBACK_LOCAL_STATE_REGISTRY_VERSION) issues.push({ path: "$.local_state.registry_version", message: "unsupported local state registry" });
|
|
3304
|
+
const entriesInput = property(input, "entries");
|
|
3305
|
+
if (!Array.isArray(entriesInput)) {
|
|
3306
|
+
issues.push({ path: "$.local_state.entries", message: "local state entries must be an array" });
|
|
3307
|
+
return void 0;
|
|
3308
|
+
}
|
|
3309
|
+
if (entriesInput.length > FEEDBACK_MAX_LOCAL_STATE_ENTRIES) issues.push({ path: "$.local_state.entries", message: "too many local state entries" });
|
|
3310
|
+
const entries = [];
|
|
3311
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3312
|
+
entriesInput.slice(0, FEEDBACK_MAX_LOCAL_STATE_ENTRIES).forEach((entry, index) => {
|
|
3313
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
3314
|
+
issues.push({ path: `$.local_state.entries[${index}]`, message: "local state entry must be an object" });
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
checkKeys(entry, ["key", "value"], issues, `$.local_state.entries[${index}]`, false);
|
|
3318
|
+
const key = property(entry, "key");
|
|
3319
|
+
const entryValue = property(entry, "value");
|
|
3320
|
+
if (typeof key !== "string" || key.length === 0 || seen.has(key)) {
|
|
3321
|
+
issues.push({ path: `$.local_state.entries[${index}].key`, message: "local state key is invalid or duplicated" });
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
seen.add(key);
|
|
3325
|
+
if (!validLocalStateEntry(key, entryValue)) issues.push({ path: `$.local_state.entries[${index}]`, message: "local state entry is not allowlisted" });
|
|
3326
|
+
else if (isFeedbackLocalStateValue(entryValue)) entries.push({ key, value: entryValue });
|
|
3327
|
+
});
|
|
3328
|
+
return { registry_version: FEEDBACK_LOCAL_STATE_REGISTRY_VERSION, entries };
|
|
3329
|
+
}
|
|
3330
|
+
function validateCookies(input, issues) {
|
|
3331
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3332
|
+
issues.push({ path: "$.cookies", message: "cookies must be an object" });
|
|
3333
|
+
return void 0;
|
|
3334
|
+
}
|
|
3335
|
+
checkKeys(input, ["registry_version", "entries"], issues, "$.cookies", false);
|
|
3336
|
+
if (property(input, "registry_version") !== FEEDBACK_COOKIE_REGISTRY_VERSION) issues.push({ path: "$.cookies.registry_version", message: "unsupported cookie registry" });
|
|
3337
|
+
const entriesInput = property(input, "entries");
|
|
3338
|
+
if (!Array.isArray(entriesInput)) {
|
|
3339
|
+
issues.push({ path: "$.cookies.entries", message: "cookie entries must be an array" });
|
|
3340
|
+
return void 0;
|
|
3341
|
+
}
|
|
3342
|
+
if (entriesInput.length > FEEDBACK_MAX_COOKIE_ENTRIES) issues.push({ path: "$.cookies.entries", message: "too many cookie entries" });
|
|
3343
|
+
const entries = [];
|
|
3344
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3345
|
+
for (const [index, entry] of entriesInput.slice(0, FEEDBACK_MAX_COOKIE_ENTRIES).entries()) {
|
|
3346
|
+
const parsed = validateCookieEntry(entry, index, seen, issues);
|
|
3347
|
+
if (parsed) entries.push(parsed);
|
|
3348
|
+
}
|
|
3349
|
+
return { registry_version: FEEDBACK_COOKIE_REGISTRY_VERSION, entries };
|
|
3350
|
+
}
|
|
3351
|
+
function validateCookieEntry(input, index, seen, issues) {
|
|
3352
|
+
const path = `$.cookies.entries[${index}]`;
|
|
3353
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3354
|
+
issues.push({ path, message: "cookie entry must be an object" });
|
|
3355
|
+
return void 0;
|
|
3356
|
+
}
|
|
3357
|
+
checkKeys(input, ["name", "present", "value"], issues, path, false);
|
|
3358
|
+
const name = enumValue(property(input, "name"), ["chalk_theme", "chalk_sidebar_state", "account", "csrf"], `${path}.name`, issues);
|
|
3359
|
+
const present = property(input, "present");
|
|
3360
|
+
if (!name || typeof present !== "boolean" || seen.has(name)) {
|
|
3361
|
+
issues.push({ path, message: "cookie entry is invalid or duplicated" });
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
seen.add(name);
|
|
3365
|
+
return buildCookieEntry(name, present, property(input, "value"), path, issues);
|
|
3366
|
+
}
|
|
3367
|
+
function buildCookieEntry(name, present, input, path, issues) {
|
|
3368
|
+
const value = cookieValue(name, input, `${path}.value`, issues);
|
|
3369
|
+
if (!present && value !== void 0) issues.push({ path: `${path}.value`, message: "absent cookies cannot include values" });
|
|
3370
|
+
return { name, present, ...value !== void 0 ? { value } : {} };
|
|
3371
|
+
}
|
|
3372
|
+
function cookieValue(name, input, path, issues) {
|
|
3373
|
+
if (input === void 0) return void 0;
|
|
3374
|
+
if (typeof input !== "string") {
|
|
3375
|
+
issues.push({ path, message: "cookie value is invalid" });
|
|
3376
|
+
return void 0;
|
|
3377
|
+
}
|
|
3378
|
+
if (name === "account" || name === "csrf") {
|
|
3379
|
+
issues.push({ path, message: "account and CSRF cookie values are never collected" });
|
|
3380
|
+
return void 0;
|
|
3381
|
+
}
|
|
3382
|
+
if (name === "chalk_theme") {
|
|
3383
|
+
const value2 = ["light", "dark", "system"];
|
|
3384
|
+
const match2 = value2.find((candidate) => candidate === input);
|
|
3385
|
+
if (match2 !== void 0) return match2;
|
|
3386
|
+
issues.push({ path, message: "theme cookie value is invalid" });
|
|
3387
|
+
return void 0;
|
|
3388
|
+
}
|
|
3389
|
+
const value = ["true", "false"];
|
|
3390
|
+
const match = value.find((candidate) => candidate === input);
|
|
3391
|
+
if (match !== void 0) return match;
|
|
3392
|
+
issues.push({ path, message: "sidebar cookie value is invalid" });
|
|
3393
|
+
return void 0;
|
|
3394
|
+
}
|
|
3395
|
+
function validateScreenshotState(input, issues) {
|
|
3396
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3397
|
+
issues.push({ path: "$.screenshot", message: "screenshot state must be an object" });
|
|
3398
|
+
return void 0;
|
|
3399
|
+
}
|
|
3400
|
+
checkKeys(input, ["state", "captured_at", "failure_code"], issues, "$.screenshot", false);
|
|
3401
|
+
const state = enumValue(property(input, "state"), ["captured", "partial", "removed", "unavailable"], "$.screenshot.state", issues);
|
|
3402
|
+
const failureInput = property(input, "failure_code");
|
|
3403
|
+
const failure3 = failureInput === void 0 ? void 0 : enumValue(failureInput, ["capture_failed", "unsupported", "tainted", "secure_surface", "too_large"], "$.screenshot.failure_code", issues);
|
|
3404
|
+
const capturedAtInput = property(input, "captured_at");
|
|
3405
|
+
const capturedAt = capturedAtInput === void 0 ? void 0 : checkDateTimeValue(capturedAtInput, "$.screenshot.captured_at", issues);
|
|
3406
|
+
validateScreenshotStateConsistency(state, capturedAt, failure3, issues);
|
|
3407
|
+
if (!state) return void 0;
|
|
3408
|
+
return { state, ...capturedAt ? { captured_at: capturedAt } : {}, ...failure3 ? { failure_code: failure3 } : {} };
|
|
3409
|
+
}
|
|
3410
|
+
function validateScreenshotStateConsistency(state, capturedAt, failure3, issues) {
|
|
3411
|
+
const captured = state === "captured" || state === "partial";
|
|
3412
|
+
if (captured && !capturedAt) issues.push({ path: "$.screenshot.captured_at", message: "captured screenshots require a timestamp" });
|
|
3413
|
+
if (captured && failure3) issues.push({ path: "$.screenshot.failure_code", message: "captured screenshots cannot have a failure code" });
|
|
3414
|
+
}
|
|
3415
|
+
function validLocalStateEntry(key, value) {
|
|
3416
|
+
if (key === "chalk.tenant-hint") return isUUID(value);
|
|
3417
|
+
if (key === "chalk.web.telemetry.v1" || key === "chalk.mobile.telemetry.v1") return validTelemetrySummary(value);
|
|
3418
|
+
if (key.startsWith("chalk.dashboard-request.")) return DASHBOARD_ACTION_PATTERN.test(key.slice("chalk.dashboard-request.".length)) && value === true;
|
|
3419
|
+
return false;
|
|
3420
|
+
}
|
|
3421
|
+
function isFeedbackLocalStateValue(value) {
|
|
3422
|
+
if (typeof value === "string" || typeof value === "boolean") return true;
|
|
3423
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
3424
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).every((key) => {
|
|
3425
|
+
const entry = property(value, key);
|
|
3426
|
+
return typeof entry === "number" && Number.isFinite(entry);
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
function validTelemetrySummary(value) {
|
|
3430
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3431
|
+
const keys = ["pending_count", "timeline_count", "dropped_count"];
|
|
3432
|
+
if (!keys.some((key) => property(value, key) !== void 0)) return false;
|
|
3433
|
+
return keys.every((key) => {
|
|
3434
|
+
const entry = property(value, key);
|
|
3435
|
+
return entry === void 0 || typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 0 && entry <= 500;
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
function metadata(value, path, issues, required) {
|
|
3439
|
+
if (value === void 0 && !required) return void 0;
|
|
3440
|
+
if (typeof value !== "string" || !isSafeMetadata(value)) {
|
|
3441
|
+
issues.push({ path, message: required ? "metadata is required and unsafe" : "metadata is unsafe" });
|
|
3442
|
+
return void 0;
|
|
3443
|
+
}
|
|
3444
|
+
return value;
|
|
3445
|
+
}
|
|
3446
|
+
function isSafeMetadata(value) {
|
|
3447
|
+
return value.length > 0 && value.length <= 128 && value.trim() === value && !isControlText(value) && !UNSAFE_VALUE_PATTERN.test(value);
|
|
3448
|
+
}
|
|
3449
|
+
function safeToken(value, path, issues, required) {
|
|
3450
|
+
if (value === void 0 && !required) return void 0;
|
|
3451
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128 || !SAFE_TOKEN_PATTERN.test(value) || UNSAFE_VALUE_PATTERN.test(value)) {
|
|
3452
|
+
issues.push({ path, message: required ? "token is required and unsafe" : "token is unsafe" });
|
|
3453
|
+
return void 0;
|
|
3454
|
+
}
|
|
3455
|
+
return value;
|
|
3456
|
+
}
|
|
3457
|
+
function optionalUUID(value, path, issues) {
|
|
3458
|
+
if (value === void 0) return void 0;
|
|
3459
|
+
return requiredUUID(value, path, issues);
|
|
3460
|
+
}
|
|
3461
|
+
function requiredUUID(value, path, issues) {
|
|
3462
|
+
if (typeof value !== "string" || !isUUID(value)) {
|
|
3463
|
+
issues.push({ path, message: "expected a non-zero UUID" });
|
|
3464
|
+
return void 0;
|
|
3465
|
+
}
|
|
3466
|
+
return value;
|
|
3467
|
+
}
|
|
3468
|
+
function optionalHex(value, pattern, path, issues) {
|
|
3469
|
+
if (value === void 0) return void 0;
|
|
3470
|
+
if (typeof value !== "string" || !pattern.test(value.toLowerCase()) || value !== value.toLowerCase()) {
|
|
3471
|
+
issues.push({ path, message: "expected a lower-case non-zero hexadecimal ID" });
|
|
3472
|
+
return void 0;
|
|
3473
|
+
}
|
|
3474
|
+
return value;
|
|
3475
|
+
}
|
|
3476
|
+
function optionalTraceparent(value, path, issues) {
|
|
3477
|
+
if (value === void 0) return void 0;
|
|
3478
|
+
if (typeof value !== "string" || !TRACEPARENT_PATTERN.test(value)) {
|
|
3479
|
+
issues.push({ path, message: "traceparent is malformed" });
|
|
3480
|
+
return void 0;
|
|
3481
|
+
}
|
|
3482
|
+
return value;
|
|
3483
|
+
}
|
|
3484
|
+
function optionalSafeText(value, path, length, issues) {
|
|
3485
|
+
if (value === void 0) return void 0;
|
|
3486
|
+
if (typeof value !== "string" || value.length > length || isControlText(value) || UNSAFE_VALUE_PATTERN.test(value)) {
|
|
3487
|
+
issues.push({ path, message: "text is unsafe" });
|
|
3488
|
+
return void 0;
|
|
3489
|
+
}
|
|
3490
|
+
return value;
|
|
3491
|
+
}
|
|
3492
|
+
function checkDateTime(value, path, issues) {
|
|
3493
|
+
checkDateTimeValue(value, path, issues);
|
|
3494
|
+
}
|
|
3495
|
+
function checkDateTimeValue(value, path, issues) {
|
|
3496
|
+
if (typeof value !== "string" || value.length > 64 || !Number.isFinite(Date.parse(value))) {
|
|
3497
|
+
issues.push({ path, message: "expected an ISO date-time" });
|
|
3498
|
+
return void 0;
|
|
3499
|
+
}
|
|
3500
|
+
return value;
|
|
3501
|
+
}
|
|
3502
|
+
function boundedInteger2(value, minimum, maximum, path, issues) {
|
|
3503
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
3504
|
+
issues.push({ path, message: "expected a bounded integer" });
|
|
3505
|
+
return void 0;
|
|
3506
|
+
}
|
|
3507
|
+
return value;
|
|
3508
|
+
}
|
|
3509
|
+
function enumValue(value, values, path, issues) {
|
|
3510
|
+
const match = values.find((candidate) => candidate === value);
|
|
3511
|
+
if (match !== void 0) return match;
|
|
3512
|
+
issues.push({ path, message: `expected one of: ${values.join(", ")}` });
|
|
3513
|
+
return void 0;
|
|
3514
|
+
}
|
|
3515
|
+
function checkKeys(value, allowed, issues, path, allowNullable) {
|
|
3516
|
+
const allowedSet = new Set(allowed);
|
|
3517
|
+
for (const key of Object.keys(value)) if (!allowedSet.has(key) && !(allowNullable && key === "value")) issues.push({ path: `${path}.${key}`, message: "unknown property" });
|
|
3518
|
+
}
|
|
3519
|
+
function failure2(path, message) {
|
|
3520
|
+
return { ok: false, issues: [{ path, message }] };
|
|
3521
|
+
}
|
|
3522
|
+
function property(value, key) {
|
|
3523
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
3524
|
+
return Object.prototype.hasOwnProperty.call(value, key) ? Reflect.get(value, key) : void 0;
|
|
3525
|
+
}
|
|
3526
|
+
function isUUID(value) {
|
|
3527
|
+
return typeof value === "string" && UUID_PATTERN.test(value) && value.replaceAll("0", "").replaceAll("-", "") !== "";
|
|
3528
|
+
}
|
|
3529
|
+
function validBase64(value) {
|
|
3530
|
+
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);
|
|
3531
|
+
}
|
|
3532
|
+
function base64ByteLength(value) {
|
|
3533
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
3534
|
+
return Math.floor(value.length * 3 / 4) - padding;
|
|
3535
|
+
}
|
|
3536
|
+
function encodedByteLength(value) {
|
|
3537
|
+
return encoder3.encode(JSON.stringify(value)).byteLength;
|
|
3538
|
+
}
|
|
3539
|
+
function isControl(character) {
|
|
3540
|
+
return /\p{Cc}/u.test(character);
|
|
3541
|
+
}
|
|
3542
|
+
function isControlText(value) {
|
|
3543
|
+
for (const character of value) if (isControl(character)) return true;
|
|
3544
|
+
return false;
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
// src/feedback/collector.ts
|
|
3548
|
+
import { validateDiagnosticEventDraft } from "@q9labsai/diagnostics-contracts";
|
|
3549
|
+
|
|
3550
|
+
// src/feedback/context.ts
|
|
3551
|
+
function safeSubject(snapshot) {
|
|
3552
|
+
const subject = snapshot.subject;
|
|
3553
|
+
if (!subject) return void 0;
|
|
3554
|
+
return {
|
|
3555
|
+
tenant_id: subject.tenantId,
|
|
3556
|
+
space_id: subject.spaceId,
|
|
3557
|
+
episode_id: subject.episodeId,
|
|
3558
|
+
participant_id: subject.participantId,
|
|
3559
|
+
participant_generation: subject.participantGeneration
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// src/feedback/collector.ts
|
|
3564
|
+
function collectFeedbackEvidence(input, context = {}) {
|
|
3565
|
+
const collectedAt = new Date((context.now ?? Date.now)()).toISOString();
|
|
3566
|
+
const evidence = {
|
|
3567
|
+
schema_version: FEEDBACK_EVIDENCE_SCHEMA_VERSION,
|
|
3568
|
+
collected_at: collectedAt,
|
|
3569
|
+
...input.app ? { app: input.app } : {},
|
|
3570
|
+
sdk: input.sdk,
|
|
3571
|
+
platform: input.platform,
|
|
3572
|
+
...context.connection ? { connection: context.connection } : {},
|
|
3573
|
+
...context.scope ? { scope: context.scope } : {},
|
|
3574
|
+
correlations: context.correlations ?? {},
|
|
3575
|
+
diagnostics: context.diagnostics ?? unavailableDiagnostics(),
|
|
3576
|
+
local_state: { registry_version: FEEDBACK_LOCAL_STATE_REGISTRY_VERSION, entries: collectLocalState(input.local_state) },
|
|
3577
|
+
cookies: { registry_version: FEEDBACK_COOKIE_REGISTRY_VERSION, entries: collectCookies(input.cookies) },
|
|
3578
|
+
screenshot: screenshotState(input.screenshot)
|
|
3579
|
+
};
|
|
3580
|
+
return parseFeedbackEvidence(evidence);
|
|
3581
|
+
}
|
|
3582
|
+
function collectFeedbackEvidenceFromContext(input, context) {
|
|
3583
|
+
const snapshot = context.connection();
|
|
3584
|
+
const subject = safeSubject(snapshot);
|
|
3585
|
+
const activeTelemetry = context.telemetry;
|
|
3586
|
+
const activeDiagnostics = context.diagnosticContext();
|
|
3587
|
+
const correlationContext = activeTelemetry ?? activeDiagnostics;
|
|
3588
|
+
const trace = parseTraceparent(correlationContext?.traceparent);
|
|
3589
|
+
const diagnostics = context.diagnosticSnapshot();
|
|
3590
|
+
const availability = context.diagnosticAvailability();
|
|
3591
|
+
return collectFeedbackEvidence(input, {
|
|
3592
|
+
now: context.now,
|
|
3593
|
+
connection: {
|
|
3594
|
+
state: snapshot.state,
|
|
3595
|
+
...snapshot.failure?.code ? { error_code: snapshot.failure.code } : {}
|
|
3596
|
+
},
|
|
3597
|
+
...subject ? {
|
|
3598
|
+
scope: {
|
|
3599
|
+
space_id: subject.space_id,
|
|
3600
|
+
episode_id: subject.episode_id,
|
|
3601
|
+
participant_id: subject.participant_id
|
|
3602
|
+
}
|
|
3603
|
+
} : {},
|
|
3604
|
+
correlations: {
|
|
3605
|
+
...correlationContext ? { journey_id: correlationContext.journeyId } : {},
|
|
3606
|
+
...activeTelemetry ? { root_journey_id: activeTelemetry.rootJourneyId } : {},
|
|
3607
|
+
...trace ? { trace_id: trace.traceId, span_id: trace.spanId } : {}
|
|
3608
|
+
},
|
|
3609
|
+
diagnostics: {
|
|
3610
|
+
availability,
|
|
3611
|
+
dropped_count: diagnostics.dropped,
|
|
3612
|
+
telemetry_events: input.local_state?.telemetry?.events ?? [],
|
|
3613
|
+
diagnostic_events: diagnostics.events.filter(isDiagnosticEvent)
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
function collectLocalState(input) {
|
|
3618
|
+
if (!input) return [];
|
|
3619
|
+
const entries = [];
|
|
3620
|
+
if (input.telemetry) {
|
|
3621
|
+
const summary = telemetrySummary(input.telemetry);
|
|
3622
|
+
if (summary) entries.push({ key: input.telemetry.storage_key ?? "chalk.web.telemetry.v1", value: summary });
|
|
3623
|
+
}
|
|
3624
|
+
if (input.tenant_hint && validUUID(input.tenant_hint)) entries.push({ key: "chalk.tenant-hint", value: input.tenant_hint });
|
|
3625
|
+
for (const request of input.dashboard_requests ?? []) {
|
|
3626
|
+
if (!request.pending || !/^[a-z][a-z0-9_-]{0,63}$/u.test(request.action)) continue;
|
|
3627
|
+
entries.push({ key: `chalk.dashboard-request.${request.action}`, value: true });
|
|
3628
|
+
}
|
|
3629
|
+
return entries.slice(0, 32);
|
|
3630
|
+
}
|
|
3631
|
+
function collectCookies(input) {
|
|
3632
|
+
if (!input) return [];
|
|
3633
|
+
const entries = [];
|
|
3634
|
+
if (input.theme !== void 0) entries.push({ name: "chalk_theme", present: true, value: input.theme });
|
|
3635
|
+
if (input.sidebar_state !== void 0) entries.push({ name: "chalk_sidebar_state", present: true, value: input.sidebar_state ? "true" : "false" });
|
|
3636
|
+
if (input.account_present !== void 0) entries.push({ name: "account", present: input.account_present });
|
|
3637
|
+
if (input.csrf_present !== void 0) entries.push({ name: "csrf", present: input.csrf_present });
|
|
3638
|
+
return entries;
|
|
3639
|
+
}
|
|
3640
|
+
function telemetrySummary(input) {
|
|
3641
|
+
const summary = {
|
|
3642
|
+
...input.pending_count !== void 0 && boundedCount(input.pending_count) ? { pending_count: input.pending_count } : {},
|
|
3643
|
+
...input.timeline_count !== void 0 && boundedCount(input.timeline_count) ? { timeline_count: input.timeline_count } : {},
|
|
3644
|
+
...input.dropped_count !== void 0 && boundedCount(input.dropped_count) ? { dropped_count: input.dropped_count } : {}
|
|
3645
|
+
};
|
|
3646
|
+
return Object.keys(summary).length > 0 ? summary : void 0;
|
|
3647
|
+
}
|
|
3648
|
+
function boundedCount(value) {
|
|
3649
|
+
return Number.isSafeInteger(value) && value >= 0 && value <= 500;
|
|
3650
|
+
}
|
|
3651
|
+
function screenshotState(input) {
|
|
3652
|
+
if (!input) return { state: "unavailable", failure_code: "unsupported" };
|
|
3653
|
+
if (input.state === "captured" || input.state === "partial") return { state: input.state, captured_at: input.captured_at };
|
|
3654
|
+
return "failure_code" in input && input.failure_code ? { state: input.state, failure_code: input.failure_code } : { state: input.state };
|
|
3655
|
+
}
|
|
3656
|
+
function unavailableDiagnostics() {
|
|
3657
|
+
return { availability: "unavailable", dropped_count: 0, telemetry_events: [], diagnostic_events: [] };
|
|
3658
|
+
}
|
|
3659
|
+
function isDiagnosticEvent(value) {
|
|
3660
|
+
return validateDiagnosticEventDraft(value).ok;
|
|
3661
|
+
}
|
|
3662
|
+
function validUUID(value) {
|
|
3663
|
+
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("-", "") !== "";
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// src/feedback/transport.ts
|
|
3667
|
+
var FeedbackTransportError = class extends Error {
|
|
3668
|
+
code;
|
|
3669
|
+
status;
|
|
3670
|
+
recoverable;
|
|
3671
|
+
constructor(input) {
|
|
3672
|
+
super(input.message);
|
|
3673
|
+
this.name = "FeedbackTransportError";
|
|
3674
|
+
this.code = input.code;
|
|
3675
|
+
this.recoverable = input.recoverable;
|
|
3676
|
+
this.status = input.status;
|
|
3677
|
+
}
|
|
3678
|
+
};
|
|
3679
|
+
function createFeedbackTransport(options) {
|
|
3680
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
3681
|
+
const baseUrl = options.baseUrl.replace(/\/+$/u, "");
|
|
3682
|
+
return {
|
|
3683
|
+
send: async ({ request, idempotencyKey }) => {
|
|
3684
|
+
const parsedRequest = parseFeedbackReportRequest(request);
|
|
3685
|
+
validateTransportBounds(parsedRequest);
|
|
3686
|
+
validateIdempotencyKey(idempotencyKey);
|
|
3687
|
+
const credential = options.credential();
|
|
3688
|
+
if (!credential) throw new FeedbackTransportError({ code: "unauthenticated", recoverable: false, message: "Feedback authentication is unavailable.", status: 401 });
|
|
3689
|
+
const headers = feedbackHeaders(idempotencyKey, credential.token, options.telemetry?.());
|
|
3690
|
+
const response = await sendFeedbackRequest(fetchImplementation, `${baseUrl}/v1/feedback-reports`, headers, parsedRequest);
|
|
3691
|
+
return parseFeedbackResponse(response);
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
function validateIdempotencyKey(idempotencyKey) {
|
|
3696
|
+
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 });
|
|
3697
|
+
}
|
|
3698
|
+
function feedbackHeaders(idempotencyKey, credentialToken, telemetry) {
|
|
3699
|
+
const headers = new Headers({ "content-type": "application/json", authorization: `Bearer ${credentialToken}`, "idempotency-key": idempotencyKey });
|
|
3700
|
+
if (!telemetry) return headers;
|
|
3701
|
+
for (const [key, value] of Object.entries(journeyHeaders(telemetry))) headers.set(key, value);
|
|
3702
|
+
return headers;
|
|
3703
|
+
}
|
|
3704
|
+
async function sendFeedbackRequest(fetchImplementation, url, headers, request) {
|
|
3705
|
+
try {
|
|
3706
|
+
return await fetchImplementation(url, { method: "POST", headers, body: JSON.stringify(request) });
|
|
3707
|
+
} catch {
|
|
3708
|
+
throw new FeedbackTransportError({ code: "unavailable", recoverable: true, message: "Feedback service is unavailable." });
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
async function parseFeedbackResponse(response) {
|
|
3712
|
+
if (!response.ok) throw responseError3(response.status);
|
|
3713
|
+
let body;
|
|
3714
|
+
try {
|
|
3715
|
+
body = await response.json();
|
|
3716
|
+
} catch {
|
|
3717
|
+
throw new FeedbackTransportError({ code: "invalid_response", recoverable: true, message: "Feedback service returned an invalid receipt.", status: response.status });
|
|
3718
|
+
}
|
|
3719
|
+
try {
|
|
3720
|
+
return assertFeedbackReceipt(body);
|
|
3721
|
+
} catch {
|
|
3722
|
+
throw new FeedbackTransportError({ code: "invalid_response", recoverable: true, message: "Feedback service returned an invalid receipt.", status: response.status });
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
function validateTransportBounds(request) {
|
|
3726
|
+
const evidenceBytes = new TextEncoder().encode(JSON.stringify(request.evidence)).byteLength;
|
|
3727
|
+
const requestBytes = new TextEncoder().encode(JSON.stringify(request)).byteLength;
|
|
3728
|
+
if (evidenceBytes > FEEDBACK_MAX_EVIDENCE_BYTES) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback evidence exceeds the size limit." });
|
|
3729
|
+
if (requestBytes > FEEDBACK_MAX_REQUEST_BYTES) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request exceeds the size limit." });
|
|
3730
|
+
}
|
|
3731
|
+
function responseError3(status) {
|
|
3732
|
+
if (status === 400) return new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request was rejected.", status });
|
|
3733
|
+
if (status === 413) return new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request exceeds the size limit.", status });
|
|
3734
|
+
if (status === 401) return new FeedbackTransportError({ code: "unauthenticated", recoverable: false, message: "Feedback authentication was rejected.", status });
|
|
3735
|
+
if (status === 403) return new FeedbackTransportError({ code: "forbidden", recoverable: false, message: "Feedback submission is not allowed.", status });
|
|
3736
|
+
if (status === 409) return new FeedbackTransportError({ code: "conflict", recoverable: false, message: "Feedback idempotency key conflicts with another submission.", status });
|
|
3737
|
+
if (status === 429) return new FeedbackTransportError({ code: "rate_limited", recoverable: true, message: "Feedback submissions are temporarily limited.", status });
|
|
3738
|
+
return new FeedbackTransportError({ code: "unavailable", recoverable: true, message: "Feedback service is unavailable.", status });
|
|
3739
|
+
}
|
|
3740
|
+
|
|
2731
3741
|
// src/space-client/core.ts
|
|
2732
|
-
import { Clock as Clock5, Context as
|
|
3742
|
+
import { Clock as Clock5, Context as Context13, Duration as Duration2, Effect as Effect12, Layer as Layer13 } from "effect";
|
|
2733
3743
|
|
|
2734
3744
|
// src/space-client/controller-effects.ts
|
|
2735
|
-
import { Context as
|
|
3745
|
+
import { Context as Context12, Effect as Effect11, Layer as Layer12 } from "effect";
|
|
2736
3746
|
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
2737
3747
|
|
|
2738
3748
|
// src/space-client/chat-controller.ts
|
|
@@ -2744,14 +3754,14 @@ var MAX_CHAT_PAGE_SIZE = 100;
|
|
|
2744
3754
|
var MAX_LOADED_CHAT_MESSAGES = 500;
|
|
2745
3755
|
var MAX_CHAT_TEXT_BYTES = 16384;
|
|
2746
3756
|
var MAX_CHAT_TEXT_SCALARS = 4e3;
|
|
2747
|
-
var
|
|
3757
|
+
var encoder4 = new TextEncoder();
|
|
2748
3758
|
var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
|
|
2749
3759
|
function validateChatMessage(input) {
|
|
2750
3760
|
const attachments = input.attachments ?? [];
|
|
2751
3761
|
if (input.text.length === 0 && attachments.length === 0) return "A chat message requires text or an attachment";
|
|
2752
|
-
if (Array.from(input.text).length > MAX_CHAT_TEXT_SCALARS ||
|
|
3762
|
+
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`;
|
|
2753
3763
|
if (attachments.length > CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `A chat message supports at most ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} attachments`;
|
|
2754
|
-
if (new Set(attachments.map((
|
|
3764
|
+
if (new Set(attachments.map((attachment2) => attachment2.attachmentId)).size !== attachments.length) return "Chat attachment IDs must be unique";
|
|
2755
3765
|
return attachments.map(validateChatAttachment).find((value) => value !== null) ?? null;
|
|
2756
3766
|
}
|
|
2757
3767
|
function validateChatUpload(file, bytes, clientAttachmentId) {
|
|
@@ -2782,16 +3792,16 @@ function compareChatSequence(left, right) {
|
|
|
2782
3792
|
const normalizedRight = right.replace(/^0+/u, "") || "0";
|
|
2783
3793
|
return normalizedLeft.length === normalizedRight.length ? normalizedLeft < normalizedRight ? -1 : 1 : normalizedLeft.length - normalizedRight.length;
|
|
2784
3794
|
}
|
|
2785
|
-
function validateChatAttachment(
|
|
2786
|
-
const fileNameBytes =
|
|
2787
|
-
if (!
|
|
2788
|
-
if (!Number.isSafeInteger(
|
|
2789
|
-
return allowedMimeTypes.has(
|
|
3795
|
+
function validateChatAttachment(attachment2) {
|
|
3796
|
+
const fileNameBytes = encoder4.encode(attachment2.fileName).byteLength;
|
|
3797
|
+
if (!attachment2.attachmentId || fileNameBytes < 1 || fileNameBytes > CHALK_CHAT_ATTACHMENT_LIMITS.maximumFileNameBytes) return "Chat attachment metadata is invalid";
|
|
3798
|
+
if (!Number.isSafeInteger(attachment2.byteLength) || attachment2.byteLength < 1 || attachment2.byteLength > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Chat attachment metadata is invalid";
|
|
3799
|
+
return allowedMimeTypes.has(attachment2.mimeType) ? null : "Chat attachment MIME type is not allowed";
|
|
2790
3800
|
}
|
|
2791
3801
|
function sameChatMessage(left, right) {
|
|
2792
|
-
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((
|
|
3802
|
+
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) => {
|
|
2793
3803
|
const candidate = right.attachments[index];
|
|
2794
|
-
return candidate?.attachmentId ===
|
|
3804
|
+
return candidate?.attachmentId === attachment2.attachmentId && candidate.fileName === attachment2.fileName && candidate.mimeType === attachment2.mimeType && candidate.byteLength === attachment2.byteLength;
|
|
2795
3805
|
});
|
|
2796
3806
|
}
|
|
2797
3807
|
|
|
@@ -2807,7 +3817,7 @@ var makeChatController = (input) => Effect4.gen(function* () {
|
|
|
2807
3817
|
const controller = yield* Effect4.sync(() => {
|
|
2808
3818
|
let instance = null;
|
|
2809
3819
|
const transport = input.createTransport?.({ token: () => Effect4.runPromiseWith(context)(input.connection.getSyncToken()) }) ?? null;
|
|
2810
|
-
instance = new ChatControllerRuntime(input.connection, input.store, transport, input.apiBaseUrl ?? "https://api.chalkmeet.com", input.fetch ?? globalThis.fetch, fork, input.episodeDiagnostics);
|
|
3820
|
+
instance = new ChatControllerRuntime(input.connection, input.store, transport, input.apiBaseUrl ?? "https://api.chalkmeet.com", input.fetch ?? globalThis.fetch, fork, input.episodeDiagnostics, input.offline);
|
|
2811
3821
|
return instance;
|
|
2812
3822
|
});
|
|
2813
3823
|
yield* Effect4.addFinalizer(() => Effect4.sync(() => controller.dispose()));
|
|
@@ -2821,6 +3831,7 @@ var ChatControllerRuntime = class {
|
|
|
2821
3831
|
#fetch;
|
|
2822
3832
|
#fork;
|
|
2823
3833
|
#diagnostics;
|
|
3834
|
+
#offline;
|
|
2824
3835
|
#catchUpRunning = false;
|
|
2825
3836
|
#catchUpRequested = false;
|
|
2826
3837
|
#hasOlder = false;
|
|
@@ -2838,7 +3849,8 @@ var ChatControllerRuntime = class {
|
|
|
2838
3849
|
#unsubscribeConnection = null;
|
|
2839
3850
|
#unsubscribeEvents = null;
|
|
2840
3851
|
#unsubscribeSnapshot = null;
|
|
2841
|
-
constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics) {
|
|
3852
|
+
constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics, offline) {
|
|
3853
|
+
this.#offline = offline;
|
|
2842
3854
|
this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
|
|
2843
3855
|
this.#connection = connection;
|
|
2844
3856
|
this.#store = store;
|
|
@@ -2854,9 +3866,12 @@ var ChatControllerRuntime = class {
|
|
|
2854
3866
|
Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "validation"))),
|
|
2855
3867
|
Effect4.map((attachments) => ({ clientMessageId: this.#connection.createId(), attachments })),
|
|
2856
3868
|
Effect4.flatMap(({ clientMessageId, attachments }) => {
|
|
2857
|
-
this.#
|
|
3869
|
+
const command = this.#connection.runCommand(({ sync }) => foreign(() => sync.sendChatMessage({ text: input.text, attachments, clientMessageId })));
|
|
3870
|
+
const sendNow = () => Effect4.sync(() => this.#upsertPending(clientMessageId, input.text, attachments, "sending", null)).pipe(Effect4.andThen(command));
|
|
3871
|
+
const held = this.#offline?.hold({ kind: "chat_message", text: input.text, clientMessageId, attachments }, sendNow);
|
|
3872
|
+
if (held) this.#upsertPending(clientMessageId, input.text, attachments, "queued", null);
|
|
2858
3873
|
operation?.observe("observed", "authorization");
|
|
2859
|
-
return
|
|
3874
|
+
return (held ?? sendNow()).pipe(Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "durable_commit")))).pipe(
|
|
2860
3875
|
Effect4.map(chatMessageFor),
|
|
2861
3876
|
Effect4.tap(() => Effect4.sync(() => this.#removePending(clientMessageId))),
|
|
2862
3877
|
Effect4.map((message) => this.#observeMessage(message, false)),
|
|
@@ -2870,7 +3885,8 @@ var ChatControllerRuntime = class {
|
|
|
2870
3885
|
Effect4.catch((cause) => {
|
|
2871
3886
|
const error2 = normalizeClientError(cause, "chat.payload_invalid");
|
|
2872
3887
|
return Effect4.sync(() => {
|
|
2873
|
-
|
|
3888
|
+
if (error2.code === "offline.discarded") this.#removePending(clientMessageId);
|
|
3889
|
+
else this.#upsertPending(clientMessageId, input.text, attachments, "failed", failureFromError(error2));
|
|
2874
3890
|
operation?.fail("send_failed");
|
|
2875
3891
|
}).pipe(Effect4.andThen(Effect4.fail(error2)));
|
|
2876
3892
|
})
|
|
@@ -2930,6 +3946,7 @@ var ChatControllerRuntime = class {
|
|
|
2930
3946
|
});
|
|
2931
3947
|
};
|
|
2932
3948
|
upload = (file) => {
|
|
3949
|
+
const fetch = this.#fetch;
|
|
2933
3950
|
const prepareOperation = this.#diagnostics?.startOperation("chat.attachment.prepare");
|
|
2934
3951
|
return Effect4.suspend(() => {
|
|
2935
3952
|
if (!this.#transport) {
|
|
@@ -2950,7 +3967,7 @@ var ChatControllerRuntime = class {
|
|
|
2950
3967
|
prepareOperation?.succeed();
|
|
2951
3968
|
}).pipe(
|
|
2952
3969
|
Effect4.andThen(
|
|
2953
|
-
foreign(() =>
|
|
3970
|
+
foreign(() => fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes })).pipe(
|
|
2954
3971
|
Effect4.flatMap((response) => {
|
|
2955
3972
|
const commitOperation = this.#diagnostics?.startOperation("chat.attachment.commit");
|
|
2956
3973
|
if (!response.ok) {
|
|
@@ -2988,7 +4005,11 @@ var ChatControllerRuntime = class {
|
|
|
2988
4005
|
);
|
|
2989
4006
|
});
|
|
2990
4007
|
};
|
|
2991
|
-
url = (
|
|
4008
|
+
url = (attachment2) => `${this.#apiBaseUrl}/v1/chat/attachments/${encodeURIComponent(attachment2.attachmentId)}/download`;
|
|
4009
|
+
resolveUrl = (attachment2) => Effect4.suspend(() => {
|
|
4010
|
+
if (!this.#transport) return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file download is unavailable" }));
|
|
4011
|
+
return this.#connection.runPortCommand(() => foreign(() => this.#transport.getDownloadUrl(attachment2.attachmentId)).pipe(Effect4.map((descriptor) => descriptor.downloadUrl))).pipe(Effect4.mapError(normalizeClientError));
|
|
4012
|
+
});
|
|
2992
4013
|
dispose() {
|
|
2993
4014
|
this.#unsubscribeConnection?.();
|
|
2994
4015
|
this.#unsubscribeConnection = null;
|
|
@@ -3190,7 +4211,7 @@ function validateUpload(file, bytes, clientAttachmentId) {
|
|
|
3190
4211
|
return upload;
|
|
3191
4212
|
}
|
|
3192
4213
|
function bytesFor(file) {
|
|
3193
|
-
return "
|
|
4214
|
+
return "fileName" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
|
|
3194
4215
|
}
|
|
3195
4216
|
function catchUpRequest(latestSequence, ports) {
|
|
3196
4217
|
const extension = ports.sync.getCollaborationExtensionState();
|
|
@@ -3242,6 +4263,10 @@ var MediaControllerRuntime = class {
|
|
|
3242
4263
|
#fork;
|
|
3243
4264
|
#diagnostics;
|
|
3244
4265
|
#tracks = /* @__PURE__ */ new Map();
|
|
4266
|
+
#observedPublicationIds = /* @__PURE__ */ new Map();
|
|
4267
|
+
#intentRevisions = /* @__PURE__ */ new Map();
|
|
4268
|
+
#pendingEnables = /* @__PURE__ */ new Map();
|
|
4269
|
+
#pendingSources = /* @__PURE__ */ new Set();
|
|
3245
4270
|
#requestGenerations = /* @__PURE__ */ new Map();
|
|
3246
4271
|
#intent = { microphone: true, camera: true };
|
|
3247
4272
|
#requests = Object.freeze([]);
|
|
@@ -3368,161 +4393,268 @@ var MediaControllerRuntime = class {
|
|
|
3368
4393
|
#set(source, enabled) {
|
|
3369
4394
|
const operation = this.#diagnostics?.startOperation(`${source}.${enabled ? "publish" : "unpublish"}`);
|
|
3370
4395
|
operation?.observe("observed", "intent");
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
4396
|
+
let previousIntent = this.#intent[source];
|
|
4397
|
+
let intentRevision = 0;
|
|
4398
|
+
let earlyPause = null;
|
|
4399
|
+
const localPause = Effect5.suspend(() => {
|
|
4400
|
+
previousIntent = this.#intent[source];
|
|
4401
|
+
intentRevision = (this.#intentRevisions.get(source) ?? 0) + 1;
|
|
4402
|
+
this.#intentRevisions.set(source, intentRevision);
|
|
4403
|
+
this.#intent[source] = enabled;
|
|
4404
|
+
if (enabled) this.#pendingEnables.set(source, intentRevision);
|
|
4405
|
+
this.#ports?.media.setLocalSourceIntent?.(source, enabled);
|
|
4406
|
+
if (!enabled && this.#ports) earlyPause = this.#beginLocalPause(this.#ports, source);
|
|
4407
|
+
return Effect5.void;
|
|
4408
|
+
});
|
|
4409
|
+
return localPause.pipe(
|
|
4410
|
+
Effect5.andThen(
|
|
4411
|
+
this.#serialize(
|
|
4412
|
+
source,
|
|
4413
|
+
this.#connection.runCommand((ports) => {
|
|
4414
|
+
let captured = null;
|
|
4415
|
+
let prepared = false;
|
|
4416
|
+
const action = source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled";
|
|
4417
|
+
return Effect5.suspend(() => {
|
|
4418
|
+
if (!enabled) {
|
|
4419
|
+
let paused = false;
|
|
4420
|
+
return (earlyPause ? foreign2(() => earlyPause) : this.#localTarget(ports, source, false)).pipe(
|
|
4421
|
+
Effect5.tap(
|
|
4422
|
+
() => Effect5.sync(() => {
|
|
4423
|
+
paused = true;
|
|
4424
|
+
})
|
|
4425
|
+
),
|
|
4426
|
+
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
|
|
4427
|
+
Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(false)) : foreign2(() => ports.sync.setCameraEnabled(false))),
|
|
4428
|
+
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
|
|
4429
|
+
Effect5.tap(() => Effect5.sync(() => operation?.succeed())),
|
|
4430
|
+
Effect5.asVoid,
|
|
4431
|
+
Effect5.catch((cause) => {
|
|
4432
|
+
if (paused && !isSyncRejection(cause) || this.#intentRevisions.get(source) !== intentRevision) return Effect5.fail(cause);
|
|
4433
|
+
this.#intent[source] = previousIntent;
|
|
4434
|
+
ports.media.setLocalSourceIntent?.(source, previousIntent);
|
|
4435
|
+
return this.#localTarget(ports, source, previousIntent).pipe(
|
|
4436
|
+
Effect5.catch(() => Effect5.void),
|
|
4437
|
+
Effect5.andThen(Effect5.fail(cause))
|
|
4438
|
+
);
|
|
4439
|
+
})
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
const acquire = enabled && !this.#tracks.has(source) ? this.#captureSource(source).pipe(
|
|
4443
|
+
Effect5.tap(
|
|
4444
|
+
(track) => Effect5.sync(() => {
|
|
4445
|
+
captured = track;
|
|
4446
|
+
try {
|
|
4447
|
+
this.#assertActivePorts(ports, action);
|
|
4448
|
+
} catch (cause) {
|
|
4449
|
+
track.stop();
|
|
4450
|
+
throw cause;
|
|
4451
|
+
}
|
|
4452
|
+
this.#tracks.set(source, track);
|
|
4453
|
+
ports.media.prepareLocalTrack(source, track);
|
|
4454
|
+
prepared = true;
|
|
4455
|
+
operation?.observe("observed", "local_track_state");
|
|
4456
|
+
this.#publish();
|
|
4457
|
+
})
|
|
4458
|
+
)
|
|
4459
|
+
) : Effect5.void;
|
|
4460
|
+
return acquire.pipe(
|
|
4461
|
+
Effect5.asVoid,
|
|
4462
|
+
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
|
|
4463
|
+
Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(enabled)) : foreign2(() => ports.sync.setCameraEnabled(enabled))),
|
|
4464
|
+
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
|
|
4465
|
+
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, action))),
|
|
4466
|
+
Effect5.tap(
|
|
4467
|
+
() => Effect5.sync(() => {
|
|
4468
|
+
operation?.observe("observed", "sfu_publication");
|
|
4469
|
+
operation?.succeed();
|
|
4470
|
+
})
|
|
4471
|
+
),
|
|
4472
|
+
Effect5.asVoid,
|
|
4473
|
+
Effect5.catch(
|
|
4474
|
+
(cause) => this.#rollbackCapture({
|
|
4475
|
+
action,
|
|
4476
|
+
cause,
|
|
4477
|
+
fallback: Effect5.void,
|
|
4478
|
+
media: ports.media,
|
|
4479
|
+
permissionMessage: `${source} permission was denied`,
|
|
4480
|
+
prepared,
|
|
4481
|
+
restoreIntent: () => {
|
|
4482
|
+
if (this.#intentRevisions.get(source) === intentRevision) this.#intent[source] = previousIntent;
|
|
4483
|
+
},
|
|
4484
|
+
source,
|
|
4485
|
+
track: captured
|
|
4486
|
+
})
|
|
4487
|
+
)
|
|
4488
|
+
);
|
|
4489
|
+
});
|
|
4490
|
+
})
|
|
4491
|
+
)
|
|
4492
|
+
)
|
|
3429
4493
|
).pipe(
|
|
3430
4494
|
Effect5.mapError(normalizeClientError),
|
|
3431
|
-
Effect5.tapError(() => Effect5.sync(() => operation?.fail("media_failed")))
|
|
4495
|
+
Effect5.tapError(() => Effect5.sync(() => operation?.fail("media_failed"))),
|
|
4496
|
+
Effect5.ensuring(
|
|
4497
|
+
Effect5.sync(() => {
|
|
4498
|
+
if (this.#pendingEnables.get(source) === intentRevision) this.#pendingEnables.delete(source);
|
|
4499
|
+
})
|
|
4500
|
+
)
|
|
3432
4501
|
);
|
|
3433
4502
|
}
|
|
3434
4503
|
#startScreen() {
|
|
3435
4504
|
const operation = this.#diagnostics?.startOperation("screen.start");
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
this.#
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
this.#assertActivePorts(ports, "startScreenShare");
|
|
3457
|
-
track = requireDisplayVideoTrack(captured);
|
|
3458
|
-
this.#tracks.set("screen", track);
|
|
3459
|
-
ports.media.prepareLocalTrack("screen", track);
|
|
3460
|
-
prepared = true;
|
|
3461
|
-
operation?.observe("observed", "track_acquisition");
|
|
3462
|
-
track.addEventListener("ended", () => this.#handleScreenEnded(track));
|
|
3463
|
-
this.#screenEndedPending = false;
|
|
3464
|
-
this.#publish();
|
|
3465
|
-
})
|
|
3466
|
-
),
|
|
3467
|
-
Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
|
|
3468
|
-
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
|
|
3469
|
-
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
|
|
3470
|
-
Effect5.tap(
|
|
3471
|
-
() => Effect5.sync(() => {
|
|
3472
|
-
operation?.observe("observed", "sfu_publication");
|
|
4505
|
+
let intentRevision = 0;
|
|
4506
|
+
return Effect5.sync(() => {
|
|
4507
|
+
intentRevision = (this.#intentRevisions.get("screen") ?? 0) + 1;
|
|
4508
|
+
this.#intentRevisions.set("screen", intentRevision);
|
|
4509
|
+
this.#pendingEnables.set("screen", intentRevision);
|
|
4510
|
+
this.#ports?.media.setLocalSourceIntent?.("screen", true);
|
|
4511
|
+
}).pipe(
|
|
4512
|
+
Effect5.andThen(
|
|
4513
|
+
this.#serialize(
|
|
4514
|
+
"screen",
|
|
4515
|
+
this.#connection.runCommand((ports) => {
|
|
4516
|
+
let stream = null;
|
|
4517
|
+
let track = null;
|
|
4518
|
+
let prepared = false;
|
|
4519
|
+
return Effect5.suspend(() => {
|
|
4520
|
+
if (this.#tracks.has("screen")) {
|
|
4521
|
+
operation?.notObservable("permission", "already_active");
|
|
4522
|
+
operation?.notObservable("track_acquisition", "already_active");
|
|
4523
|
+
operation?.notObservable("sync_commit", "already_active");
|
|
4524
|
+
operation?.notObservable("sfu_publication", "already_active");
|
|
3473
4525
|
operation?.succeed();
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
4526
|
+
return Effect5.void;
|
|
4527
|
+
}
|
|
4528
|
+
return foreign2(() => this.#selection.getDisplayMedia({ video: true, audio: false })).pipe(
|
|
4529
|
+
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "permission"))),
|
|
4530
|
+
Effect5.tap(
|
|
4531
|
+
(captured) => Effect5.sync(() => {
|
|
4532
|
+
stream = captured;
|
|
4533
|
+
this.#assertActivePorts(ports, "startScreenShare");
|
|
4534
|
+
track = requireDisplayVideoTrack(captured);
|
|
4535
|
+
this.#tracks.set("screen", track);
|
|
4536
|
+
ports.media.prepareLocalTrack("screen", track);
|
|
4537
|
+
prepared = true;
|
|
4538
|
+
operation?.observe("observed", "track_acquisition");
|
|
4539
|
+
track.addEventListener("ended", () => this.#handleScreenEnded(track));
|
|
4540
|
+
this.#screenEndedPending = false;
|
|
4541
|
+
this.#publish();
|
|
4542
|
+
})
|
|
4543
|
+
),
|
|
4544
|
+
Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
|
|
4545
|
+
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
|
|
4546
|
+
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
|
|
4547
|
+
Effect5.tap(
|
|
4548
|
+
() => Effect5.sync(() => {
|
|
4549
|
+
operation?.observe("observed", "sfu_publication");
|
|
4550
|
+
operation?.succeed();
|
|
4551
|
+
})
|
|
4552
|
+
),
|
|
4553
|
+
Effect5.asVoid,
|
|
4554
|
+
Effect5.catch((cause) => this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
|
|
4555
|
+
);
|
|
4556
|
+
});
|
|
4557
|
+
})
|
|
4558
|
+
)
|
|
4559
|
+
)
|
|
3481
4560
|
).pipe(
|
|
3482
4561
|
Effect5.mapError(normalizeClientError),
|
|
3483
|
-
Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_start_failed")))
|
|
4562
|
+
Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_start_failed"))),
|
|
4563
|
+
Effect5.ensuring(
|
|
4564
|
+
Effect5.sync(() => {
|
|
4565
|
+
if (this.#pendingEnables.get("screen") === intentRevision) this.#pendingEnables.delete("screen");
|
|
4566
|
+
})
|
|
4567
|
+
)
|
|
3484
4568
|
);
|
|
3485
4569
|
}
|
|
3486
4570
|
#stopScreen() {
|
|
3487
4571
|
const operation = this.#diagnostics?.startOperation("screen.stop");
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
4572
|
+
let intentRevision = 0;
|
|
4573
|
+
let earlyPause = null;
|
|
4574
|
+
const localPause = Effect5.suspend(() => {
|
|
4575
|
+
intentRevision = (this.#intentRevisions.get("screen") ?? 0) + 1;
|
|
4576
|
+
this.#intentRevisions.set("screen", intentRevision);
|
|
4577
|
+
this.#ports?.media.setLocalSourceIntent?.("screen", false);
|
|
4578
|
+
if (this.#ports) earlyPause = this.#beginLocalPause(this.#ports, "screen");
|
|
4579
|
+
return Effect5.void;
|
|
4580
|
+
});
|
|
4581
|
+
return localPause.pipe(
|
|
4582
|
+
Effect5.andThen(
|
|
4583
|
+
this.#serialize(
|
|
4584
|
+
"screen",
|
|
4585
|
+
this.#connection.runCommand(
|
|
4586
|
+
(ports) => Effect5.suspend(() => {
|
|
4587
|
+
const track = this.#tracks.get("screen");
|
|
4588
|
+
let paused = false;
|
|
4589
|
+
return (earlyPause ? foreign2(() => earlyPause) : this.#localTarget(ports, "screen", false)).pipe(
|
|
4590
|
+
Effect5.tap(
|
|
4591
|
+
() => Effect5.sync(() => {
|
|
4592
|
+
paused = true;
|
|
4593
|
+
})
|
|
4594
|
+
),
|
|
4595
|
+
Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(false))),
|
|
4596
|
+
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "stopScreenShare"))),
|
|
4597
|
+
Effect5.andThen(foreign2(() => ports.media.clearPreparedLocalTrack("screen"))),
|
|
4598
|
+
Effect5.tap(
|
|
4599
|
+
() => Effect5.sync(() => {
|
|
4600
|
+
this.#assertActivePorts(ports, "stopScreenShare");
|
|
4601
|
+
if (this.#tracks.get("screen") === track) this.#tracks.delete("screen");
|
|
4602
|
+
track?.stop();
|
|
4603
|
+
this.#screenEndedPending = false;
|
|
4604
|
+
this.#publish();
|
|
4605
|
+
operation?.observe("observed", "stop_confirmation");
|
|
4606
|
+
operation?.succeed();
|
|
4607
|
+
})
|
|
4608
|
+
),
|
|
4609
|
+
Effect5.asVoid,
|
|
4610
|
+
Effect5.catch(
|
|
4611
|
+
(cause) => paused && !isSyncRejection(cause) || this.#intentRevisions.get("screen") !== intentRevision ? Effect5.fail(cause) : Effect5.sync(() => ports.media.setLocalSourceIntent?.("screen", true)).pipe(
|
|
4612
|
+
Effect5.andThen(this.#localTarget(ports, "screen", true)),
|
|
4613
|
+
Effect5.catch(() => Effect5.void),
|
|
4614
|
+
Effect5.andThen(Effect5.fail(cause))
|
|
4615
|
+
)
|
|
4616
|
+
)
|
|
4617
|
+
);
|
|
4618
|
+
})
|
|
4619
|
+
)
|
|
4620
|
+
)
|
|
3515
4621
|
)
|
|
3516
4622
|
).pipe(
|
|
3517
4623
|
Effect5.mapError(normalizeClientError),
|
|
3518
4624
|
Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_stop_failed")))
|
|
3519
4625
|
);
|
|
3520
4626
|
}
|
|
4627
|
+
#beginLocalPause(ports, source) {
|
|
4628
|
+
const pause = Effect5.runPromise(this.#localTarget(ports, source, false));
|
|
4629
|
+
void pause.catch(() => void 0);
|
|
4630
|
+
return pause;
|
|
4631
|
+
}
|
|
3521
4632
|
#serialize(source, effect) {
|
|
3522
|
-
return this.#gates.get(source).withPermit(
|
|
4633
|
+
return this.#gates.get(source).withPermit(
|
|
4634
|
+
Effect5.sync(() => {
|
|
4635
|
+
this.#pendingSources.add(source);
|
|
4636
|
+
this.#publish();
|
|
4637
|
+
}).pipe(
|
|
4638
|
+
Effect5.andThen(effect),
|
|
4639
|
+
Effect5.ensuring(
|
|
4640
|
+
Effect5.sync(() => {
|
|
4641
|
+
this.#pendingSources.delete(source);
|
|
4642
|
+
this.#publish();
|
|
4643
|
+
})
|
|
4644
|
+
)
|
|
4645
|
+
)
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
#localTarget(ports, source, enabled) {
|
|
4649
|
+
return foreign2(() => ports.media.setLocalPublicationTarget({ operationId: `privacy-${source}-${enabled ? "restore" : "off"}`, participantId: ports.sync.getSnapshot().participantId ?? "", source, enabled })).pipe(
|
|
4650
|
+
Effect5.flatMap(
|
|
4651
|
+
(result) => result.outcome === "confirmed" || result.outcome === "satisfied" || !enabled && result.errorCode === "source_unavailable" ? Effect5.void : Effect5.fail(new SpaceClientError({ code: "media.capture_failed", recoverable: true, message: `Could not ${enabled ? "restore" : "pause"} ${source}` }))
|
|
4652
|
+
)
|
|
4653
|
+
);
|
|
3523
4654
|
}
|
|
3524
4655
|
#captureSource(source) {
|
|
3525
4656
|
return foreign2(() => this.#selection.getUserMedia({ audio: source === "microphone", video: source === "camera" })).pipe(
|
|
4657
|
+
Effect5.mapError((cause) => captureError(cause, source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled", `${source} permission was denied`)),
|
|
3526
4658
|
Effect5.tap(() => this.#refreshDevices().pipe(Effect5.ignore)),
|
|
3527
4659
|
Effect5.flatMap(
|
|
3528
4660
|
(stream) => Effect5.try({
|
|
@@ -3568,8 +4700,8 @@ var MediaControllerRuntime = class {
|
|
|
3568
4700
|
input.restoreIntent?.();
|
|
3569
4701
|
const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
|
|
3570
4702
|
const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
|
|
3571
|
-
const
|
|
3572
|
-
return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(
|
|
4703
|
+
const failure3 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
|
|
4704
|
+
return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure3)));
|
|
3573
4705
|
}
|
|
3574
4706
|
#assertActivePorts(ports, action) {
|
|
3575
4707
|
if (this.#connection.getSnapshot().state === "live" && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
|
|
@@ -3582,6 +4714,7 @@ var MediaControllerRuntime = class {
|
|
|
3582
4714
|
this.#unsubscribeMedia = null;
|
|
3583
4715
|
this.#unsubscribeRequests = null;
|
|
3584
4716
|
this.#unsubscribeSync = null;
|
|
4717
|
+
if (this.#ports?.media !== ports?.media) this.#observedPublicationIds.clear();
|
|
3585
4718
|
this.#ports = ports;
|
|
3586
4719
|
if (!ports) {
|
|
3587
4720
|
for (const track of this.#tracks.values()) track.stop();
|
|
@@ -3591,11 +4724,57 @@ var MediaControllerRuntime = class {
|
|
|
3591
4724
|
this.#publish();
|
|
3592
4725
|
return;
|
|
3593
4726
|
}
|
|
3594
|
-
this.#unsubscribeMedia = ports.media.subscribe(() =>
|
|
4727
|
+
this.#unsubscribeMedia = ports.media.subscribe(() => {
|
|
4728
|
+
this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
|
|
4729
|
+
this.#publish();
|
|
4730
|
+
});
|
|
3595
4731
|
this.#unsubscribeRequests = ports.sync.onDirectedRequest((request) => this.#request(request));
|
|
3596
|
-
this.#unsubscribeSync = ports.sync.subscribe((snapshot) =>
|
|
4732
|
+
this.#unsubscribeSync = ports.sync.subscribe((snapshot) => {
|
|
4733
|
+
this.#removeRequestsFromMissingParticipants(snapshot);
|
|
4734
|
+
this.#applyRemoteMediaState(ports, snapshot);
|
|
4735
|
+
this.#applyForcedMediaState(ports, snapshot);
|
|
4736
|
+
});
|
|
4737
|
+
this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
|
|
4738
|
+
this.#applyForcedMediaState(ports, ports.sync.getSnapshot());
|
|
3597
4739
|
this.#publish();
|
|
3598
4740
|
}
|
|
4741
|
+
#applyRemoteMediaState(ports, snapshot) {
|
|
4742
|
+
for (const publication of ports.media.getSnapshot().remoteTracks) {
|
|
4743
|
+
publication.track.enabled = snapshot.media?.items.some((item) => item.participantId === publication.participantId && item.source === publication.source && item.enabled) ?? false;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
#applyForcedMediaState(ports, snapshot) {
|
|
4747
|
+
if (!snapshot.media || !snapshot.participantId) return;
|
|
4748
|
+
for (const source of MEDIA_SOURCES) {
|
|
4749
|
+
const projected = snapshot.media.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
4750
|
+
if (projected?.publicationId) {
|
|
4751
|
+
this.#observedPublicationIds.set(source, projected.publicationId);
|
|
4752
|
+
continue;
|
|
4753
|
+
}
|
|
4754
|
+
const previousPublicationId = this.#observedPublicationIds.get(source);
|
|
4755
|
+
if (!this.#observedPublicationIds.delete(source)) continue;
|
|
4756
|
+
const local = ports.media.getSnapshot().localTracks.find((publication) => publication.source === source);
|
|
4757
|
+
if (local && local.publicationId !== previousPublicationId) continue;
|
|
4758
|
+
this.#pauseForcedSource(ports, source);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
#pauseForcedSource(ports, source) {
|
|
4762
|
+
const pendingEnable = this.#hasPendingEnable(source);
|
|
4763
|
+
if (!pendingEnable) {
|
|
4764
|
+
this.#intentRevisions.set(source, (this.#intentRevisions.get(source) ?? 0) + 1);
|
|
4765
|
+
if (source !== "screen") this.#intent[source] = false;
|
|
4766
|
+
}
|
|
4767
|
+
const track = this.#tracks.get(source);
|
|
4768
|
+
if (track) track.enabled = false;
|
|
4769
|
+
ports.media.setLocalSourceIntent?.(source, false);
|
|
4770
|
+
if (ports.media.closeForcedLocalPublication) void ports.media.closeForcedLocalPublication(source).catch(() => void 0);
|
|
4771
|
+
if (pendingEnable) ports.media.setLocalSourceIntent?.(source, true);
|
|
4772
|
+
this.#publish();
|
|
4773
|
+
}
|
|
4774
|
+
#hasPendingEnable(source) {
|
|
4775
|
+
const revision = this.#pendingEnables.get(source);
|
|
4776
|
+
return revision !== void 0 && revision === this.#intentRevisions.get(source);
|
|
4777
|
+
}
|
|
3599
4778
|
#request(request) {
|
|
3600
4779
|
if (request.expires_at_ms <= this.#now()) return;
|
|
3601
4780
|
const control = this.#ports?.sync.getSnapshot().optimisticControl ?? this.#ports?.sync.getSnapshot().control;
|
|
@@ -3665,7 +4844,11 @@ var MediaControllerRuntime = class {
|
|
|
3665
4844
|
const current = this.#store.getSnapshot();
|
|
3666
4845
|
const snapshot = this.#ports?.media.getSnapshot();
|
|
3667
4846
|
const state = this.#connection.getSnapshot().state;
|
|
3668
|
-
const local = Object.freeze({
|
|
4847
|
+
const local = Object.freeze({
|
|
4848
|
+
microphone: localMedia("microphone", this.#tracks, snapshot, this.#intent.microphone, state, this.#pendingSources.has("microphone")),
|
|
4849
|
+
camera: localMedia("camera", this.#tracks, snapshot, this.#intent.camera, state, this.#pendingSources.has("camera")),
|
|
4850
|
+
screen: localMedia("screen", this.#tracks, snapshot, false, state, this.#pendingSources.has("screen"))
|
|
4851
|
+
});
|
|
3669
4852
|
const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
|
|
3670
4853
|
const media = Object.freeze({ ...current.media, local, remote, screenShare: local.screen, incomingRequests: this.#requests });
|
|
3671
4854
|
if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
|
|
@@ -3702,12 +4885,18 @@ function initialTrackEntries(microphone, camera) {
|
|
|
3702
4885
|
if (camera) entries.push(["camera", camera]);
|
|
3703
4886
|
return entries;
|
|
3704
4887
|
}
|
|
3705
|
-
function localMedia(source, tracks, snapshot, intended, connectionState) {
|
|
4888
|
+
function localMedia(source, tracks, snapshot, intended, connectionState, pending) {
|
|
3706
4889
|
const track = tracks.get(source) ?? null;
|
|
3707
4890
|
const publication = snapshot?.localTracks.find((candidate) => candidate.source === source);
|
|
4891
|
+
if (locallyMuted(intended, track, publication)) return Object.freeze({ source, state: "disabled", track });
|
|
4892
|
+
if (pending) return Object.freeze({ source, state: "requesting", track });
|
|
3708
4893
|
if (publication?.enabled) return Object.freeze({ source, state: "enabled", track });
|
|
4894
|
+
if (publication && connectionState === "live") return Object.freeze({ source, state: "disabled", track });
|
|
3709
4895
|
return Object.freeze({ source, state: localMediaState(source, track, intended, connectionState), track });
|
|
3710
4896
|
}
|
|
4897
|
+
function locallyMuted(intended, track, publication) {
|
|
4898
|
+
return !intended && track?.enabled === false && publication?.enabled === false;
|
|
4899
|
+
}
|
|
3711
4900
|
function localMediaState(source, track, intended, connectionState) {
|
|
3712
4901
|
if (!mediaIsDesired(source, track, intended)) return inactiveLocalMediaState(connectionState, track);
|
|
3713
4902
|
if (connectionState === "joining" || track) return "requesting";
|
|
@@ -3750,19 +4939,265 @@ function sameDeviceList(left, right) {
|
|
|
3750
4939
|
return left.length === right.length && left.every((device, index) => device.deviceId === right[index]?.deviceId && device.label === right[index]?.label);
|
|
3751
4940
|
}
|
|
3752
4941
|
function captureError(cause, action, permissionMessage) {
|
|
4942
|
+
if (cause instanceof ConnectionError) return cause;
|
|
3753
4943
|
if (isPermissionDenied(cause)) return new ConnectionError({ code: "permission_denied", action, recoverable: true, message: permissionMessage }, { cause });
|
|
3754
|
-
|
|
4944
|
+
const failure3 = describeMediaCaptureError(cause);
|
|
4945
|
+
return new ConnectionError({ ...failure3, action, message: action === "startScreenShare" ? "Screen sharing could not start. Check screen recording permission and try again in a supported browser or app." : failure3.message }, { cause });
|
|
3755
4946
|
}
|
|
3756
4947
|
function isPermissionDenied(cause) {
|
|
3757
|
-
return
|
|
4948
|
+
return describeMediaCaptureError(cause).code === "permission_denied";
|
|
3758
4949
|
}
|
|
3759
4950
|
function isAccessInvalid2(cause) {
|
|
3760
4951
|
return typeof cause === "object" && cause !== null && "code" in cause && (cause.code === "access.invalid" || cause.code === "invalid_access");
|
|
3761
4952
|
}
|
|
4953
|
+
function isSyncRejection(cause) {
|
|
4954
|
+
return isAccessInvalid2(cause) || cause instanceof V1SyncError && cause.code === "terminal_failure";
|
|
4955
|
+
}
|
|
3762
4956
|
|
|
3763
|
-
// src/space-client/
|
|
4957
|
+
// src/space-client/offline-actions.ts
|
|
3764
4958
|
import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
|
|
3765
4959
|
|
|
4960
|
+
// src/space-client/offline-actions-storage.ts
|
|
4961
|
+
import { Schema as Schema3 } from "effect";
|
|
4962
|
+
var OFFLINE_ACTION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
4963
|
+
function browserOfflineActionsStorage() {
|
|
4964
|
+
if (typeof window === "undefined") return void 0;
|
|
4965
|
+
try {
|
|
4966
|
+
return globalThis.localStorage;
|
|
4967
|
+
} catch {
|
|
4968
|
+
return void 0;
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
var attachment = Schema3.Struct({ attachmentId: Schema3.String, fileName: Schema3.String, mimeType: ChatAttachmentMimeTypeSchema, byteLength: Schema3.Number });
|
|
4972
|
+
var StoredOfflineAction = Schema3.Struct({
|
|
4973
|
+
id: Schema3.String,
|
|
4974
|
+
kind: Schema3.Literals(["chat_message", "hand_raise", "hand_lower"]),
|
|
4975
|
+
text: Schema3.NullOr(Schema3.String),
|
|
4976
|
+
queuedAt: Schema3.Number,
|
|
4977
|
+
clientMessageId: Schema3.optional(Schema3.String),
|
|
4978
|
+
attachments: Schema3.optional(Schema3.Array(attachment))
|
|
4979
|
+
});
|
|
4980
|
+
var StoredOfflineActions = Schema3.Struct({
|
|
4981
|
+
version: Schema3.Literal(1),
|
|
4982
|
+
subject: Schema3.Struct({ tenantId: Schema3.String, spaceId: Schema3.String, episodeId: Schema3.String, participantId: Schema3.String, participantGeneration: Schema3.Number }),
|
|
4983
|
+
actions: Schema3.Array(StoredOfflineAction)
|
|
4984
|
+
});
|
|
4985
|
+
var decodeOfflineActions = Schema3.decodeUnknownSync(StoredOfflineActions);
|
|
4986
|
+
|
|
4987
|
+
// src/space-client/offline-actions.ts
|
|
4988
|
+
var OfflineActionsService = class extends Context7.Service()("@chalk/client/OfflineActions") {
|
|
4989
|
+
};
|
|
4990
|
+
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;
|
|
4991
|
+
var terminalConnection = (snapshot) => snapshot.state === "left" || snapshot.state === "leaving" || snapshot.state === "failed" && !snapshot.failure?.recoverable;
|
|
4992
|
+
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);
|
|
4993
|
+
var discardedError = () => new SpaceClientError({ code: "offline.discarded", recoverable: false, message: "This action was not sent." });
|
|
4994
|
+
var makeOfflineActions = (input) => Effect6.gen(function* () {
|
|
4995
|
+
const runtime = new OfflineActionsRuntime(input.connection, input.store, input.storage ?? browserOfflineActionsStorage(), input.storageKey);
|
|
4996
|
+
yield* Effect6.addFinalizer(() => Effect6.sync(() => runtime.dispose()));
|
|
4997
|
+
return runtime;
|
|
4998
|
+
});
|
|
4999
|
+
var OfflineActionsRuntime = class {
|
|
5000
|
+
#connection;
|
|
5001
|
+
#store;
|
|
5002
|
+
#unsubscribe;
|
|
5003
|
+
#waiters = [];
|
|
5004
|
+
#decisionNeeded = false;
|
|
5005
|
+
#policy = "send";
|
|
5006
|
+
#policyExplicit = false;
|
|
5007
|
+
#restored = false;
|
|
5008
|
+
#subject = null;
|
|
5009
|
+
#notice;
|
|
5010
|
+
#storage;
|
|
5011
|
+
#storageKey;
|
|
5012
|
+
constructor(connection, store, storage, storageKey = "chalk:offline-actions") {
|
|
5013
|
+
this.#connection = connection;
|
|
5014
|
+
this.#store = store;
|
|
5015
|
+
this.#storage = storage;
|
|
5016
|
+
this.#storageKey = storageKey;
|
|
5017
|
+
this.#unsubscribe = connection.subscribe(() => this.#connectionChanged());
|
|
5018
|
+
this.#connectionChanged();
|
|
5019
|
+
}
|
|
5020
|
+
send = () => Effect6.sync(() => this.#settle("send"));
|
|
5021
|
+
discard = () => Effect6.sync(() => this.#settle("discard"));
|
|
5022
|
+
setPolicy = (policy) => Effect6.sync(() => {
|
|
5023
|
+
this.#policyExplicit = true;
|
|
5024
|
+
this.#policy = policy;
|
|
5025
|
+
if (this.#decisionNeeded && policy !== "ask") this.#settle(policy);
|
|
5026
|
+
else this.#publish();
|
|
5027
|
+
});
|
|
5028
|
+
hold = (input, run) => {
|
|
5029
|
+
if (this.#connection.getSnapshot().state !== "reconnecting" && !this.#decisionNeeded) return null;
|
|
5030
|
+
const queuedSubject = this.#connection.getSnapshot().subject;
|
|
5031
|
+
const action = Object.freeze({ ...input, id: this.#connection.createId(), text: input.text ?? null, queuedAt: this.#connection.nowUnsafe() });
|
|
5032
|
+
return Effect6.callback((resume) => {
|
|
5033
|
+
if (this.#waiters.length === 0) this.#subject = queuedSubject;
|
|
5034
|
+
this.#notice = void 0;
|
|
5035
|
+
this.#waiters = [...this.#waiters, { action, resume }];
|
|
5036
|
+
this.#persist();
|
|
5037
|
+
this.#publish();
|
|
5038
|
+
return Effect6.sync(() => this.#drop(action.id));
|
|
5039
|
+
}).pipe(Effect6.andThen(Effect6.suspend(() => sameSubject(this.#connection.getSnapshot().subject, queuedSubject) ? run() : Effect6.fail(discardedError()))));
|
|
5040
|
+
};
|
|
5041
|
+
dispose() {
|
|
5042
|
+
this.#unsubscribe();
|
|
5043
|
+
const waiters = this.#waiters;
|
|
5044
|
+
this.#waiters = [];
|
|
5045
|
+
for (const waiter of waiters) waiter.resume(Effect6.fail(discardedError()));
|
|
5046
|
+
}
|
|
5047
|
+
#connectionChanged() {
|
|
5048
|
+
const snapshot = this.#connection.getSnapshot();
|
|
5049
|
+
const state = snapshot.state;
|
|
5050
|
+
const terminal = terminalConnection(snapshot);
|
|
5051
|
+
if (state === "live") this.#restore();
|
|
5052
|
+
if (this.#waiters.length === 0) {
|
|
5053
|
+
if (terminal) this.#discardSaved();
|
|
5054
|
+
return;
|
|
5055
|
+
}
|
|
5056
|
+
if (state === "live") {
|
|
5057
|
+
if (!this.#validScope()) {
|
|
5058
|
+
this.#notice = "Your offline actions were not sent because the Episode or participant changed.";
|
|
5059
|
+
this.#settle("discard");
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
if (this.#policy === "ask") {
|
|
5063
|
+
this.#decisionNeeded = true;
|
|
5064
|
+
this.#publish();
|
|
5065
|
+
} else this.#settle(this.#policy);
|
|
5066
|
+
} else if (terminal) {
|
|
5067
|
+
this.#notice = "Your offline actions were not sent because the Episode ended or the connection closed.";
|
|
5068
|
+
this.#settle("discard");
|
|
5069
|
+
this.#clearStorage();
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
#settle(decision) {
|
|
5073
|
+
if (decision === "send" && this.#connection.getSnapshot().state !== "live") return;
|
|
5074
|
+
if (decision === "send" && !this.#validScope()) {
|
|
5075
|
+
this.#notice = "Your offline actions were not sent because the Episode or participant changed.";
|
|
5076
|
+
decision = "discard";
|
|
5077
|
+
}
|
|
5078
|
+
if (!this.#clearStorage() && this.#storage) {
|
|
5079
|
+
this.#notice = "Your offline actions could not be sent safely. Try again when storage is available.";
|
|
5080
|
+
this.#publish();
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
const waiters = this.#waiters;
|
|
5084
|
+
this.#waiters = [];
|
|
5085
|
+
this.#decisionNeeded = false;
|
|
5086
|
+
this.#publish();
|
|
5087
|
+
for (const waiter of waiters) this.#resumeWaiter(waiter, decision);
|
|
5088
|
+
this.#publish();
|
|
5089
|
+
}
|
|
5090
|
+
#drop(id) {
|
|
5091
|
+
const next = this.#waiters.filter((waiter) => waiter.action.id !== id);
|
|
5092
|
+
if (next.length === this.#waiters.length) return;
|
|
5093
|
+
this.#waiters = next;
|
|
5094
|
+
this.#persist();
|
|
5095
|
+
if (next.length === 0) this.#decisionNeeded = false;
|
|
5096
|
+
this.#publish();
|
|
5097
|
+
}
|
|
5098
|
+
#validScope() {
|
|
5099
|
+
const current = this.#connection.getSnapshot().subject;
|
|
5100
|
+
const saved = this.#subject;
|
|
5101
|
+
return sameSubject(current, saved);
|
|
5102
|
+
}
|
|
5103
|
+
#clearStorage() {
|
|
5104
|
+
try {
|
|
5105
|
+
this.#storage?.removeItem(this.#storageKey);
|
|
5106
|
+
return true;
|
|
5107
|
+
} catch {
|
|
5108
|
+
return false;
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
#persist() {
|
|
5112
|
+
if (!this.#storage || !this.#subject) return;
|
|
5113
|
+
try {
|
|
5114
|
+
if (this.#waiters.length === 0) this.#storage.removeItem(this.#storageKey);
|
|
5115
|
+
else this.#storage.setItem(this.#storageKey, JSON.stringify({ version: 1, subject: this.#subject, actions: this.#waiters.map(({ action }) => action) }));
|
|
5116
|
+
} catch {
|
|
5117
|
+
this.#notice = "Your offline actions are held, but could not be saved. Keep this page open.";
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
#restore() {
|
|
5121
|
+
if (this.#restored || !this.#connection.getSnapshot().subject) return;
|
|
5122
|
+
this.#restored = true;
|
|
5123
|
+
if (this.#waiters.length > 0) return;
|
|
5124
|
+
const saved = this.#readStored();
|
|
5125
|
+
if (!saved) return;
|
|
5126
|
+
this.#subject = saved.subject;
|
|
5127
|
+
if (!this.#validScope()) {
|
|
5128
|
+
this.#notice = "Your offline actions were not sent because the Episode or participant changed.";
|
|
5129
|
+
this.#clearStorage();
|
|
5130
|
+
this.#publish();
|
|
5131
|
+
return;
|
|
5132
|
+
}
|
|
5133
|
+
const actions = saved.actions.filter((action) => this.#connection.nowUnsafe() - action.queuedAt < OFFLINE_ACTION_MAX_AGE_MS);
|
|
5134
|
+
if (actions.length !== saved.actions.length) this.#notice = "Your expired offline actions were not sent. Actions are kept for up to 24 hours.";
|
|
5135
|
+
if (!this.#policyExplicit) this.#policy = "ask";
|
|
5136
|
+
const restoredSubject = saved.subject;
|
|
5137
|
+
this.#waiters = actions.map((action) => ({
|
|
5138
|
+
action,
|
|
5139
|
+
resume: (permission) => {
|
|
5140
|
+
const command = this.#connection.runCommand(
|
|
5141
|
+
({ sync }) => Effect6.tryPromise({
|
|
5142
|
+
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"),
|
|
5143
|
+
catch: () => discardedError()
|
|
5144
|
+
})
|
|
5145
|
+
);
|
|
5146
|
+
void Effect6.runPromiseExit(
|
|
5147
|
+
permission.pipe(
|
|
5148
|
+
Effect6.andThen(
|
|
5149
|
+
Effect6.suspend(() => sameSubject(this.#connection.getSnapshot().subject, restoredSubject) ? command : Effect6.fail(discardedError())).pipe(
|
|
5150
|
+
Effect6.tapError(
|
|
5151
|
+
() => Effect6.sync(() => {
|
|
5152
|
+
this.#notice = "An offline action was not sent. Please try it again.";
|
|
5153
|
+
this.#publish();
|
|
5154
|
+
})
|
|
5155
|
+
)
|
|
5156
|
+
)
|
|
5157
|
+
)
|
|
5158
|
+
)
|
|
5159
|
+
);
|
|
5160
|
+
}
|
|
5161
|
+
}));
|
|
5162
|
+
this.#persist();
|
|
5163
|
+
this.#publish();
|
|
5164
|
+
}
|
|
5165
|
+
#resumeWaiter(waiter, decision) {
|
|
5166
|
+
const expired = this.#connection.nowUnsafe() - waiter.action.queuedAt >= OFFLINE_ACTION_MAX_AGE_MS;
|
|
5167
|
+
if (expired) this.#notice = "Your expired offline actions were not sent. Actions are kept for up to 24 hours.";
|
|
5168
|
+
waiter.resume(decision === "send" && !expired ? Effect6.void : Effect6.fail(discardedError()));
|
|
5169
|
+
}
|
|
5170
|
+
#discardSaved() {
|
|
5171
|
+
try {
|
|
5172
|
+
if (!this.#storage?.getItem(this.#storageKey)) return;
|
|
5173
|
+
this.#notice = "Your offline actions were not sent because the Episode ended or the connection closed.";
|
|
5174
|
+
this.#clearStorage();
|
|
5175
|
+
} catch {
|
|
5176
|
+
this.#notice = "Saved offline actions could not be read. They will not be sent.";
|
|
5177
|
+
}
|
|
5178
|
+
this.#publish();
|
|
5179
|
+
}
|
|
5180
|
+
#readStored() {
|
|
5181
|
+
try {
|
|
5182
|
+
const raw = this.#storage?.getItem(this.#storageKey);
|
|
5183
|
+
if (!raw) return null;
|
|
5184
|
+
const saved = decodeOfflineActions(JSON.parse(raw));
|
|
5185
|
+
if (saved.actions.some((action) => !validStoredAction(action, this.#connection.nowUnsafe()))) throw new Error("Invalid offline action");
|
|
5186
|
+
return saved;
|
|
5187
|
+
} catch {
|
|
5188
|
+
this.#clearStorage();
|
|
5189
|
+
return null;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
#publish() {
|
|
5193
|
+
const slice = Object.freeze({ pending: Object.freeze(this.#waiters.map((waiter) => waiter.action)), decisionNeeded: this.#decisionNeeded, policy: this.#policy, ...this.#notice ? { notice: this.#notice } : {} });
|
|
5194
|
+
this.#store.updateOffline(slice);
|
|
5195
|
+
}
|
|
5196
|
+
};
|
|
5197
|
+
|
|
5198
|
+
// src/space-client/participants-controller.ts
|
|
5199
|
+
import { Context as Context8, Effect as Effect7, Layer as Layer8 } from "effect";
|
|
5200
|
+
|
|
3766
5201
|
// src/sync/v1-types.ts
|
|
3767
5202
|
var V1_CAPABILITIES = [
|
|
3768
5203
|
"publishAudio",
|
|
@@ -3793,19 +5228,21 @@ var V1_CAPABILITIES = [
|
|
|
3793
5228
|
// src/space-client/participants-controller.ts
|
|
3794
5229
|
var CAPABILITIES = new Set(V1_CAPABILITIES);
|
|
3795
5230
|
var EMPTY_PARTICIPANTS = Object.freeze({ roster: Object.freeze([]), admissionQueue: Object.freeze([]) });
|
|
3796
|
-
var ParticipantsControllerService = class extends
|
|
5231
|
+
var ParticipantsControllerService = class extends Context8.Service()("@chalk/client/ParticipantsController") {
|
|
3797
5232
|
};
|
|
3798
|
-
var makeParticipantsController = (connection, store, diagnostics) =>
|
|
3799
|
-
|
|
3800
|
-
(controller) =>
|
|
5233
|
+
var makeParticipantsController = (connection, store, diagnostics, offline) => Effect7.acquireRelease(
|
|
5234
|
+
Effect7.sync(() => new ParticipantsControllerRuntime(connection, store, diagnostics, offline)),
|
|
5235
|
+
(controller) => Effect7.sync(() => controller.dispose())
|
|
3801
5236
|
);
|
|
3802
5237
|
var ParticipantsControllerRuntime = class {
|
|
3803
5238
|
#connection;
|
|
3804
5239
|
#store;
|
|
3805
5240
|
#diagnostics;
|
|
5241
|
+
#offline;
|
|
3806
5242
|
#unsubscribePorts = null;
|
|
3807
5243
|
#unsubscribe = null;
|
|
3808
|
-
constructor(connection, store, diagnostics) {
|
|
5244
|
+
constructor(connection, store, diagnostics, offline) {
|
|
5245
|
+
this.#offline = offline;
|
|
3809
5246
|
this.#connection = connection;
|
|
3810
5247
|
this.#store = store;
|
|
3811
5248
|
this.#diagnostics = diagnostics;
|
|
@@ -3831,7 +5268,7 @@ var ParticipantsControllerRuntime = class {
|
|
|
3831
5268
|
requestMedia = (participantId, kind) => this.#command("media_request.request", ["capability_decision", "command_commit", "target_result"], () => {
|
|
3832
5269
|
assertIdentifier(participantId, "participant ID");
|
|
3833
5270
|
if (kind !== "microphone" && kind !== "camera") throw invalid("Media requests must target a microphone or camera");
|
|
3834
|
-
return ({ sync }) => (kind === "microphone" ? foreign3(() => sync.requestUnmute(participantId)) : foreign3(() => sync.requestStartCamera(participantId))).pipe(
|
|
5271
|
+
return ({ sync }) => (kind === "microphone" ? foreign3(() => sync.requestUnmute(participantId)) : foreign3(() => sync.requestStartCamera(participantId))).pipe(Effect7.map(directed));
|
|
3835
5272
|
});
|
|
3836
5273
|
remove = (participantId) => this.#command("moderation.remove", ["capability_decision", "command_commit", "target_delivery"], () => {
|
|
3837
5274
|
assertIdentifier(participantId, "participant ID");
|
|
@@ -3845,16 +5282,16 @@ var ParticipantsControllerRuntime = class {
|
|
|
3845
5282
|
assertIdentifier(requestId, "admission request ID");
|
|
3846
5283
|
return ({ sync }) => foreign3(() => sync.deny(requestId));
|
|
3847
5284
|
});
|
|
3848
|
-
raiseHand = () => this.#
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
5285
|
+
raiseHand = () => this.#setHandRaised(true);
|
|
5286
|
+
lowerHand = () => this.#setHandRaised(false);
|
|
5287
|
+
#setHandRaised(raised) {
|
|
5288
|
+
const send = () => this.#command(
|
|
5289
|
+
"participant.raised_hand.set",
|
|
5290
|
+
["membership_transition", "participant_result"],
|
|
5291
|
+
() => ({ sync }) => foreign3(() => sync.setHandRaised(raised))
|
|
5292
|
+
);
|
|
5293
|
+
return this.#offline?.hold({ kind: raised ? "hand_raise" : "hand_lower" }, send) ?? send();
|
|
5294
|
+
}
|
|
3858
5295
|
renameSelf = (displayName) => this.#command("participant.rename", ["membership_transition", "participant_result"], () => {
|
|
3859
5296
|
assertName(displayName, "display name");
|
|
3860
5297
|
return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
|
|
@@ -3867,18 +5304,18 @@ var ParticipantsControllerRuntime = class {
|
|
|
3867
5304
|
}
|
|
3868
5305
|
#command(name, checkpoints, operation) {
|
|
3869
5306
|
const diagnostic = this.#diagnostics?.startOperation(name);
|
|
3870
|
-
return
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
() =>
|
|
5307
|
+
return Effect7.try({ try: operation, catch: normalizeClientError }).pipe(
|
|
5308
|
+
Effect7.tap(() => Effect7.sync(() => diagnostic?.observe("observed", checkpoints[0]))),
|
|
5309
|
+
Effect7.flatMap((run) => this.#connection.runCommand(run)),
|
|
5310
|
+
Effect7.mapError(normalizeClientError),
|
|
5311
|
+
Effect7.tap(
|
|
5312
|
+
() => Effect7.sync(() => {
|
|
3876
5313
|
for (const checkpoint of checkpoints.slice(1)) diagnostic?.observe("observed", checkpoint);
|
|
3877
5314
|
if (name.startsWith("moderation.")) diagnostic?.notObservable("target_application", "target_projection_is_conditional");
|
|
3878
5315
|
diagnostic?.succeed();
|
|
3879
5316
|
})
|
|
3880
5317
|
),
|
|
3881
|
-
|
|
5318
|
+
Effect7.tapError(() => Effect7.sync(() => diagnostic?.fail("command_failed")))
|
|
3882
5319
|
);
|
|
3883
5320
|
}
|
|
3884
5321
|
#bind(ports) {
|
|
@@ -3900,7 +5337,7 @@ var ParticipantsControllerRuntime = class {
|
|
|
3900
5337
|
}
|
|
3901
5338
|
};
|
|
3902
5339
|
function foreign3(operation) {
|
|
3903
|
-
return
|
|
5340
|
+
return Effect7.tryPromise({ try: operation, catch: (cause) => cause });
|
|
3904
5341
|
}
|
|
3905
5342
|
function assertIdentifier(value, label) {
|
|
3906
5343
|
if (typeof value !== "string" || value.trim().length === 0) throw invalid(`A non-empty ${label} is required`);
|
|
@@ -4010,20 +5447,20 @@ function sameValues(left, right) {
|
|
|
4010
5447
|
}
|
|
4011
5448
|
|
|
4012
5449
|
// src/space-client/reactions-controller.ts
|
|
4013
|
-
import { Clock as Clock4, Context as
|
|
5450
|
+
import { Clock as Clock4, Context as Context9, Effect as Effect8, Layer as Layer9 } from "effect";
|
|
4014
5451
|
var MAX_VISIBLE_REACTIONS = 24;
|
|
4015
5452
|
var REACTIONS = /* @__PURE__ */ new Set(["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F389}"]);
|
|
4016
|
-
var ReactionsControllerService = class extends
|
|
5453
|
+
var ReactionsControllerService = class extends Context9.Service()("@chalk/client/ReactionsController") {
|
|
4017
5454
|
};
|
|
4018
|
-
var makeReactionsController = (connection, store, diagnostics) =>
|
|
5455
|
+
var makeReactionsController = (connection, store, diagnostics) => Effect8.gen(function* () {
|
|
4019
5456
|
const clock = yield* Clock4.Clock;
|
|
4020
|
-
const scope = yield*
|
|
4021
|
-
const context = yield*
|
|
5457
|
+
const scope = yield* Effect8.scope;
|
|
5458
|
+
const context = yield* Effect8.context();
|
|
4022
5459
|
const fork = (effect) => {
|
|
4023
|
-
void
|
|
5460
|
+
void Effect8.runForkWith(context)(Effect8.forkIn(effect, scope).pipe(Effect8.asVoid));
|
|
4024
5461
|
};
|
|
4025
5462
|
const controller = new ReactionsControllerRuntime(connection, store, clock.currentTimeMillisUnsafe, fork, diagnostics);
|
|
4026
|
-
yield*
|
|
5463
|
+
yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
|
|
4027
5464
|
return controller;
|
|
4028
5465
|
});
|
|
4029
5466
|
var ReactionsControllerRuntime = class {
|
|
@@ -4046,26 +5483,26 @@ var ReactionsControllerRuntime = class {
|
|
|
4046
5483
|
}
|
|
4047
5484
|
send = (emoji) => {
|
|
4048
5485
|
const operation = this.#diagnostics?.startOperation("reaction.send");
|
|
4049
|
-
return
|
|
5486
|
+
return Effect8.try({
|
|
4050
5487
|
try: () => {
|
|
4051
5488
|
if (!REACTIONS.has(emoji)) throw new SpaceClientError({ code: "reaction.invalid", recoverable: false, message: "The reaction is not supported" });
|
|
4052
5489
|
},
|
|
4053
5490
|
catch: normalizeClientError
|
|
4054
5491
|
}).pipe(
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
(reaction) =>
|
|
5492
|
+
Effect8.tap(() => Effect8.sync(() => operation?.observe("observed", "authorization"))),
|
|
5493
|
+
Effect8.andThen(this.#connection.runCommand(({ sync }) => foreign4(() => sync.sendReaction(emoji)))),
|
|
5494
|
+
Effect8.tap(() => Effect8.sync(() => operation?.observe("observed", "accepted_commit"))),
|
|
5495
|
+
Effect8.map(reactionFor),
|
|
5496
|
+
Effect8.tap(
|
|
5497
|
+
(reaction) => Effect8.sync(() => {
|
|
4061
5498
|
this.#observe(reaction);
|
|
4062
5499
|
operation?.observe("observed", "sender_result");
|
|
4063
5500
|
operation?.notObservable("recipient_projection", "recipient_projection_is_conditional");
|
|
4064
5501
|
operation?.succeed();
|
|
4065
5502
|
})
|
|
4066
5503
|
),
|
|
4067
|
-
|
|
4068
|
-
|
|
5504
|
+
Effect8.mapError(normalizeClientError),
|
|
5505
|
+
Effect8.tapError(() => Effect8.sync(() => operation?.fail("send_failed")))
|
|
4069
5506
|
);
|
|
4070
5507
|
};
|
|
4071
5508
|
dispose() {
|
|
@@ -4095,9 +5532,9 @@ var ReactionsControllerRuntime = class {
|
|
|
4095
5532
|
this.#expiryGenerations.set(reaction.eventId, generation);
|
|
4096
5533
|
const delay = Math.max(0, expiresAt2 - this.#now());
|
|
4097
5534
|
this.#fork(
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
5535
|
+
Effect8.sleep(delay).pipe(
|
|
5536
|
+
Effect8.andThen(
|
|
5537
|
+
Effect8.sync(() => {
|
|
4101
5538
|
if (this.#expiryGenerations.get(reaction.eventId) === generation) this.#remove(reaction.eventId);
|
|
4102
5539
|
})
|
|
4103
5540
|
)
|
|
@@ -4124,7 +5561,7 @@ var ReactionsControllerRuntime = class {
|
|
|
4124
5561
|
}
|
|
4125
5562
|
};
|
|
4126
5563
|
function foreign4(operation) {
|
|
4127
|
-
return
|
|
5564
|
+
return Effect8.tryPromise({ try: operation, catch: (cause) => cause });
|
|
4128
5565
|
}
|
|
4129
5566
|
function reactionFor(reaction) {
|
|
4130
5567
|
return Object.freeze({ ...reaction });
|
|
@@ -4139,23 +5576,122 @@ function sameReactions(left, right) {
|
|
|
4139
5576
|
});
|
|
4140
5577
|
}
|
|
4141
5578
|
|
|
5579
|
+
// src/space-client/recording-controller.ts
|
|
5580
|
+
import { Context as Context10, Effect as Effect9, Layer as Layer10 } from "effect";
|
|
5581
|
+
var RecordingControllerService = class extends Context10.Service()("@chalk/client/RecordingController") {
|
|
5582
|
+
};
|
|
5583
|
+
var makeRecordingController = (connection, store, diagnostics) => Effect9.acquireRelease(
|
|
5584
|
+
Effect9.sync(() => new RecordingControllerRuntime(connection, store, diagnostics)),
|
|
5585
|
+
(controller) => Effect9.sync(() => controller.dispose())
|
|
5586
|
+
);
|
|
5587
|
+
var RecordingControllerRuntime = class {
|
|
5588
|
+
#connection;
|
|
5589
|
+
#store;
|
|
5590
|
+
#diagnostics;
|
|
5591
|
+
#subscriptions = { ports: null, recording: null };
|
|
5592
|
+
constructor(connection, store, diagnostics) {
|
|
5593
|
+
this.#connection = connection;
|
|
5594
|
+
this.#store = store;
|
|
5595
|
+
this.#diagnostics = diagnostics;
|
|
5596
|
+
this.#subscriptions.ports = connection.subscribePorts((ports) => this.#bind(ports));
|
|
5597
|
+
}
|
|
5598
|
+
start = () => this.#command(
|
|
5599
|
+
"recording.start",
|
|
5600
|
+
"start_failed",
|
|
5601
|
+
() => this.#authorizeStart(),
|
|
5602
|
+
(_, { sync }) => foreign5(() => sync.startRecording()).pipe(Effect9.map(({ recordingId }) => Object.freeze({ recordingId })))
|
|
5603
|
+
);
|
|
5604
|
+
stop = () => this.#command(
|
|
5605
|
+
"recording.stop",
|
|
5606
|
+
"stop_failed",
|
|
5607
|
+
() => this.#authorizeStop(),
|
|
5608
|
+
(recordingId, { sync }) => foreign5(() => sync.stopRecording(recordingId)).pipe(Effect9.asVoid)
|
|
5609
|
+
);
|
|
5610
|
+
dispose() {
|
|
5611
|
+
this.#subscriptions.ports?.();
|
|
5612
|
+
this.#subscriptions.recording?.();
|
|
5613
|
+
this.#subscriptions.ports = null;
|
|
5614
|
+
this.#subscriptions.recording = null;
|
|
5615
|
+
this.#store.updateRecording(EMPTY_RECORDING);
|
|
5616
|
+
}
|
|
5617
|
+
#authorizeStart() {
|
|
5618
|
+
const snapshot = this.#store.getSnapshot();
|
|
5619
|
+
assertConnectedAndAllowed(snapshot);
|
|
5620
|
+
const current = snapshot.recording.current;
|
|
5621
|
+
if (current && current.status !== "stopped" && current.status !== "failed") throw invalidState("A Recording is already active");
|
|
5622
|
+
}
|
|
5623
|
+
#authorizeStop() {
|
|
5624
|
+
const snapshot = this.#store.getSnapshot();
|
|
5625
|
+
assertConnectedAndAllowed(snapshot);
|
|
5626
|
+
const current = snapshot.recording.current;
|
|
5627
|
+
if (!current || current.status !== "recording") throw invalidState("No active Recording can be stopped");
|
|
5628
|
+
return current.recordingId;
|
|
5629
|
+
}
|
|
5630
|
+
#command(name, failureCode, authorize, execute) {
|
|
5631
|
+
const operation = this.#diagnostics?.startOperation(name);
|
|
5632
|
+
const authorized = Effect9.try({ try: authorize, catch: normalizeClientError });
|
|
5633
|
+
return authorized.pipe(
|
|
5634
|
+
Effect9.tap(() => Effect9.sync(() => operation?.observe("observed", "authorization"))),
|
|
5635
|
+
Effect9.flatMap((authorization) => this.#connection.runCommand((ports) => execute(authorization, ports))),
|
|
5636
|
+
Effect9.tap(() => Effect9.sync(() => operation?.observe("observed", "accepted_commit"))),
|
|
5637
|
+
Effect9.tap(() => Effect9.sync(() => operation?.succeed())),
|
|
5638
|
+
Effect9.mapError(normalizeClientError),
|
|
5639
|
+
Effect9.tapError(() => Effect9.sync(() => operation?.fail(failureCode)))
|
|
5640
|
+
);
|
|
5641
|
+
}
|
|
5642
|
+
#bind(ports) {
|
|
5643
|
+
this.#subscriptions.recording?.();
|
|
5644
|
+
this.#subscriptions.recording = null;
|
|
5645
|
+
if (!ports) {
|
|
5646
|
+
this.#store.updateRecording(EMPTY_RECORDING);
|
|
5647
|
+
return;
|
|
5648
|
+
}
|
|
5649
|
+
this.#subscriptions.recording = ports.sync.subscribe((snapshot) => this.#observe(recordingFor(snapshot)));
|
|
5650
|
+
}
|
|
5651
|
+
#observe(next) {
|
|
5652
|
+
const current = this.#store.getSnapshot().recording;
|
|
5653
|
+
if (sameRecording(current, next)) return;
|
|
5654
|
+
this.#store.updateRecording(next);
|
|
5655
|
+
}
|
|
5656
|
+
};
|
|
5657
|
+
var EMPTY_RECORDING = Object.freeze({ current: null });
|
|
5658
|
+
function recordingFor(snapshot) {
|
|
5659
|
+
const recording = (snapshot.optimisticControl ?? snapshot.control)?.recording ?? null;
|
|
5660
|
+
return Object.freeze({ current: recording ? Object.freeze({ ...recording }) : null });
|
|
5661
|
+
}
|
|
5662
|
+
function sameRecording(left, right) {
|
|
5663
|
+
if (left.current === right.current) return true;
|
|
5664
|
+
if (!left.current || !right.current) return false;
|
|
5665
|
+
return left.current.recordingId === right.current.recordingId && left.current.status === right.current.status && left.current.failureCode === right.current.failureCode;
|
|
5666
|
+
}
|
|
5667
|
+
function assertConnectedAndAllowed(snapshot) {
|
|
5668
|
+
if (snapshot.connection.status !== "live" || !snapshot.connection.episode) throw invalidState("Recording commands require a live Episode");
|
|
5669
|
+
if (!snapshot.self.can("manageRecording")) throw new SpaceClientError({ code: "command.rejected", recoverable: false, message: "Recording is not allowed for this Participant" });
|
|
5670
|
+
}
|
|
5671
|
+
function invalidState(message) {
|
|
5672
|
+
return new SpaceClientError({ code: "connection.invalid_state", recoverable: false, message });
|
|
5673
|
+
}
|
|
5674
|
+
function foreign5(operation) {
|
|
5675
|
+
return Effect9.tryPromise({ try: operation, catch: (cause) => cause });
|
|
5676
|
+
}
|
|
5677
|
+
|
|
4142
5678
|
// src/space-client/whiteboard-controller.ts
|
|
4143
|
-
import { Context as
|
|
5679
|
+
import { Context as Context11, Effect as Effect10, Layer as Layer11 } from "effect";
|
|
4144
5680
|
var EMPTY_SUMMARY = Object.freeze({ status: "unsubscribed", sceneId: null, revision: null, capabilities: Object.freeze([]), canDraw: false, canClear: false, presenting: false, error: null });
|
|
4145
|
-
var WhiteboardControllerService = class extends
|
|
5681
|
+
var WhiteboardControllerService = class extends Context11.Service()("@chalk/client/WhiteboardController") {
|
|
4146
5682
|
};
|
|
4147
|
-
var makeWhiteboardController = (connection, store, create) =>
|
|
4148
|
-
const context = yield*
|
|
4149
|
-
const controller = yield*
|
|
5683
|
+
var makeWhiteboardController = (connection, store, create) => Effect10.gen(function* () {
|
|
5684
|
+
const context = yield* Effect10.context();
|
|
5685
|
+
const controller = yield* Effect10.sync(() => {
|
|
4150
5686
|
let instance = null;
|
|
4151
5687
|
const transport = create?.({
|
|
4152
|
-
token: () =>
|
|
5688
|
+
token: () => Effect10.runPromiseWith(context)(connection.getSyncToken()),
|
|
4153
5689
|
onSummary: (summary) => instance?.observe(summary)
|
|
4154
5690
|
}) ?? null;
|
|
4155
5691
|
instance = new WhiteboardControllerRuntime(connection, store, transport);
|
|
4156
5692
|
return instance;
|
|
4157
5693
|
});
|
|
4158
|
-
yield*
|
|
5694
|
+
yield* Effect10.addFinalizer(() => Effect10.sync(() => controller.dispose()));
|
|
4159
5695
|
return controller;
|
|
4160
5696
|
});
|
|
4161
5697
|
var WhiteboardControllerRuntime = class {
|
|
@@ -4217,15 +5753,17 @@ function sameError2(left, right) {
|
|
|
4217
5753
|
}
|
|
4218
5754
|
|
|
4219
5755
|
// src/space-client/controller-effects.ts
|
|
4220
|
-
var ControllerEffectsService = class extends
|
|
5756
|
+
var ControllerEffectsService = class extends Context12.Service()("@chalk/client/ControllerEffects") {
|
|
4221
5757
|
};
|
|
4222
|
-
var makeControllerEffects = (input) =>
|
|
5758
|
+
var makeControllerEffects = (input) => Effect11.gen(function* () {
|
|
5759
|
+
const offlineActions = yield* makeOfflineActions({ connection: input.connection, store: input.store, storage: input.offlineActionsStorage, storageKey: input.offlineActionsStorageKey });
|
|
4223
5760
|
const media = yield* makeMediaController(input.connection, input.store, input.mediaDeviceSelection, input.episodeDiagnostics);
|
|
4224
|
-
const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics });
|
|
4225
|
-
const participants = yield* makeParticipantsController(input.connection, input.store, input.episodeDiagnostics);
|
|
5761
|
+
const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics, offline: offlineActions });
|
|
5762
|
+
const participants = yield* makeParticipantsController(input.connection, input.store, input.episodeDiagnostics, offlineActions);
|
|
4226
5763
|
const reactions = yield* makeReactionsController(input.connection, input.store, input.episodeDiagnostics);
|
|
5764
|
+
const recording = yield* makeRecordingController(input.connection, input.store, input.episodeDiagnostics);
|
|
4227
5765
|
const whiteboard = yield* makeWhiteboardController(input.connection, input.store, input.featureFactories?.createWhiteboardClient);
|
|
4228
|
-
return { media, chat, participants, reactions, whiteboard };
|
|
5766
|
+
return { media, offline: offlineActions, chat, participants, reactions, recording, whiteboard };
|
|
4229
5767
|
});
|
|
4230
5768
|
|
|
4231
5769
|
// src/space-client/media-device-selection.ts
|
|
@@ -4280,7 +5818,7 @@ function requireDeviceId(deviceId) {
|
|
|
4280
5818
|
}
|
|
4281
5819
|
|
|
4282
5820
|
// src/space-client/episode-diagnostic-runtime.ts
|
|
4283
|
-
import { encodedEventSize, getActionContract, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
|
|
5821
|
+
import { encodedEventSize, getActionContract, parseDiagnosticEventDraft as parseDiagnosticEventDraft2, redactDiagnosticAttributes as redactDiagnosticAttributes2 } from "@q9labsai/diagnostics-contracts";
|
|
4284
5822
|
var DEFAULT_RING_EVENTS = 256;
|
|
4285
5823
|
var DEFAULT_RING_BYTES = 256 * 1024;
|
|
4286
5824
|
var DEFAULT_RING_AGE_MS = 2 * 60 * 1e3;
|
|
@@ -4388,6 +5926,15 @@ var EpisodeDiagnosticRuntime = class {
|
|
|
4388
5926
|
activeContext() {
|
|
4389
5927
|
return this.#enabled() ? this.context() : void 0;
|
|
4390
5928
|
}
|
|
5929
|
+
/** Internal Feedback seam. It returns only a short-lived verified credential to the private transport. */
|
|
5930
|
+
feedbackCredentialUnsafe() {
|
|
5931
|
+
if (!this.#enabled() || !this.#credential) return null;
|
|
5932
|
+
return Object.freeze({ ...this.#credential });
|
|
5933
|
+
}
|
|
5934
|
+
feedbackAvailabilityUnsafe() {
|
|
5935
|
+
if (this.#disposed) return "disposed";
|
|
5936
|
+
return this.#enabled() ? "available" : "disabled";
|
|
5937
|
+
}
|
|
4391
5938
|
startOperation(name, attributes, parentOperationRef, producerOperationRef, correlationInput) {
|
|
4392
5939
|
if (!this.#enabled()) return void 0;
|
|
4393
5940
|
const credentialGeneration = this.#credential?.generation;
|
|
@@ -4453,7 +6000,7 @@ var EpisodeDiagnosticRuntime = class {
|
|
|
4453
6000
|
});
|
|
4454
6001
|
}
|
|
4455
6002
|
#draft(input) {
|
|
4456
|
-
return
|
|
6003
|
+
return parseDiagnosticEventDraft2(
|
|
4457
6004
|
buildDraftInput(input, {
|
|
4458
6005
|
eventId: safeId(this.#createId()),
|
|
4459
6006
|
producerSequence: ++this.#sequence,
|
|
@@ -4764,7 +6311,7 @@ function buildDraftInput(input, context) {
|
|
|
4764
6311
|
expectation: expectationFor(contract, checkpoint),
|
|
4765
6312
|
correlation: optionalCorrelation(correlation),
|
|
4766
6313
|
release: context.release,
|
|
4767
|
-
attributes:
|
|
6314
|
+
attributes: redactDiagnosticAttributes2(input.attributes).attributes
|
|
4768
6315
|
};
|
|
4769
6316
|
}
|
|
4770
6317
|
function eventReferences(input) {
|
|
@@ -4885,9 +6432,125 @@ function renderRegistry() {
|
|
|
4885
6432
|
return target[REGISTRY];
|
|
4886
6433
|
}
|
|
4887
6434
|
|
|
6435
|
+
// src/feedback/controller.ts
|
|
6436
|
+
var DEFAULT_SDK = Object.freeze({ client: "@q9labsai/chalk-client" });
|
|
6437
|
+
var DEFAULT_PLATFORM = Object.freeze({ kind: "web" });
|
|
6438
|
+
function createFeedbackController(context) {
|
|
6439
|
+
const transport = createFeedbackTransport({
|
|
6440
|
+
baseUrl: context.apiBaseUrl,
|
|
6441
|
+
fetch: context.fetch,
|
|
6442
|
+
credential: context.diagnosticCredential,
|
|
6443
|
+
telemetry: () => context.telemetry ?? context.diagnosticContext()
|
|
6444
|
+
});
|
|
6445
|
+
let disposed = false;
|
|
6446
|
+
const ensureActive = () => {
|
|
6447
|
+
if (disposed) throw new FeedbackTransportError({ code: "unavailable", recoverable: false, message: "Feedback has been disposed." });
|
|
6448
|
+
};
|
|
6449
|
+
const prepare = async (input = {}) => {
|
|
6450
|
+
ensureActive();
|
|
6451
|
+
const collectionInput = evidenceInput(input.evidence);
|
|
6452
|
+
const screenshot = await resolveScreenshot(input.screenshot_provider ?? input.screenshotProvider);
|
|
6453
|
+
ensureActive();
|
|
6454
|
+
const initialInput = { ...collectionInput, screenshot };
|
|
6455
|
+
const idempotencyKey = idempotencyKeyFor(context.createId);
|
|
6456
|
+
const preparedEvidence = collectFeedbackEvidenceFromContext(initialInput, context);
|
|
6457
|
+
const preparedScreenshot = capturedScreenshot(screenshot) ? screenshotPayload(screenshot).screenshot : void 0;
|
|
6458
|
+
let cachedRequest;
|
|
6459
|
+
const prepared = {
|
|
6460
|
+
idempotency_key: idempotencyKey,
|
|
6461
|
+
evidence: preparedEvidence,
|
|
6462
|
+
...preparedScreenshot ? { screenshot: preparedScreenshot } : {},
|
|
6463
|
+
collect: (nextInput) => {
|
|
6464
|
+
ensureActive();
|
|
6465
|
+
return collectFeedbackEvidenceFromContext(nextInput ?? initialInput, context);
|
|
6466
|
+
},
|
|
6467
|
+
send: async ({ category, message }) => {
|
|
6468
|
+
ensureActive();
|
|
6469
|
+
const normalizedMessage = assertFeedbackMessage(message);
|
|
6470
|
+
const request = cachedRequest ?? parseFeedbackReportRequest({
|
|
6471
|
+
schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION,
|
|
6472
|
+
category,
|
|
6473
|
+
message: normalizedMessage,
|
|
6474
|
+
source: input.source ?? context.source,
|
|
6475
|
+
evidence: preparedEvidence,
|
|
6476
|
+
...preparedScreenshot ? { screenshot: preparedScreenshot } : {}
|
|
6477
|
+
});
|
|
6478
|
+
cachedRequest ??= request;
|
|
6479
|
+
return transport.send({ request, idempotencyKey });
|
|
6480
|
+
}
|
|
6481
|
+
};
|
|
6482
|
+
return prepared;
|
|
6483
|
+
};
|
|
6484
|
+
const send = async (input) => {
|
|
6485
|
+
ensureActive();
|
|
6486
|
+
const source = input.source ?? context.source;
|
|
6487
|
+
const evidenceInput2 = input.evidence ?? defaultEvidenceInput();
|
|
6488
|
+
const screenshot = input.screenshot ?? evidenceInput2.screenshot;
|
|
6489
|
+
return sendRequest({ category: input.category, message: input.message, source, evidence: { ...evidenceInput2, ...screenshot ? { screenshot } : {} }, idempotencyKey: input.idempotency_key ?? input.idempotencyKey ?? idempotencyKeyFor(context.createId) });
|
|
6490
|
+
};
|
|
6491
|
+
const sendRequest = async (input) => {
|
|
6492
|
+
ensureActive();
|
|
6493
|
+
const evidence = collectFeedbackEvidenceFromContext(input.evidence, context);
|
|
6494
|
+
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) });
|
|
6495
|
+
return transport.send({ request, idempotencyKey: input.idempotencyKey });
|
|
6496
|
+
};
|
|
6497
|
+
return {
|
|
6498
|
+
prepare,
|
|
6499
|
+
send,
|
|
6500
|
+
dispose: () => {
|
|
6501
|
+
disposed = true;
|
|
6502
|
+
}
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6505
|
+
function evidenceInput(input) {
|
|
6506
|
+
const base = { sdk: input?.sdk ?? DEFAULT_SDK, platform: input?.platform ?? DEFAULT_PLATFORM };
|
|
6507
|
+
if (!input) return base;
|
|
6508
|
+
return { ...base, ...optionalEvidenceInput(input) };
|
|
6509
|
+
}
|
|
6510
|
+
function optionalEvidenceInput(input) {
|
|
6511
|
+
return {
|
|
6512
|
+
...input.app ? { app: input.app } : {},
|
|
6513
|
+
...input.local_state ? { local_state: input.local_state } : {},
|
|
6514
|
+
...input.cookies ? { cookies: input.cookies } : {}
|
|
6515
|
+
};
|
|
6516
|
+
}
|
|
6517
|
+
function defaultEvidenceInput() {
|
|
6518
|
+
return { sdk: DEFAULT_SDK, platform: DEFAULT_PLATFORM };
|
|
6519
|
+
}
|
|
6520
|
+
async function resolveScreenshot(provider) {
|
|
6521
|
+
if (!provider) return { state: "unavailable", failure_code: "unsupported" };
|
|
6522
|
+
let timeout;
|
|
6523
|
+
try {
|
|
6524
|
+
return await Promise.race([
|
|
6525
|
+
provider(),
|
|
6526
|
+
new Promise((resolve) => {
|
|
6527
|
+
timeout = globalThis.setTimeout(() => resolve({ state: "unavailable", failure_code: "capture_failed" }), 5e3);
|
|
6528
|
+
})
|
|
6529
|
+
]);
|
|
6530
|
+
} catch {
|
|
6531
|
+
return { state: "unavailable", failure_code: "capture_failed" };
|
|
6532
|
+
} finally {
|
|
6533
|
+
globalThis.clearTimeout(timeout);
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
function capturedScreenshot(input) {
|
|
6537
|
+
return input.state === "captured" || input.state === "partial" ? input : void 0;
|
|
6538
|
+
}
|
|
6539
|
+
function screenshotPayload(input) {
|
|
6540
|
+
if (!input) return {};
|
|
6541
|
+
const screenshot = capturedScreenshot(input);
|
|
6542
|
+
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 } } : {};
|
|
6543
|
+
}
|
|
6544
|
+
function idempotencyKeyFor(createId) {
|
|
6545
|
+
const generated = createId();
|
|
6546
|
+
if (/^[A-Za-z0-9_-]{16,128}$/u.test(generated)) return generated;
|
|
6547
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
6548
|
+
return `feedback-${Date.now()}-${Math.random().toString(36).slice(2, 18)}`;
|
|
6549
|
+
}
|
|
6550
|
+
|
|
4888
6551
|
// src/space-client/core.ts
|
|
4889
6552
|
var syncClientsByDependencies = /* @__PURE__ */ new WeakMap();
|
|
4890
|
-
var SpaceClientCoreService = class extends
|
|
6553
|
+
var SpaceClientCoreService = class extends Context13.Service()("@chalk/client/SpaceClientCore") {
|
|
4891
6554
|
};
|
|
4892
6555
|
var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
4893
6556
|
const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
|
|
@@ -4900,7 +6563,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
4900
6563
|
now: baseDependencies.clock.now,
|
|
4901
6564
|
setTimeout: baseDependencies.clock.setTimeout,
|
|
4902
6565
|
clearTimeout: baseDependencies.clock.clearTimeout,
|
|
4903
|
-
release: { id: "chalk-client@4.1.
|
|
6566
|
+
release: { id: "chalk-client@4.1.16" }
|
|
4904
6567
|
});
|
|
4905
6568
|
const diagnosticSyncClients = /* @__PURE__ */ new Set();
|
|
4906
6569
|
const dependencies = {
|
|
@@ -4922,6 +6585,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
4922
6585
|
initialCameraEnabled: platform.initialCameraEnabled,
|
|
4923
6586
|
telemetry: platform.telemetry,
|
|
4924
6587
|
recordRtcSummary: platform.recordRtcSummary,
|
|
6588
|
+
recordReconnect: platform.recordReconnect,
|
|
4925
6589
|
dependencies,
|
|
4926
6590
|
diagnostics: {
|
|
4927
6591
|
onEvent: (event) => {
|
|
@@ -4930,30 +6594,49 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
4930
6594
|
}
|
|
4931
6595
|
}
|
|
4932
6596
|
});
|
|
4933
|
-
return makeSpaceClientCoreLayerFromServices(options, platform).pipe(
|
|
6597
|
+
return makeSpaceClientCoreLayerFromServices(options, platform).pipe(Layer13.provideMerge(Layer13.mergeAll(makeSpaceStoreLayer, lifecycle, makeConnectionPlatformLayer(dependencies), liveClock(dependencies))));
|
|
4934
6598
|
};
|
|
4935
|
-
var makeSpaceClientCoreLayerFromServices = (options, platform = {}) =>
|
|
6599
|
+
var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer13.effect(
|
|
4936
6600
|
SpaceClientCoreService,
|
|
4937
|
-
|
|
4938
|
-
const store = yield*
|
|
4939
|
-
const lifecycle = yield*
|
|
4940
|
-
const dependencies = yield*
|
|
6601
|
+
Effect12.gen(function* () {
|
|
6602
|
+
const store = yield* Effect12.service(SpaceStoreService);
|
|
6603
|
+
const lifecycle = yield* Effect12.service(ConnectionLifecycleService);
|
|
6604
|
+
const dependencies = yield* Effect12.service(ConnectionPlatformService);
|
|
4941
6605
|
const space = options.space.trim();
|
|
4942
|
-
if (space.length === 0) return yield*
|
|
4943
|
-
if (typeof options.getAccess !== "function") return yield*
|
|
6606
|
+
if (space.length === 0) return yield* Effect12.die(new TypeError("A Space slug is required"));
|
|
6607
|
+
if (typeof options.getAccess !== "function") return yield* Effect12.die(new TypeError("getAccess is required"));
|
|
4944
6608
|
const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
|
|
4945
6609
|
const selection = new MediaDeviceSelection(dependencies.mediaDevices);
|
|
4946
6610
|
const episodeDiagnostics = episodeDiagnosticsForDependencies(dependencies);
|
|
4947
6611
|
if (episodeDiagnostics) registerEpisodeDiagnosticConnection(lifecycle, episodeDiagnostics);
|
|
4948
|
-
const controllers = yield* makeControllerEffects({
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
6612
|
+
const controllers = yield* makeControllerEffects({
|
|
6613
|
+
offlineActionsStorage: options.offlineActionsStorage,
|
|
6614
|
+
offlineActionsStorageKey: `chalk:offline-actions:${JSON.stringify([apiBaseUrl, space])}`,
|
|
6615
|
+
apiBaseUrl,
|
|
6616
|
+
connection: lifecycle,
|
|
6617
|
+
store,
|
|
6618
|
+
mediaDeviceSelection: selection,
|
|
6619
|
+
featureFactories: dependencies,
|
|
6620
|
+
fetch: platform.fetch,
|
|
6621
|
+
episodeDiagnostics
|
|
6622
|
+
});
|
|
6623
|
+
return yield* Effect12.acquireRelease(
|
|
6624
|
+
Effect12.sync(
|
|
6625
|
+
() => new SpaceClientCore(lifecycle, selection, store, controllers, episodeDiagnostics, dependencies, syncClientsByDependencies.get(dependencies), {
|
|
6626
|
+
apiBaseUrl,
|
|
6627
|
+
fetch: platform.fetch,
|
|
6628
|
+
createId: platform.randomUUID ?? dependencies.createId ?? lifecycle.createId,
|
|
6629
|
+
source: platform.feedbackSource ?? "embedded",
|
|
6630
|
+
telemetry: platform.telemetry
|
|
6631
|
+
})
|
|
6632
|
+
),
|
|
6633
|
+
(core) => Effect12.sync(() => core.dispose())
|
|
4952
6634
|
);
|
|
4953
6635
|
})
|
|
4954
6636
|
);
|
|
4955
6637
|
var SpaceClientCore = class {
|
|
4956
6638
|
controllers;
|
|
6639
|
+
feedback;
|
|
4957
6640
|
#connection;
|
|
4958
6641
|
#store;
|
|
4959
6642
|
#episodeDiagnostics;
|
|
@@ -4968,13 +6651,29 @@ var SpaceClientCore = class {
|
|
|
4968
6651
|
#leaveOperation;
|
|
4969
6652
|
#participantJoinOperation;
|
|
4970
6653
|
#syncConnectOperation;
|
|
4971
|
-
constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients) {
|
|
6654
|
+
constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients, feedback) {
|
|
4972
6655
|
this.#connection = connection;
|
|
4973
6656
|
this.#store = store;
|
|
4974
6657
|
this.controllers = controllers;
|
|
4975
6658
|
this.#episodeDiagnostics = episodeDiagnostics;
|
|
4976
6659
|
this.#episodeDiagnosticDependencies = episodeDiagnosticDependencies;
|
|
4977
6660
|
this.#episodeDiagnosticSyncClients = episodeDiagnosticSyncClients;
|
|
6661
|
+
this.feedback = createFeedbackController({
|
|
6662
|
+
apiBaseUrl: feedback?.apiBaseUrl ?? "https://api.chalkmeet.com",
|
|
6663
|
+
fetch: feedback?.fetch,
|
|
6664
|
+
createId: feedback?.createId ?? connection.createId,
|
|
6665
|
+
now: connection.nowUnsafe,
|
|
6666
|
+
source: feedback?.source ?? "embedded",
|
|
6667
|
+
telemetry: feedback?.telemetry,
|
|
6668
|
+
diagnosticContext: () => episodeDiagnostics?.activeContext(),
|
|
6669
|
+
connection: connection.getSnapshot,
|
|
6670
|
+
diagnosticCredential: () => episodeDiagnostics?.feedbackCredentialUnsafe() ?? null,
|
|
6671
|
+
diagnosticAvailability: () => episodeDiagnostics?.feedbackAvailabilityUnsafe() ?? "unavailable",
|
|
6672
|
+
diagnosticSnapshot: () => {
|
|
6673
|
+
const inspected = episodeDiagnostics?.inspect();
|
|
6674
|
+
return { dropped: inspected?.dropped ?? 0, events: inspected?.ring ?? [] };
|
|
6675
|
+
}
|
|
6676
|
+
});
|
|
4978
6677
|
this.#store.updateConnection(connection.getSnapshot());
|
|
4979
6678
|
this.#previous = this.#store.getSnapshot();
|
|
4980
6679
|
this.#unsubscribeStore = this.#store.subscribe(() => this.#publishStore());
|
|
@@ -4992,47 +6691,47 @@ var SpaceClientCore = class {
|
|
|
4992
6691
|
join(options = {}) {
|
|
4993
6692
|
this.#participantJoinOperation = this.#episodeDiagnostics?.startOperation("participant.join");
|
|
4994
6693
|
this.#syncConnectOperation = this.#episodeDiagnostics?.startOperation("sync.connect");
|
|
4995
|
-
return
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
() =>
|
|
6694
|
+
return Effect12.sync(() => this.#requireActive()).pipe(
|
|
6695
|
+
Effect12.tap(() => Effect12.sync(() => this.controllers.media.configure(options))),
|
|
6696
|
+
Effect12.andThen(this.#connection.configureJoin(options)),
|
|
6697
|
+
Effect12.andThen(this.#connection.join()),
|
|
6698
|
+
Effect12.andThen(options.displayName === void 0 ? Effect12.void : this.controllers.participants.renameSelf(options.displayName)),
|
|
6699
|
+
Effect12.tap(
|
|
6700
|
+
() => Effect12.sync(() => {
|
|
5002
6701
|
this.#participantJoinOperation?.succeed();
|
|
5003
6702
|
this.#syncConnectOperation?.succeed();
|
|
5004
6703
|
})
|
|
5005
6704
|
),
|
|
5006
|
-
|
|
5007
|
-
() =>
|
|
6705
|
+
Effect12.tapError(
|
|
6706
|
+
() => Effect12.sync(() => {
|
|
5008
6707
|
this.#participantJoinOperation?.fail("join_failed");
|
|
5009
6708
|
this.#syncConnectOperation?.fail("connect_failed");
|
|
5010
6709
|
})
|
|
5011
6710
|
),
|
|
5012
|
-
|
|
6711
|
+
Effect12.mapError(normalizeClientError)
|
|
5013
6712
|
);
|
|
5014
6713
|
}
|
|
5015
6714
|
leave() {
|
|
5016
6715
|
this.#leaveOperation = this.#episodeDiagnostics?.startOperation("participant.leave");
|
|
5017
6716
|
return this.#connection.leave().pipe(
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
6717
|
+
Effect12.tap(() => Effect12.sync(() => this.#leaveOperation?.succeed())),
|
|
6718
|
+
Effect12.tapError(() => Effect12.sync(() => this.#leaveOperation?.fail("leave_failed"))),
|
|
6719
|
+
Effect12.mapError(normalizeClientError)
|
|
5021
6720
|
);
|
|
5022
6721
|
}
|
|
5023
6722
|
endEpisode() {
|
|
5024
6723
|
return trackedEffect(
|
|
5025
6724
|
this.#episodeDiagnostics,
|
|
5026
6725
|
"episode.end.authorized",
|
|
5027
|
-
this.#connection.runCommand(({ sync }) =>
|
|
5028
|
-
).pipe(
|
|
6726
|
+
this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode()).pipe(Effect12.tap(() => this.#connection.confirmEpisodeEnded)))
|
|
6727
|
+
).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
5029
6728
|
}
|
|
5030
6729
|
extendEpisode(minutes) {
|
|
5031
6730
|
return trackedEffect(
|
|
5032
6731
|
this.#episodeDiagnostics,
|
|
5033
6732
|
"episode.deadline.extend",
|
|
5034
|
-
this.#connection.runCommand(({ sync }) =>
|
|
5035
|
-
).pipe(
|
|
6733
|
+
this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes)))
|
|
6734
|
+
).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
5036
6735
|
}
|
|
5037
6736
|
on(event, handler) {
|
|
5038
6737
|
this.#handlers[event].add(handler);
|
|
@@ -5045,6 +6744,7 @@ var SpaceClientCore = class {
|
|
|
5045
6744
|
this.#unsubscribeStore?.();
|
|
5046
6745
|
this.#unsubscribeConnection = null;
|
|
5047
6746
|
this.#unsubscribeStore = null;
|
|
6747
|
+
this.feedback.dispose();
|
|
5048
6748
|
this.#disposeDiagnostics();
|
|
5049
6749
|
this.#clearHandlers();
|
|
5050
6750
|
}
|
|
@@ -5146,25 +6846,25 @@ var SpaceClientCore = class {
|
|
|
5146
6846
|
}
|
|
5147
6847
|
};
|
|
5148
6848
|
function liveClock(dependencies) {
|
|
5149
|
-
return
|
|
6849
|
+
return Layer13.succeed(Clock5.Clock, {
|
|
5150
6850
|
currentTimeMillisUnsafe: dependencies.clock.now,
|
|
5151
|
-
currentTimeMillis:
|
|
6851
|
+
currentTimeMillis: Effect12.sync(dependencies.clock.now),
|
|
5152
6852
|
currentTimeNanosUnsafe: () => BigInt(dependencies.clock.now()) * 1000000n,
|
|
5153
|
-
currentTimeNanos:
|
|
5154
|
-
sleep: (duration) =>
|
|
5155
|
-
const handle = dependencies.clock.setTimeout(() => resume(
|
|
5156
|
-
return
|
|
6853
|
+
currentTimeNanos: Effect12.sync(() => BigInt(dependencies.clock.now()) * 1000000n),
|
|
6854
|
+
sleep: (duration) => Effect12.callback((resume) => {
|
|
6855
|
+
const handle = dependencies.clock.setTimeout(() => resume(Effect12.void), Duration2.toMillis(duration));
|
|
6856
|
+
return Effect12.sync(() => dependencies.clock.clearTimeout(handle));
|
|
5157
6857
|
})
|
|
5158
6858
|
});
|
|
5159
6859
|
}
|
|
5160
|
-
function
|
|
5161
|
-
return
|
|
6860
|
+
function foreign6(operation) {
|
|
6861
|
+
return Effect12.tryPromise({ try: operation, catch: (cause) => cause });
|
|
5162
6862
|
}
|
|
5163
6863
|
function trackedEffect(diagnostics, name, effect) {
|
|
5164
6864
|
const operation = diagnostics?.startOperation(name);
|
|
5165
6865
|
return effect.pipe(
|
|
5166
|
-
|
|
5167
|
-
|
|
6866
|
+
Effect12.tap(() => Effect12.sync(() => operation?.succeed())),
|
|
6867
|
+
Effect12.tapError(() => Effect12.sync(() => operation?.fail("operation_failed")))
|
|
5168
6868
|
);
|
|
5169
6869
|
}
|
|
5170
6870
|
async function resolveAccessGrant(input) {
|
|
@@ -5186,7 +6886,7 @@ function diagnosticAccessOperation(request) {
|
|
|
5186
6886
|
}
|
|
5187
6887
|
async function fetchAccessGrant2(input) {
|
|
5188
6888
|
if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
|
|
5189
|
-
return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason) });
|
|
6889
|
+
return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason), signal: input.request?.signal });
|
|
5190
6890
|
}
|
|
5191
6891
|
function accessReason(reason) {
|
|
5192
6892
|
if (reason === void 0 || reason === "join") return "join";
|
|
@@ -5225,17 +6925,17 @@ function sameDiagnosticTrack(left, right) {
|
|
|
5225
6925
|
}
|
|
5226
6926
|
|
|
5227
6927
|
// src/space-client/space-client.ts
|
|
5228
|
-
import { Effect as
|
|
6928
|
+
import { Effect as Effect14, ManagedRuntime } from "effect";
|
|
5229
6929
|
|
|
5230
6930
|
// src/space-client/promise-facade.ts
|
|
5231
|
-
import { Effect as
|
|
6931
|
+
import { Effect as Effect13 } from "effect";
|
|
5232
6932
|
function toPromiseController(runtime, controller) {
|
|
5233
6933
|
const projected = {};
|
|
5234
6934
|
for (const [name, command] of Object.entries(controller)) {
|
|
5235
6935
|
if (name === "configure" || name === "dispose") continue;
|
|
5236
6936
|
projected[name] = (...args) => {
|
|
5237
6937
|
const result = command(...args);
|
|
5238
|
-
return
|
|
6938
|
+
return Effect13.isEffect(result) ? runtime.runPromise(result) : result;
|
|
5239
6939
|
};
|
|
5240
6940
|
}
|
|
5241
6941
|
return projected;
|
|
@@ -5243,24 +6943,30 @@ function toPromiseController(runtime, controller) {
|
|
|
5243
6943
|
|
|
5244
6944
|
// src/space-client/space-client.ts
|
|
5245
6945
|
var PromiseSpaceClient = class {
|
|
6946
|
+
feedback;
|
|
5246
6947
|
media;
|
|
5247
6948
|
chat;
|
|
5248
6949
|
participants;
|
|
6950
|
+
offline;
|
|
5249
6951
|
reactions;
|
|
6952
|
+
recording;
|
|
5250
6953
|
whiteboard;
|
|
5251
6954
|
#core;
|
|
5252
6955
|
#runtime;
|
|
5253
6956
|
#disposed = false;
|
|
5254
6957
|
constructor(options, platform) {
|
|
5255
6958
|
this.#runtime = ManagedRuntime.make(makeSpaceClientCoreLayer(options, platform));
|
|
5256
|
-
this.#core = this.#runtime.runSync(
|
|
6959
|
+
this.#core = this.#runtime.runSync(Effect14.service(SpaceClientCoreService));
|
|
5257
6960
|
const controllers = this.#core.controllers;
|
|
6961
|
+
this.feedback = this.#core.feedback;
|
|
5258
6962
|
this.media = toPromiseController(this.#runtime, controllers.media);
|
|
5259
6963
|
const chat = toPromiseController(this.#runtime, controllers.chat);
|
|
5260
|
-
const { upload, url, ...chatCommands } = chat;
|
|
5261
|
-
this.chat = { ...chatCommands, files: { upload, url } };
|
|
6964
|
+
const { upload, url, resolveUrl, ...chatCommands } = chat;
|
|
6965
|
+
this.chat = { ...chatCommands, files: { upload, url, resolveUrl } };
|
|
6966
|
+
this.offline = toPromiseController(this.#runtime, controllers.offline);
|
|
5262
6967
|
this.participants = toPromiseController(this.#runtime, controllers.participants);
|
|
5263
6968
|
this.reactions = toPromiseController(this.#runtime, controllers.reactions);
|
|
6969
|
+
this.recording = toPromiseController(this.#runtime, controllers.recording);
|
|
5264
6970
|
this.whiteboard = toPromiseController(this.#runtime, controllers.whiteboard);
|
|
5265
6971
|
}
|
|
5266
6972
|
join = (options) => this.#runtime.runPromise(this.#core.join(options));
|
|
@@ -5312,6 +7018,35 @@ export {
|
|
|
5312
7018
|
SpaceStoreService,
|
|
5313
7019
|
makeSpaceStoreLayer,
|
|
5314
7020
|
makeFakeSpaceStoreLayer,
|
|
7021
|
+
FEEDBACK_EVIDENCE_SCHEMA_VERSION,
|
|
7022
|
+
FEEDBACK_REQUEST_SCHEMA_VERSION,
|
|
7023
|
+
FEEDBACK_RECEIPT_SCHEMA_VERSION,
|
|
7024
|
+
FEEDBACK_SCREENSHOT_SCHEMA_VERSION,
|
|
7025
|
+
FEEDBACK_LOCAL_STATE_REGISTRY_VERSION,
|
|
7026
|
+
FEEDBACK_COOKIE_REGISTRY_VERSION,
|
|
7027
|
+
FEEDBACK_MAX_MESSAGE_BYTES,
|
|
7028
|
+
FEEDBACK_MAX_EVIDENCE_BYTES,
|
|
7029
|
+
FEEDBACK_MAX_REQUEST_BYTES,
|
|
7030
|
+
FEEDBACK_MAX_SCREENSHOT_BYTES,
|
|
7031
|
+
FEEDBACK_MAX_SCREENSHOT_WIDTH,
|
|
7032
|
+
FEEDBACK_MAX_SCREENSHOT_HEIGHT,
|
|
7033
|
+
FEEDBACK_MAX_TELEMETRY_EVENTS,
|
|
7034
|
+
FEEDBACK_MAX_DIAGNOSTIC_EVENTS,
|
|
7035
|
+
FEEDBACK_MAX_LOCAL_STATE_ENTRIES,
|
|
7036
|
+
FEEDBACK_MAX_COOKIE_ENTRIES,
|
|
7037
|
+
FeedbackValidationError,
|
|
7038
|
+
validateFeedbackReportRequest,
|
|
7039
|
+
parseFeedbackReportRequest,
|
|
7040
|
+
validateFeedbackEvidence,
|
|
7041
|
+
parseFeedbackEvidence,
|
|
7042
|
+
validateFeedbackScreenshot,
|
|
7043
|
+
parseFeedbackScreenshot,
|
|
7044
|
+
assertFeedbackReceipt,
|
|
7045
|
+
assertFeedbackMessage,
|
|
7046
|
+
collectFeedbackEvidence,
|
|
7047
|
+
collectLocalState,
|
|
7048
|
+
collectCookies,
|
|
7049
|
+
FeedbackTransportError,
|
|
5315
7050
|
SpaceClientCoreService,
|
|
5316
7051
|
makeSpaceClientCoreLayer,
|
|
5317
7052
|
makeSpaceClientCoreLayerFromServices,
|