@q9labsai/chalk-client 4.1.14 → 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/grant.d.ts +1 -0
- package/dist/access/grant.d.ts.map +1 -1
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-JCNUF3DP.js → chunk-BD7FRQ67.js} +2082 -328
- package/dist/{chunk-2NUTY2VP.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-HE6CLPZM.js → chunk-M3S7HNQF.js} +1663 -516
- package/dist/{chunk-S5OUHFGC.js → chunk-O7WTAXAY.js} +670 -73
- package/dist/{chunk-PWUU6T3R.js → chunk-OKIL2ELF.js} +1185 -874
- package/dist/chunk-RCDXIDCN.js +33 -0
- package/dist/client.d.ts +1420 -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 +1409 -462
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +2613 -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 +130 -15
- 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 +189 -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 +187 -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
|
-
|
|
4
|
+
createCloudflareSFUHTTPTransport,
|
|
5
|
+
describeMediaCaptureError
|
|
6
|
+
} from "./chunk-O7WTAXAY.js";
|
|
6
7
|
import {
|
|
7
8
|
AccessGrantError,
|
|
8
9
|
requireParsedAccessGrant,
|
|
9
10
|
validEpisodeDiagnosticCredential
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-M3S7HNQF.js";
|
|
12
|
+
import {
|
|
13
|
+
normalizeTelemetryAttributes
|
|
14
|
+
} from "./chunk-EWTITNJ3.js";
|
|
11
15
|
import {
|
|
12
16
|
createTraceContext,
|
|
17
|
+
journeyHeaders,
|
|
13
18
|
parseTraceparent
|
|
14
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-HW5TTZL2.js";
|
|
15
20
|
import {
|
|
21
|
+
ChatAttachmentMimeTypeSchema,
|
|
22
|
+
V1SyncError,
|
|
16
23
|
createBrowserSyncLifecycle,
|
|
17
24
|
createBrowserWebSocketFactory,
|
|
18
25
|
createV1SyncClient
|
|
19
|
-
} from "./chunk-
|
|
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,9 +2055,9 @@ 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
|
-
const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe()
|
|
2060
|
+
const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe())).pipe(
|
|
2029
2061
|
Effect2.tap((snapshot) => SubscriptionRef.set(snapshotRef, snapshot)),
|
|
2030
2062
|
Effect2.asVoid,
|
|
2031
2063
|
Effect2.tap(
|
|
@@ -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;
|
|
@@ -2496,16 +2557,28 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2496
2557
|
function idleSnapshot() {
|
|
2497
2558
|
return Object.freeze({ state: "idle", subject: null, episode: null, connection: Object.freeze({ sync: "idle", media: "idle" }), failure: null });
|
|
2498
2559
|
}
|
|
2499
|
-
function snapshotFor(model,
|
|
2560
|
+
function snapshotFor(model, access) {
|
|
2500
2561
|
const control = model.syncSnapshot?.optimisticControl ?? model.syncSnapshot?.control;
|
|
2501
2562
|
return Object.freeze({
|
|
2502
2563
|
state: model.state,
|
|
2503
|
-
subject:
|
|
2504
|
-
episode:
|
|
2505
|
-
connection: Object.freeze({ sync: syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
2564
|
+
subject: subjectFor(access),
|
|
2565
|
+
episode: episodeFor(access, control),
|
|
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
|
}
|
|
2570
|
+
function subjectFor(access) {
|
|
2571
|
+
if (!access) return null;
|
|
2572
|
+
return Object.freeze({ ...access.subject });
|
|
2573
|
+
}
|
|
2574
|
+
function episodeFor(access, control) {
|
|
2575
|
+
if (!access) return null;
|
|
2576
|
+
return Object.freeze({
|
|
2577
|
+
id: access.subject.episodeId,
|
|
2578
|
+
startedAt: access.episodeStartedAt ?? null,
|
|
2579
|
+
deadline: control ? new Date(control.deadlineAtMs).toISOString() : null
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2509
2582
|
function portsFor(model) {
|
|
2510
2583
|
return model.sync && model.media ? { sync: model.sync, media: model.media } : null;
|
|
2511
2584
|
}
|
|
@@ -2566,6 +2639,10 @@ var CONNECTION_ERROR_CODE_MAP = {
|
|
|
2566
2639
|
invalid_access: "access.invalid",
|
|
2567
2640
|
access_unavailable: "access.unavailable",
|
|
2568
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",
|
|
2569
2646
|
sync_start_failed: "connection.sync_start_failed",
|
|
2570
2647
|
media_start_failed: "connection.media_start_failed",
|
|
2571
2648
|
join_cleanup_unconfirmed: "connection.join_cleanup_unconfirmed",
|
|
@@ -2598,6 +2675,9 @@ function normalizeClientError(cause, fallback = "client.internal_error") {
|
|
|
2598
2675
|
message: cause.message
|
|
2599
2676
|
});
|
|
2600
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
|
+
}
|
|
2601
2681
|
if (isAccessInvalid(cause)) {
|
|
2602
2682
|
return new SpaceClientError({ code: "access.invalid", recoverable: true, message: "The access grant was rejected" });
|
|
2603
2683
|
}
|
|
@@ -2621,6 +2701,7 @@ function connectionErrorCode(code, action) {
|
|
|
2621
2701
|
|
|
2622
2702
|
// src/space-client/store.ts
|
|
2623
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"]);
|
|
2624
2705
|
var empty = (value) => Object.freeze(value);
|
|
2625
2706
|
var EMPTY = empty({
|
|
2626
2707
|
connection: empty({ status: "idle", episode: null, lastError: null }),
|
|
@@ -2636,6 +2717,8 @@ var EMPTY = empty({
|
|
|
2636
2717
|
}),
|
|
2637
2718
|
chat: empty({ status: "idle", messages: empty([]), pendingSends: empty([]), readReceipts: empty([]), unreadCount: 0, pagination: empty({ cursor: null, hasOlder: false, historyTruncated: false }), lastError: null }),
|
|
2638
2719
|
reactions: empty({ active: empty([]) }),
|
|
2720
|
+
recording: empty({ current: null }),
|
|
2721
|
+
offline: empty({ pending: empty([]), decisionNeeded: false, policy: "send" }),
|
|
2639
2722
|
whiteboard: empty({ open: false, engine: empty({ status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null }) })
|
|
2640
2723
|
});
|
|
2641
2724
|
var SpaceStore = class _SpaceStore {
|
|
@@ -2660,8 +2743,9 @@ var SpaceStore = class _SpaceStore {
|
|
|
2660
2743
|
updateConnection(snapshot) {
|
|
2661
2744
|
const current = this.getSnapshot().connection;
|
|
2662
2745
|
const error2 = snapshot.failure ? empty({ code: mapCode(snapshot.failure.code), recoverable: snapshot.failure.recoverable, message: snapshot.failure.message }) : null;
|
|
2663
|
-
|
|
2664
|
-
|
|
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 }));
|
|
2665
2749
|
}
|
|
2666
2750
|
updateSelf(value) {
|
|
2667
2751
|
this.#replace("self", value);
|
|
@@ -2678,6 +2762,12 @@ var SpaceStore = class _SpaceStore {
|
|
|
2678
2762
|
updateReactions(value) {
|
|
2679
2763
|
this.#replace("reactions", value);
|
|
2680
2764
|
}
|
|
2765
|
+
updateRecording(value) {
|
|
2766
|
+
this.#replace("recording", value);
|
|
2767
|
+
}
|
|
2768
|
+
updateOffline(value) {
|
|
2769
|
+
this.#replace("offline", value);
|
|
2770
|
+
}
|
|
2681
2771
|
updateWhiteboard(value) {
|
|
2682
2772
|
this.#replace("whiteboard", value);
|
|
2683
2773
|
}
|
|
@@ -2716,11 +2806,943 @@ function sameError(left, right) {
|
|
|
2716
2806
|
return left.code === right.code && left.recoverable === right.recoverable && left.message === right.message;
|
|
2717
2807
|
}
|
|
2718
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
|
+
|
|
2719
3741
|
// src/space-client/core.ts
|
|
2720
|
-
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";
|
|
2721
3743
|
|
|
2722
3744
|
// src/space-client/controller-effects.ts
|
|
2723
|
-
import { Context as
|
|
3745
|
+
import { Context as Context12, Effect as Effect11, Layer as Layer12 } from "effect";
|
|
2724
3746
|
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
2725
3747
|
|
|
2726
3748
|
// src/space-client/chat-controller.ts
|
|
@@ -2732,14 +3754,14 @@ var MAX_CHAT_PAGE_SIZE = 100;
|
|
|
2732
3754
|
var MAX_LOADED_CHAT_MESSAGES = 500;
|
|
2733
3755
|
var MAX_CHAT_TEXT_BYTES = 16384;
|
|
2734
3756
|
var MAX_CHAT_TEXT_SCALARS = 4e3;
|
|
2735
|
-
var
|
|
3757
|
+
var encoder4 = new TextEncoder();
|
|
2736
3758
|
var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
|
|
2737
3759
|
function validateChatMessage(input) {
|
|
2738
3760
|
const attachments = input.attachments ?? [];
|
|
2739
3761
|
if (input.text.length === 0 && attachments.length === 0) return "A chat message requires text or an attachment";
|
|
2740
|
-
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`;
|
|
2741
3763
|
if (attachments.length > CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `A chat message supports at most ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} attachments`;
|
|
2742
|
-
if (new Set(attachments.map((
|
|
3764
|
+
if (new Set(attachments.map((attachment2) => attachment2.attachmentId)).size !== attachments.length) return "Chat attachment IDs must be unique";
|
|
2743
3765
|
return attachments.map(validateChatAttachment).find((value) => value !== null) ?? null;
|
|
2744
3766
|
}
|
|
2745
3767
|
function validateChatUpload(file, bytes, clientAttachmentId) {
|
|
@@ -2770,16 +3792,16 @@ function compareChatSequence(left, right) {
|
|
|
2770
3792
|
const normalizedRight = right.replace(/^0+/u, "") || "0";
|
|
2771
3793
|
return normalizedLeft.length === normalizedRight.length ? normalizedLeft < normalizedRight ? -1 : 1 : normalizedLeft.length - normalizedRight.length;
|
|
2772
3794
|
}
|
|
2773
|
-
function validateChatAttachment(
|
|
2774
|
-
const fileNameBytes =
|
|
2775
|
-
if (!
|
|
2776
|
-
if (!Number.isSafeInteger(
|
|
2777
|
-
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";
|
|
2778
3800
|
}
|
|
2779
3801
|
function sameChatMessage(left, right) {
|
|
2780
|
-
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) => {
|
|
2781
3803
|
const candidate = right.attachments[index];
|
|
2782
|
-
return candidate?.attachmentId ===
|
|
3804
|
+
return candidate?.attachmentId === attachment2.attachmentId && candidate.fileName === attachment2.fileName && candidate.mimeType === attachment2.mimeType && candidate.byteLength === attachment2.byteLength;
|
|
2783
3805
|
});
|
|
2784
3806
|
}
|
|
2785
3807
|
|
|
@@ -2795,7 +3817,7 @@ var makeChatController = (input) => Effect4.gen(function* () {
|
|
|
2795
3817
|
const controller = yield* Effect4.sync(() => {
|
|
2796
3818
|
let instance = null;
|
|
2797
3819
|
const transport = input.createTransport?.({ token: () => Effect4.runPromiseWith(context)(input.connection.getSyncToken()) }) ?? null;
|
|
2798
|
-
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);
|
|
2799
3821
|
return instance;
|
|
2800
3822
|
});
|
|
2801
3823
|
yield* Effect4.addFinalizer(() => Effect4.sync(() => controller.dispose()));
|
|
@@ -2809,6 +3831,7 @@ var ChatControllerRuntime = class {
|
|
|
2809
3831
|
#fetch;
|
|
2810
3832
|
#fork;
|
|
2811
3833
|
#diagnostics;
|
|
3834
|
+
#offline;
|
|
2812
3835
|
#catchUpRunning = false;
|
|
2813
3836
|
#catchUpRequested = false;
|
|
2814
3837
|
#hasOlder = false;
|
|
@@ -2826,7 +3849,8 @@ var ChatControllerRuntime = class {
|
|
|
2826
3849
|
#unsubscribeConnection = null;
|
|
2827
3850
|
#unsubscribeEvents = null;
|
|
2828
3851
|
#unsubscribeSnapshot = null;
|
|
2829
|
-
constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics) {
|
|
3852
|
+
constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics, offline) {
|
|
3853
|
+
this.#offline = offline;
|
|
2830
3854
|
this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
|
|
2831
3855
|
this.#connection = connection;
|
|
2832
3856
|
this.#store = store;
|
|
@@ -2842,9 +3866,12 @@ var ChatControllerRuntime = class {
|
|
|
2842
3866
|
Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "validation"))),
|
|
2843
3867
|
Effect4.map((attachments) => ({ clientMessageId: this.#connection.createId(), attachments })),
|
|
2844
3868
|
Effect4.flatMap(({ clientMessageId, attachments }) => {
|
|
2845
|
-
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);
|
|
2846
3873
|
operation?.observe("observed", "authorization");
|
|
2847
|
-
return
|
|
3874
|
+
return (held ?? sendNow()).pipe(Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "durable_commit")))).pipe(
|
|
2848
3875
|
Effect4.map(chatMessageFor),
|
|
2849
3876
|
Effect4.tap(() => Effect4.sync(() => this.#removePending(clientMessageId))),
|
|
2850
3877
|
Effect4.map((message) => this.#observeMessage(message, false)),
|
|
@@ -2858,7 +3885,8 @@ var ChatControllerRuntime = class {
|
|
|
2858
3885
|
Effect4.catch((cause) => {
|
|
2859
3886
|
const error2 = normalizeClientError(cause, "chat.payload_invalid");
|
|
2860
3887
|
return Effect4.sync(() => {
|
|
2861
|
-
|
|
3888
|
+
if (error2.code === "offline.discarded") this.#removePending(clientMessageId);
|
|
3889
|
+
else this.#upsertPending(clientMessageId, input.text, attachments, "failed", failureFromError(error2));
|
|
2862
3890
|
operation?.fail("send_failed");
|
|
2863
3891
|
}).pipe(Effect4.andThen(Effect4.fail(error2)));
|
|
2864
3892
|
})
|
|
@@ -2918,6 +3946,7 @@ var ChatControllerRuntime = class {
|
|
|
2918
3946
|
});
|
|
2919
3947
|
};
|
|
2920
3948
|
upload = (file) => {
|
|
3949
|
+
const fetch = this.#fetch;
|
|
2921
3950
|
const prepareOperation = this.#diagnostics?.startOperation("chat.attachment.prepare");
|
|
2922
3951
|
return Effect4.suspend(() => {
|
|
2923
3952
|
if (!this.#transport) {
|
|
@@ -2938,7 +3967,7 @@ var ChatControllerRuntime = class {
|
|
|
2938
3967
|
prepareOperation?.succeed();
|
|
2939
3968
|
}).pipe(
|
|
2940
3969
|
Effect4.andThen(
|
|
2941
|
-
foreign(() =>
|
|
3970
|
+
foreign(() => fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes })).pipe(
|
|
2942
3971
|
Effect4.flatMap((response) => {
|
|
2943
3972
|
const commitOperation = this.#diagnostics?.startOperation("chat.attachment.commit");
|
|
2944
3973
|
if (!response.ok) {
|
|
@@ -2976,7 +4005,11 @@ var ChatControllerRuntime = class {
|
|
|
2976
4005
|
);
|
|
2977
4006
|
});
|
|
2978
4007
|
};
|
|
2979
|
-
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
|
+
});
|
|
2980
4013
|
dispose() {
|
|
2981
4014
|
this.#unsubscribeConnection?.();
|
|
2982
4015
|
this.#unsubscribeConnection = null;
|
|
@@ -3178,7 +4211,7 @@ function validateUpload(file, bytes, clientAttachmentId) {
|
|
|
3178
4211
|
return upload;
|
|
3179
4212
|
}
|
|
3180
4213
|
function bytesFor(file) {
|
|
3181
|
-
return "
|
|
4214
|
+
return "fileName" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
|
|
3182
4215
|
}
|
|
3183
4216
|
function catchUpRequest(latestSequence, ports) {
|
|
3184
4217
|
const extension = ports.sync.getCollaborationExtensionState();
|
|
@@ -3230,6 +4263,10 @@ var MediaControllerRuntime = class {
|
|
|
3230
4263
|
#fork;
|
|
3231
4264
|
#diagnostics;
|
|
3232
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();
|
|
3233
4270
|
#requestGenerations = /* @__PURE__ */ new Map();
|
|
3234
4271
|
#intent = { microphone: true, camera: true };
|
|
3235
4272
|
#requests = Object.freeze([]);
|
|
@@ -3356,161 +4393,268 @@ var MediaControllerRuntime = class {
|
|
|
3356
4393
|
#set(source, enabled) {
|
|
3357
4394
|
const operation = this.#diagnostics?.startOperation(`${source}.${enabled ? "publish" : "unpublish"}`);
|
|
3358
4395
|
operation?.observe("observed", "intent");
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
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
|
-
|
|
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
|
+
)
|
|
3417
4493
|
).pipe(
|
|
3418
4494
|
Effect5.mapError(normalizeClientError),
|
|
3419
|
-
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
|
+
)
|
|
3420
4501
|
);
|
|
3421
4502
|
}
|
|
3422
4503
|
#startScreen() {
|
|
3423
4504
|
const operation = this.#diagnostics?.startOperation("screen.start");
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
this.#
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
this.#assertActivePorts(ports, "startScreenShare");
|
|
3445
|
-
track = requireDisplayVideoTrack(captured);
|
|
3446
|
-
this.#tracks.set("screen", track);
|
|
3447
|
-
ports.media.prepareLocalTrack("screen", track);
|
|
3448
|
-
prepared = true;
|
|
3449
|
-
operation?.observe("observed", "track_acquisition");
|
|
3450
|
-
track.addEventListener("ended", () => this.#handleScreenEnded(track));
|
|
3451
|
-
this.#screenEndedPending = false;
|
|
3452
|
-
this.#publish();
|
|
3453
|
-
})
|
|
3454
|
-
),
|
|
3455
|
-
Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
|
|
3456
|
-
Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
|
|
3457
|
-
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
|
|
3458
|
-
Effect5.tap(
|
|
3459
|
-
() => Effect5.sync(() => {
|
|
3460
|
-
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");
|
|
3461
4525
|
operation?.succeed();
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
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
|
+
)
|
|
3469
4560
|
).pipe(
|
|
3470
4561
|
Effect5.mapError(normalizeClientError),
|
|
3471
|
-
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
|
+
)
|
|
3472
4568
|
);
|
|
3473
4569
|
}
|
|
3474
4570
|
#stopScreen() {
|
|
3475
4571
|
const operation = this.#diagnostics?.startOperation("screen.stop");
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
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
|
+
)
|
|
3503
4621
|
)
|
|
3504
4622
|
).pipe(
|
|
3505
4623
|
Effect5.mapError(normalizeClientError),
|
|
3506
4624
|
Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_stop_failed")))
|
|
3507
4625
|
);
|
|
3508
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
|
+
}
|
|
3509
4632
|
#serialize(source, effect) {
|
|
3510
|
-
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
|
+
);
|
|
3511
4654
|
}
|
|
3512
4655
|
#captureSource(source) {
|
|
3513
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`)),
|
|
3514
4658
|
Effect5.tap(() => this.#refreshDevices().pipe(Effect5.ignore)),
|
|
3515
4659
|
Effect5.flatMap(
|
|
3516
4660
|
(stream) => Effect5.try({
|
|
@@ -3556,8 +4700,8 @@ var MediaControllerRuntime = class {
|
|
|
3556
4700
|
input.restoreIntent?.();
|
|
3557
4701
|
const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
|
|
3558
4702
|
const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
|
|
3559
|
-
const
|
|
3560
|
-
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)));
|
|
3561
4705
|
}
|
|
3562
4706
|
#assertActivePorts(ports, action) {
|
|
3563
4707
|
if (this.#connection.getSnapshot().state === "live" && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
|
|
@@ -3570,6 +4714,7 @@ var MediaControllerRuntime = class {
|
|
|
3570
4714
|
this.#unsubscribeMedia = null;
|
|
3571
4715
|
this.#unsubscribeRequests = null;
|
|
3572
4716
|
this.#unsubscribeSync = null;
|
|
4717
|
+
if (this.#ports?.media !== ports?.media) this.#observedPublicationIds.clear();
|
|
3573
4718
|
this.#ports = ports;
|
|
3574
4719
|
if (!ports) {
|
|
3575
4720
|
for (const track of this.#tracks.values()) track.stop();
|
|
@@ -3579,11 +4724,57 @@ var MediaControllerRuntime = class {
|
|
|
3579
4724
|
this.#publish();
|
|
3580
4725
|
return;
|
|
3581
4726
|
}
|
|
3582
|
-
this.#unsubscribeMedia = ports.media.subscribe(() =>
|
|
4727
|
+
this.#unsubscribeMedia = ports.media.subscribe(() => {
|
|
4728
|
+
this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
|
|
4729
|
+
this.#publish();
|
|
4730
|
+
});
|
|
3583
4731
|
this.#unsubscribeRequests = ports.sync.onDirectedRequest((request) => this.#request(request));
|
|
3584
|
-
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());
|
|
3585
4739
|
this.#publish();
|
|
3586
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
|
+
}
|
|
3587
4778
|
#request(request) {
|
|
3588
4779
|
if (request.expires_at_ms <= this.#now()) return;
|
|
3589
4780
|
const control = this.#ports?.sync.getSnapshot().optimisticControl ?? this.#ports?.sync.getSnapshot().control;
|
|
@@ -3653,7 +4844,11 @@ var MediaControllerRuntime = class {
|
|
|
3653
4844
|
const current = this.#store.getSnapshot();
|
|
3654
4845
|
const snapshot = this.#ports?.media.getSnapshot();
|
|
3655
4846
|
const state = this.#connection.getSnapshot().state;
|
|
3656
|
-
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
|
+
});
|
|
3657
4852
|
const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
|
|
3658
4853
|
const media = Object.freeze({ ...current.media, local, remote, screenShare: local.screen, incomingRequests: this.#requests });
|
|
3659
4854
|
if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
|
|
@@ -3690,12 +4885,18 @@ function initialTrackEntries(microphone, camera) {
|
|
|
3690
4885
|
if (camera) entries.push(["camera", camera]);
|
|
3691
4886
|
return entries;
|
|
3692
4887
|
}
|
|
3693
|
-
function localMedia(source, tracks, snapshot, intended, connectionState) {
|
|
4888
|
+
function localMedia(source, tracks, snapshot, intended, connectionState, pending) {
|
|
3694
4889
|
const track = tracks.get(source) ?? null;
|
|
3695
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 });
|
|
3696
4893
|
if (publication?.enabled) return Object.freeze({ source, state: "enabled", track });
|
|
4894
|
+
if (publication && connectionState === "live") return Object.freeze({ source, state: "disabled", track });
|
|
3697
4895
|
return Object.freeze({ source, state: localMediaState(source, track, intended, connectionState), track });
|
|
3698
4896
|
}
|
|
4897
|
+
function locallyMuted(intended, track, publication) {
|
|
4898
|
+
return !intended && track?.enabled === false && publication?.enabled === false;
|
|
4899
|
+
}
|
|
3699
4900
|
function localMediaState(source, track, intended, connectionState) {
|
|
3700
4901
|
if (!mediaIsDesired(source, track, intended)) return inactiveLocalMediaState(connectionState, track);
|
|
3701
4902
|
if (connectionState === "joining" || track) return "requesting";
|
|
@@ -3738,19 +4939,265 @@ function sameDeviceList(left, right) {
|
|
|
3738
4939
|
return left.length === right.length && left.every((device, index) => device.deviceId === right[index]?.deviceId && device.label === right[index]?.label);
|
|
3739
4940
|
}
|
|
3740
4941
|
function captureError(cause, action, permissionMessage) {
|
|
4942
|
+
if (cause instanceof ConnectionError) return cause;
|
|
3741
4943
|
if (isPermissionDenied(cause)) return new ConnectionError({ code: "permission_denied", action, recoverable: true, message: permissionMessage }, { cause });
|
|
3742
|
-
|
|
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 });
|
|
3743
4946
|
}
|
|
3744
4947
|
function isPermissionDenied(cause) {
|
|
3745
|
-
return
|
|
4948
|
+
return describeMediaCaptureError(cause).code === "permission_denied";
|
|
3746
4949
|
}
|
|
3747
4950
|
function isAccessInvalid2(cause) {
|
|
3748
4951
|
return typeof cause === "object" && cause !== null && "code" in cause && (cause.code === "access.invalid" || cause.code === "invalid_access");
|
|
3749
4952
|
}
|
|
4953
|
+
function isSyncRejection(cause) {
|
|
4954
|
+
return isAccessInvalid2(cause) || cause instanceof V1SyncError && cause.code === "terminal_failure";
|
|
4955
|
+
}
|
|
3750
4956
|
|
|
3751
|
-
// src/space-client/
|
|
4957
|
+
// src/space-client/offline-actions.ts
|
|
3752
4958
|
import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
|
|
3753
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
|
+
|
|
3754
5201
|
// src/sync/v1-types.ts
|
|
3755
5202
|
var V1_CAPABILITIES = [
|
|
3756
5203
|
"publishAudio",
|
|
@@ -3781,19 +5228,21 @@ var V1_CAPABILITIES = [
|
|
|
3781
5228
|
// src/space-client/participants-controller.ts
|
|
3782
5229
|
var CAPABILITIES = new Set(V1_CAPABILITIES);
|
|
3783
5230
|
var EMPTY_PARTICIPANTS = Object.freeze({ roster: Object.freeze([]), admissionQueue: Object.freeze([]) });
|
|
3784
|
-
var ParticipantsControllerService = class extends
|
|
5231
|
+
var ParticipantsControllerService = class extends Context8.Service()("@chalk/client/ParticipantsController") {
|
|
3785
5232
|
};
|
|
3786
|
-
var makeParticipantsController = (connection, store, diagnostics) =>
|
|
3787
|
-
|
|
3788
|
-
(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())
|
|
3789
5236
|
);
|
|
3790
5237
|
var ParticipantsControllerRuntime = class {
|
|
3791
5238
|
#connection;
|
|
3792
5239
|
#store;
|
|
3793
5240
|
#diagnostics;
|
|
5241
|
+
#offline;
|
|
3794
5242
|
#unsubscribePorts = null;
|
|
3795
5243
|
#unsubscribe = null;
|
|
3796
|
-
constructor(connection, store, diagnostics) {
|
|
5244
|
+
constructor(connection, store, diagnostics, offline) {
|
|
5245
|
+
this.#offline = offline;
|
|
3797
5246
|
this.#connection = connection;
|
|
3798
5247
|
this.#store = store;
|
|
3799
5248
|
this.#diagnostics = diagnostics;
|
|
@@ -3819,7 +5268,7 @@ var ParticipantsControllerRuntime = class {
|
|
|
3819
5268
|
requestMedia = (participantId, kind) => this.#command("media_request.request", ["capability_decision", "command_commit", "target_result"], () => {
|
|
3820
5269
|
assertIdentifier(participantId, "participant ID");
|
|
3821
5270
|
if (kind !== "microphone" && kind !== "camera") throw invalid("Media requests must target a microphone or camera");
|
|
3822
|
-
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));
|
|
3823
5272
|
});
|
|
3824
5273
|
remove = (participantId) => this.#command("moderation.remove", ["capability_decision", "command_commit", "target_delivery"], () => {
|
|
3825
5274
|
assertIdentifier(participantId, "participant ID");
|
|
@@ -3833,16 +5282,16 @@ var ParticipantsControllerRuntime = class {
|
|
|
3833
5282
|
assertIdentifier(requestId, "admission request ID");
|
|
3834
5283
|
return ({ sync }) => foreign3(() => sync.deny(requestId));
|
|
3835
5284
|
});
|
|
3836
|
-
raiseHand = () => this.#
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
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
|
+
}
|
|
3846
5295
|
renameSelf = (displayName) => this.#command("participant.rename", ["membership_transition", "participant_result"], () => {
|
|
3847
5296
|
assertName(displayName, "display name");
|
|
3848
5297
|
return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
|
|
@@ -3855,18 +5304,18 @@ var ParticipantsControllerRuntime = class {
|
|
|
3855
5304
|
}
|
|
3856
5305
|
#command(name, checkpoints, operation) {
|
|
3857
5306
|
const diagnostic = this.#diagnostics?.startOperation(name);
|
|
3858
|
-
return
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
() =>
|
|
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(() => {
|
|
3864
5313
|
for (const checkpoint of checkpoints.slice(1)) diagnostic?.observe("observed", checkpoint);
|
|
3865
5314
|
if (name.startsWith("moderation.")) diagnostic?.notObservable("target_application", "target_projection_is_conditional");
|
|
3866
5315
|
diagnostic?.succeed();
|
|
3867
5316
|
})
|
|
3868
5317
|
),
|
|
3869
|
-
|
|
5318
|
+
Effect7.tapError(() => Effect7.sync(() => diagnostic?.fail("command_failed")))
|
|
3870
5319
|
);
|
|
3871
5320
|
}
|
|
3872
5321
|
#bind(ports) {
|
|
@@ -3888,7 +5337,7 @@ var ParticipantsControllerRuntime = class {
|
|
|
3888
5337
|
}
|
|
3889
5338
|
};
|
|
3890
5339
|
function foreign3(operation) {
|
|
3891
|
-
return
|
|
5340
|
+
return Effect7.tryPromise({ try: operation, catch: (cause) => cause });
|
|
3892
5341
|
}
|
|
3893
5342
|
function assertIdentifier(value, label) {
|
|
3894
5343
|
if (typeof value !== "string" || value.trim().length === 0) throw invalid(`A non-empty ${label} is required`);
|
|
@@ -3998,20 +5447,20 @@ function sameValues(left, right) {
|
|
|
3998
5447
|
}
|
|
3999
5448
|
|
|
4000
5449
|
// src/space-client/reactions-controller.ts
|
|
4001
|
-
import { Clock as Clock4, Context as
|
|
5450
|
+
import { Clock as Clock4, Context as Context9, Effect as Effect8, Layer as Layer9 } from "effect";
|
|
4002
5451
|
var MAX_VISIBLE_REACTIONS = 24;
|
|
4003
5452
|
var REACTIONS = /* @__PURE__ */ new Set(["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F389}"]);
|
|
4004
|
-
var ReactionsControllerService = class extends
|
|
5453
|
+
var ReactionsControllerService = class extends Context9.Service()("@chalk/client/ReactionsController") {
|
|
4005
5454
|
};
|
|
4006
|
-
var makeReactionsController = (connection, store, diagnostics) =>
|
|
5455
|
+
var makeReactionsController = (connection, store, diagnostics) => Effect8.gen(function* () {
|
|
4007
5456
|
const clock = yield* Clock4.Clock;
|
|
4008
|
-
const scope = yield*
|
|
4009
|
-
const context = yield*
|
|
5457
|
+
const scope = yield* Effect8.scope;
|
|
5458
|
+
const context = yield* Effect8.context();
|
|
4010
5459
|
const fork = (effect) => {
|
|
4011
|
-
void
|
|
5460
|
+
void Effect8.runForkWith(context)(Effect8.forkIn(effect, scope).pipe(Effect8.asVoid));
|
|
4012
5461
|
};
|
|
4013
5462
|
const controller = new ReactionsControllerRuntime(connection, store, clock.currentTimeMillisUnsafe, fork, diagnostics);
|
|
4014
|
-
yield*
|
|
5463
|
+
yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
|
|
4015
5464
|
return controller;
|
|
4016
5465
|
});
|
|
4017
5466
|
var ReactionsControllerRuntime = class {
|
|
@@ -4034,26 +5483,26 @@ var ReactionsControllerRuntime = class {
|
|
|
4034
5483
|
}
|
|
4035
5484
|
send = (emoji) => {
|
|
4036
5485
|
const operation = this.#diagnostics?.startOperation("reaction.send");
|
|
4037
|
-
return
|
|
5486
|
+
return Effect8.try({
|
|
4038
5487
|
try: () => {
|
|
4039
5488
|
if (!REACTIONS.has(emoji)) throw new SpaceClientError({ code: "reaction.invalid", recoverable: false, message: "The reaction is not supported" });
|
|
4040
5489
|
},
|
|
4041
5490
|
catch: normalizeClientError
|
|
4042
5491
|
}).pipe(
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
(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(() => {
|
|
4049
5498
|
this.#observe(reaction);
|
|
4050
5499
|
operation?.observe("observed", "sender_result");
|
|
4051
5500
|
operation?.notObservable("recipient_projection", "recipient_projection_is_conditional");
|
|
4052
5501
|
operation?.succeed();
|
|
4053
5502
|
})
|
|
4054
5503
|
),
|
|
4055
|
-
|
|
4056
|
-
|
|
5504
|
+
Effect8.mapError(normalizeClientError),
|
|
5505
|
+
Effect8.tapError(() => Effect8.sync(() => operation?.fail("send_failed")))
|
|
4057
5506
|
);
|
|
4058
5507
|
};
|
|
4059
5508
|
dispose() {
|
|
@@ -4083,9 +5532,9 @@ var ReactionsControllerRuntime = class {
|
|
|
4083
5532
|
this.#expiryGenerations.set(reaction.eventId, generation);
|
|
4084
5533
|
const delay = Math.max(0, expiresAt2 - this.#now());
|
|
4085
5534
|
this.#fork(
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
5535
|
+
Effect8.sleep(delay).pipe(
|
|
5536
|
+
Effect8.andThen(
|
|
5537
|
+
Effect8.sync(() => {
|
|
4089
5538
|
if (this.#expiryGenerations.get(reaction.eventId) === generation) this.#remove(reaction.eventId);
|
|
4090
5539
|
})
|
|
4091
5540
|
)
|
|
@@ -4112,7 +5561,7 @@ var ReactionsControllerRuntime = class {
|
|
|
4112
5561
|
}
|
|
4113
5562
|
};
|
|
4114
5563
|
function foreign4(operation) {
|
|
4115
|
-
return
|
|
5564
|
+
return Effect8.tryPromise({ try: operation, catch: (cause) => cause });
|
|
4116
5565
|
}
|
|
4117
5566
|
function reactionFor(reaction) {
|
|
4118
5567
|
return Object.freeze({ ...reaction });
|
|
@@ -4127,23 +5576,122 @@ function sameReactions(left, right) {
|
|
|
4127
5576
|
});
|
|
4128
5577
|
}
|
|
4129
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
|
+
|
|
4130
5678
|
// src/space-client/whiteboard-controller.ts
|
|
4131
|
-
import { Context as
|
|
5679
|
+
import { Context as Context11, Effect as Effect10, Layer as Layer11 } from "effect";
|
|
4132
5680
|
var EMPTY_SUMMARY = Object.freeze({ status: "unsubscribed", sceneId: null, revision: null, capabilities: Object.freeze([]), canDraw: false, canClear: false, presenting: false, error: null });
|
|
4133
|
-
var WhiteboardControllerService = class extends
|
|
5681
|
+
var WhiteboardControllerService = class extends Context11.Service()("@chalk/client/WhiteboardController") {
|
|
4134
5682
|
};
|
|
4135
|
-
var makeWhiteboardController = (connection, store, create) =>
|
|
4136
|
-
const context = yield*
|
|
4137
|
-
const controller = yield*
|
|
5683
|
+
var makeWhiteboardController = (connection, store, create) => Effect10.gen(function* () {
|
|
5684
|
+
const context = yield* Effect10.context();
|
|
5685
|
+
const controller = yield* Effect10.sync(() => {
|
|
4138
5686
|
let instance = null;
|
|
4139
5687
|
const transport = create?.({
|
|
4140
|
-
token: () =>
|
|
5688
|
+
token: () => Effect10.runPromiseWith(context)(connection.getSyncToken()),
|
|
4141
5689
|
onSummary: (summary) => instance?.observe(summary)
|
|
4142
5690
|
}) ?? null;
|
|
4143
5691
|
instance = new WhiteboardControllerRuntime(connection, store, transport);
|
|
4144
5692
|
return instance;
|
|
4145
5693
|
});
|
|
4146
|
-
yield*
|
|
5694
|
+
yield* Effect10.addFinalizer(() => Effect10.sync(() => controller.dispose()));
|
|
4147
5695
|
return controller;
|
|
4148
5696
|
});
|
|
4149
5697
|
var WhiteboardControllerRuntime = class {
|
|
@@ -4205,15 +5753,17 @@ function sameError2(left, right) {
|
|
|
4205
5753
|
}
|
|
4206
5754
|
|
|
4207
5755
|
// src/space-client/controller-effects.ts
|
|
4208
|
-
var ControllerEffectsService = class extends
|
|
5756
|
+
var ControllerEffectsService = class extends Context12.Service()("@chalk/client/ControllerEffects") {
|
|
4209
5757
|
};
|
|
4210
|
-
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 });
|
|
4211
5760
|
const media = yield* makeMediaController(input.connection, input.store, input.mediaDeviceSelection, input.episodeDiagnostics);
|
|
4212
|
-
const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics });
|
|
4213
|
-
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);
|
|
4214
5763
|
const reactions = yield* makeReactionsController(input.connection, input.store, input.episodeDiagnostics);
|
|
5764
|
+
const recording = yield* makeRecordingController(input.connection, input.store, input.episodeDiagnostics);
|
|
4215
5765
|
const whiteboard = yield* makeWhiteboardController(input.connection, input.store, input.featureFactories?.createWhiteboardClient);
|
|
4216
|
-
return { media, chat, participants, reactions, whiteboard };
|
|
5766
|
+
return { media, offline: offlineActions, chat, participants, reactions, recording, whiteboard };
|
|
4217
5767
|
});
|
|
4218
5768
|
|
|
4219
5769
|
// src/space-client/media-device-selection.ts
|
|
@@ -4268,7 +5818,7 @@ function requireDeviceId(deviceId) {
|
|
|
4268
5818
|
}
|
|
4269
5819
|
|
|
4270
5820
|
// src/space-client/episode-diagnostic-runtime.ts
|
|
4271
|
-
import { encodedEventSize, getActionContract, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
|
|
5821
|
+
import { encodedEventSize, getActionContract, parseDiagnosticEventDraft as parseDiagnosticEventDraft2, redactDiagnosticAttributes as redactDiagnosticAttributes2 } from "@q9labsai/diagnostics-contracts";
|
|
4272
5822
|
var DEFAULT_RING_EVENTS = 256;
|
|
4273
5823
|
var DEFAULT_RING_BYTES = 256 * 1024;
|
|
4274
5824
|
var DEFAULT_RING_AGE_MS = 2 * 60 * 1e3;
|
|
@@ -4376,6 +5926,15 @@ var EpisodeDiagnosticRuntime = class {
|
|
|
4376
5926
|
activeContext() {
|
|
4377
5927
|
return this.#enabled() ? this.context() : void 0;
|
|
4378
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
|
+
}
|
|
4379
5938
|
startOperation(name, attributes, parentOperationRef, producerOperationRef, correlationInput) {
|
|
4380
5939
|
if (!this.#enabled()) return void 0;
|
|
4381
5940
|
const credentialGeneration = this.#credential?.generation;
|
|
@@ -4441,7 +6000,7 @@ var EpisodeDiagnosticRuntime = class {
|
|
|
4441
6000
|
});
|
|
4442
6001
|
}
|
|
4443
6002
|
#draft(input) {
|
|
4444
|
-
return
|
|
6003
|
+
return parseDiagnosticEventDraft2(
|
|
4445
6004
|
buildDraftInput(input, {
|
|
4446
6005
|
eventId: safeId(this.#createId()),
|
|
4447
6006
|
producerSequence: ++this.#sequence,
|
|
@@ -4752,7 +6311,7 @@ function buildDraftInput(input, context) {
|
|
|
4752
6311
|
expectation: expectationFor(contract, checkpoint),
|
|
4753
6312
|
correlation: optionalCorrelation(correlation),
|
|
4754
6313
|
release: context.release,
|
|
4755
|
-
attributes:
|
|
6314
|
+
attributes: redactDiagnosticAttributes2(input.attributes).attributes
|
|
4756
6315
|
};
|
|
4757
6316
|
}
|
|
4758
6317
|
function eventReferences(input) {
|
|
@@ -4776,8 +6335,15 @@ function stripTrailingUrlSlash(value) {
|
|
|
4776
6335
|
}
|
|
4777
6336
|
function intakeResponse(value) {
|
|
4778
6337
|
if (!isRecord3(value) || typeof value.diagnosticReference !== "string" || !Number.isSafeInteger(value.committedCursor)) return null;
|
|
4779
|
-
|
|
4780
|
-
|
|
6338
|
+
const accepted = legacyEmptyAcknowledgement(value.accepted);
|
|
6339
|
+
const duplicates = legacyEmptyAcknowledgement(value.duplicates);
|
|
6340
|
+
const conflicts = legacyEmptyAcknowledgement(value.conflicts);
|
|
6341
|
+
if (accepted === null || duplicates === null || conflicts === null) return null;
|
|
6342
|
+
return { accepted, duplicates, conflicts };
|
|
6343
|
+
}
|
|
6344
|
+
function legacyEmptyAcknowledgement(value) {
|
|
6345
|
+
if (value === null) return [];
|
|
6346
|
+
return Array.isArray(value) ? value : null;
|
|
4781
6347
|
}
|
|
4782
6348
|
function completeDelivery(delivery, expected) {
|
|
4783
6349
|
const all = [...delivery.accepted, ...delivery.duplicates, ...delivery.conflicts];
|
|
@@ -4866,9 +6432,125 @@ function renderRegistry() {
|
|
|
4866
6432
|
return target[REGISTRY];
|
|
4867
6433
|
}
|
|
4868
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
|
+
|
|
4869
6551
|
// src/space-client/core.ts
|
|
4870
6552
|
var syncClientsByDependencies = /* @__PURE__ */ new WeakMap();
|
|
4871
|
-
var SpaceClientCoreService = class extends
|
|
6553
|
+
var SpaceClientCoreService = class extends Context13.Service()("@chalk/client/SpaceClientCore") {
|
|
4872
6554
|
};
|
|
4873
6555
|
var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
4874
6556
|
const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
|
|
@@ -4881,7 +6563,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
4881
6563
|
now: baseDependencies.clock.now,
|
|
4882
6564
|
setTimeout: baseDependencies.clock.setTimeout,
|
|
4883
6565
|
clearTimeout: baseDependencies.clock.clearTimeout,
|
|
4884
|
-
release: { id: "chalk-client@4.1.
|
|
6566
|
+
release: { id: "chalk-client@4.1.16" }
|
|
4885
6567
|
});
|
|
4886
6568
|
const diagnosticSyncClients = /* @__PURE__ */ new Set();
|
|
4887
6569
|
const dependencies = {
|
|
@@ -4903,6 +6585,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
4903
6585
|
initialCameraEnabled: platform.initialCameraEnabled,
|
|
4904
6586
|
telemetry: platform.telemetry,
|
|
4905
6587
|
recordRtcSummary: platform.recordRtcSummary,
|
|
6588
|
+
recordReconnect: platform.recordReconnect,
|
|
4906
6589
|
dependencies,
|
|
4907
6590
|
diagnostics: {
|
|
4908
6591
|
onEvent: (event) => {
|
|
@@ -4911,30 +6594,49 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
4911
6594
|
}
|
|
4912
6595
|
}
|
|
4913
6596
|
});
|
|
4914
|
-
return makeSpaceClientCoreLayerFromServices(options, platform).pipe(
|
|
6597
|
+
return makeSpaceClientCoreLayerFromServices(options, platform).pipe(Layer13.provideMerge(Layer13.mergeAll(makeSpaceStoreLayer, lifecycle, makeConnectionPlatformLayer(dependencies), liveClock(dependencies))));
|
|
4915
6598
|
};
|
|
4916
|
-
var makeSpaceClientCoreLayerFromServices = (options, platform = {}) =>
|
|
6599
|
+
var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer13.effect(
|
|
4917
6600
|
SpaceClientCoreService,
|
|
4918
|
-
|
|
4919
|
-
const store = yield*
|
|
4920
|
-
const lifecycle = yield*
|
|
4921
|
-
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);
|
|
4922
6605
|
const space = options.space.trim();
|
|
4923
|
-
if (space.length === 0) return yield*
|
|
4924
|
-
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"));
|
|
4925
6608
|
const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
|
|
4926
6609
|
const selection = new MediaDeviceSelection(dependencies.mediaDevices);
|
|
4927
6610
|
const episodeDiagnostics = episodeDiagnosticsForDependencies(dependencies);
|
|
4928
6611
|
if (episodeDiagnostics) registerEpisodeDiagnosticConnection(lifecycle, episodeDiagnostics);
|
|
4929
|
-
const controllers = yield* makeControllerEffects({
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
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())
|
|
4933
6634
|
);
|
|
4934
6635
|
})
|
|
4935
6636
|
);
|
|
4936
6637
|
var SpaceClientCore = class {
|
|
4937
6638
|
controllers;
|
|
6639
|
+
feedback;
|
|
4938
6640
|
#connection;
|
|
4939
6641
|
#store;
|
|
4940
6642
|
#episodeDiagnostics;
|
|
@@ -4949,13 +6651,29 @@ var SpaceClientCore = class {
|
|
|
4949
6651
|
#leaveOperation;
|
|
4950
6652
|
#participantJoinOperation;
|
|
4951
6653
|
#syncConnectOperation;
|
|
4952
|
-
constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients) {
|
|
6654
|
+
constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients, feedback) {
|
|
4953
6655
|
this.#connection = connection;
|
|
4954
6656
|
this.#store = store;
|
|
4955
6657
|
this.controllers = controllers;
|
|
4956
6658
|
this.#episodeDiagnostics = episodeDiagnostics;
|
|
4957
6659
|
this.#episodeDiagnosticDependencies = episodeDiagnosticDependencies;
|
|
4958
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
|
+
});
|
|
4959
6677
|
this.#store.updateConnection(connection.getSnapshot());
|
|
4960
6678
|
this.#previous = this.#store.getSnapshot();
|
|
4961
6679
|
this.#unsubscribeStore = this.#store.subscribe(() => this.#publishStore());
|
|
@@ -4973,47 +6691,47 @@ var SpaceClientCore = class {
|
|
|
4973
6691
|
join(options = {}) {
|
|
4974
6692
|
this.#participantJoinOperation = this.#episodeDiagnostics?.startOperation("participant.join");
|
|
4975
6693
|
this.#syncConnectOperation = this.#episodeDiagnostics?.startOperation("sync.connect");
|
|
4976
|
-
return
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
() =>
|
|
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(() => {
|
|
4983
6701
|
this.#participantJoinOperation?.succeed();
|
|
4984
6702
|
this.#syncConnectOperation?.succeed();
|
|
4985
6703
|
})
|
|
4986
6704
|
),
|
|
4987
|
-
|
|
4988
|
-
() =>
|
|
6705
|
+
Effect12.tapError(
|
|
6706
|
+
() => Effect12.sync(() => {
|
|
4989
6707
|
this.#participantJoinOperation?.fail("join_failed");
|
|
4990
6708
|
this.#syncConnectOperation?.fail("connect_failed");
|
|
4991
6709
|
})
|
|
4992
6710
|
),
|
|
4993
|
-
|
|
6711
|
+
Effect12.mapError(normalizeClientError)
|
|
4994
6712
|
);
|
|
4995
6713
|
}
|
|
4996
6714
|
leave() {
|
|
4997
6715
|
this.#leaveOperation = this.#episodeDiagnostics?.startOperation("participant.leave");
|
|
4998
6716
|
return this.#connection.leave().pipe(
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
6717
|
+
Effect12.tap(() => Effect12.sync(() => this.#leaveOperation?.succeed())),
|
|
6718
|
+
Effect12.tapError(() => Effect12.sync(() => this.#leaveOperation?.fail("leave_failed"))),
|
|
6719
|
+
Effect12.mapError(normalizeClientError)
|
|
5002
6720
|
);
|
|
5003
6721
|
}
|
|
5004
6722
|
endEpisode() {
|
|
5005
6723
|
return trackedEffect(
|
|
5006
6724
|
this.#episodeDiagnostics,
|
|
5007
6725
|
"episode.end.authorized",
|
|
5008
|
-
this.#connection.runCommand(({ sync }) =>
|
|
5009
|
-
).pipe(
|
|
6726
|
+
this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode()).pipe(Effect12.tap(() => this.#connection.confirmEpisodeEnded)))
|
|
6727
|
+
).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
5010
6728
|
}
|
|
5011
6729
|
extendEpisode(minutes) {
|
|
5012
6730
|
return trackedEffect(
|
|
5013
6731
|
this.#episodeDiagnostics,
|
|
5014
6732
|
"episode.deadline.extend",
|
|
5015
|
-
this.#connection.runCommand(({ sync }) =>
|
|
5016
|
-
).pipe(
|
|
6733
|
+
this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes)))
|
|
6734
|
+
).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
5017
6735
|
}
|
|
5018
6736
|
on(event, handler) {
|
|
5019
6737
|
this.#handlers[event].add(handler);
|
|
@@ -5026,6 +6744,7 @@ var SpaceClientCore = class {
|
|
|
5026
6744
|
this.#unsubscribeStore?.();
|
|
5027
6745
|
this.#unsubscribeConnection = null;
|
|
5028
6746
|
this.#unsubscribeStore = null;
|
|
6747
|
+
this.feedback.dispose();
|
|
5029
6748
|
this.#disposeDiagnostics();
|
|
5030
6749
|
this.#clearHandlers();
|
|
5031
6750
|
}
|
|
@@ -5127,25 +6846,25 @@ var SpaceClientCore = class {
|
|
|
5127
6846
|
}
|
|
5128
6847
|
};
|
|
5129
6848
|
function liveClock(dependencies) {
|
|
5130
|
-
return
|
|
6849
|
+
return Layer13.succeed(Clock5.Clock, {
|
|
5131
6850
|
currentTimeMillisUnsafe: dependencies.clock.now,
|
|
5132
|
-
currentTimeMillis:
|
|
6851
|
+
currentTimeMillis: Effect12.sync(dependencies.clock.now),
|
|
5133
6852
|
currentTimeNanosUnsafe: () => BigInt(dependencies.clock.now()) * 1000000n,
|
|
5134
|
-
currentTimeNanos:
|
|
5135
|
-
sleep: (duration) =>
|
|
5136
|
-
const handle = dependencies.clock.setTimeout(() => resume(
|
|
5137
|
-
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));
|
|
5138
6857
|
})
|
|
5139
6858
|
});
|
|
5140
6859
|
}
|
|
5141
|
-
function
|
|
5142
|
-
return
|
|
6860
|
+
function foreign6(operation) {
|
|
6861
|
+
return Effect12.tryPromise({ try: operation, catch: (cause) => cause });
|
|
5143
6862
|
}
|
|
5144
6863
|
function trackedEffect(diagnostics, name, effect) {
|
|
5145
6864
|
const operation = diagnostics?.startOperation(name);
|
|
5146
6865
|
return effect.pipe(
|
|
5147
|
-
|
|
5148
|
-
|
|
6866
|
+
Effect12.tap(() => Effect12.sync(() => operation?.succeed())),
|
|
6867
|
+
Effect12.tapError(() => Effect12.sync(() => operation?.fail("operation_failed")))
|
|
5149
6868
|
);
|
|
5150
6869
|
}
|
|
5151
6870
|
async function resolveAccessGrant(input) {
|
|
@@ -5167,7 +6886,7 @@ function diagnosticAccessOperation(request) {
|
|
|
5167
6886
|
}
|
|
5168
6887
|
async function fetchAccessGrant2(input) {
|
|
5169
6888
|
if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
|
|
5170
|
-
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 });
|
|
5171
6890
|
}
|
|
5172
6891
|
function accessReason(reason) {
|
|
5173
6892
|
if (reason === void 0 || reason === "join") return "join";
|
|
@@ -5206,17 +6925,17 @@ function sameDiagnosticTrack(left, right) {
|
|
|
5206
6925
|
}
|
|
5207
6926
|
|
|
5208
6927
|
// src/space-client/space-client.ts
|
|
5209
|
-
import { Effect as
|
|
6928
|
+
import { Effect as Effect14, ManagedRuntime } from "effect";
|
|
5210
6929
|
|
|
5211
6930
|
// src/space-client/promise-facade.ts
|
|
5212
|
-
import { Effect as
|
|
6931
|
+
import { Effect as Effect13 } from "effect";
|
|
5213
6932
|
function toPromiseController(runtime, controller) {
|
|
5214
6933
|
const projected = {};
|
|
5215
6934
|
for (const [name, command] of Object.entries(controller)) {
|
|
5216
6935
|
if (name === "configure" || name === "dispose") continue;
|
|
5217
6936
|
projected[name] = (...args) => {
|
|
5218
6937
|
const result = command(...args);
|
|
5219
|
-
return
|
|
6938
|
+
return Effect13.isEffect(result) ? runtime.runPromise(result) : result;
|
|
5220
6939
|
};
|
|
5221
6940
|
}
|
|
5222
6941
|
return projected;
|
|
@@ -5224,24 +6943,30 @@ function toPromiseController(runtime, controller) {
|
|
|
5224
6943
|
|
|
5225
6944
|
// src/space-client/space-client.ts
|
|
5226
6945
|
var PromiseSpaceClient = class {
|
|
6946
|
+
feedback;
|
|
5227
6947
|
media;
|
|
5228
6948
|
chat;
|
|
5229
6949
|
participants;
|
|
6950
|
+
offline;
|
|
5230
6951
|
reactions;
|
|
6952
|
+
recording;
|
|
5231
6953
|
whiteboard;
|
|
5232
6954
|
#core;
|
|
5233
6955
|
#runtime;
|
|
5234
6956
|
#disposed = false;
|
|
5235
6957
|
constructor(options, platform) {
|
|
5236
6958
|
this.#runtime = ManagedRuntime.make(makeSpaceClientCoreLayer(options, platform));
|
|
5237
|
-
this.#core = this.#runtime.runSync(
|
|
6959
|
+
this.#core = this.#runtime.runSync(Effect14.service(SpaceClientCoreService));
|
|
5238
6960
|
const controllers = this.#core.controllers;
|
|
6961
|
+
this.feedback = this.#core.feedback;
|
|
5239
6962
|
this.media = toPromiseController(this.#runtime, controllers.media);
|
|
5240
6963
|
const chat = toPromiseController(this.#runtime, controllers.chat);
|
|
5241
|
-
const { upload, url, ...chatCommands } = chat;
|
|
5242
|
-
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);
|
|
5243
6967
|
this.participants = toPromiseController(this.#runtime, controllers.participants);
|
|
5244
6968
|
this.reactions = toPromiseController(this.#runtime, controllers.reactions);
|
|
6969
|
+
this.recording = toPromiseController(this.#runtime, controllers.recording);
|
|
5245
6970
|
this.whiteboard = toPromiseController(this.#runtime, controllers.whiteboard);
|
|
5246
6971
|
}
|
|
5247
6972
|
join = (options) => this.#runtime.runPromise(this.#core.join(options));
|
|
@@ -5293,6 +7018,35 @@ export {
|
|
|
5293
7018
|
SpaceStoreService,
|
|
5294
7019
|
makeSpaceStoreLayer,
|
|
5295
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,
|
|
5296
7050
|
SpaceClientCoreService,
|
|
5297
7051
|
makeSpaceClientCoreLayer,
|
|
5298
7052
|
makeSpaceClientCoreLayerFromServices,
|