@q9labsai/chalk-client 5.1.0 → 5.2.0
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-RJRSLXVT.js → chunk-BCO3AZUB.js} +647 -221
- package/dist/{chunk-K7PNW3KI.js → chunk-D2GXPX2P.js} +143 -13
- package/dist/{chunk-KHHULLBZ.js → chunk-DHGAKJXP.js} +1 -1
- package/dist/{chunk-3ZN7IXOZ.js → chunk-HO4745TX.js} +250 -2
- package/dist/client.d.ts +201 -0
- package/dist/connection/dependencies.d.ts +3 -0
- package/dist/connection/diagnostics.d.ts +3 -2
- 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 +197 -0
- package/dist/generated/schemas.d.ts +366 -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 +18 -14
- package/dist/public-invites/index.js +2 -2
- package/dist/space-client/reload-rejoin.d.ts +1 -1
- package/dist/space-client/types.d.ts +8 -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 +4 -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
|
@@ -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-HO4745TX.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-D2GXPX2P.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 {
|
|
@@ -2293,15 +2630,16 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2293
2630
|
refreshInFlight: false,
|
|
2294
2631
|
recoveryQueued: null,
|
|
2295
2632
|
joinCancelled: false,
|
|
2633
|
+
admissionCleanupConfirmed: true,
|
|
2296
2634
|
closed: false,
|
|
2297
2635
|
epoch: 0
|
|
2298
2636
|
};
|
|
2299
2637
|
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) });
|
|
2638
|
+
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
2639
|
const withinForegroundBudget = (effect, milliseconds, failure2) => Effect2.raceFirst(
|
|
2302
2640
|
effect,
|
|
2303
2641
|
Effect2.callback((resume) => {
|
|
2304
|
-
const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(failure2)));
|
|
2642
|
+
const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(typeof failure2 === "function" ? failure2() : failure2)));
|
|
2305
2643
|
return Effect2.sync(cancel);
|
|
2306
2644
|
})
|
|
2307
2645
|
);
|
|
@@ -2422,13 +2760,23 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2422
2760
|
model.syncNoticeUnresponsive = false;
|
|
2423
2761
|
let latestSnapshot = null;
|
|
2424
2762
|
let observedDialTimeouts = 0;
|
|
2763
|
+
let observedConnectFailure;
|
|
2425
2764
|
let snapshotQueued = false;
|
|
2426
|
-
const
|
|
2765
|
+
const recordSyncDiagnostics = (snapshot) => {
|
|
2766
|
+
if (sync !== model.sync) return;
|
|
2767
|
+
const failure2 = snapshot.connection.connectFailure;
|
|
2768
|
+
if (failure2 && failure2 !== observedConnectFailure) {
|
|
2769
|
+
observedConnectFailure = failure2;
|
|
2770
|
+
diagnostics.record({ event: "sync_connect_failed", state: model.state, epoch: model.epoch, code: "sync_hello_timeout", sync: failure2 });
|
|
2771
|
+
}
|
|
2427
2772
|
const count = snapshot.connection.dialTimeoutCount ?? 0;
|
|
2428
|
-
if (
|
|
2773
|
+
if (count > observedDialTimeouts) {
|
|
2429
2774
|
observedDialTimeouts = count;
|
|
2430
2775
|
diagnostics.record({ event: "sync_dial_timeout", state: model.state, epoch: model.epoch, attempt: count });
|
|
2431
2776
|
}
|
|
2777
|
+
};
|
|
2778
|
+
const unsubscribe = sync.subscribe((snapshot) => {
|
|
2779
|
+
recordSyncDiagnostics(snapshot);
|
|
2432
2780
|
if (sync === model.sync && snapshot.connection.phase === "live") {
|
|
2433
2781
|
const notice = snapshot.connection.noticeUnresponsive === true;
|
|
2434
2782
|
if (notice !== model.syncNoticeUnresponsive) {
|
|
@@ -2490,7 +2838,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2490
2838
|
const unsubscribe = sync.subscribe(observe);
|
|
2491
2839
|
observe(sync.getSnapshot());
|
|
2492
2840
|
return Effect2.sync(unsubscribe);
|
|
2493
|
-
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs,
|
|
2841
|
+
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs, () => syncStartupFailure(sync.getSnapshot())));
|
|
2494
2842
|
const trace = (step, effect) => {
|
|
2495
2843
|
const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
|
|
2496
2844
|
return effect.pipe(
|
|
@@ -2498,6 +2846,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2498
2846
|
Effect2.tapError(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "failed" })))
|
|
2499
2847
|
);
|
|
2500
2848
|
};
|
|
2849
|
+
const confirmSyncEntry = (sync) => {
|
|
2850
|
+
const confirmEntered = sync.confirmEntered?.bind(sync);
|
|
2851
|
+
return confirmEntered ? trace("confirm_entered", foreign7(confirmEntered, "sync_start_failed", "Entry could not be confirmed")) : Effect2.void;
|
|
2852
|
+
};
|
|
2501
2853
|
const performJoin = (reason = "join", retried = false) => Effect2.gen(function* () {
|
|
2502
2854
|
if (model.state === "live") return;
|
|
2503
2855
|
if (model.state === "leaving" || model.closed) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Cannot join an inactive Connection"));
|
|
@@ -2517,7 +2869,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2517
2869
|
stream = captured.stream;
|
|
2518
2870
|
captureWarning = captured.failure;
|
|
2519
2871
|
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));
|
|
2872
|
+
const grant = yield* trace("access_initialize", reason === "access_retry" ? access.refreshAfterRejection() : access.initialize(reason));
|
|
2873
|
+
model.admissionCleanupConfirmed = false;
|
|
2521
2874
|
const media = yield* trace(
|
|
2522
2875
|
"create_media_client",
|
|
2523
2876
|
Effect2.try({
|
|
@@ -2550,10 +2903,18 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2550
2903
|
"start_media",
|
|
2551
2904
|
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
2905
|
),
|
|
2553
|
-
trace(
|
|
2906
|
+
trace(
|
|
2907
|
+
"start_sync",
|
|
2908
|
+
foreign7(() => sync.start(), "sync_start_failed", "The sync layer could not start").pipe(
|
|
2909
|
+
Effect2.mapError((failure2) => sync.getSnapshot().connection.connectFailure ? syncStartupFailure(sync.getSnapshot()) : failure2),
|
|
2910
|
+
Effect2.andThen(trace("wait_for_sync_live", waitForSyncLive(sync, boundedInteger(options.syncStartupTimeoutMs, START_TIMEOUT_MS, 1, 6e4))))
|
|
2911
|
+
)
|
|
2912
|
+
)
|
|
2554
2913
|
],
|
|
2555
2914
|
{ concurrency: "unbounded", discard: true }
|
|
2556
2915
|
);
|
|
2916
|
+
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2917
|
+
yield* confirmSyncEntry(sync);
|
|
2557
2918
|
}).pipe(
|
|
2558
2919
|
Effect2.matchEffect({
|
|
2559
2920
|
onFailure: (cause) => Effect2.gen(function* () {
|
|
@@ -2567,8 +2928,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2567
2928
|
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure2.code });
|
|
2568
2929
|
const retry = !retried && failure2.code === "invalid_access";
|
|
2569
2930
|
if (!retry) yield* recordFailure(failure2);
|
|
2931
|
+
model.admissionCleanupConfirmed = yield* cancelPendingAdmission();
|
|
2570
2932
|
yield* stopPorts();
|
|
2571
|
-
if (retry) return yield* performJoin("access_retry", true);
|
|
2933
|
+
if (retry) return yield* performJoin(model.admissionCleanupConfirmed ? "join" : "access_retry", true);
|
|
2572
2934
|
yield* transition("failed");
|
|
2573
2935
|
return yield* Effect2.fail(failure2);
|
|
2574
2936
|
}),
|
|
@@ -2585,7 +2947,11 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2585
2947
|
);
|
|
2586
2948
|
});
|
|
2587
2949
|
const performLeave = () => Effect2.gen(function* performLeaveEffect() {
|
|
2588
|
-
if (model.state === "idle"
|
|
2950
|
+
if (model.state === "idle") return;
|
|
2951
|
+
if (model.state === "left") {
|
|
2952
|
+
if (model.failure) return yield* Effect2.fail(lifecycleFailure("leave_unconfirmed", false, model.failure.message));
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2589
2955
|
yield* transition("leaving");
|
|
2590
2956
|
const confirmed = yield* leaveIsConfirmed();
|
|
2591
2957
|
yield* stopPorts();
|
|
@@ -2594,11 +2960,27 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2594
2960
|
yield* transition("left");
|
|
2595
2961
|
if (!confirmed) return yield* Effect2.fail(lifecycleFailure("leave_unconfirmed", false, "The Episode left locally without a durable Leave acknowledgement"));
|
|
2596
2962
|
});
|
|
2963
|
+
const cancelPendingAdmission = () => Effect2.gen(function* () {
|
|
2964
|
+
const grant = access.currentUnsafe();
|
|
2965
|
+
if (!grant) return model.admissionCleanupConfirmed;
|
|
2966
|
+
const cancel = platform.cancelPendingAdmission;
|
|
2967
|
+
if (!cancel) return false;
|
|
2968
|
+
const result = yield* Effect2.exit(
|
|
2969
|
+
Effect2.tryPromise({
|
|
2970
|
+
try: (signal) => cancel(grant, signal),
|
|
2971
|
+
catch: (cause) => lifecycleFailure("internal_error", true, "Admission cancellation failed", cause)
|
|
2972
|
+
}).pipe(Effect2.timeout(3e3))
|
|
2973
|
+
);
|
|
2974
|
+
return result._tag === "Success" && result.value;
|
|
2975
|
+
});
|
|
2597
2976
|
const leaveIsConfirmed = () => Effect2.gen(function* leaveIsConfirmedEffect() {
|
|
2598
2977
|
const sync = model.sync;
|
|
2599
|
-
if (
|
|
2600
|
-
|
|
2601
|
-
|
|
2978
|
+
if (model.episodeEndConfirmed) return true;
|
|
2979
|
+
if (sync && (sync.getSnapshot().connection.phase === "live" || sync.getSnapshot().control !== null)) {
|
|
2980
|
+
const leave = foreign7(() => sync.leave()).pipe(Effect2.timeout(LEAVE_TIMEOUT_MS));
|
|
2981
|
+
if ((yield* Effect2.exit(leave))._tag === "Success") return true;
|
|
2982
|
+
}
|
|
2983
|
+
return yield* cancelPendingAdmission();
|
|
2602
2984
|
});
|
|
2603
2985
|
const publishFreshAccess = () => enqueue(
|
|
2604
2986
|
Effect2.gen(function* () {
|
|
@@ -2812,7 +3194,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2812
3194
|
if (snapshot.connection.phase === "idle" || snapshot.connection.phase === "stopped") return Effect2.succeed(true);
|
|
2813
3195
|
if (snapshot.connection.phase !== "terminal") return Effect2.succeed(false);
|
|
2814
3196
|
const failure2 = syncTerminalFailure(snapshot);
|
|
2815
|
-
return failure2
|
|
3197
|
+
return syncAutoRecoverable(snapshot, failure2) ? Effect2.succeed(true) : Effect2.fail(failure2);
|
|
2816
3198
|
};
|
|
2817
3199
|
const recoverSync = (attempt) => Effect2.gen(function* () {
|
|
2818
3200
|
const sync = model.sync;
|
|
@@ -2856,6 +3238,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2856
3238
|
else if (attempt.client === sync) attempt.started = true;
|
|
2857
3239
|
})
|
|
2858
3240
|
).pipe(
|
|
3241
|
+
Effect2.andThen(waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4))),
|
|
3242
|
+
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
3243
|
Effect2.tapError(
|
|
2860
3244
|
() => enqueue(
|
|
2861
3245
|
Effect2.gen(function* () {
|
|
@@ -2868,7 +3252,6 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2868
3252
|
)
|
|
2869
3253
|
)
|
|
2870
3254
|
);
|
|
2871
|
-
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
2872
3255
|
});
|
|
2873
3256
|
const handleSyncSnapshot = (sync, snapshot) => Effect2.gen(function* handleSyncSnapshotEffect() {
|
|
2874
3257
|
if (!isCurrentSyncSnapshot(sync, snapshot)) return;
|
|
@@ -2889,7 +3272,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2889
3272
|
}
|
|
2890
3273
|
if (snapshot.connection.phase !== "terminal") return false;
|
|
2891
3274
|
const failure2 = syncTerminalFailure(snapshot);
|
|
2892
|
-
if (failure2
|
|
3275
|
+
if (syncAutoRecoverable(snapshot, failure2)) return false;
|
|
2893
3276
|
yield* failForSnapshot(failure2);
|
|
2894
3277
|
return true;
|
|
2895
3278
|
});
|
|
@@ -2904,7 +3287,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2904
3287
|
});
|
|
2905
3288
|
const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "connecting" || snapshot.connection.phase === "recovering" || snapshot.connection.phase === "terminal");
|
|
2906
3289
|
const recordFailure = (failure2) => Effect2.sync(() => {
|
|
2907
|
-
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code });
|
|
3290
|
+
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code, ...failure2.sync ? { sync: failure2.sync } : {} });
|
|
2908
3291
|
});
|
|
2909
3292
|
const failForSnapshot = (failure2) => Effect2.gen(function* failForSnapshotEffect() {
|
|
2910
3293
|
model.failure = toFailure(failure2);
|
|
@@ -3096,7 +3479,7 @@ function lifecycleFailure(code, recoverable, message, cause) {
|
|
|
3096
3479
|
return new ConnectionLifecycleFailure({ code, recoverable, message, ...cause === void 0 ? {} : { cause } });
|
|
3097
3480
|
}
|
|
3098
3481
|
function toFailure(value) {
|
|
3099
|
-
return Object.freeze({ code: value.code, action: null, recoverable: value.recoverable, message: value.message });
|
|
3482
|
+
return Object.freeze({ code: value.code, action: null, recoverable: value.recoverable, message: value.message, ...value.sync ? { sync: value.sync } : {} });
|
|
3100
3483
|
}
|
|
3101
3484
|
function accessFailure(value) {
|
|
3102
3485
|
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 +3573,18 @@ function syncTerminalFailure(snapshot) {
|
|
|
3190
3573
|
const reason = snapshot.connection.terminalReason;
|
|
3191
3574
|
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
3575
|
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");
|
|
3576
|
+
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");
|
|
3577
|
+
if (reason === "entry_timeout") return lifecycleFailure("sync_start_failed", true, "Call entry was not confirmed in time");
|
|
3194
3578
|
return lifecycleFailure("sync_start_failed", true, "The Sync transport needs recovery");
|
|
3195
3579
|
}
|
|
3580
|
+
function syncStartupFailure(snapshot) {
|
|
3581
|
+
const sync = snapshot.connection.connectFailure ?? snapshot.connection.connectAttempt;
|
|
3582
|
+
const message = sync?.opened === false ? "Sync could not reach the call service" : "Sync could not connect";
|
|
3583
|
+
return new ConnectionLifecycleFailure({ code: "sync_hello_timeout", recoverable: true, message: `${message}${sync ? ` ${formatSyncConnectDiagnostic(sync)}` : " before the startup deadline"}`, ...sync ? { sync: Object.freeze({ ...sync }) } : {} });
|
|
3584
|
+
}
|
|
3585
|
+
function syncAutoRecoverable(snapshot, failure2) {
|
|
3586
|
+
return failure2.recoverable && snapshot.connection.terminalReason !== "entry_timeout";
|
|
3587
|
+
}
|
|
3196
3588
|
|
|
3197
3589
|
// src/space-client/errors.ts
|
|
3198
3590
|
import { Data as Data3 } from "effect";
|
|
@@ -3233,6 +3625,7 @@ var INVALID_PAYLOAD_CODE_BY_ACTION = /* @__PURE__ */ new Map([
|
|
|
3233
3625
|
]);
|
|
3234
3626
|
var MEDIA_TIMEOUT_FAILURE = { code: "media.timeout", recoverable: true, message: "Media did not respond in time. Try again." };
|
|
3235
3627
|
var SYNC_FAILURES = /* @__PURE__ */ new Map([
|
|
3628
|
+
["sync_hello_timeout", { failure: { code: "connection.sync_hello_timeout", recoverable: true, message: "Sync could not reach the call service. Try again." } }],
|
|
3236
3629
|
["not_live", { failure: { code: "connection.invalid_state", recoverable: true, message: "Wait for the call to connect, then try again." } }],
|
|
3237
3630
|
["signaling_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
3238
3631
|
["negotiation_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
@@ -3247,11 +3640,14 @@ function normalizeMediaError(cause) {
|
|
|
3247
3640
|
}
|
|
3248
3641
|
function normalizeClientError(cause, fallback = "client.internal_error", scope) {
|
|
3249
3642
|
if (cause instanceof SpaceClientError) return cause;
|
|
3643
|
+
const chatFileError2 = normalizeChatFileError(cause);
|
|
3644
|
+
if (chatFileError2) return chatFileError2;
|
|
3250
3645
|
if (cause instanceof ConnectionLifecycleFailure || isConnectionError(cause)) {
|
|
3251
3646
|
return new SpaceClientError({
|
|
3252
3647
|
code: connectionErrorCode(cause.code, cause instanceof ConnectionLifecycleFailure ? null : cause.action),
|
|
3253
3648
|
recoverable: cause.recoverable,
|
|
3254
|
-
message: cause.message
|
|
3649
|
+
message: cause.message,
|
|
3650
|
+
...cause.sync ? { sync: cause.sync } : {}
|
|
3255
3651
|
});
|
|
3256
3652
|
}
|
|
3257
3653
|
if (cause instanceof V1SyncError) {
|
|
@@ -3264,8 +3660,15 @@ function normalizeClientError(cause, fallback = "client.internal_error", scope)
|
|
|
3264
3660
|
const message = cause instanceof Error ? cause.message : "The client operation failed";
|
|
3265
3661
|
return new SpaceClientError({ code: fallback, recoverable: false, message });
|
|
3266
3662
|
}
|
|
3663
|
+
function normalizeChatFileError(cause) {
|
|
3664
|
+
if (!(cause instanceof ChalkChatFileError) || cause.code !== "unavailable") return null;
|
|
3665
|
+
if (cause.stage === "token" && isAccessInvalid(cause.cause)) {
|
|
3666
|
+
return new SpaceClientError({ code: "access.invalid", recoverable: false, message: cause.cause instanceof Error ? cause.cause.message : "Access was rejected" });
|
|
3667
|
+
}
|
|
3668
|
+
return new SpaceClientError({ code: cause.stage === "token" ? "access.unavailable" : "collaboration.unavailable", recoverable: cause.recoverable, message: cause.message });
|
|
3669
|
+
}
|
|
3267
3670
|
function failureFromError(error2) {
|
|
3268
|
-
return Object.freeze({ code: error2.code, recoverable: error2.recoverable, message: error2.message });
|
|
3671
|
+
return Object.freeze({ code: error2.code, recoverable: error2.recoverable, message: error2.message, ...error2.sync ? { sync: error2.sync } : {} });
|
|
3269
3672
|
}
|
|
3270
3673
|
function isAccessInvalid(cause) {
|
|
3271
3674
|
if (typeof cause !== "object" || cause === null || !("code" in cause)) return false;
|
|
@@ -3299,7 +3702,7 @@ var EMPTY = empty({
|
|
|
3299
3702
|
reactions: empty({ active: empty([]) }),
|
|
3300
3703
|
recording: empty({ current: null }),
|
|
3301
3704
|
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 }) })
|
|
3705
|
+
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
3706
|
});
|
|
3304
3707
|
var SpaceStore = class _SpaceStore {
|
|
3305
3708
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -3380,7 +3783,7 @@ var SpaceStoreService = class extends Context4.Service()("@chalk/client/SpaceSto
|
|
|
3380
3783
|
var makeSpaceStoreLayer = Layer4.effect(SpaceStoreService, SpaceStore.make());
|
|
3381
3784
|
var makeFakeSpaceStoreLayer = (store = new SpaceStore()) => Layer4.succeed(SpaceStoreService, store);
|
|
3382
3785
|
function connectionError(failure2) {
|
|
3383
|
-
return failure2 ? empty({ code: connectionErrorCode(failure2.code, failure2.action), recoverable: failure2.recoverable, message: failure2.message }) : null;
|
|
3786
|
+
return failure2 ? empty({ code: connectionErrorCode(failure2.code, failure2.action), recoverable: failure2.recoverable, message: failure2.message, ...failure2.sync ? { sync: failure2.sync } : {} }) : null;
|
|
3384
3787
|
}
|
|
3385
3788
|
function connectionStatus(snapshot) {
|
|
3386
3789
|
const interrupted = INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media);
|
|
@@ -3520,13 +3923,13 @@ var ChatControllerRuntime = class {
|
|
|
3520
3923
|
#unsubscribeConnection = null;
|
|
3521
3924
|
#unsubscribeEvents = null;
|
|
3522
3925
|
#unsubscribeSnapshot = null;
|
|
3523
|
-
constructor(connection, store, transport, apiBaseUrl,
|
|
3926
|
+
constructor(connection, store, transport, apiBaseUrl, fetch2, fork, offline) {
|
|
3524
3927
|
this.#offline = offline;
|
|
3525
3928
|
this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
|
|
3526
3929
|
this.#connection = connection;
|
|
3527
3930
|
this.#store = store;
|
|
3528
3931
|
this.#transport = transport;
|
|
3529
|
-
this.#fetch =
|
|
3932
|
+
this.#fetch = fetch2;
|
|
3530
3933
|
this.#fork = fork;
|
|
3531
3934
|
this.#unsubscribeConnection = connection.subscribePorts((ports) => this.#bind(ports));
|
|
3532
3935
|
}
|
|
@@ -3604,7 +4007,7 @@ var ChatControllerRuntime = class {
|
|
|
3604
4007
|
});
|
|
3605
4008
|
};
|
|
3606
4009
|
upload = (file) => {
|
|
3607
|
-
const
|
|
4010
|
+
const fetch2 = this.#fetch;
|
|
3608
4011
|
return Effect4.suspend(() => {
|
|
3609
4012
|
if (!this.#transport) {
|
|
3610
4013
|
return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file upload is unavailable" }));
|
|
@@ -3619,7 +4022,10 @@ var ChatControllerRuntime = class {
|
|
|
3619
4022
|
Effect4.flatMap(
|
|
3620
4023
|
(upload) => Effect4.void.pipe(
|
|
3621
4024
|
Effect4.andThen(
|
|
3622
|
-
|
|
4025
|
+
Effect4.tryPromise({
|
|
4026
|
+
try: () => fetch2(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes }),
|
|
4027
|
+
catch: () => new SpaceClientError({ code: "collaboration.unavailable", recoverable: true, message: CHAT_FILE_REACHABILITY_MESSAGE })
|
|
4028
|
+
}).pipe(
|
|
3623
4029
|
Effect4.flatMap((response) => {
|
|
3624
4030
|
if (!response.ok) {
|
|
3625
4031
|
return Effect4.fail(new SpaceClientError({ code: "command.rejected", recoverable: response.status >= 500, message: `Attachment upload failed with HTTP ${response.status}` }));
|
|
@@ -5364,6 +5770,7 @@ var ParticipantsControllerRuntime = class {
|
|
|
5364
5770
|
publish();
|
|
5365
5771
|
}
|
|
5366
5772
|
#publish(snapshot, collaborationCapabilities, media) {
|
|
5773
|
+
if (snapshot && !controlFor(snapshot)) return;
|
|
5367
5774
|
const audioParticipants = new Set(media?.remoteTracks.filter((track) => track.source === "screen_audio" && track.track.readyState === "live").map((track) => track.participantId));
|
|
5368
5775
|
if (snapshot?.participantId && media?.localTracks.some((track) => track.source === "screen_audio" && track.enabled && track.track.readyState === "live")) audioParticipants.add(snapshot.participantId);
|
|
5369
5776
|
const projected = participantsFor(snapshot, collaborationCapabilities);
|
|
@@ -5698,7 +6105,7 @@ function foreign5(operation) {
|
|
|
5698
6105
|
|
|
5699
6106
|
// src/space-client/whiteboard-controller.ts
|
|
5700
6107
|
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 });
|
|
6108
|
+
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
6109
|
var WhiteboardControllerService = class extends Context11.Service()("@chalk/client/WhiteboardController") {
|
|
5703
6110
|
};
|
|
5704
6111
|
var makeWhiteboardController = (connection, store, create) => Effect10.gen(function* () {
|
|
@@ -5721,13 +6128,22 @@ var WhiteboardControllerRuntime = class {
|
|
|
5721
6128
|
#connected = false;
|
|
5722
6129
|
#summary = EMPTY_SUMMARY;
|
|
5723
6130
|
#unsubscribe = null;
|
|
6131
|
+
#unsubscribeRoster = null;
|
|
5724
6132
|
constructor(connection, store, transport) {
|
|
5725
6133
|
this.#store = store;
|
|
5726
6134
|
this.#transport = transport;
|
|
6135
|
+
let roster = new Set(store.getSnapshot().participants.roster.map((person) => person.participantId));
|
|
6136
|
+
this.#unsubscribeRoster = store.subscribe(() => {
|
|
6137
|
+
const next = new Set(store.getSnapshot().participants.roster.map((person) => person.participantId));
|
|
6138
|
+
for (const id of roster) if (!next.has(id)) transport?.forgetParticipant?.(id);
|
|
6139
|
+
roster = next;
|
|
6140
|
+
});
|
|
5727
6141
|
this.#unsubscribe = connection.subscribePorts((ports) => this.#bind(ports));
|
|
5728
6142
|
}
|
|
5729
6143
|
transport = () => this.#transport;
|
|
5730
6144
|
dispose() {
|
|
6145
|
+
this.#unsubscribeRoster?.();
|
|
6146
|
+
this.#unsubscribeRoster = null;
|
|
5731
6147
|
this.#unsubscribe?.();
|
|
5732
6148
|
this.#unsubscribe = null;
|
|
5733
6149
|
this.#bind(null);
|
|
@@ -5741,6 +6157,8 @@ var WhiteboardControllerRuntime = class {
|
|
|
5741
6157
|
capabilities: Object.freeze([...summary.capabilities]),
|
|
5742
6158
|
canDraw: summary.canDraw,
|
|
5743
6159
|
canClear: summary.canClear,
|
|
6160
|
+
drawPolicy: summary.drawPolicy,
|
|
6161
|
+
drawPermissions: Object.freeze(summary.drawPermissions.map((permission) => Object.freeze({ ...permission }))),
|
|
5744
6162
|
presenting: summary.presenting,
|
|
5745
6163
|
error: summary.error ? Object.freeze({ ...summary.error }) : null
|
|
5746
6164
|
});
|
|
@@ -5758,14 +6176,21 @@ var WhiteboardControllerRuntime = class {
|
|
|
5758
6176
|
this.#publish();
|
|
5759
6177
|
}
|
|
5760
6178
|
#publish() {
|
|
5761
|
-
const slice = Object.freeze({
|
|
6179
|
+
const slice = Object.freeze({
|
|
6180
|
+
open: this.#summary.status !== "unsubscribed",
|
|
6181
|
+
engine: Object.freeze({ status: this.#summary.status, sceneId: this.#summary.sceneId, revision: this.#summary.revision, presenting: this.#summary.presenting, error: this.#summary.error }),
|
|
6182
|
+
access: Object.freeze({ canDraw: this.#summary.canDraw, policy: this.#summary.drawPolicy, permissions: this.#summary.drawPermissions })
|
|
6183
|
+
});
|
|
5762
6184
|
const current = this.#store.getSnapshot().whiteboard;
|
|
5763
6185
|
if (sameWhiteboard(current, slice)) return;
|
|
5764
6186
|
this.#store.updateWhiteboard(slice);
|
|
5765
6187
|
}
|
|
5766
6188
|
};
|
|
5767
6189
|
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);
|
|
6190
|
+
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);
|
|
6191
|
+
}
|
|
6192
|
+
function sameAccess(left, right) {
|
|
6193
|
+
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
6194
|
}
|
|
5770
6195
|
function sameError2(left, right) {
|
|
5771
6196
|
if (left === right) return true;
|
|
@@ -5857,7 +6282,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
5857
6282
|
diagnostics: {
|
|
5858
6283
|
onEvent: (event) => {
|
|
5859
6284
|
platform.onConnectionDiagnostic?.(event);
|
|
5860
|
-
options.logger?.debug?.("SpaceClient connection event", { event: event.event, state: event.state, epoch: event.epoch, ...event.code ? { code: event.code } : {} });
|
|
6285
|
+
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
6286
|
}
|
|
5862
6287
|
}
|
|
5863
6288
|
});
|
|
@@ -6118,13 +6543,14 @@ export {
|
|
|
6118
6543
|
CHALK_CHAT_ATTACHMENT_MIME_TYPES,
|
|
6119
6544
|
ChalkChatFileError,
|
|
6120
6545
|
createChalkChatFileHttpTransport,
|
|
6546
|
+
ChalkWhiteboardViewBounds,
|
|
6121
6547
|
createChalkWhiteboardV1FileDownloadTransport,
|
|
6548
|
+
ChalkWhiteboardViewVersions,
|
|
6122
6549
|
encodeWhiteboardV1ClientFrame,
|
|
6123
6550
|
decodeWhiteboardV1ServerFrame,
|
|
6124
6551
|
InMemoryChalkWhiteboardV1PendingOperationStore,
|
|
6125
6552
|
ChalkWhiteboardV1Client,
|
|
6126
6553
|
createChalkWhiteboardV1Client,
|
|
6127
|
-
ChalkWhiteboardViewBounds,
|
|
6128
6554
|
makeProductionConnectionPlatformLayer,
|
|
6129
6555
|
ConnectionLifecycleService,
|
|
6130
6556
|
makeConnectionLifecycleLayer,
|