@q9labsai/chalk-client 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/dist/chat-files/types.d.ts +3 -0
- package/dist/chunk-6LNRDKJF.js +162 -0
- package/dist/{chunk-3ZN7IXOZ.js → chunk-GA3JDPFI.js} +252 -2
- package/dist/{chunk-RJRSLXVT.js → chunk-RVUN6LW4.js} +764 -265
- package/dist/{chunk-K7PNW3KI.js → chunk-TVNI2XUZ.js} +172 -18
- package/dist/client.d.ts +205 -0
- package/dist/connection/dependencies.d.ts +3 -0
- package/dist/connection/diagnostics.d.ts +3 -2
- package/dist/connection/index.d.ts +1 -0
- package/dist/connection/lifecycle.d.ts +1 -0
- package/dist/connection/types.d.ts +2 -0
- package/dist/effect.js +62 -4
- package/dist/generated/http-api.d.ts +201 -0
- package/dist/generated/schemas.d.ts +373 -0
- package/dist/generated/sync.d.ts +74 -3
- package/dist/generated/whiteboard-v1.d.ts +71 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -14
- package/dist/public-invites/code.d.ts +1 -0
- package/dist/public-invites/index.d.ts +1 -0
- package/dist/public-invites/index.js +6 -4
- package/dist/public-invites/words.d.ts +2 -0
- package/dist/space-client/reload-rejoin.d.ts +1 -1
- package/dist/space-client/types.d.ts +10 -1
- package/dist/sync/connect-diagnostics.d.ts +10 -0
- package/dist/sync/index.js +1 -1
- package/dist/sync/types.d.ts +1 -0
- package/dist/sync/v1-client.d.ts +2 -0
- package/dist/sync/v1-types.d.ts +8 -0
- package/dist/whiteboard/index.d.ts +2 -5
- package/dist/whiteboard/types.d.ts +26 -3
- package/dist/whiteboard/v1-client.d.ts +3 -1
- package/dist/whiteboard/v1-codec.d.ts +5 -1
- package/dist/whiteboard/v1-multipart.d.ts +1 -1
- package/dist/whiteboard/view-bounds.d.ts +5 -0
- package/dist/whiteboard/view-versions.d.ts +9 -0
- package/package.json +1 -1
- package/dist/chunk-KHHULLBZ.js +0 -142
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AccessGrantError,
|
|
3
|
-
requireParsedAccessGrant
|
|
4
|
-
} from "./chunk-3ZN7IXOZ.js";
|
|
5
1
|
import {
|
|
6
2
|
CloudflareSFUClient,
|
|
7
3
|
createCloudflareSFUHTTPTransport,
|
|
8
4
|
describeMediaCaptureError
|
|
9
5
|
} from "./chunk-DWKTGBHR.js";
|
|
6
|
+
import {
|
|
7
|
+
AccessGrantError,
|
|
8
|
+
requireParsedAccessGrant
|
|
9
|
+
} from "./chunk-GA3JDPFI.js";
|
|
10
10
|
import {
|
|
11
11
|
ChatAttachmentMimeTypeSchema,
|
|
12
12
|
ChatReactionSetFrameSchema,
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
createBrowserSyncLifecycle,
|
|
18
18
|
createBrowserWebSocketFactory,
|
|
19
19
|
createV1SyncClient,
|
|
20
|
+
formatSyncConnectDiagnostic,
|
|
20
21
|
scheduleSyncDialDeadline
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-TVNI2XUZ.js";
|
|
22
23
|
|
|
23
24
|
// src/access/manager.ts
|
|
24
25
|
import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
|
|
@@ -33,24 +34,24 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
|
|
|
33
34
|
Effect.gen(function* () {
|
|
34
35
|
const state = yield* SynchronizedRef.make({ access: null });
|
|
35
36
|
const automaticMediaReplacementListeners = /* @__PURE__ */ new Set();
|
|
36
|
-
const
|
|
37
|
+
const fetch2 = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(state, (current) => fetchAccessGrant(provider, { current, reason, replaceMediaConnection, force, refreshWindowMs })).pipe(
|
|
37
38
|
Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
|
|
38
39
|
Effect.map((result) => result.access)
|
|
39
40
|
);
|
|
40
|
-
const initialize = (reason = "join") =>
|
|
41
|
+
const initialize = (reason = "join") => fetch2(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure2) => failure2.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch2(reason, false, true) : Effect.fail(failure2)));
|
|
41
42
|
const millisecondsUntilRefresh = Effect.gen(function* () {
|
|
42
43
|
const access = yield* SynchronizedRef.get(state);
|
|
43
44
|
if (!access.access) return null;
|
|
44
45
|
return millisecondsUntil(access.access, yield* Clock.currentTimeMillis, refreshWindowMs);
|
|
45
46
|
});
|
|
46
|
-
const ensureFresh = (reason = "scheduled_refresh") =>
|
|
47
|
+
const ensureFresh = (reason = "scheduled_refresh") => fetch2(reason, false, false);
|
|
47
48
|
return {
|
|
48
49
|
current: SynchronizedRef.get(state).pipe(Effect.map((value) => value.access)),
|
|
49
50
|
currentUnsafe: () => SynchronizedRef.getUnsafe(state).access,
|
|
50
51
|
initialize,
|
|
51
52
|
ensureFresh,
|
|
52
|
-
refresh: (reason, replaceMediaConnection) =>
|
|
53
|
-
refreshAfterRejection: () =>
|
|
53
|
+
refresh: (reason, replaceMediaConnection) => fetch2(reason, replaceMediaConnection, true),
|
|
54
|
+
refreshAfterRejection: () => fetch2("access_retry", true, true),
|
|
54
55
|
getSyncToken: (reason = "sync_recovery") => ensureFresh(reason).pipe(Effect.map((access) => access.sync.token)),
|
|
55
56
|
getMediaToken: () => ensureFresh("scheduled_refresh").pipe(Effect.map((access) => access.media.token)),
|
|
56
57
|
subscribeAutomaticMediaReplacement: (listener) => Effect.sync(() => {
|
|
@@ -184,16 +185,19 @@ var CHALK_CHAT_ATTACHMENT_MIME_TYPES = [
|
|
|
184
185
|
];
|
|
185
186
|
|
|
186
187
|
// src/chat-files/types.ts
|
|
188
|
+
var CHAT_FILE_REACHABILITY_MESSAGE = "Could not reach the chat attachment service (network or browser access failure).";
|
|
187
189
|
var ChalkChatFileError = class extends Error {
|
|
188
190
|
operation;
|
|
189
191
|
code;
|
|
190
192
|
recoverable;
|
|
193
|
+
stage;
|
|
191
194
|
constructor(failure2, options) {
|
|
192
195
|
super(failure2.message, options);
|
|
193
196
|
this.name = "ChalkChatFileError";
|
|
194
197
|
this.operation = failure2.operation;
|
|
195
198
|
this.code = failure2.code;
|
|
196
199
|
this.recoverable = failure2.recoverable;
|
|
200
|
+
this.stage = failure2.stage ?? "request";
|
|
197
201
|
}
|
|
198
202
|
};
|
|
199
203
|
|
|
@@ -240,15 +244,19 @@ function createChatFileRequest(options) {
|
|
|
240
244
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
241
245
|
const baseUrl = options.baseUrl.replace(/\/+$/u, "");
|
|
242
246
|
return async function request(operation, path, init, accepts) {
|
|
243
|
-
|
|
247
|
+
const headers = new Headers(init.headers);
|
|
244
248
|
try {
|
|
245
|
-
const headers = new Headers(init.headers);
|
|
246
249
|
headers.set("authorization", `Bearer ${await options.token()}`);
|
|
250
|
+
} catch (cause) {
|
|
251
|
+
throw new ChalkChatFileError({ operation, code: "unavailable", stage: "token", recoverable: true, message: CHAT_FILE_REACHABILITY_MESSAGE }, { cause });
|
|
252
|
+
}
|
|
253
|
+
let response;
|
|
254
|
+
try {
|
|
247
255
|
response = await fetchImplementation(`${baseUrl}${path}`, { ...init, headers });
|
|
248
256
|
} catch (cause) {
|
|
249
|
-
throw new ChalkChatFileError({ operation, code: "unavailable", recoverable: true, message:
|
|
257
|
+
throw new ChalkChatFileError({ operation, code: "unavailable", recoverable: true, message: CHAT_FILE_REACHABILITY_MESSAGE }, { cause });
|
|
250
258
|
}
|
|
251
|
-
if (!response.ok) throw responseError(response, operation);
|
|
259
|
+
if (!response.ok) throw await responseError(response, operation);
|
|
252
260
|
let value;
|
|
253
261
|
try {
|
|
254
262
|
value = await response.json();
|
|
@@ -259,9 +267,14 @@ function createChatFileRequest(options) {
|
|
|
259
267
|
return value;
|
|
260
268
|
};
|
|
261
269
|
}
|
|
262
|
-
function responseError(response, operation) {
|
|
270
|
+
async function responseError(response, operation) {
|
|
263
271
|
const code = responseErrorCode(response.status);
|
|
264
|
-
|
|
272
|
+
let message = "Chat attachment request failed.";
|
|
273
|
+
if (response.status === 503) {
|
|
274
|
+
const value = await response.json().catch(() => null);
|
|
275
|
+
if (isRecord(value) && isRecord(value.error) && value.error.code === "chat.storage_unavailable") message = "Chat attachment storage is unavailable.";
|
|
276
|
+
}
|
|
277
|
+
return chatFileError(operation, code, response.status === 429 || response.status >= 500, message);
|
|
265
278
|
}
|
|
266
279
|
function responseErrorCode(status) {
|
|
267
280
|
if (status === 401) return "access.invalid";
|
|
@@ -306,71 +319,6 @@ function isAllowedMimeType(value) {
|
|
|
306
319
|
return typeof value === "string" && CHALK_CHAT_ATTACHMENT_MIME_TYPES.includes(value);
|
|
307
320
|
}
|
|
308
321
|
|
|
309
|
-
// src/whiteboard/file-download-transport.ts
|
|
310
|
-
function createChalkWhiteboardV1FileDownloadTransport(options) {
|
|
311
|
-
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
312
|
-
let baseUrlEnd = options.baseUrl.length;
|
|
313
|
-
while (baseUrlEnd > 0 && options.baseUrl[baseUrlEnd - 1] === "/") baseUrlEnd -= 1;
|
|
314
|
-
const baseUrl = options.baseUrl.slice(0, baseUrlEnd);
|
|
315
|
-
return {
|
|
316
|
-
getDownloadUrl: async (fileId) => {
|
|
317
|
-
const response = await authorizedFetch(fetchImplementation, options.token, `${baseUrl}/v1/whiteboard/files/${encodeURIComponent(fileId)}/download`, { method: "GET" });
|
|
318
|
-
if (!response.ok) throw await responseError2(response, "get_file_download");
|
|
319
|
-
const value = await response.json();
|
|
320
|
-
if (!isDownloadResponse2(value)) {
|
|
321
|
-
throw whiteboardError("get_file_download", "file_transfer_failed", false, "Whiteboard download service returned an invalid response.");
|
|
322
|
-
}
|
|
323
|
-
return value;
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
async function authorizedFetch(fetchImplementation, token, url, init) {
|
|
328
|
-
try {
|
|
329
|
-
const authorization = await token();
|
|
330
|
-
const headers = new Headers(init.headers);
|
|
331
|
-
headers.set("authorization", `Bearer ${authorization}`);
|
|
332
|
-
return await fetchImplementation(url, { ...init, headers });
|
|
333
|
-
} catch (cause) {
|
|
334
|
-
throw new ChalkWhiteboardV1Error(
|
|
335
|
-
{
|
|
336
|
-
operation: "get_file_download",
|
|
337
|
-
code: "storage_unavailable",
|
|
338
|
-
recoverable: true,
|
|
339
|
-
message: "Whiteboard file storage is unavailable."
|
|
340
|
-
},
|
|
341
|
-
{ cause }
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
async function responseError2(response, operation) {
|
|
346
|
-
const code = response.status === 401 || response.status === 403 ? "permission_denied" : response.status === 400 ? "invalid_payload" : response.status >= 500 || response.status === 429 ? "storage_unavailable" : "file_transfer_failed";
|
|
347
|
-
return whiteboardError(operation, code, response.status >= 500 || response.status === 429, "Whiteboard file request failed.");
|
|
348
|
-
}
|
|
349
|
-
function whiteboardError(operation, code, recoverable, message) {
|
|
350
|
-
return new ChalkWhiteboardV1Error({ operation, code, recoverable, message });
|
|
351
|
-
}
|
|
352
|
-
function isDownloadResponse2(value) {
|
|
353
|
-
return isExactObject(value, ["downloadUrl", "expiresAt"]) && isHttpUrl2(value.downloadUrl) && isDate2(value.expiresAt);
|
|
354
|
-
}
|
|
355
|
-
function isExactObject(value, keys) {
|
|
356
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
357
|
-
}
|
|
358
|
-
function isHttpUrl2(value) {
|
|
359
|
-
if (typeof value !== "string") return false;
|
|
360
|
-
try {
|
|
361
|
-
const url = new URL(value);
|
|
362
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
363
|
-
} catch {
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
function isDate2(value) {
|
|
368
|
-
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// src/whiteboard/v1-codec.ts
|
|
372
|
-
import { Schema as Schema2 } from "effect";
|
|
373
|
-
|
|
374
322
|
// src/generated/whiteboard-v1.ts
|
|
375
323
|
import { Schema } from "effect";
|
|
376
324
|
var WhiteboardV1ProtocolMetadata = {
|
|
@@ -389,7 +337,12 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
389
337
|
view: {
|
|
390
338
|
name: "view_v1",
|
|
391
339
|
exactFields: ["name"],
|
|
392
|
-
extendedExactFields: ["name", "origins"]
|
|
340
|
+
extendedExactFields: ["name", "origins"],
|
|
341
|
+
ownerExactFields: ["name", "origins", "owner"]
|
|
342
|
+
},
|
|
343
|
+
drawPolicy: {
|
|
344
|
+
name: "draw_policy_v1",
|
|
345
|
+
exactFields: ["name"]
|
|
393
346
|
}
|
|
394
347
|
},
|
|
395
348
|
identity: {
|
|
@@ -398,6 +351,7 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
398
351
|
generationFenced: true
|
|
399
352
|
},
|
|
400
353
|
capabilities: ["drawWhiteboard", "manageWhiteboard"],
|
|
354
|
+
drawPolicies: ["everyone", "hosts", "picked"],
|
|
401
355
|
phases: [
|
|
402
356
|
{
|
|
403
357
|
id: "awaiting_hello",
|
|
@@ -406,7 +360,7 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
406
360
|
},
|
|
407
361
|
{
|
|
408
362
|
id: "live",
|
|
409
|
-
accepts: ["submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_presentation", "cursor", "view", "ping"],
|
|
363
|
+
accepts: ["submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_draw_policy", "set_presentation", "cursor", "view", "ping"],
|
|
410
364
|
heartbeatIntervalMs: 2e4,
|
|
411
365
|
missedDeadlinesBeforeClose: 2
|
|
412
366
|
}
|
|
@@ -436,6 +390,7 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
436
390
|
socketQueueMaxAgeMs: 5e3,
|
|
437
391
|
cursorTtlMs: 1e4,
|
|
438
392
|
cursorRatePerSecond: 60,
|
|
393
|
+
viewPeopleMaxItems: 500,
|
|
439
394
|
pendingOperationMaxItems: 128,
|
|
440
395
|
multipartUpdateMaxParts: 128,
|
|
441
396
|
multipartUpdateMaxItems: 1e4,
|
|
@@ -443,7 +398,8 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
443
398
|
multipartUpdateTimeoutMs: 1e4,
|
|
444
399
|
sceneElementMaxItems: 1e4,
|
|
445
400
|
sceneJsonMaxBytes: 67108864,
|
|
446
|
-
sceneObjectMaxBytes: 268435456
|
|
401
|
+
sceneObjectMaxBytes: 268435456,
|
|
402
|
+
drawAccessMaxGrants: 1024
|
|
447
403
|
},
|
|
448
404
|
cursor: {
|
|
449
405
|
sceneId: "uuid",
|
|
@@ -479,6 +435,9 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
479
435
|
setDrawPermission: {
|
|
480
436
|
exactFields: ["type", "operation_id", "participant_id", "can_draw"]
|
|
481
437
|
},
|
|
438
|
+
setDrawPolicy: {
|
|
439
|
+
exactFields: ["type", "operation_id", "policy"]
|
|
440
|
+
},
|
|
482
441
|
setPresentation: {
|
|
483
442
|
exactFields: ["type", "operation_id", "presenting"]
|
|
484
443
|
},
|
|
@@ -490,11 +449,14 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
490
449
|
maxCoordinate: 1e6,
|
|
491
450
|
maxExtent: 2e6,
|
|
492
451
|
epochField: "origin_epoch",
|
|
452
|
+
inputFields: ["input_id", "move"],
|
|
453
|
+
ownerFields: ["owner_id", "owner_sequence"],
|
|
493
454
|
originFields: ["origin_participant_id", "origin_sequence"],
|
|
494
455
|
clientExactFields: ["type", "x", "y", "width", "height", "bring"],
|
|
495
456
|
serverExactFields: ["type", "participant_id", "display_name", "x", "y", "width", "height", "bring", "occurred_at"]
|
|
496
457
|
},
|
|
497
458
|
welcome: {
|
|
459
|
+
ownerField: "view_owner_id",
|
|
498
460
|
exactFields: ["type", "protocol", "participant_id", "participant_generation", "capabilities", "participant_capabilities", "scene_id", "revision", "can_draw"],
|
|
499
461
|
extendedExactFields: ["type", "protocol", "participant_id", "participant_generation", "capabilities", "participant_capabilities", "scene_id", "revision", "can_draw", "presenting"]
|
|
500
462
|
},
|
|
@@ -513,6 +475,10 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
513
475
|
permissionUpdated: {
|
|
514
476
|
exactFields: ["type", "participant_id", "can_draw"]
|
|
515
477
|
},
|
|
478
|
+
drawAccess: {
|
|
479
|
+
exactFields: ["type", "policy", "can_draw", "capabilities", "grants"],
|
|
480
|
+
grantExactFields: ["participant_id", "can_draw"]
|
|
481
|
+
},
|
|
516
482
|
presentationUpdated: {
|
|
517
483
|
exactFields: ["type", "scene_id", "revision", "presenting"]
|
|
518
484
|
},
|
|
@@ -523,9 +489,9 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
523
489
|
exactFields: ["type", "correlation_id", "operation", "code", "recoverable", "message"]
|
|
524
490
|
}
|
|
525
491
|
},
|
|
526
|
-
clientFrames: ["hello", "submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_presentation", "cursor", "view", "ping"],
|
|
527
|
-
serverFrames: ["welcome", "snapshot_page", "update", "update_part", "commit", "cursor", "view", "permission_updated", "presentation_updated", "reset_required", "operation_error", "pong"],
|
|
528
|
-
receiptOperations: ["submit_update", "clear", "set_draw_permission", "set_presentation"],
|
|
492
|
+
clientFrames: ["hello", "submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_draw_policy", "set_presentation", "cursor", "view", "ping"],
|
|
493
|
+
serverFrames: ["welcome", "snapshot_page", "update", "update_part", "commit", "cursor", "view", "permission_updated", "draw_access", "presentation_updated", "reset_required", "operation_error", "view_ack", "pong"],
|
|
494
|
+
receiptOperations: ["submit_update", "clear", "set_draw_permission", "set_draw_policy", "set_presentation"],
|
|
529
495
|
receiptOutcomes: ["committed", "duplicate"],
|
|
530
496
|
resetReasons: ["scene_changed", "cursor_expired", "gap"],
|
|
531
497
|
errorCodes: ["unavailable", "permission_denied", "invalid_payload", "stale_scene", "cursor_reset_required", "rate_limited", "overloaded", "storage_unavailable"],
|
|
@@ -640,18 +606,33 @@ function isClientFrame(value) {
|
|
|
640
606
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.clear.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && isUuid(value.scene_id);
|
|
641
607
|
case "set_draw_permission":
|
|
642
608
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.setDrawPermission.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && isUuid(value.participant_id) && typeof value.can_draw === "boolean";
|
|
609
|
+
case "set_draw_policy":
|
|
610
|
+
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.setDrawPolicy.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && isDrawPolicy(value.policy);
|
|
643
611
|
case "set_presentation":
|
|
644
612
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.setPresentation.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && typeof value.presenting === "boolean";
|
|
645
613
|
case "cursor":
|
|
646
614
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.cursor.clientExactFields) && isFiniteNumber(value.x) && isFiniteNumber(value.y) && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
647
615
|
case "view":
|
|
648
|
-
return
|
|
616
|
+
return isViewInput(value, WhiteboardV1ProtocolMetadata.frames.view.clientExactFields) && isViewRect(value) && typeof value.bring === "boolean" && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
649
617
|
case "ping":
|
|
650
618
|
return hasExactKeys(value, ["type"]);
|
|
651
619
|
default:
|
|
652
620
|
return false;
|
|
653
621
|
}
|
|
654
622
|
}
|
|
623
|
+
function isViewInputId(input_id) {
|
|
624
|
+
return typeof input_id === "string" && /^[0-9a-fA-F-]{36}:[1-9][0-9]{0,15}$/.test(input_id) && isUuid(input_id.split(":")[0]) && Number.isSafeInteger(Number(input_id.split(":")[1]));
|
|
625
|
+
}
|
|
626
|
+
function isViewInput(value, fields) {
|
|
627
|
+
if (!("input_id" in value || "move" in value)) return isViewFields(value, fields);
|
|
628
|
+
const { input_id, move, ...rectangle } = value;
|
|
629
|
+
return isViewInputId(input_id) && typeof move === "boolean" && isViewFields(rectangle, fields);
|
|
630
|
+
}
|
|
631
|
+
function isViewOrder(value, fields) {
|
|
632
|
+
if (!("owner_id" in value || "owner_sequence" in value)) return isViewFields(value, fields);
|
|
633
|
+
const { owner_id, owner_sequence, ...rectangle } = value;
|
|
634
|
+
return isUuid(owner_id) && Number.isSafeInteger(owner_sequence) && Number(owner_sequence) > 0 && isViewFields(rectangle, fields);
|
|
635
|
+
}
|
|
655
636
|
function isViewFields(value, fields) {
|
|
656
637
|
if (hasExactRecordKeys(value, fields)) return true;
|
|
657
638
|
const originFields = [...fields, ...WhiteboardV1ProtocolMetadata.frames.view.originFields];
|
|
@@ -668,10 +649,24 @@ function isExtension(value, extension) {
|
|
|
668
649
|
return hasExactKeys(value, extension.exactFields) && value.name === extension.name;
|
|
669
650
|
}
|
|
670
651
|
function isExtensions(value) {
|
|
671
|
-
const { presentation, view } = WhiteboardV1ProtocolMetadata.extensions;
|
|
672
|
-
|
|
652
|
+
const { presentation, view, drawPolicy } = WhiteboardV1ProtocolMetadata.extensions;
|
|
653
|
+
if (!Array.isArray(value)) return false;
|
|
654
|
+
const drawPolicyLast = value.length > 0 && isExtension(value[value.length - 1], drawPolicy);
|
|
655
|
+
const base = drawPolicyLast ? value.slice(0, -1) : value;
|
|
656
|
+
if (base.length === 0) return drawPolicyLast;
|
|
657
|
+
return (base.length === 1 || base.length === 2) && isExtension(base[0], presentation) && (base.length === 1 || isExtension(base[1], view) || hasExactKeys(base[1], view.ownerExactFields) && base[1].name === view.name && base[1].origins === true && base[1].owner === true || hasExactKeys(base[1], view.extendedExactFields) && base[1].name === view.name && base[1].origins === true);
|
|
658
|
+
}
|
|
659
|
+
function isDrawPolicy(value) {
|
|
660
|
+
return typeof value === "string" && WhiteboardV1ProtocolMetadata.drawPolicies.some((policy) => policy === value);
|
|
661
|
+
}
|
|
662
|
+
function isDrawGrant(value) {
|
|
663
|
+
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.drawAccess.grantExactFields) && isUuid(value.participant_id) && typeof value.can_draw === "boolean";
|
|
673
664
|
}
|
|
674
665
|
function isWelcome(value) {
|
|
666
|
+
if ("view_owner_id" in value) {
|
|
667
|
+
const { view_owner_id, ...welcome } = value;
|
|
668
|
+
return isUuid(view_owner_id) && isWelcome(welcome);
|
|
669
|
+
}
|
|
675
670
|
const exact = hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.welcome.exactFields) || hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.welcome.extendedExactFields) && typeof value.presenting === "boolean";
|
|
676
671
|
return exact && value.protocol === "whiteboard-v1" && isUuid(value.participant_id) && Number.isSafeInteger(value.participant_generation) && Number(value.participant_generation) > 0 && isCapabilities(value.capabilities) && isCapabilities(value.participant_capabilities) && isUuid(value.scene_id) && isUnsignedDecimal(value.revision) && typeof value.can_draw === "boolean";
|
|
677
672
|
}
|
|
@@ -691,15 +686,19 @@ function isServerFrame(value) {
|
|
|
691
686
|
case "cursor":
|
|
692
687
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.cursor.serverExactFields) && isUuid(value.participant_id) && typeof value.display_name === "string" && utf8Bytes(value.display_name) > 0 && utf8Bytes(value.display_name) <= WhiteboardV1ProtocolLimits.displayNameMaxBytes && isFiniteNumber(value.x) && isFiniteNumber(value.y) && typeof value.occurred_at === "string" && !Number.isNaN(Date.parse(value.occurred_at)) && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
693
688
|
case "view":
|
|
694
|
-
return
|
|
689
|
+
return isViewOrder(value, WhiteboardV1ProtocolMetadata.frames.view.serverExactFields) && isUuid(value.participant_id) && typeof value.display_name === "string" && utf8Bytes(value.display_name) > 0 && utf8Bytes(value.display_name) <= WhiteboardV1ProtocolLimits.displayNameMaxBytes && isViewRect(value) && typeof value.bring === "boolean" && typeof value.occurred_at === "string" && !Number.isNaN(Date.parse(value.occurred_at)) && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
695
690
|
case "permission_updated":
|
|
696
691
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.permissionUpdated.exactFields) && isUuid(value.participant_id) && typeof value.can_draw === "boolean";
|
|
692
|
+
case "draw_access":
|
|
693
|
+
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.drawAccess.exactFields) && isDrawPolicy(value.policy) && typeof value.can_draw === "boolean" && isCapabilities(value.capabilities) && Array.isArray(value.grants) && value.grants.length <= WhiteboardV1ProtocolLimits.drawAccessMaxGrants && value.grants.every(isDrawGrant);
|
|
697
694
|
case "presentation_updated":
|
|
698
695
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.presentationUpdated.exactFields) && isUuid(value.scene_id) && isUnsignedDecimal(value.revision) && typeof value.presenting === "boolean";
|
|
699
696
|
case "reset_required":
|
|
700
697
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.resetRequired.exactFields) && isUuid(value.scene_id) && typeof value.reason === "string" && WhiteboardV1ProtocolMetadata.resetReasons.some((reason) => reason === value.reason);
|
|
701
698
|
case "operation_error":
|
|
702
|
-
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.operationError.exactFields) && isBoundedIdentifier(value.correlation_id, WhiteboardV1ProtocolLimits.requestIdMinBytes, WhiteboardV1ProtocolLimits.requestIdMaxBytes) && ["submit_update", "request_snapshot", "clear", "set_draw_permission", "set_presentation"].includes(String(value.operation)) && typeof value.code === "string" && WhiteboardV1ProtocolMetadata.errorCodes.some((code) => code === value.code) && typeof value.recoverable === "boolean" && typeof value.message === "string" && utf8Bytes(value.message) <= WhiteboardV1ProtocolLimits.errorMessageMaxBytes;
|
|
699
|
+
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.operationError.exactFields) && isBoundedIdentifier(value.correlation_id, WhiteboardV1ProtocolLimits.requestIdMinBytes, WhiteboardV1ProtocolLimits.requestIdMaxBytes) && ["submit_update", "request_snapshot", "clear", "set_draw_permission", "set_draw_policy", "set_presentation"].includes(String(value.operation)) && typeof value.code === "string" && WhiteboardV1ProtocolMetadata.errorCodes.some((code) => code === value.code) && typeof value.recoverable === "boolean" && typeof value.message === "string" && utf8Bytes(value.message) <= WhiteboardV1ProtocolLimits.errorMessageMaxBytes;
|
|
700
|
+
case "view_ack":
|
|
701
|
+
return hasExactKeys(value, ["type", "input_id"]) && isViewInputId(value.input_id);
|
|
703
702
|
case "pong":
|
|
704
703
|
return hasExactKeys(value, ["type"]);
|
|
705
704
|
default:
|
|
@@ -717,11 +716,118 @@ function encodeWhiteboardV1Frame(frame) {
|
|
|
717
716
|
return JSON.stringify(frame);
|
|
718
717
|
}
|
|
719
718
|
|
|
719
|
+
// src/whiteboard/view-bounds.ts
|
|
720
|
+
var ChalkWhiteboardViewBounds = {
|
|
721
|
+
maxCoordinate: WhiteboardV1ProtocolMetadata.frames.view.maxCoordinate,
|
|
722
|
+
maxExtent: WhiteboardV1ProtocolMetadata.frames.view.maxExtent
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/whiteboard/file-download-transport.ts
|
|
726
|
+
function createChalkWhiteboardV1FileDownloadTransport(options) {
|
|
727
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
728
|
+
let baseUrlEnd = options.baseUrl.length;
|
|
729
|
+
while (baseUrlEnd > 0 && options.baseUrl[baseUrlEnd - 1] === "/") baseUrlEnd -= 1;
|
|
730
|
+
const baseUrl = options.baseUrl.slice(0, baseUrlEnd);
|
|
731
|
+
return {
|
|
732
|
+
getDownloadUrl: async (fileId) => {
|
|
733
|
+
const response = await authorizedFetch(fetchImplementation, options.token, `${baseUrl}/v1/whiteboard/files/${encodeURIComponent(fileId)}/download`, { method: "GET" });
|
|
734
|
+
if (!response.ok) throw await responseError2(response, "get_file_download");
|
|
735
|
+
const value = await response.json();
|
|
736
|
+
if (!isDownloadResponse2(value)) {
|
|
737
|
+
throw whiteboardError("get_file_download", "file_transfer_failed", false, "Whiteboard download service returned an invalid response.");
|
|
738
|
+
}
|
|
739
|
+
return value;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function authorizedFetch(fetchImplementation, token, url, init) {
|
|
744
|
+
try {
|
|
745
|
+
const authorization = await token();
|
|
746
|
+
const headers = new Headers(init.headers);
|
|
747
|
+
headers.set("authorization", `Bearer ${authorization}`);
|
|
748
|
+
return await fetchImplementation(url, { ...init, headers });
|
|
749
|
+
} catch (cause) {
|
|
750
|
+
throw new ChalkWhiteboardV1Error(
|
|
751
|
+
{
|
|
752
|
+
operation: "get_file_download",
|
|
753
|
+
code: "storage_unavailable",
|
|
754
|
+
recoverable: true,
|
|
755
|
+
message: "Whiteboard file storage is unavailable."
|
|
756
|
+
},
|
|
757
|
+
{ cause }
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function responseError2(response, operation) {
|
|
762
|
+
const code = response.status === 401 || response.status === 403 ? "permission_denied" : response.status === 400 ? "invalid_payload" : response.status >= 500 || response.status === 429 ? "storage_unavailable" : "file_transfer_failed";
|
|
763
|
+
return whiteboardError(operation, code, response.status >= 500 || response.status === 429, "Whiteboard file request failed.");
|
|
764
|
+
}
|
|
765
|
+
function whiteboardError(operation, code, recoverable, message) {
|
|
766
|
+
return new ChalkWhiteboardV1Error({ operation, code, recoverable, message });
|
|
767
|
+
}
|
|
768
|
+
function isDownloadResponse2(value) {
|
|
769
|
+
return isExactObject(value, ["downloadUrl", "expiresAt"]) && isHttpUrl2(value.downloadUrl) && isDate2(value.expiresAt);
|
|
770
|
+
}
|
|
771
|
+
function isExactObject(value, keys) {
|
|
772
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
773
|
+
}
|
|
774
|
+
function isHttpUrl2(value) {
|
|
775
|
+
if (typeof value !== "string") return false;
|
|
776
|
+
try {
|
|
777
|
+
const url = new URL(value);
|
|
778
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
779
|
+
} catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function isDate2(value) {
|
|
784
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/whiteboard/view-versions.ts
|
|
788
|
+
var ChalkWhiteboardViewVersions = class {
|
|
789
|
+
#versions = /* @__PURE__ */ new Map();
|
|
790
|
+
isNew(view, participantId) {
|
|
791
|
+
const key = this.#key(view, participantId);
|
|
792
|
+
const sequence = view.ownerSequence ?? view.originSequence;
|
|
793
|
+
return key === void 0 || sequence === void 0 || sequence > (this.#versions.get(key) ?? 0);
|
|
794
|
+
}
|
|
795
|
+
remember(view, participantId) {
|
|
796
|
+
const key = this.#key(view, participantId);
|
|
797
|
+
const sequence = view.ownerSequence ?? view.originSequence;
|
|
798
|
+
if (key === void 0 || sequence === void 0 || !this.isNew(view, participantId)) return;
|
|
799
|
+
this.#versions.delete(key);
|
|
800
|
+
this.#versions.set(key, sequence);
|
|
801
|
+
if (this.#versions.size > WhiteboardV1ProtocolLimits.viewPeopleMaxItems) {
|
|
802
|
+
const oldest = this.#versions.keys().next().value;
|
|
803
|
+
if (oldest !== void 0) this.#versions.delete(oldest);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
forgetParticipant(participantId) {
|
|
807
|
+
for (const key of this.#versions.keys()) {
|
|
808
|
+
if (key.startsWith(`${participantId}:`) || key.endsWith(`:${participantId}`)) this.#versions.delete(key);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
clear() {
|
|
812
|
+
this.#versions.clear();
|
|
813
|
+
}
|
|
814
|
+
#key(view, participantId) {
|
|
815
|
+
if (view.ownerId && participantId) return `${view.ownerId}:${participantId}`;
|
|
816
|
+
return view.originParticipantId ? `${view.originParticipantId}:${view.originEpoch ?? ""}` : void 0;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
720
820
|
// src/whiteboard/v1-codec.ts
|
|
821
|
+
import { Schema as Schema2 } from "effect";
|
|
721
822
|
function encodeWhiteboardV1ClientFrame(frame) {
|
|
722
823
|
return encodeWhiteboardV1Frame(Schema2.decodeUnknownSync(WhiteboardV1ClientFrameSchema)(frame));
|
|
723
824
|
}
|
|
724
825
|
function decodeWhiteboardV1ServerFrame(wire) {
|
|
826
|
+
const frame = decodeWhiteboardV1WireFrame(wire);
|
|
827
|
+
if (frame.type === "view_ack" || frame.type === "draw_access") throw new Error("Negotiated whiteboard control frame");
|
|
828
|
+
return frame;
|
|
829
|
+
}
|
|
830
|
+
function decodeWhiteboardV1WireFrame(wire) {
|
|
725
831
|
return Schema2.decodeUnknownSync(WhiteboardV1ServerFrameSchema)(JSON.parse(wire));
|
|
726
832
|
}
|
|
727
833
|
|
|
@@ -916,6 +1022,11 @@ var MAX_TIMER_DRIFT_MS = 250;
|
|
|
916
1022
|
var MAX_SLOW_CONNECTION_BACKOFF_MS = 4e3;
|
|
917
1023
|
var DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
918
1024
|
var MAX_RECONNECT_DELAY_MS = 5e3;
|
|
1025
|
+
var MAX_REJECTED_HELLO_RETRIES = 4;
|
|
1026
|
+
var CLOSE_STEPS = {
|
|
1027
|
+
1008: { connecting: "retry-hello", authenticating: "retry-hello" },
|
|
1028
|
+
1009: { authenticating: "step-down" }
|
|
1029
|
+
};
|
|
919
1030
|
var encoder2 = new TextEncoder();
|
|
920
1031
|
var ChalkWhiteboardV1Client = class {
|
|
921
1032
|
files;
|
|
@@ -925,7 +1036,14 @@ var ChalkWhiteboardV1Client = class {
|
|
|
925
1036
|
#latestViews = /* @__PURE__ */ new Map();
|
|
926
1037
|
#summaryListeners = /* @__PURE__ */ new Set();
|
|
927
1038
|
#operations = /* @__PURE__ */ new Map();
|
|
1039
|
+
/** Strictly increasing, so a restore replays operations made in the same millisecond in the order they were made. */
|
|
1040
|
+
#lastCreatedAt = 0;
|
|
928
1041
|
#reservedOperationIds = /* @__PURE__ */ new Set();
|
|
1042
|
+
/**
|
|
1043
|
+
* Policy changes and per-person choices in the order the host made them. Only the first one is sent, and only once it
|
|
1044
|
+
* is saved, so the order comes from creation and never from which save finishes first.
|
|
1045
|
+
*/
|
|
1046
|
+
#accessChanges = [];
|
|
929
1047
|
#awaitingOperationIds = /* @__PURE__ */ new Set();
|
|
930
1048
|
#pendingStoreWrites = /* @__PURE__ */ new Set();
|
|
931
1049
|
#operationRetryTimers = /* @__PURE__ */ new Map();
|
|
@@ -942,13 +1060,17 @@ var ChalkWhiteboardV1Client = class {
|
|
|
942
1060
|
#revision;
|
|
943
1061
|
#capabilities = [];
|
|
944
1062
|
#canDraw = false;
|
|
1063
|
+
#drawPolicy = null;
|
|
1064
|
+
#drawPermissions = [];
|
|
945
1065
|
#presentationSupported = false;
|
|
946
1066
|
#viewsSupported = false;
|
|
947
1067
|
#presenting = false;
|
|
948
|
-
#hello = "
|
|
1068
|
+
#hello = "owner-view";
|
|
949
1069
|
#viewSequence = 0;
|
|
1070
|
+
#viewOwnerId = null;
|
|
1071
|
+
#viewVersions = new ChalkWhiteboardViewVersions();
|
|
950
1072
|
#viewEpoch = crypto.randomUUID();
|
|
951
|
-
#
|
|
1073
|
+
#pendingLeadingView = null;
|
|
952
1074
|
#summaryStatus = "unsubscribed";
|
|
953
1075
|
#summaryError = null;
|
|
954
1076
|
#latestSnapshot = null;
|
|
@@ -958,6 +1080,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
958
1080
|
#waitingForOperations = false;
|
|
959
1081
|
#reconnectTimer;
|
|
960
1082
|
#reconnectAttempt = 0;
|
|
1083
|
+
#rejectedHelloRetries = 0;
|
|
961
1084
|
#heartbeatTimer;
|
|
962
1085
|
#heartbeats = [];
|
|
963
1086
|
#ignoredHeartbeats = 0;
|
|
@@ -995,7 +1118,8 @@ var ChalkWhiteboardV1Client = class {
|
|
|
995
1118
|
if (this.#started && this.#startPromise) return this.#startPromise;
|
|
996
1119
|
this.#started = true;
|
|
997
1120
|
globalThis.document?.addEventListener("visibilitychange", this.#visibilityChanged);
|
|
998
|
-
this.#hello = "
|
|
1121
|
+
this.#hello = "owner-view";
|
|
1122
|
+
this.#rejectedHelloRetries = 0;
|
|
999
1123
|
this.#viewsSupported = false;
|
|
1000
1124
|
this.#phase = "connecting";
|
|
1001
1125
|
this.#publishSummary("loading", null);
|
|
@@ -1021,7 +1145,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1021
1145
|
#stopNow() {
|
|
1022
1146
|
this.#started = false;
|
|
1023
1147
|
this.#viewsSupported = false;
|
|
1024
|
-
this.#
|
|
1148
|
+
this.#pendingLeadingView = null;
|
|
1025
1149
|
this.#latestViews.clear();
|
|
1026
1150
|
this.#removeVisibilityListener();
|
|
1027
1151
|
this.#startupGeneration += 1;
|
|
@@ -1045,6 +1169,8 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1045
1169
|
this.#participantId = null;
|
|
1046
1170
|
this.#capabilities = [];
|
|
1047
1171
|
this.#canDraw = false;
|
|
1172
|
+
this.#drawPolicy = null;
|
|
1173
|
+
this.#drawPermissions = [];
|
|
1048
1174
|
this.#presenting = false;
|
|
1049
1175
|
this.#publishSummary("unsubscribed", null);
|
|
1050
1176
|
}
|
|
@@ -1070,7 +1196,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1070
1196
|
}
|
|
1071
1197
|
submitUpdate(input) {
|
|
1072
1198
|
if (!this.#started || !this.#sceneId) this.#assertLive("submit_update");
|
|
1073
|
-
if (!this.#canDraw
|
|
1199
|
+
if (!this.#canDraw) {
|
|
1074
1200
|
throw error(failure("submit_update", "permission_denied", false, "Whiteboard draw permission is required."));
|
|
1075
1201
|
}
|
|
1076
1202
|
const operationId = this.#nextId();
|
|
@@ -1095,35 +1221,52 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1095
1221
|
return this.#started && this.#viewsSupported && this.#viewExtensionEnabled;
|
|
1096
1222
|
}
|
|
1097
1223
|
get #viewExtensionEnabled() {
|
|
1098
|
-
return this.#hello
|
|
1224
|
+
return viewsNegotiated(this.#hello);
|
|
1099
1225
|
}
|
|
1100
1226
|
get #presenceReady() {
|
|
1101
1227
|
return this.#phase === "live" && this.#summaryStatus === "ready" && this.#snapshots.size === 0;
|
|
1102
1228
|
}
|
|
1229
|
+
forgetParticipant(participantId) {
|
|
1230
|
+
this.#latestViews.delete(participantId);
|
|
1231
|
+
this.#viewVersions.forgetParticipant(participantId);
|
|
1232
|
+
}
|
|
1103
1233
|
sendView(view) {
|
|
1104
1234
|
if (!this.#started || !this.#viewExtensionEnabled) return false;
|
|
1105
1235
|
const frame = this.#viewFrame(view);
|
|
1106
1236
|
encodeWhiteboardV1ClientFrame(frame);
|
|
1107
|
-
if (
|
|
1108
|
-
|
|
1109
|
-
return true;
|
|
1110
|
-
}
|
|
1111
|
-
if (view.bring) this.#pendingBring = null;
|
|
1237
|
+
if (view.bring || view.lead !== false) this.#pendingLeadingView = { sceneId: this.#sceneId, view: { ...view }, frame };
|
|
1238
|
+
if (!this.#presenceReady) return true;
|
|
1112
1239
|
this.#send(frame);
|
|
1240
|
+
if (!this.#viewOwnerId && (view.bring || view.lead !== false)) this.#pendingLeadingView = null;
|
|
1113
1241
|
return true;
|
|
1114
1242
|
}
|
|
1115
1243
|
#viewFrame(view) {
|
|
1116
1244
|
const rectangle = { type: "view", x: view.x, y: view.y, width: view.width, height: view.height, bring: view.bring };
|
|
1117
|
-
if (
|
|
1245
|
+
if (this.#viewOwnerId) {
|
|
1246
|
+
const input = { input_id: `${this.#viewEpoch}:${++this.#viewSequence}`, move: view.lead !== false || view.bring };
|
|
1247
|
+
return view.originParticipantId ? { ...rectangle, ...input, origin_participant_id: view.originParticipantId, origin_sequence: view.originSequence } : { ...rectangle, ...input };
|
|
1248
|
+
}
|
|
1249
|
+
if (view.bring || !viewOriginsNegotiated(this.#hello)) return rectangle;
|
|
1118
1250
|
if (view.originParticipantId !== void 0) return { ...rectangle, origin_participant_id: view.originParticipantId, origin_sequence: view.originSequence, ...view.originEpoch !== void 0 ? { origin_epoch: view.originEpoch } : {} };
|
|
1119
1251
|
if (!this.#participantId) return rectangle;
|
|
1120
1252
|
return { ...rectangle, origin_participant_id: this.#participantId, origin_sequence: ++this.#viewSequence, origin_epoch: this.#viewEpoch };
|
|
1121
1253
|
}
|
|
1122
1254
|
#flushPendingBring() {
|
|
1123
1255
|
if (!this.#presenceReady) return;
|
|
1124
|
-
const pending = this.#
|
|
1125
|
-
|
|
1126
|
-
if (pending
|
|
1256
|
+
const pending = this.#pendingLeadingView;
|
|
1257
|
+
if (!pending) return;
|
|
1258
|
+
if (pending.sceneId !== null && pending.sceneId !== this.#sceneId) {
|
|
1259
|
+
this.#pendingLeadingView = null;
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!this.#viewOwnerId) {
|
|
1263
|
+
this.#pendingLeadingView = null;
|
|
1264
|
+
this.sendView(pending.view);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const frame = pending.frame.input_id ? pending.frame : this.#viewFrame(pending.view);
|
|
1268
|
+
this.#pendingLeadingView = { ...pending, frame };
|
|
1269
|
+
this.#send(frame);
|
|
1127
1270
|
}
|
|
1128
1271
|
requestSnapshot() {
|
|
1129
1272
|
this.#assertLive("request_snapshot");
|
|
@@ -1151,12 +1294,22 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1151
1294
|
};
|
|
1152
1295
|
await this.#queueOperation(frame);
|
|
1153
1296
|
}
|
|
1297
|
+
async setDrawPolicy(policy) {
|
|
1298
|
+
this.#assertLive("set_draw_policy");
|
|
1299
|
+
if (this.#drawPolicy === null) {
|
|
1300
|
+
throw error(policyUnavailable());
|
|
1301
|
+
}
|
|
1302
|
+
if (!this.#capabilities.includes("manageWhiteboard")) {
|
|
1303
|
+
throw error(failure("set_draw_policy", "permission_denied", false, "Whiteboard management permission is required."));
|
|
1304
|
+
}
|
|
1305
|
+
await this.#queueOperation({ type: "set_draw_policy", operation_id: this.#nextId(), policy });
|
|
1306
|
+
}
|
|
1154
1307
|
async setPresentation(presenting) {
|
|
1155
1308
|
this.#assertLive("set_presentation");
|
|
1156
1309
|
if (!this.#presentationSupported) {
|
|
1157
1310
|
throw error(failure("set_presentation", "unavailable", true, "Whiteboard presentation is unavailable on this Sync server."));
|
|
1158
1311
|
}
|
|
1159
|
-
if (!this.#canDraw
|
|
1312
|
+
if (!this.#canDraw) {
|
|
1160
1313
|
throw error(failure("set_presentation", "permission_denied", false, "Whiteboard draw permission is required."));
|
|
1161
1314
|
}
|
|
1162
1315
|
const frame = {
|
|
@@ -1186,8 +1339,10 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1186
1339
|
void this.#store.remove(pending.operationId).catch(() => void 0);
|
|
1187
1340
|
continue;
|
|
1188
1341
|
}
|
|
1189
|
-
if (this.#
|
|
1190
|
-
this.#
|
|
1342
|
+
if (this.#operationCount() >= WhiteboardV1ProtocolLimits.pendingOperationMaxItems) return;
|
|
1343
|
+
this.#lastCreatedAt = Math.max(this.#lastCreatedAt, pending.createdAt);
|
|
1344
|
+
if (!isAccessChange(pending.frame)) this.#operations.set(pending.operationId, { pending, retries: 0 });
|
|
1345
|
+
else if (!this.#accessChange(pending.operationId)) this.#restoreAccessChange(pending);
|
|
1191
1346
|
}
|
|
1192
1347
|
}
|
|
1193
1348
|
#validRestoredOperation(pending) {
|
|
@@ -1250,7 +1405,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1250
1405
|
if (typeof data !== "string" || encoder2.encode(data).byteLength > WhiteboardV1ProtocolLimits.encodedOutboundFrameBytes) {
|
|
1251
1406
|
throw new Error("invalid whiteboard frame size");
|
|
1252
1407
|
}
|
|
1253
|
-
const frame =
|
|
1408
|
+
const frame = decodeWhiteboardV1WireFrame(data);
|
|
1254
1409
|
if (frame.type !== "pong") {
|
|
1255
1410
|
this.#lastProgressAt = this.#elapsedNow();
|
|
1256
1411
|
this.#resetProbeState();
|
|
@@ -1290,9 +1445,14 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1290
1445
|
occurredAt: frame.occurred_at
|
|
1291
1446
|
});
|
|
1292
1447
|
return;
|
|
1448
|
+
case "view_ack":
|
|
1449
|
+
this.#requireLive();
|
|
1450
|
+
if (this.#pendingLeadingView?.frame.input_id === frame.input_id) this.#pendingLeadingView = null;
|
|
1451
|
+
return;
|
|
1293
1452
|
case "view": {
|
|
1294
1453
|
this.#requireLive();
|
|
1295
|
-
|
|
1454
|
+
if (this.#viewOwnerId && frame.owner_id !== this.#viewOwnerId) return;
|
|
1455
|
+
const rectangle = {
|
|
1296
1456
|
type: "view",
|
|
1297
1457
|
participantId: frame.participant_id,
|
|
1298
1458
|
displayName: frame.display_name,
|
|
@@ -1303,6 +1463,9 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1303
1463
|
bring: frame.bring,
|
|
1304
1464
|
occurredAt: frame.occurred_at
|
|
1305
1465
|
};
|
|
1466
|
+
const event = frame.owner_id ? { ...rectangle, ownerId: frame.owner_id, ownerSequence: frame.owner_sequence } : rectangle;
|
|
1467
|
+
if (!this.#viewVersions.isNew(event, event.participantId)) return;
|
|
1468
|
+
this.#viewVersions.remember(event, event.participantId);
|
|
1306
1469
|
this.#emit(frame.origin_participant_id ? { ...event, originParticipantId: frame.origin_participant_id, originSequence: frame.origin_sequence, ...frame.origin_epoch ? { originEpoch: frame.origin_epoch } : {} } : event);
|
|
1307
1470
|
return;
|
|
1308
1471
|
}
|
|
@@ -1311,6 +1474,14 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1311
1474
|
if (frame.participant_id === this.#participantId) this.#canDraw = frame.can_draw;
|
|
1312
1475
|
this.#publishSummary("ready", null);
|
|
1313
1476
|
return;
|
|
1477
|
+
case "draw_access":
|
|
1478
|
+
this.#requireLive();
|
|
1479
|
+
this.#canDraw = frame.can_draw;
|
|
1480
|
+
this.#capabilities = [...frame.capabilities];
|
|
1481
|
+
this.#drawPolicy = frame.policy;
|
|
1482
|
+
this.#drawPermissions = frame.grants.map((grant) => ({ participantId: grant.participant_id, canDraw: grant.can_draw }));
|
|
1483
|
+
this.#publishSummary(this.#summaryStatus, this.#summaryError);
|
|
1484
|
+
return;
|
|
1314
1485
|
case "presentation_updated":
|
|
1315
1486
|
this.#requireLive();
|
|
1316
1487
|
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
@@ -1342,7 +1513,6 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1342
1513
|
}
|
|
1343
1514
|
#update(frame) {
|
|
1344
1515
|
this.#requireLive();
|
|
1345
|
-
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1346
1516
|
this.#sceneId = frame.scene_id;
|
|
1347
1517
|
this.#revision = frame.revision;
|
|
1348
1518
|
this.#publishSummary("ready", null);
|
|
@@ -1377,27 +1547,50 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1377
1547
|
#welcome(frame) {
|
|
1378
1548
|
if (this.#phase !== "authenticating") throw new Error("unexpected whiteboard welcome");
|
|
1379
1549
|
this.#phase = "live";
|
|
1380
|
-
this.#viewsSupported = this.#
|
|
1550
|
+
this.#viewsSupported = this.#viewExtensionEnabled;
|
|
1551
|
+
this.#adoptViewOwner("view_owner_id" in frame ? frame.view_owner_id ?? null : null);
|
|
1381
1552
|
this.#reconnectAttempt = 0;
|
|
1553
|
+
this.#rejectedHelloRetries = 0;
|
|
1382
1554
|
this.#participantId = frame.participant_id;
|
|
1383
1555
|
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1384
1556
|
this.#sceneId = frame.scene_id;
|
|
1385
1557
|
this.#revision = frame.revision;
|
|
1386
1558
|
this.#capabilities = [...frame.capabilities];
|
|
1387
1559
|
this.#canDraw = frame.can_draw;
|
|
1560
|
+
this.#drawPolicy = null;
|
|
1561
|
+
this.#drawPermissions = [];
|
|
1388
1562
|
this.#presentationSupported = "presenting" in frame;
|
|
1389
1563
|
this.#presenting = "presenting" in frame ? frame.presenting : false;
|
|
1390
|
-
this.#
|
|
1564
|
+
const refused = !drawPolicyNegotiated(this.#hello) && this.#dropPolicyChanges();
|
|
1565
|
+
this.#publishSummary("loading", refused ? policyUnavailable() : null);
|
|
1391
1566
|
this.#clearHeartbeat();
|
|
1392
|
-
|
|
1567
|
+
this.#resumePendingOperations();
|
|
1568
|
+
this.#startHeartbeat(HEARTBEAT_INTERVAL_MS + Math.max(0, this.#nextSlowConnectionReplacementAt - this.#elapsedNow()));
|
|
1569
|
+
}
|
|
1570
|
+
#resumePendingOperations() {
|
|
1571
|
+
this.#clearOperationRetryTimers();
|
|
1572
|
+
for (const change of this.#accessChanges) if (change.state === "sent") change.state = "ready";
|
|
1393
1573
|
this.#awaitingOperationIds.clear();
|
|
1394
1574
|
this.#waitingForOperations = true;
|
|
1395
|
-
|
|
1396
|
-
for (const entry of pendingOperations) {
|
|
1397
|
-
this.#sendOperation(entry.pending.frame);
|
|
1398
|
-
}
|
|
1575
|
+
this.#sendInCreationOrder([...this.#operations.values()]);
|
|
1399
1576
|
this.#requestSnapshotAfterOperations();
|
|
1400
|
-
|
|
1577
|
+
}
|
|
1578
|
+
/** A Sync without draw_policy_v1 closes the socket on a policy change, so a change saved for a newer Sync is dropped, not replayed. */
|
|
1579
|
+
#dropPolicyChanges() {
|
|
1580
|
+
const dropped = this.#accessChanges.filter((change) => change.entry.pending.frame.type === "set_draw_policy");
|
|
1581
|
+
for (const { entry } of dropped) {
|
|
1582
|
+
const { operationId } = entry.pending;
|
|
1583
|
+
this.#removeAccessChange(operationId);
|
|
1584
|
+
this.#clearOperationRetryTimer(operationId);
|
|
1585
|
+
entry.deferred && rejectDeferred(entry.deferred, error(policyUnavailable()));
|
|
1586
|
+
this.#discardStoredOperation(operationId);
|
|
1587
|
+
}
|
|
1588
|
+
return dropped.length > 0;
|
|
1589
|
+
}
|
|
1590
|
+
#adoptViewOwner(ownerId) {
|
|
1591
|
+
this.#viewOwnerId = ownerId;
|
|
1592
|
+
this.#latestViews.clear();
|
|
1593
|
+
this.#viewVersions.clear();
|
|
1401
1594
|
}
|
|
1402
1595
|
#requestSnapshotAfterOperations() {
|
|
1403
1596
|
if (this.#phase !== "live" || !this.#waitingForOperations || this.#awaitingOperationIds.size > 0) return;
|
|
@@ -1467,45 +1660,78 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1467
1660
|
const pending = {
|
|
1468
1661
|
operationId: frame.operation_id,
|
|
1469
1662
|
frame,
|
|
1470
|
-
createdAt: this.#now(),
|
|
1663
|
+
createdAt: this.#lastCreatedAt = Math.max(this.#now(), this.#lastCreatedAt + 1),
|
|
1471
1664
|
bytes: whiteboardV1PendingOperationBytes(frame)
|
|
1472
1665
|
};
|
|
1473
|
-
const queuedBytes =
|
|
1666
|
+
const queuedBytes = this.#pendingEntries().reduce((total, entry) => total + entry.pending.bytes, 0);
|
|
1474
1667
|
if (queuedBytes + pending.bytes > WhiteboardV1ProtocolLimits.multipartUpdateMaxBytes) {
|
|
1475
1668
|
throw error(failure("submit_update", "unavailable", true, "Whiteboard operation byte capacity is full."));
|
|
1476
1669
|
}
|
|
1670
|
+
const { promise, deferred } = commitDeferred();
|
|
1671
|
+
if (isAccessChange(frame)) return this.#queueAccessChange({ pending, deferred, retries: 0 }, deferred, promise);
|
|
1477
1672
|
this.#reservedOperationIds.add(frame.operation_id);
|
|
1673
|
+
const saveFailure = await this.#persist(pending);
|
|
1674
|
+
this.#reservedOperationIds.delete(frame.operation_id);
|
|
1675
|
+
if (saveFailure) throw saveFailure;
|
|
1676
|
+
this.#operations.set(frame.operation_id, { pending, deferred, retries: 0 });
|
|
1677
|
+
if (this.#phase === "live" && this.#snapshots.size === 0) {
|
|
1678
|
+
if (this.#waitingForOperations) this.#awaitingOperationIds.add(frame.operation_id);
|
|
1679
|
+
this.#sendOperation(frame);
|
|
1680
|
+
}
|
|
1681
|
+
return promise;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* The change joins the list before its save starts, so a later change can't overtake it while it saves. The caller
|
|
1685
|
+
* gets its one promise at once: a refusal or a rejected connection during the save settles it without waiting for
|
|
1686
|
+
* the store, and a failed save settles it otherwise.
|
|
1687
|
+
*/
|
|
1688
|
+
#queueAccessChange(entry, deferred, promise) {
|
|
1689
|
+
const change = { entry, state: "saving" };
|
|
1690
|
+
this.#accessChanges.push(change);
|
|
1691
|
+
void this.#saveAccessChange(change, deferred);
|
|
1692
|
+
return promise;
|
|
1693
|
+
}
|
|
1694
|
+
async #saveAccessChange(change, deferred) {
|
|
1695
|
+
const { pending } = change.entry;
|
|
1696
|
+
const saveFailure = await this.#persist(pending);
|
|
1697
|
+
if (saveFailure) {
|
|
1698
|
+
this.#removeAccessChange(pending.operationId);
|
|
1699
|
+
this.#sendNextAccessChange();
|
|
1700
|
+
rejectDeferred(deferred, saveFailure);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (!this.#accessChanges.includes(change)) {
|
|
1704
|
+
this.#discardStoredOperation(pending.operationId);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
change.state = "ready";
|
|
1708
|
+
this.#sendNextAccessChange();
|
|
1709
|
+
}
|
|
1710
|
+
/** Returns the failure instead of throwing it, so an access change can hand it to the promise its caller holds. */
|
|
1711
|
+
async #persist(pending) {
|
|
1478
1712
|
let storeWrite;
|
|
1479
1713
|
try {
|
|
1480
1714
|
storeWrite = this.#store.put(pending);
|
|
1481
|
-
|
|
1482
|
-
throw error(failure(operationName(frame), "unavailable", true, "Unable to persist the whiteboard operation."), cause);
|
|
1483
|
-
}
|
|
1484
|
-
this.#pendingStoreWrites.add(storeWrite);
|
|
1485
|
-
try {
|
|
1715
|
+
this.#pendingStoreWrites.add(storeWrite);
|
|
1486
1716
|
await storeWrite;
|
|
1717
|
+
return void 0;
|
|
1487
1718
|
} catch (cause) {
|
|
1488
|
-
|
|
1719
|
+
this.#discardStoredOperation(pending.operationId);
|
|
1720
|
+
return error(failure(operationName(pending.frame), "unavailable", true, "Unable to persist the whiteboard operation."), cause);
|
|
1489
1721
|
} finally {
|
|
1490
|
-
this.#pendingStoreWrites.delete(storeWrite);
|
|
1491
|
-
this.#reservedOperationIds.delete(frame.operation_id);
|
|
1492
|
-
}
|
|
1493
|
-
let deferred;
|
|
1494
|
-
const promise = new Promise((resolve, reject) => {
|
|
1495
|
-
deferred = { resolve, reject, settled: false };
|
|
1496
|
-
});
|
|
1497
|
-
this.#operations.set(frame.operation_id, { pending, deferred, retries: 0 });
|
|
1498
|
-
if (this.#phase === "live" && this.#snapshots.size === 0) {
|
|
1499
|
-
if (this.#waitingForOperations) this.#awaitingOperationIds.add(frame.operation_id);
|
|
1500
|
-
this.#sendOperation(frame);
|
|
1722
|
+
if (storeWrite) this.#pendingStoreWrites.delete(storeWrite);
|
|
1501
1723
|
}
|
|
1502
|
-
|
|
1724
|
+
}
|
|
1725
|
+
#discardStoredOperation(operationId) {
|
|
1726
|
+
const cleanup = this.#store.remove(operationId).catch(() => void 0);
|
|
1727
|
+
this.#pendingStoreWrites.add(cleanup);
|
|
1728
|
+
void cleanup.finally(() => this.#pendingStoreWrites.delete(cleanup));
|
|
1503
1729
|
}
|
|
1504
1730
|
#commit(frame) {
|
|
1505
1731
|
this.#requireLive();
|
|
1506
|
-
const entry = this.#
|
|
1732
|
+
const entry = this.#sentEntry(frame.operation_id);
|
|
1507
1733
|
if (!entry) return;
|
|
1508
|
-
this.#
|
|
1734
|
+
this.#forgetOperation(frame.operation_id);
|
|
1509
1735
|
this.#clearOperationRetryTimer(frame.operation_id);
|
|
1510
1736
|
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1511
1737
|
this.#sceneId = frame.scene_id;
|
|
@@ -1518,27 +1744,35 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1518
1744
|
revision: frame.revision
|
|
1519
1745
|
});
|
|
1520
1746
|
void this.#store.remove(frame.operation_id).catch(() => void 0);
|
|
1747
|
+
this.#sendNextAccessChange();
|
|
1521
1748
|
this.#requestSnapshotAfterOperations();
|
|
1522
1749
|
}
|
|
1523
1750
|
#operationError(frame) {
|
|
1524
1751
|
this.#requireLive();
|
|
1525
|
-
const entry = this.#
|
|
1752
|
+
const entry = this.#sentEntry(frame.correlation_id);
|
|
1526
1753
|
if (entry) {
|
|
1527
|
-
|
|
1528
|
-
this.#scheduleOperationRetry(frame.correlation_id, entry);
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
this.#operations.delete(frame.correlation_id);
|
|
1532
|
-
this.#clearOperationRetryTimer(frame.correlation_id);
|
|
1533
|
-
this.#awaitingOperationIds.delete(frame.correlation_id);
|
|
1534
|
-
entry.deferred && rejectDeferred(entry.deferred, error(failure(operationName(entry.pending.frame), mapErrorCode(frame.code), frame.recoverable, frame.message)));
|
|
1535
|
-
this.#publishSummary("ready", failure(operationName(entry.pending.frame), mapErrorCode(frame.code), frame.recoverable, frame.message));
|
|
1536
|
-
void this.#store.remove(frame.correlation_id).catch(() => void 0);
|
|
1537
|
-
this.#requestSnapshotAfterOperations();
|
|
1754
|
+
this.#failOperation(entry, frame);
|
|
1538
1755
|
return;
|
|
1539
1756
|
}
|
|
1540
1757
|
const snapshot = this.#snapshots.get(frame.correlation_id);
|
|
1541
|
-
if (
|
|
1758
|
+
if (snapshot) this.#failSnapshot(snapshot, frame);
|
|
1759
|
+
}
|
|
1760
|
+
#failOperation(entry, frame) {
|
|
1761
|
+
if (frame.recoverable && retryableOperationCode(frame.code)) {
|
|
1762
|
+
this.#scheduleOperationRetry(frame.correlation_id, entry);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
this.#forgetOperation(frame.correlation_id);
|
|
1766
|
+
this.#clearOperationRetryTimer(frame.correlation_id);
|
|
1767
|
+
this.#awaitingOperationIds.delete(frame.correlation_id);
|
|
1768
|
+
const operationFailure = failure(operationName(entry.pending.frame), mapErrorCode(frame.code), frame.recoverable, frame.message);
|
|
1769
|
+
entry.deferred && rejectDeferred(entry.deferred, error(operationFailure));
|
|
1770
|
+
this.#publishSummary("ready", operationFailure);
|
|
1771
|
+
void this.#store.remove(frame.correlation_id).catch(() => void 0);
|
|
1772
|
+
this.#sendNextAccessChange();
|
|
1773
|
+
this.#requestSnapshotAfterOperations();
|
|
1774
|
+
}
|
|
1775
|
+
#failSnapshot(snapshot, frame) {
|
|
1542
1776
|
this.#snapshots.delete(frame.correlation_id);
|
|
1543
1777
|
const snapshotError = error(failure("request_snapshot", mapErrorCode(frame.code), frame.recoverable, frame.message));
|
|
1544
1778
|
this.#publishSummary(frame.recoverable ? "recovering" : "failed", failure("request_snapshot", mapErrorCode(frame.code), frame.recoverable, frame.message));
|
|
@@ -1548,20 +1782,69 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1548
1782
|
this.#initialSnapshot = null;
|
|
1549
1783
|
}
|
|
1550
1784
|
if (frame.recoverable) this.#flushQueuedOperations();
|
|
1785
|
+
else this.#sendNextAccessChange();
|
|
1551
1786
|
}
|
|
1552
1787
|
#flushQueuedOperations() {
|
|
1553
1788
|
if (this.#phase !== "live" || this.#snapshots.size > 0) return;
|
|
1554
|
-
|
|
1555
|
-
|
|
1789
|
+
this.#sendInCreationOrder([...this.#operations.values()].filter((entry) => !this.#awaitingOperationIds.has(entry.pending.operationId)));
|
|
1790
|
+
}
|
|
1791
|
+
#sendNextAccessChange() {
|
|
1792
|
+
this.#sendInCreationOrder([]);
|
|
1793
|
+
}
|
|
1794
|
+
/** Sends the operations, and the first access change when it may go, in the order they were made. */
|
|
1795
|
+
#sendInCreationOrder(operations) {
|
|
1796
|
+
const head = this.#sendableAccessChange();
|
|
1797
|
+
if (head) head.state = "sent";
|
|
1798
|
+
const entries = [...operations, ...head ? [head.entry] : []].sort((left, right) => compareChalkWhiteboardV1PendingOperations(left.pending, right.pending));
|
|
1799
|
+
for (const entry of entries) {
|
|
1800
|
+
if (this.#waitingForOperations) this.#awaitingOperationIds.add(entry.pending.operationId);
|
|
1801
|
+
this.#sendOperation(entry.pending.frame);
|
|
1556
1802
|
}
|
|
1557
1803
|
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Access changes go out one at a time in the host's order, so a retried older change can't land after a newer one.
|
|
1806
|
+
* Sync also stores a per-person choice according to the policy it lands under, so their order matters too.
|
|
1807
|
+
*/
|
|
1808
|
+
#sendableAccessChange() {
|
|
1809
|
+
if (this.#phase !== "live" || this.#snapshots.size > 0) return void 0;
|
|
1810
|
+
const head = this.#accessChanges[0];
|
|
1811
|
+
if (head?.state !== "ready" || this.#accessChanges.some((change) => change.state === "sent")) return void 0;
|
|
1812
|
+
return head;
|
|
1813
|
+
}
|
|
1814
|
+
/** A restart keeps the changes it already holds, so a restored one goes where its creation puts it. */
|
|
1815
|
+
#restoreAccessChange(pending) {
|
|
1816
|
+
const later = this.#accessChanges.findIndex((change) => compareChalkWhiteboardV1PendingOperations(change.entry.pending, pending) > 0);
|
|
1817
|
+
this.#accessChanges.splice(later < 0 ? this.#accessChanges.length : later, 0, { entry: { pending, retries: 0 }, state: "ready" });
|
|
1818
|
+
}
|
|
1819
|
+
#accessChange(operationId) {
|
|
1820
|
+
return this.#accessChanges.find((change) => change.entry.pending.operationId === operationId);
|
|
1821
|
+
}
|
|
1822
|
+
#removeAccessChange(operationId) {
|
|
1823
|
+
const index = this.#accessChanges.findIndex((change) => change.entry.pending.operationId === operationId);
|
|
1824
|
+
if (index >= 0) this.#accessChanges.splice(index, 1);
|
|
1825
|
+
}
|
|
1826
|
+
/** The operation a receipt or an error can be about: any queued operation, or the access change that was sent. */
|
|
1827
|
+
#sentEntry(operationId) {
|
|
1828
|
+
const change = this.#accessChange(operationId);
|
|
1829
|
+
return this.#operations.get(operationId) ?? (change?.state === "sent" ? change.entry : void 0);
|
|
1830
|
+
}
|
|
1831
|
+
#forgetOperation(operationId) {
|
|
1832
|
+
this.#operations.delete(operationId);
|
|
1833
|
+
this.#removeAccessChange(operationId);
|
|
1834
|
+
}
|
|
1835
|
+
#pendingEntries() {
|
|
1836
|
+
return [...this.#operations.values(), ...this.#accessChanges.map((change) => change.entry)];
|
|
1837
|
+
}
|
|
1838
|
+
#operationCount() {
|
|
1839
|
+
return this.#operations.size + this.#accessChanges.length;
|
|
1840
|
+
}
|
|
1558
1841
|
#scheduleOperationRetry(operationId, entry) {
|
|
1559
1842
|
if (this.#operationRetryTimers.has(operationId)) return;
|
|
1560
1843
|
entry.retries += 1;
|
|
1561
1844
|
const timer = this.#clock().setTimeout(
|
|
1562
1845
|
() => {
|
|
1563
1846
|
this.#operationRetryTimers.delete(operationId);
|
|
1564
|
-
if (this.#phase === "live" && this.#
|
|
1847
|
+
if (this.#phase === "live" && this.#sentEntry(operationId) === entry) this.#sendOperation(entry.pending.frame);
|
|
1565
1848
|
},
|
|
1566
1849
|
this.#options.retryDelayMs ?? Math.min(100 * 2 ** Math.min(entry.retries - 1, 4), 2e3)
|
|
1567
1850
|
);
|
|
@@ -1569,30 +1852,39 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1569
1852
|
}
|
|
1570
1853
|
#disconnected(socket, closeCode) {
|
|
1571
1854
|
if (socket !== this.#socket) return;
|
|
1572
|
-
const
|
|
1573
|
-
const wasLive = this.#phase === "live";
|
|
1855
|
+
const phase = this.#phase;
|
|
1574
1856
|
this.#resetDisconnectedState();
|
|
1575
1857
|
if (!this.#started || !this.#transportAvailable) return;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1858
|
+
const step = CLOSE_STEPS[closeCode]?.[phase] ?? (terminalCloseCode(closeCode) ? "fail" : "reconnect");
|
|
1859
|
+
if (this.#takeCloseStep(step, phase)) this.#scheduleReconnect();
|
|
1860
|
+
else this.#failRejectedConnection(closeCode);
|
|
1861
|
+
}
|
|
1862
|
+
/** Returns false when the step has nothing left to try, so the connection fails. */
|
|
1863
|
+
#takeCloseStep(step, phase) {
|
|
1864
|
+
if (step === "fail") return false;
|
|
1865
|
+
if (step === "step-down") return this.#stepDownHello();
|
|
1866
|
+
if (step === "retry-hello") return this.#retryHello();
|
|
1867
|
+
if (phase === "live") this.#hello = "owner-view";
|
|
1868
|
+
return true;
|
|
1587
1869
|
}
|
|
1588
|
-
#
|
|
1589
|
-
|
|
1870
|
+
#stepDownHello() {
|
|
1871
|
+
if (this.#hello === "legacy") return false;
|
|
1872
|
+
this.#fallbackHello();
|
|
1873
|
+
return true;
|
|
1874
|
+
}
|
|
1875
|
+
#retryHello() {
|
|
1876
|
+
if (this.#rejectedHelloRetries >= MAX_REJECTED_HELLO_RETRIES) return false;
|
|
1877
|
+
this.#rejectedHelloRetries += 1;
|
|
1878
|
+
return true;
|
|
1590
1879
|
}
|
|
1591
1880
|
#fallbackHello() {
|
|
1592
|
-
this.#hello = this.#hello
|
|
1593
|
-
if (this.#hello
|
|
1881
|
+
this.#hello = HELLO_LEVELS[Math.min(HELLO_LEVELS.indexOf(this.#hello) + 1, HELLO_LEVELS.length - 1)] ?? "legacy";
|
|
1882
|
+
if (!viewsNegotiated(this.#hello)) this.#pendingLeadingView = null;
|
|
1594
1883
|
}
|
|
1595
1884
|
#resetDisconnectedState() {
|
|
1885
|
+
this.#viewOwnerId = null;
|
|
1886
|
+
this.#latestViews.clear();
|
|
1887
|
+
this.#viewVersions.clear();
|
|
1596
1888
|
this.#socket = null;
|
|
1597
1889
|
this.#clearDial();
|
|
1598
1890
|
this.#clearHeartbeat();
|
|
@@ -1639,13 +1931,13 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1639
1931
|
if (this.#phase !== "live") throw new Error("whiteboard frame arrived before welcome");
|
|
1640
1932
|
}
|
|
1641
1933
|
#assertOperationCapacity() {
|
|
1642
|
-
if (this.#
|
|
1934
|
+
if (this.#operationCount() + this.#reservedOperationIds.size >= WhiteboardV1ProtocolLimits.pendingOperationMaxItems) {
|
|
1643
1935
|
throw error(failure("submit_update", "unavailable", true, "Whiteboard operation capacity is full."));
|
|
1644
1936
|
}
|
|
1645
1937
|
}
|
|
1646
1938
|
#nextId() {
|
|
1647
1939
|
const id = this.#options.ids?.next() ?? crypto.randomUUID();
|
|
1648
|
-
if (this.#operations.has(id) || this.#reservedOperationIds.has(id) || this.#snapshots.has(id)) {
|
|
1940
|
+
if (this.#operations.has(id) || this.#accessChange(id) || this.#reservedOperationIds.has(id) || this.#snapshots.has(id)) {
|
|
1649
1941
|
throw error(failure("submit_update", "invalid_payload", false, "Whiteboard operation ID is already pending."));
|
|
1650
1942
|
}
|
|
1651
1943
|
return id;
|
|
@@ -1661,17 +1953,22 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1661
1953
|
if (event.type === "view") {
|
|
1662
1954
|
this.#latestViews.delete(event.participantId);
|
|
1663
1955
|
this.#latestViews.set(event.participantId, event);
|
|
1664
|
-
if (this.#latestViews.size >
|
|
1956
|
+
if (this.#latestViews.size > WhiteboardV1ProtocolLimits.viewPeopleMaxItems) {
|
|
1665
1957
|
const oldest = this.#latestViews.keys().next().value;
|
|
1666
1958
|
if (oldest !== void 0) this.#latestViews.delete(oldest);
|
|
1667
1959
|
}
|
|
1668
1960
|
}
|
|
1669
1961
|
for (const listener of this.#listeners) listener(event);
|
|
1962
|
+
if (event.type === "snapshot") {
|
|
1963
|
+
for (const view of this.#latestViews.values()) {
|
|
1964
|
+
for (const listener of this.#listeners) listener({ ...view, bring: false });
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1670
1967
|
}
|
|
1671
1968
|
#publishSummary(status, summaryError) {
|
|
1672
1969
|
this.#summaryStatus = status;
|
|
1673
1970
|
this.#summaryError = summaryError;
|
|
1674
|
-
if (status === "failed") this.#
|
|
1971
|
+
if (status === "failed") this.#pendingLeadingView = null;
|
|
1675
1972
|
if (status === "ready") this.#flushPendingBring();
|
|
1676
1973
|
const summary = this.#summary();
|
|
1677
1974
|
try {
|
|
@@ -1688,6 +1985,8 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1688
1985
|
capabilities: [...this.#capabilities],
|
|
1689
1986
|
canDraw: this.#canDraw,
|
|
1690
1987
|
canClear: this.#capabilities.includes("manageWhiteboard"),
|
|
1988
|
+
drawPolicy: this.#drawPolicy,
|
|
1989
|
+
drawPermissions: this.#drawPermissions,
|
|
1691
1990
|
presenting: this.#presenting,
|
|
1692
1991
|
error: this.#summaryError
|
|
1693
1992
|
};
|
|
@@ -1786,7 +2085,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1786
2085
|
this.#snapshots.clear();
|
|
1787
2086
|
}
|
|
1788
2087
|
#rejectOperationCallers(value) {
|
|
1789
|
-
for (const entry of this.#
|
|
2088
|
+
for (const entry of this.#pendingEntries()) entry.deferred && rejectDeferred(entry.deferred, error({ ...value, operation: operationName(entry.pending.frame) }));
|
|
1790
2089
|
}
|
|
1791
2090
|
#clearOperationRetryTimers() {
|
|
1792
2091
|
for (const timer of this.#operationRetryTimers.values()) this.#clock().clearTimeout(timer);
|
|
@@ -1893,11 +2192,27 @@ function operationName(frame) {
|
|
|
1893
2192
|
function retryableOperationCode(code) {
|
|
1894
2193
|
return code === "unavailable" || code === "overloaded" || code === "rate_limited" || code === "storage_unavailable";
|
|
1895
2194
|
}
|
|
2195
|
+
var HELLO_LEVELS = ["owner-view", "draw-policy", "versioned-view", "view", "presentation", "legacy"];
|
|
2196
|
+
function viewsNegotiated(level) {
|
|
2197
|
+
return level === "owner-view" || level === "draw-policy" || level === "versioned-view" || level === "view";
|
|
2198
|
+
}
|
|
2199
|
+
function viewOriginsNegotiated(level) {
|
|
2200
|
+
return level === "owner-view" || level === "draw-policy" || level === "versioned-view";
|
|
2201
|
+
}
|
|
2202
|
+
function drawPolicyNegotiated(level) {
|
|
2203
|
+
return level === "owner-view" || level === "draw-policy";
|
|
2204
|
+
}
|
|
1896
2205
|
function helloFrame(level, token, cursor) {
|
|
1897
2206
|
const hello = { type: "hello", protocol: "whiteboard-v1", token, cursor };
|
|
1898
2207
|
if (level === "legacy") return hello;
|
|
1899
2208
|
if (level === "presentation") return { ...hello, extensions: [{ name: "presentation_v1" }] };
|
|
1900
|
-
return { ...hello, extensions: [{ name: "presentation_v1" },
|
|
2209
|
+
if (level === "view") return { ...hello, extensions: [{ name: "presentation_v1" }, { name: "view_v1" }] };
|
|
2210
|
+
if (level === "versioned-view") return { ...hello, extensions: [{ name: "presentation_v1" }, { name: "view_v1", origins: true }] };
|
|
2211
|
+
if (level === "draw-policy") return { ...hello, extensions: [{ name: "presentation_v1" }, { name: "view_v1", origins: true }, { name: "draw_policy_v1" }] };
|
|
2212
|
+
return { ...hello, extensions: [{ name: "presentation_v1" }, { name: "view_v1", origins: true, owner: true }, { name: "draw_policy_v1" }] };
|
|
2213
|
+
}
|
|
2214
|
+
function policyUnavailable() {
|
|
2215
|
+
return failure("set_draw_policy", "unavailable", true, "The call-wide drawing policy is unavailable on this Sync server.");
|
|
1901
2216
|
}
|
|
1902
2217
|
function terminalCloseCode(closeCode) {
|
|
1903
2218
|
return closeCode === 1008 || closeCode === 1009;
|
|
@@ -1911,6 +2226,16 @@ function failure(operation, code, recoverable, message) {
|
|
|
1911
2226
|
function error(value, cause) {
|
|
1912
2227
|
return new ChalkWhiteboardV1Error(value, cause === void 0 ? void 0 : { cause });
|
|
1913
2228
|
}
|
|
2229
|
+
function commitDeferred() {
|
|
2230
|
+
let deferred;
|
|
2231
|
+
const promise = new Promise((resolve, reject) => {
|
|
2232
|
+
deferred = { resolve, reject, settled: false };
|
|
2233
|
+
});
|
|
2234
|
+
return { promise, deferred };
|
|
2235
|
+
}
|
|
2236
|
+
function isAccessChange(frame) {
|
|
2237
|
+
return frame.type === "set_draw_policy" || frame.type === "set_draw_permission";
|
|
2238
|
+
}
|
|
1914
2239
|
function resolveDeferred(deferred, value) {
|
|
1915
2240
|
if (deferred.settled) return;
|
|
1916
2241
|
deferred.settled = true;
|
|
@@ -1939,23 +2264,19 @@ function createChalkWhiteboardV1Client(options) {
|
|
|
1939
2264
|
});
|
|
1940
2265
|
}
|
|
1941
2266
|
|
|
1942
|
-
// src/whiteboard/index.ts
|
|
1943
|
-
var ChalkWhiteboardViewBounds = {
|
|
1944
|
-
maxCoordinate: WhiteboardV1ProtocolMetadata.frames.view.maxCoordinate,
|
|
1945
|
-
maxExtent: WhiteboardV1ProtocolMetadata.frames.view.maxExtent
|
|
1946
|
-
};
|
|
1947
|
-
|
|
1948
2267
|
// src/connection/types.ts
|
|
1949
2268
|
var ConnectionError = class extends Error {
|
|
1950
2269
|
code;
|
|
1951
2270
|
action;
|
|
1952
2271
|
recoverable;
|
|
2272
|
+
sync;
|
|
1953
2273
|
constructor(failure2, options) {
|
|
1954
2274
|
super(failure2.message, options);
|
|
1955
2275
|
this.name = "ConnectionError";
|
|
1956
2276
|
this.code = failure2.code;
|
|
1957
2277
|
this.action = failure2.action;
|
|
1958
2278
|
this.recoverable = failure2.recoverable;
|
|
2279
|
+
this.sync = failure2.sync;
|
|
1959
2280
|
}
|
|
1960
2281
|
};
|
|
1961
2282
|
|
|
@@ -2018,6 +2339,21 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2018
2339
|
clearTimeout: (handle) => globalThis.clearTimeout(handle)
|
|
2019
2340
|
},
|
|
2020
2341
|
mediaDevices: createBrowserMediaDevices(),
|
|
2342
|
+
cancelPendingAdmission: async (access, signal) => {
|
|
2343
|
+
const { tenantId, spaceId, episodeId, participantId } = access.subject;
|
|
2344
|
+
const path = [tenantId, spaceId, episodeId, participantId].map(encodeURIComponent);
|
|
2345
|
+
let baseEnd = options.apiBaseURL.length;
|
|
2346
|
+
while (baseEnd > 0 && options.apiBaseURL[baseEnd - 1] === "/") baseEnd--;
|
|
2347
|
+
const baseURL = options.apiBaseURL.slice(0, baseEnd);
|
|
2348
|
+
const response = await fetch(`${baseURL}/v1/tenants/${path[0]}/spaces/${path[1]}/episodes/${path[2]}/participants/${path[3]}/cancel-join`, {
|
|
2349
|
+
method: "POST",
|
|
2350
|
+
headers: { Authorization: `Bearer ${access.media.token}` },
|
|
2351
|
+
signal
|
|
2352
|
+
});
|
|
2353
|
+
if (!response.ok) throw new Error(`Admission cancellation failed with HTTP ${response.status}`);
|
|
2354
|
+
const body = await response.json();
|
|
2355
|
+
return typeof body === "object" && body !== null && "cancelled" in body && body.cancelled === true;
|
|
2356
|
+
},
|
|
2021
2357
|
createMediaClient: (input) => createMediaClient(options.apiBaseURL, input),
|
|
2022
2358
|
createSyncClient: (input) => createSyncClient(options.syncURL, input),
|
|
2023
2359
|
createChatFileTransport: (input) => createChatFileTransport(options.apiBaseURL, input),
|
|
@@ -2110,7 +2446,8 @@ function createSyncClient(syncURL, input) {
|
|
|
2110
2446
|
token: input.token,
|
|
2111
2447
|
mediaPlane: input.media,
|
|
2112
2448
|
collaboration: { name: "collaboration_v2", chatCursor: { afterSequence: null, retainedFloorSequence: null } },
|
|
2113
|
-
persistenceScope: `${input.access.subject.tenantId}:${input.access.subject.episodeId}:${input.access.subject.participantId}
|
|
2449
|
+
persistenceScope: `${input.access.subject.tenantId}:${input.access.subject.episodeId}:${input.access.subject.participantId}`,
|
|
2450
|
+
entryConfirmation: true
|
|
2114
2451
|
});
|
|
2115
2452
|
}
|
|
2116
2453
|
function createWhiteboardClient(apiBaseURL, url, input) {
|
|
@@ -2172,7 +2509,7 @@ var ConnectionDiagnostics = class {
|
|
|
2172
2509
|
this.#onEvent = options.onEvent;
|
|
2173
2510
|
}
|
|
2174
2511
|
record(input) {
|
|
2175
|
-
const event = Object.freeze({ timestamp: this.#now(), ...input });
|
|
2512
|
+
const event = Object.freeze({ timestamp: this.#now(), ...input, ...input.sync ? { sync: Object.freeze({ ...input.sync }) } : {} });
|
|
2176
2513
|
this.#events.push(event);
|
|
2177
2514
|
if (this.#events.length > this.#limit) this.#events.splice(0, this.#events.length - this.#limit);
|
|
2178
2515
|
try {
|
|
@@ -2284,6 +2621,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2284
2621
|
initialMediaIntent: null,
|
|
2285
2622
|
intent: { microphone: options.initialMicrophoneEnabled ?? true, camera: options.initialCameraEnabled ?? true },
|
|
2286
2623
|
episodeEndConfirmed: false,
|
|
2624
|
+
episodeEndReason: void 0,
|
|
2287
2625
|
removedByHost: false,
|
|
2288
2626
|
activeScope: null,
|
|
2289
2627
|
syncBindingCleanup: null,
|
|
@@ -2293,15 +2631,16 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2293
2631
|
refreshInFlight: false,
|
|
2294
2632
|
recoveryQueued: null,
|
|
2295
2633
|
joinCancelled: false,
|
|
2634
|
+
admissionCleanupConfirmed: true,
|
|
2296
2635
|
closed: false,
|
|
2297
2636
|
epoch: 0
|
|
2298
2637
|
};
|
|
2299
2638
|
const mediaAttempt = { expiresAt: null };
|
|
2300
|
-
const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
|
|
2639
|
+
const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure(code, true, message, cause) });
|
|
2301
2640
|
const withinForegroundBudget = (effect, milliseconds, failure2) => Effect2.raceFirst(
|
|
2302
2641
|
effect,
|
|
2303
2642
|
Effect2.callback((resume) => {
|
|
2304
|
-
const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(failure2)));
|
|
2643
|
+
const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(typeof failure2 === "function" ? failure2() : failure2)));
|
|
2305
2644
|
return Effect2.sync(cancel);
|
|
2306
2645
|
})
|
|
2307
2646
|
);
|
|
@@ -2422,13 +2761,23 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2422
2761
|
model.syncNoticeUnresponsive = false;
|
|
2423
2762
|
let latestSnapshot = null;
|
|
2424
2763
|
let observedDialTimeouts = 0;
|
|
2764
|
+
let observedConnectFailure;
|
|
2425
2765
|
let snapshotQueued = false;
|
|
2426
|
-
const
|
|
2766
|
+
const recordSyncDiagnostics = (snapshot) => {
|
|
2767
|
+
if (sync !== model.sync) return;
|
|
2768
|
+
const failure2 = snapshot.connection.connectFailure;
|
|
2769
|
+
if (failure2 && failure2 !== observedConnectFailure) {
|
|
2770
|
+
observedConnectFailure = failure2;
|
|
2771
|
+
diagnostics.record({ event: "sync_connect_failed", state: model.state, epoch: model.epoch, code: "sync_hello_timeout", sync: failure2 });
|
|
2772
|
+
}
|
|
2427
2773
|
const count = snapshot.connection.dialTimeoutCount ?? 0;
|
|
2428
|
-
if (
|
|
2774
|
+
if (count > observedDialTimeouts) {
|
|
2429
2775
|
observedDialTimeouts = count;
|
|
2430
2776
|
diagnostics.record({ event: "sync_dial_timeout", state: model.state, epoch: model.epoch, attempt: count });
|
|
2431
2777
|
}
|
|
2778
|
+
};
|
|
2779
|
+
const unsubscribe = sync.subscribe((snapshot) => {
|
|
2780
|
+
recordSyncDiagnostics(snapshot);
|
|
2432
2781
|
if (sync === model.sync && snapshot.connection.phase === "live") {
|
|
2433
2782
|
const notice = snapshot.connection.noticeUnresponsive === true;
|
|
2434
2783
|
if (notice !== model.syncNoticeUnresponsive) {
|
|
@@ -2490,7 +2839,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2490
2839
|
const unsubscribe = sync.subscribe(observe);
|
|
2491
2840
|
observe(sync.getSnapshot());
|
|
2492
2841
|
return Effect2.sync(unsubscribe);
|
|
2493
|
-
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs,
|
|
2842
|
+
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs, () => syncStartupFailure(sync.getSnapshot())));
|
|
2494
2843
|
const trace = (step, effect) => {
|
|
2495
2844
|
const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
|
|
2496
2845
|
return effect.pipe(
|
|
@@ -2498,12 +2847,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2498
2847
|
Effect2.tapError(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "failed" })))
|
|
2499
2848
|
);
|
|
2500
2849
|
};
|
|
2850
|
+
const confirmSyncEntry = (sync) => {
|
|
2851
|
+
const confirmEntered = sync.confirmEntered?.bind(sync);
|
|
2852
|
+
return confirmEntered ? trace("confirm_entered", foreign7(confirmEntered, "sync_start_failed", "Entry could not be confirmed")) : Effect2.void;
|
|
2853
|
+
};
|
|
2501
2854
|
const performJoin = (reason = "join", retried = false) => Effect2.gen(function* () {
|
|
2502
2855
|
if (model.state === "live") return;
|
|
2503
2856
|
if (model.state === "leaving" || model.closed) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Cannot join an inactive Connection"));
|
|
2504
2857
|
model.epoch += 1;
|
|
2505
2858
|
model.failure = null;
|
|
2506
2859
|
model.episodeEndConfirmed = false;
|
|
2860
|
+
model.episodeEndReason = void 0;
|
|
2507
2861
|
model.removedByHost = false;
|
|
2508
2862
|
yield* transition("joining");
|
|
2509
2863
|
const span = diagnostics.startSpan({ step: "join", state: model.state, epoch: model.epoch });
|
|
@@ -2517,7 +2871,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2517
2871
|
stream = captured.stream;
|
|
2518
2872
|
captureWarning = captured.failure;
|
|
2519
2873
|
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2520
|
-
const grant = yield* trace("access_initialize", access.initialize(reason));
|
|
2874
|
+
const grant = yield* trace("access_initialize", reason === "access_retry" ? access.refreshAfterRejection() : access.initialize(reason));
|
|
2875
|
+
model.admissionCleanupConfirmed = false;
|
|
2521
2876
|
const media = yield* trace(
|
|
2522
2877
|
"create_media_client",
|
|
2523
2878
|
Effect2.try({
|
|
@@ -2550,10 +2905,18 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2550
2905
|
"start_media",
|
|
2551
2906
|
model.startInitialMedia ? model.startInitialMedia(media, stream).pipe(Effect2.mapError((cause) => lifecycleFailure("media_start_failed", true, "The media layer could not start", cause))) : foreign7(() => media.start(stream), "media_start_failed", "The media layer could not start")
|
|
2552
2907
|
),
|
|
2553
|
-
trace(
|
|
2908
|
+
trace(
|
|
2909
|
+
"start_sync",
|
|
2910
|
+
foreign7(() => sync.start(), "sync_start_failed", "The sync layer could not start").pipe(
|
|
2911
|
+
Effect2.mapError((failure2) => sync.getSnapshot().connection.connectFailure ? syncStartupFailure(sync.getSnapshot()) : failure2),
|
|
2912
|
+
Effect2.andThen(trace("wait_for_sync_live", waitForSyncLive(sync, boundedInteger(options.syncStartupTimeoutMs, START_TIMEOUT_MS, 1, 6e4))))
|
|
2913
|
+
)
|
|
2914
|
+
)
|
|
2554
2915
|
],
|
|
2555
2916
|
{ concurrency: "unbounded", discard: true }
|
|
2556
2917
|
);
|
|
2918
|
+
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2919
|
+
yield* confirmSyncEntry(sync);
|
|
2557
2920
|
}).pipe(
|
|
2558
2921
|
Effect2.matchEffect({
|
|
2559
2922
|
onFailure: (cause) => Effect2.gen(function* () {
|
|
@@ -2567,8 +2930,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2567
2930
|
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure2.code });
|
|
2568
2931
|
const retry = !retried && failure2.code === "invalid_access";
|
|
2569
2932
|
if (!retry) yield* recordFailure(failure2);
|
|
2933
|
+
model.admissionCleanupConfirmed = yield* cancelPendingAdmission();
|
|
2570
2934
|
yield* stopPorts();
|
|
2571
|
-
if (retry) return yield* performJoin("access_retry", true);
|
|
2935
|
+
if (retry) return yield* performJoin(model.admissionCleanupConfirmed ? "join" : "access_retry", true);
|
|
2572
2936
|
yield* transition("failed");
|
|
2573
2937
|
return yield* Effect2.fail(failure2);
|
|
2574
2938
|
}),
|
|
@@ -2585,7 +2949,11 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2585
2949
|
);
|
|
2586
2950
|
});
|
|
2587
2951
|
const performLeave = () => Effect2.gen(function* performLeaveEffect() {
|
|
2588
|
-
if (model.state === "idle"
|
|
2952
|
+
if (model.state === "idle") return;
|
|
2953
|
+
if (model.state === "left") {
|
|
2954
|
+
if (model.failure) return yield* Effect2.fail(lifecycleFailure("leave_unconfirmed", false, model.failure.message));
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2589
2957
|
yield* transition("leaving");
|
|
2590
2958
|
const confirmed = yield* leaveIsConfirmed();
|
|
2591
2959
|
yield* stopPorts();
|
|
@@ -2594,11 +2962,27 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2594
2962
|
yield* transition("left");
|
|
2595
2963
|
if (!confirmed) return yield* Effect2.fail(lifecycleFailure("leave_unconfirmed", false, "The Episode left locally without a durable Leave acknowledgement"));
|
|
2596
2964
|
});
|
|
2965
|
+
const cancelPendingAdmission = () => Effect2.gen(function* () {
|
|
2966
|
+
const grant = access.currentUnsafe();
|
|
2967
|
+
if (!grant) return model.admissionCleanupConfirmed;
|
|
2968
|
+
const cancel = platform.cancelPendingAdmission;
|
|
2969
|
+
if (!cancel) return false;
|
|
2970
|
+
const result = yield* Effect2.exit(
|
|
2971
|
+
Effect2.tryPromise({
|
|
2972
|
+
try: (signal) => cancel(grant, signal),
|
|
2973
|
+
catch: (cause) => lifecycleFailure("internal_error", true, "Admission cancellation failed", cause)
|
|
2974
|
+
}).pipe(Effect2.timeout(3e3))
|
|
2975
|
+
);
|
|
2976
|
+
return result._tag === "Success" && result.value;
|
|
2977
|
+
});
|
|
2597
2978
|
const leaveIsConfirmed = () => Effect2.gen(function* leaveIsConfirmedEffect() {
|
|
2598
2979
|
const sync = model.sync;
|
|
2599
|
-
if (
|
|
2600
|
-
|
|
2601
|
-
|
|
2980
|
+
if (model.episodeEndConfirmed) return true;
|
|
2981
|
+
if (sync && (sync.getSnapshot().connection.phase === "live" || sync.getSnapshot().control !== null)) {
|
|
2982
|
+
const leave = foreign7(() => sync.leave()).pipe(Effect2.timeout(LEAVE_TIMEOUT_MS));
|
|
2983
|
+
if ((yield* Effect2.exit(leave))._tag === "Success") return true;
|
|
2984
|
+
}
|
|
2985
|
+
return yield* cancelPendingAdmission();
|
|
2602
2986
|
});
|
|
2603
2987
|
const publishFreshAccess = () => enqueue(
|
|
2604
2988
|
Effect2.gen(function* () {
|
|
@@ -2812,7 +3196,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2812
3196
|
if (snapshot.connection.phase === "idle" || snapshot.connection.phase === "stopped") return Effect2.succeed(true);
|
|
2813
3197
|
if (snapshot.connection.phase !== "terminal") return Effect2.succeed(false);
|
|
2814
3198
|
const failure2 = syncTerminalFailure(snapshot);
|
|
2815
|
-
return failure2
|
|
3199
|
+
return syncAutoRecoverable(snapshot, failure2) ? Effect2.succeed(true) : Effect2.fail(failure2);
|
|
2816
3200
|
};
|
|
2817
3201
|
const recoverSync = (attempt) => Effect2.gen(function* () {
|
|
2818
3202
|
const sync = model.sync;
|
|
@@ -2856,6 +3240,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2856
3240
|
else if (attempt.client === sync) attempt.started = true;
|
|
2857
3241
|
})
|
|
2858
3242
|
).pipe(
|
|
3243
|
+
Effect2.andThen(waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4))),
|
|
3244
|
+
Effect2.andThen(Effect2.suspend(() => sync === model.sync && model.epoch === epoch && active(model) ? confirmSyncEntry(sync) : Effect2.fail(lifecycleFailure("invalid_state", false, "Sync recovery was superseded")))),
|
|
2859
3245
|
Effect2.tapError(
|
|
2860
3246
|
() => enqueue(
|
|
2861
3247
|
Effect2.gen(function* () {
|
|
@@ -2868,7 +3254,6 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2868
3254
|
)
|
|
2869
3255
|
)
|
|
2870
3256
|
);
|
|
2871
|
-
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
2872
3257
|
});
|
|
2873
3258
|
const handleSyncSnapshot = (sync, snapshot) => Effect2.gen(function* handleSyncSnapshotEffect() {
|
|
2874
3259
|
if (!isCurrentSyncSnapshot(sync, snapshot)) return;
|
|
@@ -2882,14 +3267,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2882
3267
|
const ended = episodeEnded(snapshot);
|
|
2883
3268
|
const removed = snapshot.connection.terminalReason === "participant_removed";
|
|
2884
3269
|
if (ended || removed || snapshot.connection.terminalReason === "participant_inactive") {
|
|
2885
|
-
if (ended)
|
|
3270
|
+
if (ended) {
|
|
3271
|
+
model.episodeEndConfirmed = true;
|
|
3272
|
+
model.episodeEndReason = snapshot.control?.endReason;
|
|
3273
|
+
}
|
|
2886
3274
|
if (removed) model.removedByHost = true;
|
|
2887
3275
|
yield* finishTerminalSnapshot();
|
|
2888
3276
|
return true;
|
|
2889
3277
|
}
|
|
2890
3278
|
if (snapshot.connection.phase !== "terminal") return false;
|
|
2891
3279
|
const failure2 = syncTerminalFailure(snapshot);
|
|
2892
|
-
if (failure2
|
|
3280
|
+
if (syncAutoRecoverable(snapshot, failure2)) return false;
|
|
2893
3281
|
yield* failForSnapshot(failure2);
|
|
2894
3282
|
return true;
|
|
2895
3283
|
});
|
|
@@ -2904,7 +3292,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2904
3292
|
});
|
|
2905
3293
|
const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "connecting" || snapshot.connection.phase === "recovering" || snapshot.connection.phase === "terminal");
|
|
2906
3294
|
const recordFailure = (failure2) => Effect2.sync(() => {
|
|
2907
|
-
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code });
|
|
3295
|
+
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code, ...failure2.sync ? { sync: failure2.sync } : {} });
|
|
2908
3296
|
});
|
|
2909
3297
|
const failForSnapshot = (failure2) => Effect2.gen(function* failForSnapshotEffect() {
|
|
2910
3298
|
model.failure = toFailure(failure2);
|
|
@@ -3071,6 +3459,7 @@ function snapshotFor(model, access) {
|
|
|
3071
3459
|
connection: Object.freeze({ syncHealth: model.syncSnapshot?.connection.health, sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
3072
3460
|
failure: model.failure ? Object.freeze({ ...model.failure }) : null,
|
|
3073
3461
|
episodeEnded: model.episodeEndConfirmed,
|
|
3462
|
+
episodeEndReason: model.episodeEndReason,
|
|
3074
3463
|
removedByHost: model.removedByHost
|
|
3075
3464
|
});
|
|
3076
3465
|
}
|
|
@@ -3096,7 +3485,7 @@ function lifecycleFailure(code, recoverable, message, cause) {
|
|
|
3096
3485
|
return new ConnectionLifecycleFailure({ code, recoverable, message, ...cause === void 0 ? {} : { cause } });
|
|
3097
3486
|
}
|
|
3098
3487
|
function toFailure(value) {
|
|
3099
|
-
return Object.freeze({ code: value.code, action: null, recoverable: value.recoverable, message: value.message });
|
|
3488
|
+
return Object.freeze({ code: value.code, action: null, recoverable: value.recoverable, message: value.message, ...value.sync ? { sync: value.sync } : {} });
|
|
3100
3489
|
}
|
|
3101
3490
|
function accessFailure(value) {
|
|
3102
3491
|
return lifecycleFailure(value.code === "access.invalid" ? "invalid_access" : "access_unavailable", value.code === "access.unavailable", value.code === "access.invalid" ? "Access was rejected" : "Could not reach the Space. Check your network and try again.", value.cause);
|
|
@@ -3190,9 +3579,18 @@ function syncTerminalFailure(snapshot) {
|
|
|
3190
3579
|
const reason = snapshot.connection.terminalReason;
|
|
3191
3580
|
if (reason === "state_digest_recovery_failed") return lifecycleFailure("state_digest_recovery_failed", false, "The Episode state could not be recovered from an authoritative snapshot");
|
|
3192
3581
|
if (reason === "episode_ended") return lifecycleFailure("episode_ended", false, "The Episode has ended");
|
|
3193
|
-
if (reason === "participant_inactive" || reason === "participant_removed" || reason === "stale_participant_generation") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
|
|
3582
|
+
if (reason === "participant_inactive" || reason === "participant_removed" || reason === "stale_participant_generation" || reason === "access_rejected") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
|
|
3583
|
+
if (reason === "entry_timeout") return lifecycleFailure("sync_start_failed", true, "Call entry was not confirmed in time");
|
|
3194
3584
|
return lifecycleFailure("sync_start_failed", true, "The Sync transport needs recovery");
|
|
3195
3585
|
}
|
|
3586
|
+
function syncStartupFailure(snapshot) {
|
|
3587
|
+
const sync = snapshot.connection.connectFailure ?? snapshot.connection.connectAttempt;
|
|
3588
|
+
const message = sync?.opened === false ? "Sync could not reach the call service" : "Sync could not connect";
|
|
3589
|
+
return new ConnectionLifecycleFailure({ code: "sync_hello_timeout", recoverable: true, message: `${message}${sync ? ` ${formatSyncConnectDiagnostic(sync)}` : " before the startup deadline"}`, ...sync ? { sync: Object.freeze({ ...sync }) } : {} });
|
|
3590
|
+
}
|
|
3591
|
+
function syncAutoRecoverable(snapshot, failure2) {
|
|
3592
|
+
return failure2.recoverable && snapshot.connection.terminalReason !== "entry_timeout";
|
|
3593
|
+
}
|
|
3196
3594
|
|
|
3197
3595
|
// src/space-client/errors.ts
|
|
3198
3596
|
import { Data as Data3 } from "effect";
|
|
@@ -3233,6 +3631,7 @@ var INVALID_PAYLOAD_CODE_BY_ACTION = /* @__PURE__ */ new Map([
|
|
|
3233
3631
|
]);
|
|
3234
3632
|
var MEDIA_TIMEOUT_FAILURE = { code: "media.timeout", recoverable: true, message: "Media did not respond in time. Try again." };
|
|
3235
3633
|
var SYNC_FAILURES = /* @__PURE__ */ new Map([
|
|
3634
|
+
["sync_hello_timeout", { failure: { code: "connection.sync_hello_timeout", recoverable: true, message: "Sync could not reach the call service. Try again." } }],
|
|
3236
3635
|
["not_live", { failure: { code: "connection.invalid_state", recoverable: true, message: "Wait for the call to connect, then try again." } }],
|
|
3237
3636
|
["signaling_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
3238
3637
|
["negotiation_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
@@ -3247,11 +3646,14 @@ function normalizeMediaError(cause) {
|
|
|
3247
3646
|
}
|
|
3248
3647
|
function normalizeClientError(cause, fallback = "client.internal_error", scope) {
|
|
3249
3648
|
if (cause instanceof SpaceClientError) return cause;
|
|
3649
|
+
const chatFileError2 = normalizeChatFileError(cause);
|
|
3650
|
+
if (chatFileError2) return chatFileError2;
|
|
3250
3651
|
if (cause instanceof ConnectionLifecycleFailure || isConnectionError(cause)) {
|
|
3251
3652
|
return new SpaceClientError({
|
|
3252
3653
|
code: connectionErrorCode(cause.code, cause instanceof ConnectionLifecycleFailure ? null : cause.action),
|
|
3253
3654
|
recoverable: cause.recoverable,
|
|
3254
|
-
message: cause.message
|
|
3655
|
+
message: cause.message,
|
|
3656
|
+
...cause.sync ? { sync: cause.sync } : {}
|
|
3255
3657
|
});
|
|
3256
3658
|
}
|
|
3257
3659
|
if (cause instanceof V1SyncError) {
|
|
@@ -3264,8 +3666,15 @@ function normalizeClientError(cause, fallback = "client.internal_error", scope)
|
|
|
3264
3666
|
const message = cause instanceof Error ? cause.message : "The client operation failed";
|
|
3265
3667
|
return new SpaceClientError({ code: fallback, recoverable: false, message });
|
|
3266
3668
|
}
|
|
3669
|
+
function normalizeChatFileError(cause) {
|
|
3670
|
+
if (!(cause instanceof ChalkChatFileError) || cause.code !== "unavailable") return null;
|
|
3671
|
+
if (cause.stage === "token" && isAccessInvalid(cause.cause)) {
|
|
3672
|
+
return new SpaceClientError({ code: "access.invalid", recoverable: false, message: cause.cause instanceof Error ? cause.cause.message : "Access was rejected" });
|
|
3673
|
+
}
|
|
3674
|
+
return new SpaceClientError({ code: cause.stage === "token" ? "access.unavailable" : "collaboration.unavailable", recoverable: cause.recoverable, message: cause.message });
|
|
3675
|
+
}
|
|
3267
3676
|
function failureFromError(error2) {
|
|
3268
|
-
return Object.freeze({ code: error2.code, recoverable: error2.recoverable, message: error2.message });
|
|
3677
|
+
return Object.freeze({ code: error2.code, recoverable: error2.recoverable, message: error2.message, ...error2.sync ? { sync: error2.sync } : {} });
|
|
3269
3678
|
}
|
|
3270
3679
|
function isAccessInvalid(cause) {
|
|
3271
3680
|
if (typeof cause !== "object" || cause === null || !("code" in cause)) return false;
|
|
@@ -3299,7 +3708,7 @@ var EMPTY = empty({
|
|
|
3299
3708
|
reactions: empty({ active: empty([]) }),
|
|
3300
3709
|
recording: empty({ current: null }),
|
|
3301
3710
|
offline: empty({ pending: empty([]), decisionNeeded: false, policy: "send" }),
|
|
3302
|
-
whiteboard: empty({ open: false, engine: empty({ status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null }) })
|
|
3711
|
+
whiteboard: empty({ open: false, engine: empty({ status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null }), access: empty({ canDraw: false, policy: null, permissions: empty([]) }) })
|
|
3303
3712
|
});
|
|
3304
3713
|
var SpaceStore = class _SpaceStore {
|
|
3305
3714
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -3329,6 +3738,7 @@ var SpaceStore = class _SpaceStore {
|
|
|
3329
3738
|
episode: snapshot.episode,
|
|
3330
3739
|
lastError: sameError(current.lastError, error2) ? current.lastError : error2,
|
|
3331
3740
|
episodeEnded: snapshot.episodeEnded ?? false,
|
|
3741
|
+
episodeEndReason: snapshot.episodeEndReason,
|
|
3332
3742
|
removedByHost: snapshot.removedByHost ?? false
|
|
3333
3743
|
};
|
|
3334
3744
|
if (sameConnection(current, next)) return;
|
|
@@ -3380,14 +3790,14 @@ var SpaceStoreService = class extends Context4.Service()("@chalk/client/SpaceSto
|
|
|
3380
3790
|
var makeSpaceStoreLayer = Layer4.effect(SpaceStoreService, SpaceStore.make());
|
|
3381
3791
|
var makeFakeSpaceStoreLayer = (store = new SpaceStore()) => Layer4.succeed(SpaceStoreService, store);
|
|
3382
3792
|
function connectionError(failure2) {
|
|
3383
|
-
return failure2 ? empty({ code: connectionErrorCode(failure2.code, failure2.action), recoverable: failure2.recoverable, message: failure2.message }) : null;
|
|
3793
|
+
return failure2 ? empty({ code: connectionErrorCode(failure2.code, failure2.action), recoverable: failure2.recoverable, message: failure2.message, ...failure2.sync ? { sync: failure2.sync } : {} }) : null;
|
|
3384
3794
|
}
|
|
3385
3795
|
function connectionStatus(snapshot) {
|
|
3386
3796
|
const interrupted = INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media);
|
|
3387
3797
|
return snapshot.state === "live" && interrupted ? "reconnecting" : snapshot.state;
|
|
3388
3798
|
}
|
|
3389
3799
|
function sameConnection(left, right) {
|
|
3390
|
-
return left.status === right.status && left.syncHealth === right.syncHealth && sameEpisode(left.episode, right.episode) && left.lastError === right.lastError && left.episodeEnded === right.episodeEnded && left.removedByHost === right.removedByHost;
|
|
3800
|
+
return left.status === right.status && left.syncHealth === right.syncHealth && sameEpisode(left.episode, right.episode) && left.lastError === right.lastError && left.episodeEnded === right.episodeEnded && left.episodeEndReason === right.episodeEndReason && left.removedByHost === right.removedByHost;
|
|
3391
3801
|
}
|
|
3392
3802
|
function sameEpisode(left, right) {
|
|
3393
3803
|
if (left === right) return true;
|
|
@@ -3520,13 +3930,13 @@ var ChatControllerRuntime = class {
|
|
|
3520
3930
|
#unsubscribeConnection = null;
|
|
3521
3931
|
#unsubscribeEvents = null;
|
|
3522
3932
|
#unsubscribeSnapshot = null;
|
|
3523
|
-
constructor(connection, store, transport, apiBaseUrl,
|
|
3933
|
+
constructor(connection, store, transport, apiBaseUrl, fetch2, fork, offline) {
|
|
3524
3934
|
this.#offline = offline;
|
|
3525
3935
|
this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
|
|
3526
3936
|
this.#connection = connection;
|
|
3527
3937
|
this.#store = store;
|
|
3528
3938
|
this.#transport = transport;
|
|
3529
|
-
this.#fetch =
|
|
3939
|
+
this.#fetch = fetch2;
|
|
3530
3940
|
this.#fork = fork;
|
|
3531
3941
|
this.#unsubscribeConnection = connection.subscribePorts((ports) => this.#bind(ports));
|
|
3532
3942
|
}
|
|
@@ -3604,7 +4014,7 @@ var ChatControllerRuntime = class {
|
|
|
3604
4014
|
});
|
|
3605
4015
|
};
|
|
3606
4016
|
upload = (file) => {
|
|
3607
|
-
const
|
|
4017
|
+
const fetch2 = this.#fetch;
|
|
3608
4018
|
return Effect4.suspend(() => {
|
|
3609
4019
|
if (!this.#transport) {
|
|
3610
4020
|
return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file upload is unavailable" }));
|
|
@@ -3619,7 +4029,10 @@ var ChatControllerRuntime = class {
|
|
|
3619
4029
|
Effect4.flatMap(
|
|
3620
4030
|
(upload) => Effect4.void.pipe(
|
|
3621
4031
|
Effect4.andThen(
|
|
3622
|
-
|
|
4032
|
+
Effect4.tryPromise({
|
|
4033
|
+
try: () => fetch2(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes }),
|
|
4034
|
+
catch: () => new SpaceClientError({ code: "collaboration.unavailable", recoverable: true, message: CHAT_FILE_REACHABILITY_MESSAGE })
|
|
4035
|
+
}).pipe(
|
|
3623
4036
|
Effect4.flatMap((response) => {
|
|
3624
4037
|
if (!response.ok) {
|
|
3625
4038
|
return Effect4.fail(new SpaceClientError({ code: "command.rejected", recoverable: response.status >= 500, message: `Attachment upload failed with HTTP ${response.status}` }));
|
|
@@ -3942,6 +4355,7 @@ function chatError(message) {
|
|
|
3942
4355
|
// src/space-client/media-controller.ts
|
|
3943
4356
|
import { Clock as Clock3, Context as Context6, Deferred as Deferred2, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
|
|
3944
4357
|
var MEDIA_SOURCES = ["microphone", "camera", "screen"];
|
|
4358
|
+
var MEDIA_READY_TIMEOUT_MS = 1e4;
|
|
3945
4359
|
function sourceWorker(enabled) {
|
|
3946
4360
|
return { desired: { enabled, revision: 0, previousEnabled: enabled }, result: { revision: 0, status: "confirmed" }, sentRevision: 0, initialRevision: 0, localEnabled: enabled, pause: null, serverSequence: -1, forcedRevision: null };
|
|
3947
4361
|
}
|
|
@@ -3986,8 +4400,9 @@ var MediaControllerRuntime = class {
|
|
|
3986
4400
|
#tracks = /* @__PURE__ */ new Map();
|
|
3987
4401
|
#sources = { microphone: sourceWorker(true), camera: sourceWorker(true), screen: sourceWorker(false) };
|
|
3988
4402
|
#syncLive = false;
|
|
3989
|
-
#pendingSources = /* @__PURE__ */ new
|
|
4403
|
+
#pendingSources = /* @__PURE__ */ new Map();
|
|
3990
4404
|
#pendingCaptures = /* @__PURE__ */ new Set();
|
|
4405
|
+
#readinessChecks = /* @__PURE__ */ new Set();
|
|
3991
4406
|
#requestGenerations = /* @__PURE__ */ new Map();
|
|
3992
4407
|
#camera = { status: "idle" };
|
|
3993
4408
|
#requests = Object.freeze([]);
|
|
@@ -4015,8 +4430,8 @@ var MediaControllerRuntime = class {
|
|
|
4015
4430
|
if (intent.microphone !== void 0) this.#recordIntent("microphone", intent.microphone);
|
|
4016
4431
|
if (intent.camera !== void 0) this.#recordIntent("camera", intent.camera);
|
|
4017
4432
|
}
|
|
4018
|
-
setMicrophoneEnabled = (enabled) => this.#
|
|
4019
|
-
setCameraEnabled = (enabled) => this.#
|
|
4433
|
+
setMicrophoneEnabled = (enabled) => this.#set("microphone", enabled);
|
|
4434
|
+
setCameraEnabled = (enabled) => this.#set("camera", enabled);
|
|
4020
4435
|
captureCameraPreview = () => this.#cameraSource("preview").pipe(Effect5.mapError(normalizeMediaError));
|
|
4021
4436
|
setScreenShareEnabled = (enabled, options) => enabled ? this.#mediaAction(true, this.#startScreen(options)) : this.#stopScreen();
|
|
4022
4437
|
selectMicrophone = (deviceId) => Effect5.sync(() => {
|
|
@@ -4110,6 +4525,7 @@ var MediaControllerRuntime = class {
|
|
|
4110
4525
|
this.#fork(this.#refreshDevices().pipe(Effect5.ignore));
|
|
4111
4526
|
}
|
|
4112
4527
|
dispose() {
|
|
4528
|
+
for (const check of this.#readinessChecks) check(normalizeMediaError(cancelledCaptureError()));
|
|
4113
4529
|
this.#cancelCaptures();
|
|
4114
4530
|
for (const unsubscribe of [this.#unsubscribeMedia, this.#unsubscribeRequests, this.#unsubscribePause, this.#unsubscribeSync]) unsubscribe?.();
|
|
4115
4531
|
this.#unsubscribeConnection?.();
|
|
@@ -4134,8 +4550,37 @@ var MediaControllerRuntime = class {
|
|
|
4134
4550
|
#mediaAction(enabling, operation) {
|
|
4135
4551
|
return Effect5.suspend(() => !enabling || this.#store.getSnapshot().connection.status === "live" ? operation : Effect5.fail(normalizeMediaError(new V1SyncError("Media enable requires a live call", "not_live"))));
|
|
4136
4552
|
}
|
|
4553
|
+
#waitForLive(source, revision, ports) {
|
|
4554
|
+
return Effect5.suspend(() => {
|
|
4555
|
+
let cleanup = () => {
|
|
4556
|
+
};
|
|
4557
|
+
return Effect5.callback((resume) => {
|
|
4558
|
+
const check = (failure2) => {
|
|
4559
|
+
if (failure2) return resume(Effect5.fail(failure2));
|
|
4560
|
+
if (this.#sources[source].desired.revision !== revision) return resume(Effect5.void);
|
|
4561
|
+
if (ports && (this.#ports?.sync !== ports.sync || this.#ports.media !== ports.media)) return resume(Effect5.fail(normalizeMediaError(cancelledCaptureError())));
|
|
4562
|
+
const state = this.#connection.getSnapshot().state;
|
|
4563
|
+
if (state === "live" && this.#ports?.sync.getSnapshot().connection.phase === "live" && this.#store.getSnapshot().connection.status === "live") return resume(Effect5.void);
|
|
4564
|
+
if (state !== "joining" && state !== "reconnecting" && state !== "live") resume(Effect5.fail(normalizeMediaError(new V1SyncError("Media enable requires a live call", "not_live"))));
|
|
4565
|
+
};
|
|
4566
|
+
this.#readinessChecks.add(check);
|
|
4567
|
+
const unsubscribe = this.#connection.subscribe(() => check());
|
|
4568
|
+
const unsubscribeStore = this.#store.subscribe(() => check());
|
|
4569
|
+
const unsubscribePorts = this.#connection.subscribePorts(() => check());
|
|
4570
|
+
const timer = this.#connection.scheduleUnsafe(() => check(new SpaceClientError({ code: "media.timeout", recoverable: true, message: "The call did not connect in time. Try again." })), MEDIA_READY_TIMEOUT_MS);
|
|
4571
|
+
cleanup = () => {
|
|
4572
|
+
this.#readinessChecks.delete(check);
|
|
4573
|
+
unsubscribe();
|
|
4574
|
+
unsubscribeStore();
|
|
4575
|
+
unsubscribePorts();
|
|
4576
|
+
this.#connection.cancelScheduleUnsafe(timer);
|
|
4577
|
+
};
|
|
4578
|
+
check();
|
|
4579
|
+
}).pipe(Effect5.ensuring(Effect5.sync(() => cleanup())));
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4137
4582
|
#set(source, enabled) {
|
|
4138
|
-
return this.#act(source, enabled);
|
|
4583
|
+
return Effect5.suspend(() => this.#store.getSnapshot().connection.status === "joining" ? this.#act(source, enabled) : this.#mediaAction(enabled, this.#act(source, enabled)));
|
|
4139
4584
|
}
|
|
4140
4585
|
#startScreen(options) {
|
|
4141
4586
|
return this.#act("screen", true, options);
|
|
@@ -4170,19 +4615,41 @@ var MediaControllerRuntime = class {
|
|
|
4170
4615
|
void state.pause.catch(() => void 0);
|
|
4171
4616
|
}
|
|
4172
4617
|
}
|
|
4618
|
+
for (const check of this.#readinessChecks) check();
|
|
4173
4619
|
return desired;
|
|
4174
4620
|
}
|
|
4175
4621
|
#act(source, enabled, options) {
|
|
4176
4622
|
return Effect5.suspend(() => {
|
|
4177
4623
|
const desired = this.#recordIntent(source, enabled, options);
|
|
4178
|
-
|
|
4179
|
-
Effect5.
|
|
4180
|
-
Effect5.
|
|
4181
|
-
if (
|
|
4182
|
-
|
|
4183
|
-
|
|
4624
|
+
const ready = enabled && source !== "screen" && this.#store.getSnapshot().connection.status !== "live" ? this.#waitForLive(source, desired.revision).pipe(
|
|
4625
|
+
Effect5.tapError(
|
|
4626
|
+
(cause) => Effect5.sync(() => {
|
|
4627
|
+
if (this.#sources[source].desired.revision === desired.revision) {
|
|
4628
|
+
const cancelled = this.#recordIntent(source, false);
|
|
4629
|
+
this.#sources[source].result = { revision: cancelled.revision, status: "failed", cause, cleanup: false };
|
|
4630
|
+
}
|
|
4184
4631
|
})
|
|
4185
4632
|
)
|
|
4633
|
+
) : Effect5.void;
|
|
4634
|
+
return this.#pending(
|
|
4635
|
+
source,
|
|
4636
|
+
ready.pipe(
|
|
4637
|
+
Effect5.andThen(
|
|
4638
|
+
Effect5.suspend(() => {
|
|
4639
|
+
if (this.#sources[source].desired.revision !== desired.revision) return Effect5.void;
|
|
4640
|
+
return this.#serialize(source, this.#work(source)).pipe(
|
|
4641
|
+
Effect5.andThen(
|
|
4642
|
+
Effect5.suspend(() => {
|
|
4643
|
+
const result = this.#sources[source].result;
|
|
4644
|
+
if (result.revision === desired.revision && result.status === "failed") return Effect5.fail(result.cause);
|
|
4645
|
+
if (enabled && (source === "screen" || this.#sources[source].desired.enabled) && this.#store.getSnapshot().connection.status !== "live") return this.#mediaAction(true, Effect5.void);
|
|
4646
|
+
return Effect5.void;
|
|
4647
|
+
})
|
|
4648
|
+
)
|
|
4649
|
+
);
|
|
4650
|
+
})
|
|
4651
|
+
)
|
|
4652
|
+
)
|
|
4186
4653
|
);
|
|
4187
4654
|
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
4188
4655
|
}
|
|
@@ -4257,18 +4724,24 @@ var MediaControllerRuntime = class {
|
|
|
4257
4724
|
return source === "camera" && !intent.previousEnabled || !this.#hasTrack(source) ? this.#captureForPublish(
|
|
4258
4725
|
source,
|
|
4259
4726
|
ports.media,
|
|
4260
|
-
(track) => Effect5.
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4727
|
+
(track) => Effect5.sync(() => {
|
|
4728
|
+
captured = track;
|
|
4729
|
+
}).pipe(
|
|
4730
|
+
Effect5.andThen(this.#waitForLive(source, intent.revision, ports)),
|
|
4731
|
+
Effect5.andThen(
|
|
4732
|
+
Effect5.try({
|
|
4733
|
+
try: () => {
|
|
4734
|
+
this.#assertActivePorts(ports, action);
|
|
4735
|
+
this.#assertCurrentIntent(source, intent.revision);
|
|
4736
|
+
this.#setTrack(source, track);
|
|
4737
|
+
ports.media.prepareLocalTrack(source, track);
|
|
4738
|
+
prepared = true;
|
|
4739
|
+
this.#publish();
|
|
4740
|
+
},
|
|
4741
|
+
catch: (cause) => cause
|
|
4742
|
+
})
|
|
4743
|
+
)
|
|
4744
|
+
)
|
|
4272
4745
|
) : Effect5.void;
|
|
4273
4746
|
});
|
|
4274
4747
|
return retire.pipe(Effect5.andThen(acquire), Effect5.andThen(this.#send(ports, source, intent, true)));
|
|
@@ -4384,7 +4857,7 @@ var MediaControllerRuntime = class {
|
|
|
4384
4857
|
return this.#serialize("microphone", this.#serialize("camera", publish));
|
|
4385
4858
|
}
|
|
4386
4859
|
#send(ports, source, intent, enabled) {
|
|
4387
|
-
|
|
4860
|
+
const command = this.#mediaAction(
|
|
4388
4861
|
enabled,
|
|
4389
4862
|
foreign2(async () => {
|
|
4390
4863
|
this.#assertCurrentIntent(source, intent.revision);
|
|
@@ -4392,9 +4865,12 @@ var MediaControllerRuntime = class {
|
|
|
4392
4865
|
const state = this.#sources[source];
|
|
4393
4866
|
state.sentRevision = intent.revision;
|
|
4394
4867
|
await (source === "microphone" ? ports.sync.setMicrophoneEnabled(enabled) : source === "camera" ? ports.sync.setCameraEnabled(enabled) : ports.sync.setScreenShareEnabled(enabled));
|
|
4395
|
-
|
|
4396
|
-
|
|
4868
|
+
}).pipe(
|
|
4869
|
+
Effect5.andThen(enabled && source !== "screen" ? this.#waitForLive(source, intent.revision, ports) : Effect5.void),
|
|
4870
|
+
Effect5.tap(() => Effect5.try({ try: () => this.#assertActivePorts(ports, source === "screen" ? "startScreenShare" : source === "camera" ? "setCameraEnabled" : "setMicrophoneEnabled"), catch: (cause) => cause }))
|
|
4871
|
+
)
|
|
4397
4872
|
);
|
|
4873
|
+
return enabled && source !== "screen" ? this.#waitForLive(source, intent.revision, ports).pipe(Effect5.andThen(command)) : command;
|
|
4398
4874
|
}
|
|
4399
4875
|
#reconcileIntents() {
|
|
4400
4876
|
const live = this.#connection.getSnapshot().state === "live" && this.#ports?.sync.getSnapshot().connection.phase === "live";
|
|
@@ -4419,18 +4895,21 @@ var MediaControllerRuntime = class {
|
|
|
4419
4895
|
return pause;
|
|
4420
4896
|
}
|
|
4421
4897
|
#serialize(source, effect) {
|
|
4422
|
-
return this.#gates.get(source).withPermit(
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4898
|
+
return this.#gates.get(source).withPermit(this.#pending(source, effect));
|
|
4899
|
+
}
|
|
4900
|
+
#pending(source, effect) {
|
|
4901
|
+
return Effect5.sync(() => {
|
|
4902
|
+
this.#pendingSources.set(source, (this.#pendingSources.get(source) ?? 0) + 1);
|
|
4903
|
+
this.#publish();
|
|
4904
|
+
}).pipe(
|
|
4905
|
+
Effect5.andThen(effect),
|
|
4906
|
+
Effect5.ensuring(
|
|
4907
|
+
Effect5.sync(() => {
|
|
4908
|
+
const remaining = this.#pendingSources.get(source) - 1;
|
|
4909
|
+
if (remaining === 0) this.#pendingSources.delete(source);
|
|
4910
|
+
else this.#pendingSources.set(source, remaining);
|
|
4911
|
+
this.#publish();
|
|
4912
|
+
})
|
|
4434
4913
|
)
|
|
4435
4914
|
);
|
|
4436
4915
|
}
|
|
@@ -4873,9 +5352,9 @@ var MediaControllerRuntime = class {
|
|
|
4873
5352
|
const snapshot = this.#ports?.media.getSnapshot();
|
|
4874
5353
|
const state = this.#connection.getSnapshot().state;
|
|
4875
5354
|
const local = Object.freeze({
|
|
4876
|
-
microphone: localMedia("microphone", this.#localTracks(), snapshot, this.#sources.microphone.localEnabled, state, this.#pendingSources.has("microphone")),
|
|
4877
|
-
camera: localMedia("camera", this.#localTracks(), snapshot, this.#sources.camera.localEnabled, state, this.#pendingSources.has("camera")),
|
|
4878
|
-
screen: localMedia("screen", this.#localTracks(), snapshot, false, state, this.#pendingSources.has("screen"))
|
|
5355
|
+
microphone: localMedia("microphone", this.#localTracks(), snapshot, this.#sources.microphone.localEnabled, state, this.#pendingSources.has("microphone") && this.#sources.microphone.desired.enabled),
|
|
5356
|
+
camera: localMedia("camera", this.#localTracks(), snapshot, this.#sources.camera.localEnabled, state, this.#pendingSources.has("camera") && this.#sources.camera.desired.enabled),
|
|
5357
|
+
screen: localMedia("screen", this.#localTracks(), snapshot, false, state, this.#pendingSources.has("screen") && this.#sources.screen.desired.enabled)
|
|
4879
5358
|
});
|
|
4880
5359
|
const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
|
|
4881
5360
|
const media = Object.freeze({
|
|
@@ -4929,8 +5408,8 @@ function initialTrackEntries(microphone, camera) {
|
|
|
4929
5408
|
function localMedia(source, tracks, snapshot, intended, connectionState, pending) {
|
|
4930
5409
|
const track = tracks.get(source) ?? null;
|
|
4931
5410
|
const publication = snapshot?.localTracks.find((candidate) => candidate.source === source);
|
|
4932
|
-
if (locallyMuted(intended, track, publication)) return Object.freeze({ source, state: "disabled", track });
|
|
4933
5411
|
if (pending) return Object.freeze({ source, state: "requesting", track });
|
|
5412
|
+
if (locallyMuted(intended, track, publication)) return Object.freeze({ source, state: "disabled", track });
|
|
4934
5413
|
if (publication?.enabled) return Object.freeze({ source, state: "enabled", track });
|
|
4935
5414
|
if (publication && connectionState === "live") return Object.freeze({ source, state: "disabled", track });
|
|
4936
5415
|
return Object.freeze({ source, state: localMediaState(source, track, intended, connectionState), track });
|
|
@@ -5364,6 +5843,7 @@ var ParticipantsControllerRuntime = class {
|
|
|
5364
5843
|
publish();
|
|
5365
5844
|
}
|
|
5366
5845
|
#publish(snapshot, collaborationCapabilities, media) {
|
|
5846
|
+
if (snapshot && !controlFor(snapshot)) return;
|
|
5367
5847
|
const audioParticipants = new Set(media?.remoteTracks.filter((track) => track.source === "screen_audio" && track.track.readyState === "live").map((track) => track.participantId));
|
|
5368
5848
|
if (snapshot?.participantId && media?.localTracks.some((track) => track.source === "screen_audio" && track.enabled && track.track.readyState === "live")) audioParticipants.add(snapshot.participantId);
|
|
5369
5849
|
const projected = participantsFor(snapshot, collaborationCapabilities);
|
|
@@ -5698,7 +6178,7 @@ function foreign5(operation) {
|
|
|
5698
6178
|
|
|
5699
6179
|
// src/space-client/whiteboard-controller.ts
|
|
5700
6180
|
import { Context as Context11, Effect as Effect10, Layer as Layer11 } from "effect";
|
|
5701
|
-
var EMPTY_SUMMARY = Object.freeze({ status: "unsubscribed", sceneId: null, revision: null, capabilities: Object.freeze([]), canDraw: false, canClear: false, presenting: false, error: null });
|
|
6181
|
+
var EMPTY_SUMMARY = Object.freeze({ status: "unsubscribed", sceneId: null, revision: null, capabilities: Object.freeze([]), canDraw: false, canClear: false, drawPolicy: null, drawPermissions: Object.freeze([]), presenting: false, error: null });
|
|
5702
6182
|
var WhiteboardControllerService = class extends Context11.Service()("@chalk/client/WhiteboardController") {
|
|
5703
6183
|
};
|
|
5704
6184
|
var makeWhiteboardController = (connection, store, create) => Effect10.gen(function* () {
|
|
@@ -5721,13 +6201,22 @@ var WhiteboardControllerRuntime = class {
|
|
|
5721
6201
|
#connected = false;
|
|
5722
6202
|
#summary = EMPTY_SUMMARY;
|
|
5723
6203
|
#unsubscribe = null;
|
|
6204
|
+
#unsubscribeRoster = null;
|
|
5724
6205
|
constructor(connection, store, transport) {
|
|
5725
6206
|
this.#store = store;
|
|
5726
6207
|
this.#transport = transport;
|
|
6208
|
+
let roster = new Set(store.getSnapshot().participants.roster.map((person) => person.participantId));
|
|
6209
|
+
this.#unsubscribeRoster = store.subscribe(() => {
|
|
6210
|
+
const next = new Set(store.getSnapshot().participants.roster.map((person) => person.participantId));
|
|
6211
|
+
for (const id of roster) if (!next.has(id)) transport?.forgetParticipant?.(id);
|
|
6212
|
+
roster = next;
|
|
6213
|
+
});
|
|
5727
6214
|
this.#unsubscribe = connection.subscribePorts((ports) => this.#bind(ports));
|
|
5728
6215
|
}
|
|
5729
6216
|
transport = () => this.#transport;
|
|
5730
6217
|
dispose() {
|
|
6218
|
+
this.#unsubscribeRoster?.();
|
|
6219
|
+
this.#unsubscribeRoster = null;
|
|
5731
6220
|
this.#unsubscribe?.();
|
|
5732
6221
|
this.#unsubscribe = null;
|
|
5733
6222
|
this.#bind(null);
|
|
@@ -5741,6 +6230,8 @@ var WhiteboardControllerRuntime = class {
|
|
|
5741
6230
|
capabilities: Object.freeze([...summary.capabilities]),
|
|
5742
6231
|
canDraw: summary.canDraw,
|
|
5743
6232
|
canClear: summary.canClear,
|
|
6233
|
+
drawPolicy: summary.drawPolicy,
|
|
6234
|
+
drawPermissions: Object.freeze(summary.drawPermissions.map((permission) => Object.freeze({ ...permission }))),
|
|
5744
6235
|
presenting: summary.presenting,
|
|
5745
6236
|
error: summary.error ? Object.freeze({ ...summary.error }) : null
|
|
5746
6237
|
});
|
|
@@ -5758,14 +6249,21 @@ var WhiteboardControllerRuntime = class {
|
|
|
5758
6249
|
this.#publish();
|
|
5759
6250
|
}
|
|
5760
6251
|
#publish() {
|
|
5761
|
-
const slice = Object.freeze({
|
|
6252
|
+
const slice = Object.freeze({
|
|
6253
|
+
open: this.#summary.status !== "unsubscribed",
|
|
6254
|
+
engine: Object.freeze({ status: this.#summary.status, sceneId: this.#summary.sceneId, revision: this.#summary.revision, presenting: this.#summary.presenting, error: this.#summary.error }),
|
|
6255
|
+
access: Object.freeze({ canDraw: this.#summary.canDraw, policy: this.#summary.drawPolicy, permissions: this.#summary.drawPermissions })
|
|
6256
|
+
});
|
|
5762
6257
|
const current = this.#store.getSnapshot().whiteboard;
|
|
5763
6258
|
if (sameWhiteboard(current, slice)) return;
|
|
5764
6259
|
this.#store.updateWhiteboard(slice);
|
|
5765
6260
|
}
|
|
5766
6261
|
};
|
|
5767
6262
|
function sameWhiteboard(left, right) {
|
|
5768
|
-
return left.open === right.open && left.engine.status === right.engine.status && left.engine.sceneId === right.engine.sceneId && left.engine.revision === right.engine.revision && left.engine.presenting === right.engine.presenting && sameError2(left.engine.error, right.engine.error);
|
|
6263
|
+
return left.open === right.open && left.engine.status === right.engine.status && left.engine.sceneId === right.engine.sceneId && left.engine.revision === right.engine.revision && left.engine.presenting === right.engine.presenting && sameError2(left.engine.error, right.engine.error) && sameAccess(left.access, right.access);
|
|
6264
|
+
}
|
|
6265
|
+
function sameAccess(left, right) {
|
|
6266
|
+
return left.canDraw === right.canDraw && left.policy === right.policy && left.permissions.length === right.permissions.length && left.permissions.every((permission, index) => permission.participantId === right.permissions[index]?.participantId && permission.canDraw === right.permissions[index]?.canDraw);
|
|
5769
6267
|
}
|
|
5770
6268
|
function sameError2(left, right) {
|
|
5771
6269
|
if (left === right) return true;
|
|
@@ -5857,7 +6355,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
5857
6355
|
diagnostics: {
|
|
5858
6356
|
onEvent: (event) => {
|
|
5859
6357
|
platform.onConnectionDiagnostic?.(event);
|
|
5860
|
-
options.logger?.debug?.("SpaceClient connection event", { event: event.event, state: event.state, epoch: event.epoch, ...event.code ? { code: event.code } : {} });
|
|
6358
|
+
options.logger?.debug?.("SpaceClient connection event", { event: event.event, state: event.state, epoch: event.epoch, ...event.code ? { code: event.code } : {}, ...event.sync ? { sync: event.sync } : {} });
|
|
5861
6359
|
}
|
|
5862
6360
|
}
|
|
5863
6361
|
});
|
|
@@ -5913,7 +6411,7 @@ var SpaceClientCore = class {
|
|
|
5913
6411
|
this.#store.updateConnection(snapshot);
|
|
5914
6412
|
const newlyEnded = snapshot.episodeEnded && !this.#episodeEnded;
|
|
5915
6413
|
this.#episodeEnded = snapshot.episodeEnded ?? false;
|
|
5916
|
-
if (newlyEnded) this.#emit("episodeEnded", { episode });
|
|
6414
|
+
if (newlyEnded) this.#emit("episodeEnded", { episode, reason: snapshot.episodeEndReason });
|
|
5917
6415
|
});
|
|
5918
6416
|
}
|
|
5919
6417
|
getSnapshot = () => this.#store.getSnapshot();
|
|
@@ -6118,13 +6616,14 @@ export {
|
|
|
6118
6616
|
CHALK_CHAT_ATTACHMENT_MIME_TYPES,
|
|
6119
6617
|
ChalkChatFileError,
|
|
6120
6618
|
createChalkChatFileHttpTransport,
|
|
6619
|
+
ChalkWhiteboardViewBounds,
|
|
6121
6620
|
createChalkWhiteboardV1FileDownloadTransport,
|
|
6621
|
+
ChalkWhiteboardViewVersions,
|
|
6122
6622
|
encodeWhiteboardV1ClientFrame,
|
|
6123
6623
|
decodeWhiteboardV1ServerFrame,
|
|
6124
6624
|
InMemoryChalkWhiteboardV1PendingOperationStore,
|
|
6125
6625
|
ChalkWhiteboardV1Client,
|
|
6126
6626
|
createChalkWhiteboardV1Client,
|
|
6127
|
-
ChalkWhiteboardViewBounds,
|
|
6128
6627
|
makeProductionConnectionPlatformLayer,
|
|
6129
6628
|
ConnectionLifecycleService,
|
|
6130
6629
|
makeConnectionLifecycleLayer,
|