@q9labsai/chalk-client 5.0.1 → 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 +159 -0
- package/README.md +39 -0
- package/dist/access/base64-url.d.ts +0 -1
- package/dist/access/grant.d.ts +0 -1
- package/dist/access/manager.d.ts +0 -1
- package/dist/chat-files/http-transport.d.ts +0 -1
- package/dist/chat-files/index.d.ts +0 -1
- package/dist/chat-files/types.d.ts +3 -1
- package/dist/{chunk-IPAN4LUZ.js → chunk-BCO3AZUB.js} +1463 -583
- package/dist/{chunk-AVXJ4HQU.js → chunk-D2GXPX2P.js} +298 -47
- package/dist/{chunk-CMVO7USN.js → chunk-DHGAKJXP.js} +1 -1
- package/dist/{chunk-O2NOZKJP.js → chunk-DWKTGBHR.js} +20 -8
- package/dist/{chunk-2HJIJVE2.js → chunk-HO4745TX.js} +591 -5
- package/dist/client.d.ts +277 -1
- package/dist/collaboration/index.d.ts +0 -1
- package/dist/collaboration/types.d.ts +0 -1
- package/dist/collaboration/wire.d.ts +0 -1
- package/dist/connection/dependencies.d.ts +8 -3
- package/dist/connection/diagnostics.d.ts +3 -3
- package/dist/connection/index.d.ts +1 -1
- package/dist/connection/lifecycle.d.ts +3 -3
- package/dist/connection/media-devices.d.ts +0 -1
- package/dist/connection/production.d.ts +0 -1
- package/dist/connection/types.d.ts +4 -2
- package/dist/effect.d.ts +0 -1
- package/dist/effect.js +146 -4
- package/dist/foreground-deadline.d.ts +0 -1
- package/dist/generated/http-api.d.ts +273 -1
- package/dist/generated/schemas.d.ts +732 -7
- package/dist/generated/sync.d.ts +74 -4
- package/dist/generated/whiteboard-v1.d.ts +114 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +21 -15
- package/dist/media/camera-uplink.d.ts +0 -1
- package/dist/media/capture-error.d.ts +0 -1
- package/dist/media/client.d.ts +3 -2
- package/dist/media/cloudflare-sfu.d.ts +0 -1
- package/dist/media/index.d.ts +0 -1
- package/dist/media/index.js +1 -1
- package/dist/media/observers.d.ts +0 -1
- package/dist/media/plane.d.ts +0 -1
- package/dist/media/remote-track-response.d.ts +0 -1
- package/dist/media/target.d.ts +0 -1
- package/dist/media/tracks.d.ts +0 -1
- package/dist/media/transport.d.ts +0 -1
- package/dist/media/types.d.ts +0 -1
- package/dist/public-invites/client.d.ts +0 -1
- package/dist/public-invites/index.d.ts +0 -1
- package/dist/public-invites/index.js +2 -2
- package/dist/public-invites/types.d.ts +0 -1
- package/dist/server/access-refresh.d.ts +0 -1
- package/dist/server/{chunk-C4XITY5U.js → chunk-XQOQTCUL.js} +22 -0
- package/dist/server/client.d.ts +0 -1
- package/dist/server/errors.d.cts +8 -1
- package/dist/server/errors.d.ts +8 -2
- package/dist/server/index.cjs +31 -7
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js +10 -7
- package/dist/server/server-only.d.ts +0 -1
- package/dist/server/server-only.js +1 -1
- package/dist/server/transport.d.ts +0 -1
- package/dist/server/types.d.ts +0 -1
- package/dist/space-client/chat-controller-helpers.d.ts +0 -1
- package/dist/space-client/chat-controller.d.ts +0 -1
- package/dist/space-client/controller-effects.d.ts +0 -1
- package/dist/space-client/core.d.ts +0 -1
- package/dist/space-client/effect.d.ts +0 -1
- package/dist/space-client/errors.d.ts +4 -2
- package/dist/space-client/index.d.ts +1 -2
- package/dist/space-client/media-controller.d.ts +3 -2
- package/dist/space-client/media-device-selection.d.ts +0 -1
- package/dist/space-client/offline-actions-storage.d.ts +0 -1
- package/dist/space-client/offline-actions.d.ts +0 -1
- package/dist/space-client/participants-controller.d.ts +0 -1
- package/dist/space-client/promise-facade.d.ts +0 -1
- package/dist/space-client/reactions-controller.d.ts +0 -1
- package/dist/space-client/recording-controller.d.ts +0 -1
- package/dist/space-client/reload-rejoin.d.ts +1 -2
- package/dist/space-client/space-client.d.ts +0 -1
- package/dist/space-client/store.d.ts +0 -1
- package/dist/space-client/types.d.ts +15 -3
- package/dist/space-client/whiteboard-controller.d.ts +0 -1
- package/dist/sync/browser.d.ts +0 -1
- package/dist/sync/canonical.d.ts +0 -1
- package/dist/sync/connect-diagnostics.d.ts +10 -0
- package/dist/sync/dial-deadline.d.ts +3 -0
- package/dist/sync/errors.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/react-native.d.ts +0 -1
- package/dist/sync/types.d.ts +1 -1
- package/dist/sync/v1-client.d.ts +4 -2
- package/dist/sync/v1-codec.d.ts +0 -1
- package/dist/sync/v1-collaboration-state.d.ts +1 -1
- package/dist/sync/v1-command-scheduler.d.ts +0 -1
- package/dist/sync/v1-create.d.ts +0 -1
- package/dist/sync/v1-deferred.d.ts +0 -1
- package/dist/sync/v1-error.d.ts +0 -1
- package/dist/sync/v1-frame-signature.d.ts +0 -1
- package/dist/sync/v1-health.d.ts +15 -0
- package/dist/sync/v1-live-target-coordinator.d.ts +0 -1
- package/dist/sync/v1-persistence.d.ts +0 -1
- package/dist/sync/v1-platform-persistence.d.ts +0 -1
- package/dist/sync/v1-reducer.d.ts +3 -1
- package/dist/sync/v1-types.d.ts +14 -1
- package/dist/webhooks/errors.d.ts +0 -1
- package/dist/webhooks/generated/event-v1.d.ts +0 -1
- package/dist/webhooks/generated/fixtures-v1.d.ts +0 -1
- package/dist/webhooks/index.d.ts +0 -1
- package/dist/webhooks/processor.d.ts +0 -1
- package/dist/webhooks/server-only.d.ts +0 -1
- package/dist/webhooks/test.d.ts +0 -1
- package/dist/webhooks/types.d.ts +0 -1
- package/dist/webhooks/validate.d.ts +0 -1
- package/dist/webhooks/verify.d.ts +0 -1
- package/dist/whiteboard/file-download-transport.d.ts +0 -1
- package/dist/whiteboard/index.d.ts +2 -1
- package/dist/whiteboard/types.d.ts +54 -5
- package/dist/whiteboard/v1-client.d.ts +5 -2
- package/dist/whiteboard/v1-codec.d.ts +5 -2
- package/dist/whiteboard/v1-create.d.ts +0 -1
- package/dist/whiteboard/v1-multipart.d.ts +1 -2
- package/dist/whiteboard/v1-persistence.d.ts +0 -1
- package/dist/whiteboard/view-bounds.d.ts +5 -0
- package/dist/whiteboard/view-versions.d.ts +9 -0
- package/package.json +4 -2
- package/dist/access/base64-url.d.ts.map +0 -1
- package/dist/access/grant.d.ts.map +0 -1
- package/dist/access/grant.test.helpers.d.ts +0 -10
- package/dist/access/grant.test.helpers.d.ts.map +0 -1
- package/dist/access/manager.d.ts.map +0 -1
- package/dist/chat-files/http-transport.d.ts.map +0 -1
- package/dist/chat-files/index.d.ts.map +0 -1
- package/dist/chat-files/types.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/collaboration/index.d.ts.map +0 -1
- package/dist/collaboration/types.d.ts.map +0 -1
- package/dist/collaboration/wire.d.ts.map +0 -1
- package/dist/connection/dependencies.d.ts.map +0 -1
- package/dist/connection/diagnostics.d.ts.map +0 -1
- package/dist/connection/index.d.ts.map +0 -1
- package/dist/connection/lifecycle.d.ts.map +0 -1
- package/dist/connection/media-devices.d.ts.map +0 -1
- package/dist/connection/production.d.ts.map +0 -1
- package/dist/connection/types.d.ts.map +0 -1
- package/dist/effect.d.ts.map +0 -1
- package/dist/foreground-deadline.d.ts.map +0 -1
- package/dist/generated/http-api.d.ts.map +0 -1
- package/dist/generated/schemas.d.ts.map +0 -1
- package/dist/generated/sync.d.ts.map +0 -1
- package/dist/generated/whiteboard-v1.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/media/camera-uplink.d.ts.map +0 -1
- package/dist/media/capture-error.d.ts.map +0 -1
- package/dist/media/client.d.ts.map +0 -1
- package/dist/media/cloudflare-sfu.d.ts.map +0 -1
- package/dist/media/index.d.ts.map +0 -1
- package/dist/media/observers.d.ts.map +0 -1
- package/dist/media/plane.d.ts.map +0 -1
- package/dist/media/remote-track-response.d.ts.map +0 -1
- package/dist/media/target.d.ts.map +0 -1
- package/dist/media/tracks.d.ts.map +0 -1
- package/dist/media/transport.d.ts.map +0 -1
- package/dist/media/types.d.ts.map +0 -1
- package/dist/public-invites/client.d.ts.map +0 -1
- package/dist/public-invites/index.d.ts.map +0 -1
- package/dist/public-invites/types.d.ts.map +0 -1
- package/dist/server/access-refresh.d.ts.map +0 -1
- package/dist/server/client.d.ts.map +0 -1
- package/dist/server/errors.d.ts.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/server-only.d.ts.map +0 -1
- package/dist/server/transport.d.ts.map +0 -1
- package/dist/server/types.d.ts.map +0 -1
- package/dist/space-client/chat-controller-helpers.d.ts.map +0 -1
- package/dist/space-client/chat-controller.d.ts.map +0 -1
- package/dist/space-client/controller-effects.d.ts.map +0 -1
- package/dist/space-client/core.d.ts.map +0 -1
- package/dist/space-client/core.test.helpers.d.ts +0 -15
- package/dist/space-client/core.test.helpers.d.ts.map +0 -1
- package/dist/space-client/effect.d.ts.map +0 -1
- package/dist/space-client/errors.d.ts.map +0 -1
- package/dist/space-client/index.d.ts.map +0 -1
- package/dist/space-client/media-controller.d.ts.map +0 -1
- package/dist/space-client/media-device-selection.d.ts.map +0 -1
- package/dist/space-client/offline-actions-storage.d.ts.map +0 -1
- package/dist/space-client/offline-actions.d.ts.map +0 -1
- package/dist/space-client/participants-controller.d.ts.map +0 -1
- package/dist/space-client/promise-facade.d.ts.map +0 -1
- package/dist/space-client/reactions-controller.d.ts.map +0 -1
- package/dist/space-client/recording-controller.d.ts.map +0 -1
- package/dist/space-client/reload-rejoin.d.ts.map +0 -1
- package/dist/space-client/space-client.d.ts.map +0 -1
- package/dist/space-client/store.d.ts.map +0 -1
- package/dist/space-client/terminal.test.helpers.d.ts +0 -136
- package/dist/space-client/terminal.test.helpers.d.ts.map +0 -1
- package/dist/space-client/types.d.ts.map +0 -1
- package/dist/space-client/whiteboard-controller.d.ts.map +0 -1
- package/dist/sync/browser.d.ts.map +0 -1
- package/dist/sync/canonical.d.ts.map +0 -1
- package/dist/sync/errors.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/react-native.d.ts.map +0 -1
- package/dist/sync/types.d.ts.map +0 -1
- package/dist/sync/v1-client.d.ts.map +0 -1
- package/dist/sync/v1-codec.d.ts.map +0 -1
- package/dist/sync/v1-collaboration-state.d.ts.map +0 -1
- package/dist/sync/v1-command-scheduler.d.ts.map +0 -1
- package/dist/sync/v1-create.d.ts.map +0 -1
- package/dist/sync/v1-deferred.d.ts.map +0 -1
- package/dist/sync/v1-error.d.ts.map +0 -1
- package/dist/sync/v1-frame-signature.d.ts.map +0 -1
- package/dist/sync/v1-live-target-coordinator.d.ts.map +0 -1
- package/dist/sync/v1-persistence.d.ts.map +0 -1
- package/dist/sync/v1-platform-persistence.d.ts.map +0 -1
- package/dist/sync/v1-reducer.d.ts.map +0 -1
- package/dist/sync/v1-types.d.ts.map +0 -1
- package/dist/webhooks/errors.d.ts.map +0 -1
- package/dist/webhooks/generated/event-v1.d.ts.map +0 -1
- package/dist/webhooks/generated/fixtures-v1.d.ts.map +0 -1
- package/dist/webhooks/index.d.ts.map +0 -1
- package/dist/webhooks/processor.d.ts.map +0 -1
- package/dist/webhooks/server-only.d.ts.map +0 -1
- package/dist/webhooks/test.d.ts.map +0 -1
- package/dist/webhooks/types.d.ts.map +0 -1
- package/dist/webhooks/validate.d.ts.map +0 -1
- package/dist/webhooks/verify.d.ts.map +0 -1
- package/dist/whiteboard/file-download-transport.d.ts.map +0 -1
- package/dist/whiteboard/index.d.ts.map +0 -1
- package/dist/whiteboard/types.d.ts.map +0 -1
- package/dist/whiteboard/v1-client.d.ts.map +0 -1
- package/dist/whiteboard/v1-codec.d.ts.map +0 -1
- package/dist/whiteboard/v1-create.d.ts.map +0 -1
- package/dist/whiteboard/v1-multipart.d.ts.map +0 -1
- package/dist/whiteboard/v1-persistence.d.ts.map +0 -1
|
@@ -120,7 +120,7 @@ var BrowserSyncSocket = class {
|
|
|
120
120
|
this.#socket = socket;
|
|
121
121
|
this.#socket.addEventListener("open", () => this.onopen?.());
|
|
122
122
|
this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
|
|
123
|
-
this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code }));
|
|
123
|
+
this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
|
|
124
124
|
this.#socket.addEventListener("error", () => this.onerror?.());
|
|
125
125
|
}
|
|
126
126
|
send(data) {
|
|
@@ -176,7 +176,7 @@ var ReactNativeSyncSocket = class {
|
|
|
176
176
|
this.#socket = socket;
|
|
177
177
|
this.#socket.onopen = () => this.onopen?.();
|
|
178
178
|
this.#socket.onmessage = (event) => this.onmessage?.({ data: event.data });
|
|
179
|
-
this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006 });
|
|
179
|
+
this.#socket.onclose = (event) => this.onclose?.({ code: typeof event.code === "number" ? event.code : 1006, reason: typeof event.reason === "string" ? event.reason : void 0 });
|
|
180
180
|
this.#socket.onerror = () => this.onerror?.();
|
|
181
181
|
}
|
|
182
182
|
send(data) {
|
|
@@ -196,7 +196,7 @@ var BrowserFallbackSyncSocket = class {
|
|
|
196
196
|
this.#socket = socket;
|
|
197
197
|
this.#socket.addEventListener("open", () => this.onopen?.());
|
|
198
198
|
this.#socket.addEventListener("message", (event) => this.onmessage?.({ data: event.data }));
|
|
199
|
-
this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code }));
|
|
199
|
+
this.#socket.addEventListener("close", (event) => this.onclose?.({ code: event.code, reason: event.reason }));
|
|
200
200
|
this.#socket.addEventListener("error", () => this.onerror?.());
|
|
201
201
|
}
|
|
202
202
|
send(data) {
|
|
@@ -217,6 +217,81 @@ function unsubscribe(subscription) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// src/sync/connect-diagnostics.ts
|
|
221
|
+
var SAFE_REASONS = /* @__PURE__ */ new Set([
|
|
222
|
+
"hello timeout",
|
|
223
|
+
"invalid token",
|
|
224
|
+
"policy violation",
|
|
225
|
+
"dependency unavailable",
|
|
226
|
+
"server draining",
|
|
227
|
+
"heartbeat timeout",
|
|
228
|
+
"normal",
|
|
229
|
+
"transport error",
|
|
230
|
+
"authentication failed",
|
|
231
|
+
"delivery recovery required",
|
|
232
|
+
"space action delivery recovery required",
|
|
233
|
+
"text frames only",
|
|
234
|
+
"unsupported protocol",
|
|
235
|
+
"invalid frame",
|
|
236
|
+
"WebSocket dial timeout",
|
|
237
|
+
"lifecycle transport deadline",
|
|
238
|
+
"lifecycle unavailable",
|
|
239
|
+
"invalid_frame",
|
|
240
|
+
"state_digest_mismatch",
|
|
241
|
+
"state_digest_recovery_failed",
|
|
242
|
+
"terminal event acknowledged",
|
|
243
|
+
"client stopped",
|
|
244
|
+
"participant inactive",
|
|
245
|
+
"terminal recovery"
|
|
246
|
+
]);
|
|
247
|
+
function safeSyncCloseReason(reason) {
|
|
248
|
+
return reason ? SAFE_REASONS.has(reason) ? reason : "redacted" : null;
|
|
249
|
+
}
|
|
250
|
+
function formatSyncConnectDiagnostic(diagnostic) {
|
|
251
|
+
return `[${diagnostic.stage}; open=${diagnostic.opened ? 1 : 0}; close=${diagnostic.closeCode ?? "none"}; reason=${safeSyncCloseReason(diagnostic.closeReason ?? void 0) ?? "none"}; ${Math.round(diagnostic.durationMs)}ms]`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/sync/v1-health.ts
|
|
255
|
+
var SYNC_HEALTH_BUDGET = {
|
|
256
|
+
probeIntervalMs: 500,
|
|
257
|
+
degradedAfterMs: 750,
|
|
258
|
+
recoveringAfterMs: 4e3,
|
|
259
|
+
stablePongsForMs: 2e3
|
|
260
|
+
};
|
|
261
|
+
var V1SyncHealth = class {
|
|
262
|
+
health = "healthy";
|
|
263
|
+
#lastPongAt;
|
|
264
|
+
#stableSince = null;
|
|
265
|
+
constructor(now) {
|
|
266
|
+
this.#lastPongAt = now;
|
|
267
|
+
}
|
|
268
|
+
observePong(now) {
|
|
269
|
+
this.check(now);
|
|
270
|
+
const gap = now - this.#lastPongAt;
|
|
271
|
+
this.#lastPongAt = now;
|
|
272
|
+
if (this.health === "healthy") return;
|
|
273
|
+
if (gap > SYNC_HEALTH_BUDGET.degradedAfterMs || this.#stableSince === null) this.#stableSince = now;
|
|
274
|
+
if (now - this.#stableSince >= SYNC_HEALTH_BUDGET.stablePongsForMs) {
|
|
275
|
+
this.health = "healthy";
|
|
276
|
+
this.#stableSince = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
check(now) {
|
|
280
|
+
const silence = now - this.#lastPongAt;
|
|
281
|
+
if (silence >= SYNC_HEALTH_BUDGET.degradedAfterMs) this.#stableSince = null;
|
|
282
|
+
if (silence >= SYNC_HEALTH_BUDGET.recoveringAfterMs) this.health = "recovering";
|
|
283
|
+
else if (silence >= SYNC_HEALTH_BUDGET.degradedAfterMs && this.health === "healthy") this.health = "degraded";
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/sync/dial-deadline.ts
|
|
288
|
+
var SYNC_DIAL_TIMEOUT_MS = 1e4;
|
|
289
|
+
function scheduleSyncDialDeadline(clock, onTimeout, timeoutMs = SYNC_DIAL_TIMEOUT_MS) {
|
|
290
|
+
const allowance = Number.isFinite(timeoutMs) ? Math.min(6e4, Math.max(SYNC_DIAL_TIMEOUT_MS, timeoutMs)) : SYNC_DIAL_TIMEOUT_MS;
|
|
291
|
+
const timer = clock.setTimeout(onTimeout, allowance);
|
|
292
|
+
return () => clock.clearTimeout(timer);
|
|
293
|
+
}
|
|
294
|
+
|
|
220
295
|
// src/generated/sync.ts
|
|
221
296
|
import { Schema } from "effect";
|
|
222
297
|
var SyncProtocolMetadata = {
|
|
@@ -225,7 +300,14 @@ var SyncProtocolMetadata = {
|
|
|
225
300
|
field: "protocol",
|
|
226
301
|
value: 1,
|
|
227
302
|
transport: "websocket-json-text",
|
|
228
|
-
route: "/v1/sync"
|
|
303
|
+
route: "/v1/sync",
|
|
304
|
+
entryGeneration: {
|
|
305
|
+
queryParameter: "entry_generation",
|
|
306
|
+
newEntry: "new",
|
|
307
|
+
resume: "previous welcome entry_generation",
|
|
308
|
+
confirmation: "monotonic compare-and-set",
|
|
309
|
+
scope: "participant"
|
|
310
|
+
}
|
|
229
311
|
},
|
|
230
312
|
directions: {
|
|
231
313
|
clientToServer: "client-to-server",
|
|
@@ -249,7 +331,7 @@ var SyncProtocolMetadata = {
|
|
|
249
331
|
},
|
|
250
332
|
{
|
|
251
333
|
id: "live",
|
|
252
|
-
accepts: ["command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"],
|
|
334
|
+
accepts: ["entered", "command", "live_target", "directed_request", "delivery_ack", "request_ack", "reaction_send", "chat_send", "chat_page_request", "chat_read_set", "chat_reaction_set", "ping"],
|
|
253
335
|
heartbeat: {
|
|
254
336
|
intervalMs: 2e4,
|
|
255
337
|
missedDeadlinesBeforeClose: 2
|
|
@@ -529,7 +611,7 @@ var SyncProtocolMetadata = {
|
|
|
529
611
|
{
|
|
530
612
|
code: 1008,
|
|
531
613
|
connection: "closed",
|
|
532
|
-
reasons: ["invalid token", "policy violation"]
|
|
614
|
+
reasons: ["invalid token", "policy violation", "entry timeout"]
|
|
533
615
|
},
|
|
534
616
|
{
|
|
535
617
|
code: 1009,
|
|
@@ -701,6 +783,8 @@ function strictClientFrame(value) {
|
|
|
701
783
|
return exactObject(value, ["type", "stream", "revision", "state_digest"]);
|
|
702
784
|
case "recovery_ack":
|
|
703
785
|
return exactObject(value, ["type", "recovery_id", "revision", "state_digest"]);
|
|
786
|
+
case "entered":
|
|
787
|
+
return exactObject(value, ["type"]);
|
|
704
788
|
case "ping":
|
|
705
789
|
return exactObject(value, ["type"]);
|
|
706
790
|
default:
|
|
@@ -783,7 +867,8 @@ function strictWelcome(value) {
|
|
|
783
867
|
const base = ["type", "protocol", "participant_id", "participant_generation", "recovery_id", "head", "mode"];
|
|
784
868
|
const extension = Array.isArray(value.extensions) && value.extensions.length === 1 ? value.extensions[0] : void 0;
|
|
785
869
|
const extensionValid = isObject(extension) && ["collaboration_v1", "collaboration_v2"].includes(String(extension.name)) && exactObject(extension, ["name", "capabilities", "participant_capabilities", "chat_head_sequence", "retained_floor_sequence", "read_receipts"]) && Array.isArray(extension.read_receipts);
|
|
786
|
-
|
|
870
|
+
if (Object.hasOwn(value, "entry_confirmation") && value.entry_confirmation !== true) return false;
|
|
871
|
+
const fields = [...base, ...extensionValid ? ["extensions"] : [], ...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [], ...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : []];
|
|
787
872
|
if (!strictCursor(value.head)) return false;
|
|
788
873
|
if (value.mode === "snapshot") return exactObjectWithCorrelation(value, [...fields, "snapshot"]) && strictSnapshot(value.snapshot);
|
|
789
874
|
if (value.mode === "terminal") return exactObjectWithCorrelation(value, [...fields, "reason"]);
|
|
@@ -842,6 +927,8 @@ function strictServerFrame(value) {
|
|
|
842
927
|
return exactObjectWithCorrelation(value, ["type", "command_id", "code"]);
|
|
843
928
|
case "error":
|
|
844
929
|
return exactObjectWithCorrelation(value, ["type", "code", "detail"]);
|
|
930
|
+
case "entered_ack":
|
|
931
|
+
return exactObjectWithCorrelation(value, ["type"]);
|
|
845
932
|
case "pong":
|
|
846
933
|
return exactObjectWithCorrelation(value, ["type"]);
|
|
847
934
|
default:
|
|
@@ -1382,7 +1469,16 @@ var CollaborationWelcomeExtensionSchema = Schema.Struct({
|
|
|
1382
1469
|
retained_floor_sequence: Schema.NullOr(UnsignedDecimalSchema),
|
|
1383
1470
|
read_receipts: Schema.Array(ChatReadReceiptSchema).check(Schema.isMaxLength(500))
|
|
1384
1471
|
}).check(exactKeys(["name", "capabilities", "participant_capabilities", "chat_head_sequence", "retained_floor_sequence", "read_receipts"]));
|
|
1385
|
-
var WelcomeBase = {
|
|
1472
|
+
var WelcomeBase = {
|
|
1473
|
+
type: Schema.Literal("welcome"),
|
|
1474
|
+
protocol: Schema.Literal(1),
|
|
1475
|
+
participant_id: UuidSchema,
|
|
1476
|
+
participant_generation: PositiveIntegerSchema,
|
|
1477
|
+
recovery_id: UuidSchema,
|
|
1478
|
+
head: ControlCursorSchema,
|
|
1479
|
+
entry_confirmation: Schema.optionalKey(Schema.Literal(true)),
|
|
1480
|
+
entry_generation: Schema.optionalKey(PositiveIntegerSchema.check(Schema.isLessThanOrEqualTo(9007199254740991)))
|
|
1481
|
+
};
|
|
1386
1482
|
var ExtendedWelcomeBase = { ...WelcomeBase, extensions: Schema.Tuple([CollaborationWelcomeExtensionSchema]) };
|
|
1387
1483
|
var WelcomeSnapshotFrameSchema = Schema.Struct({ ...WelcomeBase, mode: Schema.Literal("snapshot"), snapshot: SnapshotSchema, ...ServerCorrelationFields }).check(boundedServerFrame(SyncProtocolLimits.snapshotEncodedBytes));
|
|
1388
1484
|
var WelcomeReplayFrameSchema = Schema.Struct({ ...WelcomeBase, mode: Schema.Literal("replay"), ...ServerCorrelationFields });
|
|
@@ -1558,6 +1654,8 @@ var RejectedChatReadResultFrameSchema = Schema.Struct({ type: Schema.Literal("ch
|
|
|
1558
1654
|
var ChatReadResultFrameSchema = Schema.Union([AcceptedChatReadResultFrameSchema, RejectedChatReadResultFrameSchema]);
|
|
1559
1655
|
var RetryableErrorFrameSchema = Schema.Struct({ type: Schema.Literal("retryable_error"), command_id: CommandIdSchema, code: RetryableErrorCodeSchema, ...ServerCorrelationFields });
|
|
1560
1656
|
var ErrorFrameSchema = Schema.Struct({ type: Schema.Literal("error"), code: ProtocolErrorCodeSchema, detail: boundedUtf8String(1024), ...ServerCorrelationFields });
|
|
1657
|
+
var EnteredFrameSchema = Schema.Struct({ type: Schema.Literal("entered") });
|
|
1658
|
+
var EnteredAckFrameSchema = Schema.Struct({ type: Schema.Literal("entered_ack"), ...ServerCorrelationFields });
|
|
1561
1659
|
var PingFrameSchema = Schema.Struct({ type: Schema.Literal("ping") });
|
|
1562
1660
|
var PongFrameSchema = Schema.Struct({ type: Schema.Literal("pong"), ...ServerCorrelationFields });
|
|
1563
1661
|
var SyncV1ClientFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) => strictClientFrame(frame) ? void 0 : { path: [], issue: "must not contain unknown or missing fields" })).pipe(
|
|
@@ -1591,6 +1689,7 @@ var SyncV1ClientFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) =>
|
|
|
1591
1689
|
ChatReactionSetFrameSchema,
|
|
1592
1690
|
DeliveryAckFrameSchema,
|
|
1593
1691
|
RecoveryAckFrameSchema,
|
|
1692
|
+
EnteredFrameSchema,
|
|
1594
1693
|
PingFrameSchema
|
|
1595
1694
|
])
|
|
1596
1695
|
)
|
|
@@ -1622,6 +1721,7 @@ var SyncV1ServerFrameSchema = Schema.Unknown.check(Schema.makeFilter((frame) =>
|
|
|
1622
1721
|
ChatReactionResultFrameSchema,
|
|
1623
1722
|
RetryableErrorFrameSchema,
|
|
1624
1723
|
ErrorFrameSchema,
|
|
1724
|
+
EnteredAckFrameSchema,
|
|
1625
1725
|
PongFrameSchema
|
|
1626
1726
|
])
|
|
1627
1727
|
)
|
|
@@ -1719,6 +1819,12 @@ var V1ReplicaError = class extends Error {
|
|
|
1719
1819
|
this.name = "V1ReplicaError";
|
|
1720
1820
|
}
|
|
1721
1821
|
};
|
|
1822
|
+
var V1StateDigestMismatchError = class extends V1ReplicaError {
|
|
1823
|
+
constructor() {
|
|
1824
|
+
super("control state digest mismatch");
|
|
1825
|
+
this.name = "V1StateDigestMismatchError";
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1722
1828
|
async function restoreV1Snapshot(snapshot) {
|
|
1723
1829
|
const state = snapshotToState(snapshot);
|
|
1724
1830
|
assertV1ControlSemantics(state);
|
|
@@ -1832,7 +1938,7 @@ function durableProjection(state) {
|
|
|
1832
1938
|
};
|
|
1833
1939
|
}
|
|
1834
1940
|
async function assertDigest(state) {
|
|
1835
|
-
if (await computeV1StateDigest(state) !== state.stateDigest) throw new
|
|
1941
|
+
if (await computeV1StateDigest(state) !== state.stateDigest) throw new V1StateDigestMismatchError();
|
|
1836
1942
|
}
|
|
1837
1943
|
function reduceEvent(state, event) {
|
|
1838
1944
|
switch (event.name) {
|
|
@@ -1971,9 +2077,11 @@ function sameStrings2(left, right) {
|
|
|
1971
2077
|
|
|
1972
2078
|
// src/sync/v1-collaboration-state.ts
|
|
1973
2079
|
var MAX_COLLABORATION_REQUESTS_IN_FLIGHT = 64;
|
|
2080
|
+
var REACTION_ACK_TIMEOUT_MS = 5e3;
|
|
1974
2081
|
var V1CollaborationState = class {
|
|
1975
2082
|
#options;
|
|
1976
2083
|
#listeners = /* @__PURE__ */ new Set();
|
|
2084
|
+
#reactionTimers = /* @__PURE__ */ new Map();
|
|
1977
2085
|
#reactions = /* @__PURE__ */ new Map();
|
|
1978
2086
|
#chatSends = /* @__PURE__ */ new Map();
|
|
1979
2087
|
#chatPages = /* @__PURE__ */ new Map();
|
|
@@ -2044,7 +2152,18 @@ var V1CollaborationState = class {
|
|
|
2044
2152
|
this.#assertCapacity();
|
|
2045
2153
|
const operationId = this.#nextRequestId();
|
|
2046
2154
|
const frame = { type: "reaction_send", operation_id: operationId, reaction };
|
|
2047
|
-
|
|
2155
|
+
const pending = this.#sendTrackedRequest(operationId, frame, this.#reactions);
|
|
2156
|
+
this.#reactionTimers.set(
|
|
2157
|
+
operationId,
|
|
2158
|
+
this.#options.clock().setTimeout(() => {
|
|
2159
|
+
this.#reactionTimers.delete(operationId);
|
|
2160
|
+
const deferred = this.#reactions.get(operationId);
|
|
2161
|
+
if (!deferred) return;
|
|
2162
|
+
this.#reactions.delete(operationId);
|
|
2163
|
+
rejectV1Deferred(deferred, new V1SyncError("Reaction acknowledgement timed out", "reaction_ack_timeout"));
|
|
2164
|
+
}, REACTION_ACK_TIMEOUT_MS)
|
|
2165
|
+
);
|
|
2166
|
+
return pending;
|
|
2048
2167
|
}
|
|
2049
2168
|
sendChatMessage(input) {
|
|
2050
2169
|
this.#assertReady("sendChat");
|
|
@@ -2108,6 +2227,8 @@ var V1CollaborationState = class {
|
|
|
2108
2227
|
this.#requireNegotiated();
|
|
2109
2228
|
const deferred = this.#reactions.get(frame.operation_id);
|
|
2110
2229
|
if (!deferred) return;
|
|
2230
|
+
this.#options.clock().clearTimeout(this.#reactionTimers.get(frame.operation_id));
|
|
2231
|
+
this.#reactionTimers.delete(frame.operation_id);
|
|
2111
2232
|
this.#reactions.delete(frame.operation_id);
|
|
2112
2233
|
if (frame.outcome === "accepted") resolveV1Deferred(deferred, reactionFromFrame(frame.reaction));
|
|
2113
2234
|
else rejectV1Deferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
@@ -2178,6 +2299,8 @@ var V1CollaborationState = class {
|
|
|
2178
2299
|
for (const deferred of this.#chatSends.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2179
2300
|
for (const deferred of this.#chatPages.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2180
2301
|
for (const deferred of this.#chatReads.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2302
|
+
for (const timer of this.#reactionTimers.values()) this.#options.clock().clearTimeout(timer);
|
|
2303
|
+
this.#reactionTimers.clear();
|
|
2181
2304
|
this.#reactions.clear();
|
|
2182
2305
|
this.#chatSends.clear();
|
|
2183
2306
|
this.#chatPages.clear();
|
|
@@ -2863,17 +2986,21 @@ var CLIENT_RESTART_CLOSE_CODE = 4e3;
|
|
|
2863
2986
|
var DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
2864
2987
|
var MAX_RECONNECT_DELAY_MS = 5e3;
|
|
2865
2988
|
var FAST_RECONNECT_ATTEMPTS = 8;
|
|
2866
|
-
var
|
|
2867
|
-
var NOTICE_SILENCE_MS = 750;
|
|
2989
|
+
var DIGEST_RECOVERY_COOLDOWN_MS = 6e4;
|
|
2868
2990
|
var V1SyncClient = class {
|
|
2869
2991
|
#options;
|
|
2870
2992
|
#commandScheduler;
|
|
2871
2993
|
#listeners = /* @__PURE__ */ new Set();
|
|
2994
|
+
#mediaPauseListeners = /* @__PURE__ */ new Set();
|
|
2872
2995
|
#requestListeners = /* @__PURE__ */ new Set();
|
|
2873
2996
|
#collaborationState;
|
|
2874
2997
|
#controlHeads = /* @__PURE__ */ new Map();
|
|
2998
|
+
#snapshotRevision = null;
|
|
2875
2999
|
#controlEvents = /* @__PURE__ */ new Map();
|
|
2876
3000
|
#liveTargets;
|
|
3001
|
+
#entryPending = false;
|
|
3002
|
+
// Older Sync rejects new hello fields, so entry confirmation is requested on the URL and granted in welcome.
|
|
3003
|
+
#entryNegotiated = false;
|
|
2877
3004
|
#requests = /* @__PURE__ */ new Map();
|
|
2878
3005
|
#mediaEventEvidence = /* @__PURE__ */ new Map();
|
|
2879
3006
|
#presenceEventEvidence = /* @__PURE__ */ new Map();
|
|
@@ -2884,18 +3011,28 @@ var V1SyncClient = class {
|
|
|
2884
3011
|
#presence = null;
|
|
2885
3012
|
#participantId = null;
|
|
2886
3013
|
#participantGeneration = null;
|
|
3014
|
+
#entryGeneration = null;
|
|
2887
3015
|
#recovery = null;
|
|
3016
|
+
#snapshotRecoveryRequested = false;
|
|
3017
|
+
#lastDigestRecoveryAt = null;
|
|
2888
3018
|
#started = false;
|
|
2889
3019
|
#startupGeneration = 0;
|
|
2890
3020
|
#connectionGeneration = 0;
|
|
2891
3021
|
#reconnectTimer;
|
|
3022
|
+
#cancelDial;
|
|
3023
|
+
#dialTimeoutCount = 0;
|
|
3024
|
+
#connectStartedAt = null;
|
|
3025
|
+
#opened = false;
|
|
3026
|
+
#connectFailure;
|
|
3027
|
+
#connectDiagnostic() {
|
|
3028
|
+
return { stage: this.#opened ? "hello" : "upgrade", opened: this.#opened, closeCode: null, closeReason: null, durationMs: Math.max(0, this.#clock().now() - (this.#connectStartedAt ?? this.#clock().now())) };
|
|
3029
|
+
}
|
|
2892
3030
|
#reconnectAttempt = 0;
|
|
2893
3031
|
#heartbeatTimer;
|
|
2894
3032
|
#missedHeartbeats = 0;
|
|
2895
3033
|
#noticeTimer;
|
|
2896
3034
|
#noticeProbeTimer;
|
|
2897
|
-
#
|
|
2898
|
-
#noticeUnresponsive = false;
|
|
3035
|
+
#health = new V1SyncHealth(0);
|
|
2899
3036
|
#unsubscribeLifecycle;
|
|
2900
3037
|
#inbound = Promise.resolve();
|
|
2901
3038
|
#transportAvailable = true;
|
|
@@ -2935,6 +3072,7 @@ var V1SyncClient = class {
|
|
|
2935
3072
|
stateChanged: () => this.#emit()
|
|
2936
3073
|
});
|
|
2937
3074
|
this.#collaborationState = new V1CollaborationState({
|
|
3075
|
+
clock: () => this.#clock(),
|
|
2938
3076
|
request: options.collaboration,
|
|
2939
3077
|
requestIds: options.requestIds,
|
|
2940
3078
|
maxPendingRequests: options.maxPendingCollaborationRequests,
|
|
@@ -2967,10 +3105,13 @@ var V1SyncClient = class {
|
|
|
2967
3105
|
}
|
|
2968
3106
|
stop() {
|
|
2969
3107
|
this.#started = false;
|
|
3108
|
+
this.#entryPending = false;
|
|
3109
|
+
this.#entryGeneration = null;
|
|
2970
3110
|
this.#startupGeneration += 1;
|
|
2971
3111
|
this.#unsubscribeLifecycle?.();
|
|
2972
3112
|
this.#unsubscribeLifecycle = void 0;
|
|
2973
3113
|
this.#clearReconnect();
|
|
3114
|
+
this.#clearDial();
|
|
2974
3115
|
this.#clearHeartbeat();
|
|
2975
3116
|
this.#clearNoticeProbe();
|
|
2976
3117
|
this.#commandScheduler.stop("client_stopped");
|
|
@@ -2985,11 +3126,24 @@ var V1SyncClient = class {
|
|
|
2985
3126
|
this.#liveTargets.resetLocalMedia();
|
|
2986
3127
|
this.#emit();
|
|
2987
3128
|
}
|
|
3129
|
+
/** Announces completed startup; transport recovery resends until acknowledged. */
|
|
3130
|
+
async confirmEntered() {
|
|
3131
|
+
this.#requireLive();
|
|
3132
|
+
this.#entryPending = true;
|
|
3133
|
+
if (!this.#entryNegotiated) return;
|
|
3134
|
+
this.#send({ type: "entered" });
|
|
3135
|
+
}
|
|
2988
3136
|
getSnapshot() {
|
|
2989
3137
|
const pendingCommands = this.#commandScheduler.pendingCommands;
|
|
2990
3138
|
const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
|
|
2991
3139
|
return {
|
|
2992
|
-
connection:
|
|
3140
|
+
connection: {
|
|
3141
|
+
...this.#phase,
|
|
3142
|
+
...this.#connectStartedAt !== null && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectAttempt: this.#connectDiagnostic() } : {},
|
|
3143
|
+
...this.#connectFailure && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering") ? { connectFailure: this.#connectFailure } : {},
|
|
3144
|
+
...this.#phase.phase === "live" ? { noticeUnresponsive: this.#health.health === "recovering", health: this.#health.health } : {},
|
|
3145
|
+
...this.#dialTimeoutCount ? { dialTimeoutCount: this.#dialTimeoutCount } : {}
|
|
3146
|
+
},
|
|
2993
3147
|
participantId: this.#participantId,
|
|
2994
3148
|
participantGeneration: this.#participantGeneration,
|
|
2995
3149
|
control: this.#control,
|
|
@@ -3006,6 +3160,16 @@ var V1SyncClient = class {
|
|
|
3006
3160
|
listener(this.getSnapshot());
|
|
3007
3161
|
return () => this.#listeners.delete(listener);
|
|
3008
3162
|
}
|
|
3163
|
+
onMediaPause(listener) {
|
|
3164
|
+
this.#mediaPauseListeners.add(listener);
|
|
3165
|
+
return () => this.#mediaPauseListeners.delete(listener);
|
|
3166
|
+
}
|
|
3167
|
+
#notifyMediaPause(event) {
|
|
3168
|
+
const source = event.name === "participant_microphone_stopped" ? "microphone" : event.name === "participant_camera_stopped" ? "camera" : event.name === "participant_screen_share_stopped" ? "screen" : null;
|
|
3169
|
+
if (source && "participant_id" in event.payload && event.payload.participant_id === this.#participantId) {
|
|
3170
|
+
for (const listener of this.#mediaPauseListeners) listener({ source, revision: event.revision });
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3009
3173
|
onDirectedRequest(listener) {
|
|
3010
3174
|
this.#requestListeners.add(listener);
|
|
3011
3175
|
return () => this.#requestListeners.delete(listener);
|
|
@@ -3117,25 +3281,66 @@ var V1SyncClient = class {
|
|
|
3117
3281
|
return promise;
|
|
3118
3282
|
}
|
|
3119
3283
|
#connect() {
|
|
3120
|
-
if (!this.#started || !this.#transportAvailable || this.#socket) return;
|
|
3284
|
+
if (!this.#started || !this.#transportAvailable || this.#socket || this.#phase.phase === "terminal") return;
|
|
3121
3285
|
this.#phase = { phase: "connecting" };
|
|
3286
|
+
this.#connectStartedAt = this.#clock().now();
|
|
3287
|
+
this.#opened = false;
|
|
3288
|
+
this.#connectFailure = void 0;
|
|
3122
3289
|
this.#emit();
|
|
3123
|
-
|
|
3290
|
+
this.#entryNegotiated = false;
|
|
3291
|
+
let socket;
|
|
3292
|
+
try {
|
|
3293
|
+
socket = this.#options.webSocket.connect(entryUrl(this.#options.url, this.#entryGeneration, this.#options.entryConfirmation === true));
|
|
3294
|
+
} catch {
|
|
3295
|
+
this.#connectFailure = this.#connectDiagnostic();
|
|
3296
|
+
this.#emit();
|
|
3297
|
+
throw new V1SyncError("Sync WebSocket could not be created", "sync_hello_timeout");
|
|
3298
|
+
}
|
|
3124
3299
|
const connectionGeneration = ++this.#connectionGeneration;
|
|
3125
3300
|
this.#socket = socket;
|
|
3301
|
+
this.#cancelDial = scheduleSyncDialDeadline(
|
|
3302
|
+
this.#clock(),
|
|
3303
|
+
() => {
|
|
3304
|
+
this.#cancelDial = void 0;
|
|
3305
|
+
this.#dialTimeoutCount += 1;
|
|
3306
|
+
this.#connectFailure = this.#connectDiagnostic();
|
|
3307
|
+
this.#recover("WebSocket dial timeout");
|
|
3308
|
+
},
|
|
3309
|
+
this.#options.dialTimeoutMs
|
|
3310
|
+
);
|
|
3126
3311
|
socket.onopen = () => {
|
|
3312
|
+
if (socket !== this.#socket) return;
|
|
3313
|
+
this.#clearDial();
|
|
3314
|
+
this.#opened = true;
|
|
3315
|
+
this.#connectFailure = void 0;
|
|
3127
3316
|
void this.#authenticate(socket);
|
|
3128
3317
|
};
|
|
3129
3318
|
socket.onmessage = (event) => {
|
|
3130
3319
|
this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
|
|
3131
3320
|
};
|
|
3132
|
-
socket.onclose = () => {
|
|
3321
|
+
socket.onclose = (event) => {
|
|
3322
|
+
if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
|
|
3323
|
+
this.#connectFailure = { ...this.#connectDiagnostic(), closeCode: event.code, closeReason: safeSyncCloseReason(event.reason) };
|
|
3324
|
+
this.#emit();
|
|
3325
|
+
}
|
|
3133
3326
|
this.#clock().setTimeout(() => {
|
|
3134
3327
|
if (connectionGeneration !== this.#connectionGeneration) return;
|
|
3135
|
-
this.#inbound = this.#inbound.then(() =>
|
|
3328
|
+
this.#inbound = this.#inbound.then(() => {
|
|
3329
|
+
if (socket === this.#socket && event.code === 1008 && (event.reason === "" || event.reason === "invalid token" || event.reason === "policy violation" || event.reason === "entry timeout")) {
|
|
3330
|
+
this.#phase = { phase: "terminal", terminalReason: event.reason === "entry timeout" ? "entry_timeout" : "access_rejected" };
|
|
3331
|
+
this.#emit();
|
|
3332
|
+
}
|
|
3333
|
+
this.#disconnected(socket);
|
|
3334
|
+
});
|
|
3136
3335
|
}, 0);
|
|
3137
3336
|
};
|
|
3138
|
-
socket.onerror = () =>
|
|
3337
|
+
socket.onerror = () => {
|
|
3338
|
+
if (socket === this.#socket && (this.#phase.phase === "connecting" || this.#phase.phase === "recovering")) {
|
|
3339
|
+
this.#connectFailure = this.#connectDiagnostic();
|
|
3340
|
+
this.#emit();
|
|
3341
|
+
}
|
|
3342
|
+
socket.close(CLIENT_RESTART_CLOSE_CODE, "transport error");
|
|
3343
|
+
};
|
|
3139
3344
|
}
|
|
3140
3345
|
async #authenticate(socket) {
|
|
3141
3346
|
try {
|
|
@@ -3169,17 +3374,11 @@ var V1SyncClient = class {
|
|
|
3169
3374
|
try {
|
|
3170
3375
|
if (typeof data !== "string" || encoder3.encode(data).byteLength > SyncProtocolLimits.snapshotEncodedBytes) throw new V1ReplicaError("invalid inbound frame size");
|
|
3171
3376
|
const frame = decodeV1ServerFrame(data);
|
|
3172
|
-
if (this.#phase.phase === "live") {
|
|
3173
|
-
this.#noticeLastInboundAt = this.#now();
|
|
3174
|
-
this.#scheduleNoticeCheck();
|
|
3175
|
-
if (this.#noticeUnresponsive) {
|
|
3176
|
-
this.#noticeUnresponsive = false;
|
|
3177
|
-
this.#emit();
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
3377
|
await this.#handleFrame(frame);
|
|
3181
|
-
} catch {
|
|
3182
|
-
this.#
|
|
3378
|
+
} catch (error) {
|
|
3379
|
+
if (this.#snapshotRecoveryRequested) this.#failDigestRecovery();
|
|
3380
|
+
else if (error instanceof V1StateDigestMismatchError) this.#recoverDigestMismatch();
|
|
3381
|
+
else this.#recover("invalid_frame");
|
|
3183
3382
|
}
|
|
3184
3383
|
}
|
|
3185
3384
|
async #handleFrame(frame) {
|
|
@@ -3207,6 +3406,7 @@ var V1SyncClient = class {
|
|
|
3207
3406
|
this.#control = await applyV1Event(this.#requireControl(), frame);
|
|
3208
3407
|
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
3209
3408
|
this.#rememberControlEvent(frame);
|
|
3409
|
+
this.#notifyMediaPause(frame);
|
|
3210
3410
|
}
|
|
3211
3411
|
await this.#commandScheduler.settleTerminalLifecycle(frame, this.#participantId);
|
|
3212
3412
|
await this.#commandScheduler.settleProven();
|
|
@@ -3269,15 +3469,25 @@ var V1SyncClient = class {
|
|
|
3269
3469
|
return;
|
|
3270
3470
|
case "error":
|
|
3271
3471
|
throw new V1ReplicaError(frame.code);
|
|
3272
|
-
case "
|
|
3472
|
+
case "entered_ack":
|
|
3473
|
+
this.#requireLive();
|
|
3474
|
+
this.#entryPending = false;
|
|
3475
|
+
return;
|
|
3476
|
+
case "pong": {
|
|
3273
3477
|
this.#requireLive();
|
|
3274
3478
|
this.#missedHeartbeats = 0;
|
|
3479
|
+
const previous = this.#health.health;
|
|
3480
|
+
this.#health.observePong(this.#now());
|
|
3481
|
+
if (previous !== this.#health.health) this.#emit();
|
|
3275
3482
|
return;
|
|
3483
|
+
}
|
|
3276
3484
|
}
|
|
3277
3485
|
}
|
|
3278
3486
|
async #welcome(frame) {
|
|
3279
3487
|
if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
|
|
3280
3488
|
this.#collaborationState.acceptWelcome(frame);
|
|
3489
|
+
this.#entryNegotiated = frame.entry_confirmation === true;
|
|
3490
|
+
this.#entryGeneration = frame.entry_generation ?? this.#entryGeneration;
|
|
3281
3491
|
this.#participantId = frame.participant_id;
|
|
3282
3492
|
this.#participantGeneration = frame.participant_generation;
|
|
3283
3493
|
this.#updateLocalMediaStates();
|
|
@@ -3288,15 +3498,20 @@ var V1SyncClient = class {
|
|
|
3288
3498
|
this.#emit();
|
|
3289
3499
|
return;
|
|
3290
3500
|
}
|
|
3501
|
+
if (this.#snapshotRecoveryRequested && frame.mode !== "snapshot") throw new V1ReplicaError("digest recovery requires a full snapshot");
|
|
3291
3502
|
if (frame.mode === "snapshot") {
|
|
3292
|
-
|
|
3503
|
+
const control = await restoreV1Snapshot(frame.snapshot);
|
|
3504
|
+
if (!sameHead(control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
|
|
3505
|
+
this.#control = control;
|
|
3506
|
+
this.#snapshotRevision = control.revision;
|
|
3507
|
+
this.#snapshotRecoveryRequested = false;
|
|
3293
3508
|
this.#controlHeads.clear();
|
|
3294
3509
|
this.#controlEvents.clear();
|
|
3295
3510
|
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
3511
|
+
await this.#commandScheduler.settleProven();
|
|
3296
3512
|
}
|
|
3297
3513
|
if (frame.mode === "replay" && (!this.#control || this.#control.revision >= frame.head.revision)) throw new V1ReplicaError("invalid replay welcome");
|
|
3298
3514
|
if (frame.mode === "up_to_date" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("up-to-date head mismatch");
|
|
3299
|
-
if (frame.mode === "snapshot" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
|
|
3300
3515
|
this.#recovery = { id: frame.recovery_id, head: frame.head, replayEvents: 0, replayBytes: 0, controlComplete: false };
|
|
3301
3516
|
if (frame.mode === "snapshot") this.#ackRecovery();
|
|
3302
3517
|
this.#emit();
|
|
@@ -3311,6 +3526,7 @@ var V1SyncClient = class {
|
|
|
3311
3526
|
this.#control = await applyV1Event(this.#requireControl(), event);
|
|
3312
3527
|
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
3313
3528
|
this.#rememberControlEvent(event);
|
|
3529
|
+
this.#notifyMediaPause(event);
|
|
3314
3530
|
}
|
|
3315
3531
|
this.#ackRecovery();
|
|
3316
3532
|
this.#emit();
|
|
@@ -3390,8 +3606,11 @@ var V1SyncClient = class {
|
|
|
3390
3606
|
if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
|
|
3391
3607
|
this.#recovery = null;
|
|
3392
3608
|
this.#phase = { phase: "live" };
|
|
3609
|
+
this.#connectStartedAt = null;
|
|
3610
|
+
this.#connectFailure = void 0;
|
|
3393
3611
|
this.#reconnectAttempt = 0;
|
|
3394
3612
|
this.#missedHeartbeats = 0;
|
|
3613
|
+
if (this.#entryPending && this.#entryNegotiated) this.#send({ type: "entered" });
|
|
3395
3614
|
this.#commandScheduler.enterLive();
|
|
3396
3615
|
this.#liveTargets.enterLive();
|
|
3397
3616
|
this.#startHeartbeat();
|
|
@@ -3404,7 +3623,7 @@ var V1SyncClient = class {
|
|
|
3404
3623
|
this.#send({ type: "recovery_ack", recovery_id: recovery.id, revision: control.revision, state_digest: control.stateDigest });
|
|
3405
3624
|
}
|
|
3406
3625
|
#ackHeadIsProven(ack) {
|
|
3407
|
-
return this.#controlHeads.get(ack.revision) === ack.state_digest;
|
|
3626
|
+
return this.#snapshotRevision !== null && ack.revision < this.#snapshotRevision || this.#controlHeads.get(ack.revision) === ack.state_digest;
|
|
3408
3627
|
}
|
|
3409
3628
|
#rememberControlHead(revision, digest) {
|
|
3410
3629
|
this.#controlHeads.set(revision, digest);
|
|
@@ -3423,6 +3642,7 @@ var V1SyncClient = class {
|
|
|
3423
3642
|
#disconnected(socket) {
|
|
3424
3643
|
if (socket !== this.#socket) return;
|
|
3425
3644
|
this.#socket = null;
|
|
3645
|
+
this.#clearDial();
|
|
3426
3646
|
this.#recovery = null;
|
|
3427
3647
|
this.#clearHeartbeat();
|
|
3428
3648
|
this.#clearNoticeProbe();
|
|
@@ -3452,6 +3672,29 @@ var V1SyncClient = class {
|
|
|
3452
3672
|
socket.close(CLIENT_RESTART_CLOSE_CODE, reason);
|
|
3453
3673
|
this.#disconnected(socket);
|
|
3454
3674
|
}
|
|
3675
|
+
#recoverDigestMismatch() {
|
|
3676
|
+
const now = this.#clock().now();
|
|
3677
|
+
if (this.#lastDigestRecoveryAt !== null && now - this.#lastDigestRecoveryAt < DIGEST_RECOVERY_COOLDOWN_MS) {
|
|
3678
|
+
this.#failDigestRecovery();
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
this.#lastDigestRecoveryAt = now;
|
|
3682
|
+
this.#snapshotRecoveryRequested = true;
|
|
3683
|
+
this.#control = null;
|
|
3684
|
+
this.#snapshotRevision = null;
|
|
3685
|
+
this.#controlHeads.clear();
|
|
3686
|
+
this.#controlEvents.clear();
|
|
3687
|
+
this.#recover("state_digest_mismatch");
|
|
3688
|
+
}
|
|
3689
|
+
#failDigestRecovery() {
|
|
3690
|
+
this.#phase = { phase: "terminal", terminalReason: "state_digest_recovery_failed" };
|
|
3691
|
+
this.#clearReconnect();
|
|
3692
|
+
this.#commandScheduler.stop("state_digest_recovery_failed");
|
|
3693
|
+
const socket = this.#socket;
|
|
3694
|
+
socket?.close(CLIENT_RESTART_CLOSE_CODE, "state_digest_recovery_failed");
|
|
3695
|
+
if (socket) this.#disconnected(socket);
|
|
3696
|
+
this.#emit();
|
|
3697
|
+
}
|
|
3455
3698
|
#handleLifecycle(event) {
|
|
3456
3699
|
if (event === "online" || event === "offline") this.#online = event === "online";
|
|
3457
3700
|
else this.#active = event === "active";
|
|
@@ -3500,6 +3743,10 @@ var V1SyncClient = class {
|
|
|
3500
3743
|
#now() {
|
|
3501
3744
|
return this.#clock().now();
|
|
3502
3745
|
}
|
|
3746
|
+
#clearDial() {
|
|
3747
|
+
this.#cancelDial?.();
|
|
3748
|
+
this.#cancelDial = void 0;
|
|
3749
|
+
}
|
|
3503
3750
|
#clearReconnect() {
|
|
3504
3751
|
if (this.#reconnectTimer === void 0) return;
|
|
3505
3752
|
this.#clock().clearTimeout(this.#reconnectTimer);
|
|
@@ -3557,33 +3804,29 @@ var V1SyncClient = class {
|
|
|
3557
3804
|
}
|
|
3558
3805
|
#startNoticeProbe() {
|
|
3559
3806
|
this.#clearNoticeProbe();
|
|
3560
|
-
this.#
|
|
3561
|
-
this.#
|
|
3807
|
+
this.#send({ type: "ping" });
|
|
3808
|
+
this.#health = new V1SyncHealth(this.#now());
|
|
3562
3809
|
const tick = () => {
|
|
3563
3810
|
this.#noticeProbeTimer = void 0;
|
|
3564
3811
|
if (this.#phase.phase !== "live") return;
|
|
3812
|
+
const previous = this.#health.health;
|
|
3813
|
+
this.#health.check(this.#now());
|
|
3814
|
+
if (previous !== this.#health.health) this.#emit();
|
|
3565
3815
|
this.#send({ type: "ping" });
|
|
3566
|
-
this.#noticeProbeTimer = this.#clock().setTimeout(tick,
|
|
3816
|
+
this.#noticeProbeTimer = this.#clock().setTimeout(tick, SYNC_HEALTH_BUDGET.probeIntervalMs);
|
|
3567
3817
|
};
|
|
3568
|
-
this.#noticeProbeTimer = this.#clock().setTimeout(tick,
|
|
3569
|
-
}
|
|
3570
|
-
#scheduleNoticeCheck() {
|
|
3571
|
-
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3572
|
-
const remaining = Math.max(0, NOTICE_SILENCE_MS - (this.#now() - this.#noticeLastInboundAt));
|
|
3818
|
+
this.#noticeProbeTimer = this.#clock().setTimeout(tick, SYNC_HEALTH_BUDGET.probeIntervalMs);
|
|
3573
3819
|
this.#noticeTimer = this.#clock().setTimeout(() => {
|
|
3574
3820
|
this.#noticeTimer = void 0;
|
|
3575
|
-
|
|
3576
|
-
if (this.#now() - this.#noticeLastInboundAt < NOTICE_SILENCE_MS) return this.#scheduleNoticeCheck();
|
|
3577
|
-
this.#noticeUnresponsive = true;
|
|
3821
|
+
this.#health.check(this.#now());
|
|
3578
3822
|
this.#emit();
|
|
3579
|
-
},
|
|
3823
|
+
}, SYNC_HEALTH_BUDGET.degradedAfterMs);
|
|
3580
3824
|
}
|
|
3581
3825
|
#clearNoticeProbe() {
|
|
3582
3826
|
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3583
3827
|
if (this.#noticeProbeTimer !== void 0) this.#clock().clearTimeout(this.#noticeProbeTimer);
|
|
3584
3828
|
this.#noticeTimer = void 0;
|
|
3585
3829
|
this.#noticeProbeTimer = void 0;
|
|
3586
|
-
this.#noticeUnresponsive = false;
|
|
3587
3830
|
}
|
|
3588
3831
|
#emit() {
|
|
3589
3832
|
const snapshot = this.getSnapshot();
|
|
@@ -3626,6 +3869,12 @@ function copyPublication(publication) {
|
|
|
3626
3869
|
function validMediaPublication(publication) {
|
|
3627
3870
|
return typeof publication.participantId === "string" && publication.participantId.length > 0 && (publication.source === "microphone" || publication.source === "camera" || publication.source === "screen" || publication.source === "screen_audio") && typeof publication.enabled === "boolean" && (publication.publicationId === null || typeof publication.publicationId === "string") && (!publication.enabled || publication.publicationId !== null);
|
|
3628
3871
|
}
|
|
3872
|
+
function entryUrl(value, generation, confirmation) {
|
|
3873
|
+
const url = new URL(value);
|
|
3874
|
+
url.searchParams.set("entry_generation", generation === null ? "new" : String(generation));
|
|
3875
|
+
if (confirmation) url.searchParams.set("entry_confirmation", "1");
|
|
3876
|
+
return url.toString();
|
|
3877
|
+
}
|
|
3629
3878
|
function assertV1Url(value) {
|
|
3630
3879
|
const url = new URL(value);
|
|
3631
3880
|
if (url.pathname !== "/v1/sync") throw new V1SyncError("SyncEngine v1 websocket URL must use /v1/sync", "invalid_url");
|
|
@@ -3773,6 +4022,7 @@ function createV1SyncClient(options) {
|
|
|
3773
4022
|
}
|
|
3774
4023
|
|
|
3775
4024
|
export {
|
|
4025
|
+
formatSyncConnectDiagnostic,
|
|
3776
4026
|
UuidSchema,
|
|
3777
4027
|
ReactionValues,
|
|
3778
4028
|
ReactionSchema,
|
|
@@ -3786,6 +4036,7 @@ export {
|
|
|
3786
4036
|
createBrowserSyncLifecycle,
|
|
3787
4037
|
createReactNativeWebSocketFactory,
|
|
3788
4038
|
createReactNativeSyncLifecycle,
|
|
4039
|
+
scheduleSyncDialDeadline,
|
|
3789
4040
|
encodeV1ClientFrame,
|
|
3790
4041
|
decodeV1ServerFrame,
|
|
3791
4042
|
decodeV1ClientFrame,
|