@q9labsai/chalk-client 4.1.15 → 4.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -2
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-LQSXEWLL.js → chunk-BD7FRQ67.js} +2061 -326
- package/dist/{chunk-6ONSFCIY.js → chunk-E4OK5K6N.js} +7 -3
- package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
- package/dist/chunk-HW5TTZL2.js +99 -0
- package/dist/{chunk-LSDDADCN.js → chunk-M3S7HNQF.js} +1656 -517
- package/dist/{chunk-PQNSA7ZN.js → chunk-O7WTAXAY.js} +610 -73
- package/dist/{chunk-PWUU6T3R.js → chunk-OKIL2ELF.js} +1185 -874
- package/dist/chunk-RCDXIDCN.js +33 -0
- package/dist/client.d.ts +1412 -463
- package/dist/client.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +10 -0
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/index.d.ts +2 -0
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.d.ts.map +1 -1
- package/dist/connection/production.d.ts +4 -1
- package/dist/connection/production.d.ts.map +1 -1
- package/dist/connection/types.d.ts +2 -2
- package/dist/connection/types.d.ts.map +1 -1
- package/dist/effect.js +343 -130
- package/dist/feedback/collector.d.ts +14 -0
- package/dist/feedback/collector.d.ts.map +1 -0
- package/dist/feedback/context.d.ts +38 -0
- package/dist/feedback/context.d.ts.map +1 -0
- package/dist/feedback/controller.d.ts +4 -0
- package/dist/feedback/controller.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/transport.d.ts +29 -0
- package/dist/feedback/transport.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +202 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/feedback/validation.d.ts +30 -0
- package/dist/feedback/validation.d.ts.map +1 -0
- package/dist/generated/http-api.d.ts +1401 -462
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +2601 -858
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +150 -35
- package/dist/media/camera-uplink.d.ts +13 -0
- package/dist/media/camera-uplink.d.ts.map +1 -0
- package/dist/media/capture-error.d.ts +27 -0
- package/dist/media/capture-error.d.ts.map +1 -0
- package/dist/media/client.d.ts +4 -0
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +5 -1
- package/dist/media/plane.d.ts +4 -0
- package/dist/media/plane.d.ts.map +1 -1
- package/dist/media/remote-track-response.d.ts +6 -0
- package/dist/media/remote-track-response.d.ts.map +1 -0
- package/dist/media/transport.d.ts.map +1 -1
- package/dist/media/types.d.ts +20 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/public-invites/client.d.ts.map +1 -1
- package/dist/public-invites/index.js +4 -3
- package/dist/public-invites/types.d.ts +4 -2
- package/dist/public-invites/types.d.ts.map +1 -1
- package/dist/server/access-refresh.d.cts +8 -0
- package/dist/server/access-refresh.d.ts +9 -0
- package/dist/server/access-refresh.d.ts.map +1 -0
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/index.cjs +181 -20
- package/dist/server/index.d.cts +2 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +179 -19
- package/dist/server/types.d.cts +74 -0
- package/dist/server/types.d.ts +74 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/space-client/chat-controller.d.ts +3 -0
- package/dist/space-client/chat-controller.d.ts.map +1 -1
- package/dist/space-client/controller-effects.d.ts +6 -0
- package/dist/space-client/controller-effects.d.ts.map +1 -1
- package/dist/space-client/core.d.ts +12 -1
- package/dist/space-client/core.d.ts.map +1 -1
- package/dist/space-client/core.test.helpers.d.ts +1 -0
- package/dist/space-client/core.test.helpers.d.ts.map +1 -1
- package/dist/space-client/effect.d.ts +5 -0
- package/dist/space-client/effect.d.ts.map +1 -1
- package/dist/space-client/episode-diagnostic-runtime.d.ts +3 -0
- package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -1
- package/dist/space-client/errors.d.ts.map +1 -1
- package/dist/space-client/index.d.ts +4 -1
- package/dist/space-client/index.d.ts.map +1 -1
- package/dist/space-client/media-controller.d.ts.map +1 -1
- package/dist/space-client/offline-actions-storage.d.ts +66 -0
- package/dist/space-client/offline-actions-storage.d.ts.map +1 -0
- package/dist/space-client/offline-actions.d.ts +36 -0
- package/dist/space-client/offline-actions.d.ts.map +1 -0
- package/dist/space-client/participants-controller.d.ts +2 -1
- package/dist/space-client/participants-controller.d.ts.map +1 -1
- package/dist/space-client/recording-controller.d.ts +20 -0
- package/dist/space-client/recording-controller.d.ts.map +1 -0
- package/dist/space-client/reload-rejoin.d.ts +24 -0
- package/dist/space-client/reload-rejoin.d.ts.map +1 -0
- package/dist/space-client/space-client.d.ts.map +1 -1
- package/dist/space-client/store.d.ts +3 -1
- package/dist/space-client/store.d.ts.map +1 -1
- package/dist/space-client/types.d.ts +40 -4
- package/dist/space-client/types.d.ts.map +1 -1
- package/dist/sync/index.js +2 -1
- package/dist/sync/v1-client.d.ts +1 -5
- package/dist/sync/v1-client.d.ts.map +1 -1
- package/dist/sync/v1-collaboration-state.d.ts +65 -0
- package/dist/sync/v1-collaboration-state.d.ts.map +1 -0
- package/dist/sync/v1-command-scheduler.d.ts +54 -0
- package/dist/sync/v1-command-scheduler.d.ts.map +1 -0
- package/dist/sync/v1-deferred.d.ts +8 -0
- package/dist/sync/v1-deferred.d.ts.map +1 -0
- package/dist/sync/v1-error.d.ts +5 -0
- package/dist/sync/v1-error.d.ts.map +1 -0
- package/dist/sync/v1-frame-signature.d.ts +2 -0
- package/dist/sync/v1-frame-signature.d.ts.map +1 -0
- package/dist/sync/v1-live-target-coordinator.d.ts +32 -0
- package/dist/sync/v1-live-target-coordinator.d.ts.map +1 -0
- package/dist/sync/v1-types.d.ts +3 -0
- package/dist/sync/v1-types.d.ts.map +1 -1
- package/dist/telemetry/index.js +9 -7
- package/dist/telemetry/reconnect.d.ts +7 -0
- package/dist/telemetry/reconnect.d.ts.map +1 -0
- package/dist/telemetry/trace.d.ts +1 -1
- package/dist/telemetry/trace.d.ts.map +1 -1
- package/dist/whiteboard/v1-client.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordReconnect
|
|
3
|
+
} from "./chunk-RCDXIDCN.js";
|
|
1
4
|
import {
|
|
2
5
|
syncTelemetryCorrelation
|
|
3
6
|
} from "./chunk-PBSYXPNJ.js";
|
|
@@ -1381,7 +1384,7 @@ var SatisfiedAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), comma
|
|
|
1381
1384
|
var RejectedAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), command_id: CommandIdSchema, delivery: AckDeliverySchema, outcome: Schema.Literal("rejected"), reason: RejectionReasonSchema, ...ServerCorrelationFields });
|
|
1382
1385
|
var CommandIdConflictAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), command_id: CommandIdSchema, delivery: AckDeliverySchema, outcome: Schema.Literal("command_id_conflict"), reason: Schema.Literal("command_id_conflict"), ...ServerCorrelationFields });
|
|
1383
1386
|
var MediaProjectionItemSchema = Schema.Struct({ participant_id: UuidSchema, source: Schema.Union([Schema.Literal("microphone"), Schema.Literal("camera"), Schema.Literal("screen")]), enabled: Schema.Boolean, publication_id: Schema.NullOr(boundedUtf8String(256, 1)) }).check(
|
|
1384
|
-
Schema.makeFilter((item) => item.enabled
|
|
1387
|
+
Schema.makeFilter((item) => !item.enabled || item.publication_id !== null ? void 0 : { path: [], issue: "enabled requires a publication_id" })
|
|
1385
1388
|
);
|
|
1386
1389
|
var PresenceProjectionItemSchema = Schema.Struct({ participant_id: UuidSchema, state: Schema.Union([Schema.Literal("connected"), Schema.Literal("disconnected")]), speaking: Schema.Boolean, active_speaker: Schema.Boolean }).check(
|
|
1387
1390
|
Schema.makeFilter((item) => item.state !== "disconnected" || !item.speaking && !item.active_speaker ? void 0 : { path: [], issue: "disconnected presence cannot be speaking or active speaker" })
|
|
@@ -1570,6 +1573,18 @@ function encodeSyncFrame(frame) {
|
|
|
1570
1573
|
return JSON.stringify(frame);
|
|
1571
1574
|
}
|
|
1572
1575
|
|
|
1576
|
+
// src/sync/v1-codec.ts
|
|
1577
|
+
import { Schema as Schema2 } from "effect";
|
|
1578
|
+
function encodeV1ClientFrame(frame) {
|
|
1579
|
+
return encodeSyncFrame(Schema2.decodeUnknownSync(SyncV1ClientFrameSchema)(frame));
|
|
1580
|
+
}
|
|
1581
|
+
function decodeV1ServerFrame(wire) {
|
|
1582
|
+
return Schema2.decodeUnknownSync(SyncV1ServerFrameSchema)(JSON.parse(wire));
|
|
1583
|
+
}
|
|
1584
|
+
function decodeV1ClientFrame(value) {
|
|
1585
|
+
return Schema2.decodeUnknownSync(SyncV1ClientFrameSchema)(value);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1573
1588
|
// src/collaboration/wire.ts
|
|
1574
1589
|
function reactionFromFrame(frame) {
|
|
1575
1590
|
return {
|
|
@@ -1607,40 +1622,27 @@ function chatReadReceiptFromFrame(frame) {
|
|
|
1607
1622
|
};
|
|
1608
1623
|
}
|
|
1609
1624
|
|
|
1610
|
-
// src/sync/v1-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
function decodeV1ServerFrame(wire) {
|
|
1616
|
-
return Schema2.decodeUnknownSync(SyncV1ServerFrameSchema)(JSON.parse(wire));
|
|
1625
|
+
// src/sync/v1-deferred.ts
|
|
1626
|
+
function resolveV1Deferred(deferred, value) {
|
|
1627
|
+
if (deferred.settled) return;
|
|
1628
|
+
deferred.settled = true;
|
|
1629
|
+
deferred.resolve(value);
|
|
1617
1630
|
}
|
|
1618
|
-
function
|
|
1619
|
-
|
|
1631
|
+
function rejectV1Deferred(deferred, error) {
|
|
1632
|
+
if (deferred.settled) return;
|
|
1633
|
+
deferred.settled = true;
|
|
1634
|
+
deferred.reject(error);
|
|
1620
1635
|
}
|
|
1621
1636
|
|
|
1622
|
-
// src/sync/v1-
|
|
1623
|
-
var
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
async load() {
|
|
1629
|
-
return [...this.#commands.values()].map((command) => structuredClone(command)).sort(comparePending);
|
|
1630
|
-
}
|
|
1631
|
-
async put(command) {
|
|
1632
|
-
this.#commands.set(command.commandId, structuredClone(command));
|
|
1633
|
-
}
|
|
1634
|
-
async remove(commandId) {
|
|
1635
|
-
this.#commands.delete(commandId);
|
|
1637
|
+
// src/sync/v1-error.ts
|
|
1638
|
+
var V1SyncError = class extends Error {
|
|
1639
|
+
constructor(message, code) {
|
|
1640
|
+
super(message);
|
|
1641
|
+
this.code = code;
|
|
1642
|
+
this.name = "V1SyncError";
|
|
1636
1643
|
}
|
|
1644
|
+
code;
|
|
1637
1645
|
};
|
|
1638
|
-
function compareV1PendingTargets(left, right) {
|
|
1639
|
-
return comparePending(left, right);
|
|
1640
|
-
}
|
|
1641
|
-
function comparePending(left, right) {
|
|
1642
|
-
return left.createdAt - right.createdAt || left.commandId.localeCompare(right.commandId);
|
|
1643
|
-
}
|
|
1644
1646
|
|
|
1645
1647
|
// src/sync/v1-reducer.ts
|
|
1646
1648
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -1902,93 +1904,25 @@ function sameStrings2(left, right) {
|
|
|
1902
1904
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1903
1905
|
}
|
|
1904
1906
|
|
|
1905
|
-
// src/sync/v1-
|
|
1906
|
-
var encoder3 = new TextEncoder();
|
|
1907
|
-
var MAX_IN_FLIGHT = 256;
|
|
1908
|
-
var MAX_PENDING_BYTES = 1024 * 1024;
|
|
1909
|
-
var MAX_PENDING_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1910
|
-
var MAX_REPLAY_EVENTS = SyncProtocolLimits.completeReplayMaxEvents;
|
|
1911
|
-
var MAX_REPLAY_BYTES = SyncProtocolLimits.completeReplayEncodedBytes;
|
|
1912
|
-
var MAX_RETRIES = 3;
|
|
1913
|
-
var MAX_LIVE_SERVER_RETRIES = 16;
|
|
1914
|
-
var LIVE_TARGET_RETRY_BUDGET_MS = 15e3;
|
|
1915
|
-
var MAX_LIVE_TARGET_RETRY_DELAY_MS = 1e3;
|
|
1916
|
-
var MAX_PROJECTION_EVENT_EVIDENCE = 256;
|
|
1917
|
-
var OPERATION_PENDING_POLL_INTERVAL_MS = 1e3;
|
|
1918
|
-
var CLIENT_RESTART_CLOSE_CODE = 4e3;
|
|
1919
|
-
var DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
1920
|
-
var MAX_RECONNECT_DELAY_MS = 5e3;
|
|
1907
|
+
// src/sync/v1-collaboration-state.ts
|
|
1921
1908
|
var MAX_COLLABORATION_REQUESTS_IN_FLIGHT = 64;
|
|
1922
|
-
var
|
|
1923
|
-
constructor(message, code) {
|
|
1924
|
-
super(message);
|
|
1925
|
-
this.code = code;
|
|
1926
|
-
this.name = "V1SyncError";
|
|
1927
|
-
}
|
|
1928
|
-
code;
|
|
1929
|
-
};
|
|
1930
|
-
var V1SyncClient = class {
|
|
1909
|
+
var V1CollaborationState = class {
|
|
1931
1910
|
#options;
|
|
1932
|
-
#store;
|
|
1933
1911
|
#listeners = /* @__PURE__ */ new Set();
|
|
1934
|
-
#requestListeners = /* @__PURE__ */ new Set();
|
|
1935
|
-
#collaborationListeners = /* @__PURE__ */ new Set();
|
|
1936
|
-
#pendingTargets = /* @__PURE__ */ new Map();
|
|
1937
|
-
#reservedCommandIds = /* @__PURE__ */ new Set();
|
|
1938
|
-
#commands = /* @__PURE__ */ new Map();
|
|
1939
|
-
#acknowledgements = /* @__PURE__ */ new Map();
|
|
1940
|
-
#pendingRemovals = /* @__PURE__ */ new Map();
|
|
1941
|
-
#controlHeads = /* @__PURE__ */ new Map();
|
|
1942
|
-
#controlEvents = /* @__PURE__ */ new Map();
|
|
1943
|
-
#liveTargets = /* @__PURE__ */ new Map();
|
|
1944
|
-
#requests = /* @__PURE__ */ new Map();
|
|
1945
1912
|
#reactions = /* @__PURE__ */ new Map();
|
|
1946
1913
|
#chatSends = /* @__PURE__ */ new Map();
|
|
1947
1914
|
#chatPages = /* @__PURE__ */ new Map();
|
|
1948
1915
|
#chatReads = /* @__PURE__ */ new Map();
|
|
1949
|
-
#
|
|
1950
|
-
#
|
|
1951
|
-
#liveDeadlineTimers = /* @__PURE__ */ new Map();
|
|
1952
|
-
#pendingRemovalRetryTimers = /* @__PURE__ */ new Map();
|
|
1953
|
-
#mediaEventEvidence = /* @__PURE__ */ new Map();
|
|
1954
|
-
#presenceEventEvidence = /* @__PURE__ */ new Map();
|
|
1955
|
-
#phase = { phase: "idle" };
|
|
1956
|
-
#socket = null;
|
|
1957
|
-
#control = null;
|
|
1958
|
-
#media = null;
|
|
1959
|
-
#presence = null;
|
|
1960
|
-
#participantId = null;
|
|
1961
|
-
#participantGeneration = null;
|
|
1962
|
-
#recovery = null;
|
|
1963
|
-
#started = false;
|
|
1964
|
-
#startupGeneration = 0;
|
|
1965
|
-
#connectionGeneration = 0;
|
|
1966
|
-
#reconnectTimer;
|
|
1967
|
-
#reconnectAttempt = 0;
|
|
1968
|
-
#heartbeatTimer;
|
|
1969
|
-
#missedHeartbeats = 0;
|
|
1970
|
-
#unsubscribeLifecycle;
|
|
1971
|
-
#inbound = Promise.resolve();
|
|
1972
|
-
#transportAvailable = true;
|
|
1973
|
-
#online = true;
|
|
1974
|
-
#active = true;
|
|
1975
|
-
#unsubscribeLocalMedia;
|
|
1976
|
-
#unsubscribeRemoteMedia;
|
|
1977
|
-
#localPublications = [];
|
|
1978
|
-
#remotePublications = [];
|
|
1979
|
-
#collaboration;
|
|
1980
|
-
#participantCollaborationCapabilities = {};
|
|
1916
|
+
#state;
|
|
1917
|
+
#participantCapabilities = {};
|
|
1981
1918
|
#chatAfterSequence;
|
|
1982
|
-
#
|
|
1983
|
-
#localMedia = { microphone: "unknown", camera: "unknown", screen: "unknown" };
|
|
1919
|
+
#requestedVersion;
|
|
1984
1920
|
constructor(options) {
|
|
1985
|
-
assertV1Url(options.url);
|
|
1986
1921
|
this.#options = options;
|
|
1987
|
-
|
|
1988
|
-
const cursor = options.collaboration === false ? { afterSequence: null, retainedFloorSequence: null } : options.collaboration?.chatCursor ?? { afterSequence: null, retainedFloorSequence: null };
|
|
1922
|
+
const cursor = options.request === false ? { afterSequence: null, retainedFloorSequence: null } : options.request?.chatCursor ?? { afterSequence: null, retainedFloorSequence: null };
|
|
1989
1923
|
this.#chatAfterSequence = cursor.afterSequence;
|
|
1990
|
-
this.#
|
|
1991
|
-
this.#
|
|
1924
|
+
this.#requestedVersion = options.request === false ? 0 : 1;
|
|
1925
|
+
this.#state = {
|
|
1992
1926
|
negotiated: false,
|
|
1993
1927
|
version: null,
|
|
1994
1928
|
capabilities: [],
|
|
@@ -1997,142 +1931,77 @@ var V1SyncClient = class {
|
|
|
1997
1931
|
readReceipts: []
|
|
1998
1932
|
};
|
|
1999
1933
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
this.#started = true;
|
|
2003
|
-
const startupGeneration = ++this.#startupGeneration;
|
|
2004
|
-
let stored;
|
|
2005
|
-
try {
|
|
2006
|
-
stored = await this.#store.load();
|
|
2007
|
-
} catch (error) {
|
|
2008
|
-
if (startupGeneration === this.#startupGeneration) {
|
|
2009
|
-
this.#started = false;
|
|
2010
|
-
}
|
|
2011
|
-
throw error;
|
|
2012
|
-
}
|
|
2013
|
-
if (!this.#started || startupGeneration !== this.#startupGeneration) return;
|
|
2014
|
-
const storedCommandIds = new Set(stored.map((pending) => pending.commandId));
|
|
2015
|
-
for (const commandId of this.#pendingTargets.keys()) {
|
|
2016
|
-
if (storedCommandIds.has(commandId) || this.#commands.has(commandId)) continue;
|
|
2017
|
-
this.#pendingTargets.delete(commandId);
|
|
2018
|
-
}
|
|
2019
|
-
for (const commandId of this.#pendingRemovals.keys()) {
|
|
2020
|
-
if (storedCommandIds.has(commandId)) continue;
|
|
2021
|
-
this.#pendingRemovals.delete(commandId);
|
|
2022
|
-
this.#clearPendingRemovalRetryTimer(commandId);
|
|
2023
|
-
}
|
|
2024
|
-
for (const pending of [...stored].sort(compareV1PendingTargets)) {
|
|
2025
|
-
if (this.#pendingRemovals.has(pending.commandId)) {
|
|
2026
|
-
void this.#removePersistedTarget(pending.commandId, pending);
|
|
2027
|
-
} else if (this.#commands.has(pending.commandId)) {
|
|
2028
|
-
this.#pendingTargets.set(pending.commandId, pending);
|
|
2029
|
-
} else {
|
|
2030
|
-
this.#pendingTargets.delete(pending.commandId);
|
|
2031
|
-
this.#restorePending(pending);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
this.#subscribeMediaPlane(this.#options.mediaPlane);
|
|
2035
|
-
this.#unsubscribeLifecycle = this.#options.lifecycle?.subscribe((event) => this.#handleLifecycle(event));
|
|
2036
|
-
this.#connect();
|
|
1934
|
+
get requestedVersion() {
|
|
1935
|
+
return this.#requestedVersion;
|
|
2037
1936
|
}
|
|
2038
|
-
|
|
2039
|
-
this.#
|
|
2040
|
-
this.#
|
|
2041
|
-
this.#unsubscribeLifecycle?.();
|
|
2042
|
-
this.#unsubscribeLifecycle = void 0;
|
|
2043
|
-
this.#clearReconnect();
|
|
2044
|
-
this.#clearHeartbeat();
|
|
2045
|
-
this.#clearRetryTimers();
|
|
2046
|
-
this.#clearPendingRemovalRetryTimers();
|
|
2047
|
-
this.#unsubscribeMediaPlane();
|
|
2048
|
-
this.#socket?.close(1e3, "client stopped");
|
|
2049
|
-
this.#socket = null;
|
|
2050
|
-
this.#phase = { phase: "stopped" };
|
|
2051
|
-
this.#rejectEphemeral("client_stopped");
|
|
2052
|
-
this.#rejectCollaboration("client_stopped");
|
|
2053
|
-
this.#collaboration = { ...this.#collaboration, negotiated: false, version: null, capabilities: [], readReceipts: [] };
|
|
2054
|
-
this.#participantCollaborationCapabilities = {};
|
|
2055
|
-
this.#localPublications = [];
|
|
2056
|
-
this.#remotePublications = [];
|
|
2057
|
-
for (const source of ["microphone", "camera", "screen"]) this.#localMedia[source] = "unknown";
|
|
2058
|
-
for (const deferred of this.#commands.values()) rejectDeferred(deferred, new V1SyncError("client_stopped", "client_stopped"));
|
|
2059
|
-
this.#commands.clear();
|
|
2060
|
-
this.#acknowledgements.clear();
|
|
2061
|
-
this.#emit();
|
|
1937
|
+
get helloExtension() {
|
|
1938
|
+
if (this.#requestedVersion === 0) return null;
|
|
1939
|
+
return { name: "collaboration_v1", chat_cursor: { after_sequence: this.#chatAfterSequence, retained_floor_sequence: this.#state.retainedFloorSequence } };
|
|
2062
1940
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
participantId: this.#participantId,
|
|
2069
|
-
participantGeneration: this.#participantGeneration,
|
|
2070
|
-
control: this.#control,
|
|
2071
|
-
optimisticControl,
|
|
2072
|
-
media: copyProjection(this.#media),
|
|
2073
|
-
presence: copyProjection(this.#presence),
|
|
2074
|
-
mediaPlane: { local: this.#localPublications.map(copyPublication), remote: this.#remotePublications.map(copyPublication) },
|
|
2075
|
-
localMedia: { ...this.#localMedia },
|
|
2076
|
-
pendingCommandCount: this.#pendingTargets.size
|
|
2077
|
-
};
|
|
1941
|
+
getState() {
|
|
1942
|
+
return { ...this.#state, capabilities: [...this.#state.capabilities], readReceipts: this.#state.readReceipts.map((receipt) => ({ ...receipt })) };
|
|
1943
|
+
}
|
|
1944
|
+
getParticipantCapabilities() {
|
|
1945
|
+
return Object.fromEntries(Object.entries(this.#participantCapabilities).map(([participantId, capabilities]) => [participantId, [...capabilities]]));
|
|
2078
1946
|
}
|
|
2079
1947
|
subscribe(listener) {
|
|
2080
1948
|
this.#listeners.add(listener);
|
|
2081
|
-
listener(this.getSnapshot());
|
|
2082
1949
|
return () => this.#listeners.delete(listener);
|
|
2083
1950
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
1951
|
+
acceptWelcome(frame) {
|
|
1952
|
+
if ("extensions" in frame) {
|
|
1953
|
+
const extension = frame.extensions[0];
|
|
1954
|
+
const version = 1;
|
|
1955
|
+
if (this.#requestedVersion !== version) throw new V1ReplicaError("collaboration welcome version does not match the requested extension");
|
|
1956
|
+
this.#state = {
|
|
1957
|
+
negotiated: true,
|
|
1958
|
+
version,
|
|
1959
|
+
capabilities: [...extension.capabilities],
|
|
1960
|
+
chatHeadSequence: extension.chat_head_sequence,
|
|
1961
|
+
retainedFloorSequence: extension.retained_floor_sequence,
|
|
1962
|
+
readReceipts: "read_receipts" in extension ? extension.read_receipts.map(chatReadReceiptFromWire) : []
|
|
1963
|
+
};
|
|
1964
|
+
this.#participantCapabilities = copyCapabilities(extension.participant_capabilities);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
this.#requestedVersion = 0;
|
|
1968
|
+
this.#state = { ...this.#state, negotiated: false, version: null, capabilities: [], readReceipts: [] };
|
|
1969
|
+
this.#participantCapabilities = {};
|
|
2093
1970
|
}
|
|
2094
|
-
|
|
2095
|
-
this.#
|
|
2096
|
-
|
|
1971
|
+
disconnect(code) {
|
|
1972
|
+
this.#rejectPending(code);
|
|
1973
|
+
this.#state = { ...this.#state, negotiated: false, version: null, capabilities: [], readReceipts: [] };
|
|
1974
|
+
this.#participantCapabilities = {};
|
|
2097
1975
|
}
|
|
2098
1976
|
sendReaction(reaction) {
|
|
2099
|
-
this.#
|
|
2100
|
-
this.#
|
|
2101
|
-
const operationId = this.#
|
|
1977
|
+
this.#assertReady("sendReaction");
|
|
1978
|
+
this.#assertCapacity();
|
|
1979
|
+
const operationId = this.#nextRequestId();
|
|
2102
1980
|
const frame = { type: "reaction_send", operation_id: operationId, reaction };
|
|
2103
|
-
|
|
2104
|
-
const promise = new Promise((resolve, reject) => this.#reactions.set(operationId, { resolve, reject, settled: false }));
|
|
2105
|
-
this.#send(frame);
|
|
2106
|
-
return promise;
|
|
1981
|
+
return this.#sendTrackedRequest(operationId, frame, this.#reactions);
|
|
2107
1982
|
}
|
|
2108
1983
|
sendChatMessage(input) {
|
|
2109
|
-
this.#
|
|
2110
|
-
this.#
|
|
2111
|
-
const clientMessageId = input.clientMessageId ?? this.#
|
|
2112
|
-
this.#
|
|
1984
|
+
this.#assertReady("sendChat");
|
|
1985
|
+
this.#assertCapacity();
|
|
1986
|
+
const clientMessageId = input.clientMessageId ?? this.#nextRequestId();
|
|
1987
|
+
this.#assertRequestIdAvailable(clientMessageId);
|
|
2113
1988
|
const attachments = input.attachments ?? [];
|
|
2114
1989
|
const frame = { type: "chat_send", client_message_id: clientMessageId, text: input.text, attachment_ids: attachments.map((attachment) => attachment.attachmentId) };
|
|
2115
|
-
|
|
2116
|
-
const promise = new Promise((resolve, reject) => this.#chatSends.set(clientMessageId, { resolve, reject, settled: false }));
|
|
2117
|
-
this.#send(frame);
|
|
2118
|
-
return promise;
|
|
1990
|
+
return this.#sendTrackedRequest(clientMessageId, frame, this.#chatSends);
|
|
2119
1991
|
}
|
|
2120
1992
|
markChatRead(sequence) {
|
|
2121
|
-
this.#
|
|
2122
|
-
if (this.#
|
|
2123
|
-
this.#
|
|
2124
|
-
const requestId = this.#
|
|
1993
|
+
this.#assertReady();
|
|
1994
|
+
if (this.#state.version !== 1) throw new V1SyncError("durable chat read receipts require collaboration_v1", "collaboration_unavailable");
|
|
1995
|
+
this.#assertCapacity();
|
|
1996
|
+
const requestId = this.#nextRequestId();
|
|
2125
1997
|
const frame = { type: "chat_read_set", request_id: requestId, sequence };
|
|
2126
|
-
|
|
2127
|
-
const promise = new Promise((resolve, reject) => this.#chatReads.set(requestId, { resolve, reject, settled: false }));
|
|
2128
|
-
this.#send(frame);
|
|
2129
|
-
return promise;
|
|
1998
|
+
return this.#sendTrackedRequest(requestId, frame, this.#chatReads);
|
|
2130
1999
|
}
|
|
2131
2000
|
readChatPage(input) {
|
|
2132
|
-
this.#
|
|
2133
|
-
this.#
|
|
2001
|
+
this.#assertReady();
|
|
2002
|
+
this.#assertCapacity();
|
|
2134
2003
|
if (input.beforeSequence !== void 0 && input.afterSequence !== void 0) throw new V1SyncError("a chat page cannot declare both cursors", "invalid_payload");
|
|
2135
|
-
const requestId = this.#
|
|
2004
|
+
const requestId = this.#nextRequestId();
|
|
2136
2005
|
const frame = {
|
|
2137
2006
|
type: "chat_page_request",
|
|
2138
2007
|
request_id: requestId,
|
|
@@ -2140,57 +2009,986 @@ var V1SyncClient = class {
|
|
|
2140
2009
|
cursor_sequence: input.afterSequence ?? input.beforeSequence ?? null,
|
|
2141
2010
|
limit: input.limit
|
|
2142
2011
|
};
|
|
2143
|
-
|
|
2144
|
-
const promise = new Promise((resolve, reject) => this.#chatPages.set(requestId, { resolve, reject, settled: false }));
|
|
2145
|
-
this.#send(frame);
|
|
2146
|
-
return promise;
|
|
2012
|
+
return this.#sendTrackedRequest(requestId, frame, this.#chatPages);
|
|
2147
2013
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2014
|
+
reaction(frame) {
|
|
2015
|
+
this.#requireLive();
|
|
2016
|
+
this.#requireNegotiated();
|
|
2017
|
+
this.#emit({ type: "reaction", reaction: reactionFromFrame(frame) });
|
|
2150
2018
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2019
|
+
reactionResult(frame) {
|
|
2020
|
+
this.#requireLive();
|
|
2021
|
+
this.#requireNegotiated();
|
|
2022
|
+
const deferred = this.#reactions.get(frame.operation_id);
|
|
2023
|
+
if (!deferred) return;
|
|
2024
|
+
this.#reactions.delete(frame.operation_id);
|
|
2025
|
+
if (frame.outcome === "accepted") resolveV1Deferred(deferred, reactionFromFrame(frame.reaction));
|
|
2026
|
+
else rejectV1Deferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
2153
2027
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2028
|
+
chatMessage(frame) {
|
|
2029
|
+
this.#requireNegotiated();
|
|
2030
|
+
this.#observeChatMessage(frame);
|
|
2156
2031
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2032
|
+
chatSendResult(frame) {
|
|
2033
|
+
this.#requireLive();
|
|
2034
|
+
this.#requireNegotiated();
|
|
2035
|
+
const deferred = this.#chatSends.get(frame.client_message_id);
|
|
2036
|
+
if (!deferred) return;
|
|
2037
|
+
this.#chatSends.delete(frame.client_message_id);
|
|
2038
|
+
if (frame.outcome === "accepted") {
|
|
2039
|
+
this.#assertChatMessageVersion(frame.message);
|
|
2040
|
+
const message = chatMessageFromFrame(frame.message);
|
|
2041
|
+
this.#rememberChatSequence(message.sequence);
|
|
2042
|
+
resolveV1Deferred(deferred, message);
|
|
2043
|
+
} else {
|
|
2044
|
+
rejectV1Deferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
2045
|
+
}
|
|
2159
2046
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2047
|
+
chatPage(frame) {
|
|
2048
|
+
this.#requireNegotiated();
|
|
2049
|
+
const deferred = this.#chatPages.get(frame.request_id);
|
|
2050
|
+
if (!deferred) return;
|
|
2051
|
+
this.#chatPages.delete(frame.request_id);
|
|
2052
|
+
if (frame.outcome === "cursor_reset") {
|
|
2053
|
+
this.#state = { ...this.#state, retainedFloorSequence: frame.retained_floor_sequence };
|
|
2054
|
+
const result = { status: "cursor_reset", retainedFloorSequence: frame.retained_floor_sequence };
|
|
2055
|
+
this.#emit({ type: "chat_cursor_reset", retainedFloorSequence: frame.retained_floor_sequence });
|
|
2056
|
+
resolveV1Deferred(deferred, result);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
this.#state = { ...this.#state, chatHeadSequence: frame.head_sequence, retainedFloorSequence: frame.retained_floor_sequence };
|
|
2060
|
+
for (const message of frame.messages) this.#observeChatMessage(message);
|
|
2061
|
+
resolveV1Deferred(deferred, { status: "loaded", count: frame.messages.length, hasOlder: frame.has_more });
|
|
2162
2062
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2063
|
+
chatHead(frame) {
|
|
2064
|
+
this.#requireNegotiated();
|
|
2065
|
+
this.#state = { ...this.#state, chatHeadSequence: frame.head_sequence, retainedFloorSequence: frame.retained_floor_sequence };
|
|
2066
|
+
this.#options.stateChanged();
|
|
2165
2067
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2068
|
+
chatReadReceipt(frame) {
|
|
2069
|
+
this.#requireLive();
|
|
2070
|
+
this.#requireVersion(1);
|
|
2071
|
+
const receipt = chatReadReceiptFromFrame(frame);
|
|
2072
|
+
this.#rememberChatReadReceipt(receipt);
|
|
2073
|
+
this.#emit({ type: "chat_read_receipt", receipt });
|
|
2074
|
+
}
|
|
2075
|
+
chatReadResult(frame) {
|
|
2076
|
+
this.#requireLive();
|
|
2077
|
+
this.#requireVersion(1);
|
|
2078
|
+
const deferred = this.#chatReads.get(frame.request_id);
|
|
2079
|
+
if (!deferred) return;
|
|
2080
|
+
this.#chatReads.delete(frame.request_id);
|
|
2081
|
+
if (frame.outcome === "accepted") {
|
|
2082
|
+
const receipt = chatReadReceiptFromFrame(frame);
|
|
2083
|
+
this.#rememberChatReadReceipt(receipt);
|
|
2084
|
+
resolveV1Deferred(deferred, receipt);
|
|
2085
|
+
} else {
|
|
2086
|
+
rejectV1Deferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
#rejectPending(code) {
|
|
2090
|
+
for (const deferred of this.#reactions.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2091
|
+
for (const deferred of this.#chatSends.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2092
|
+
for (const deferred of this.#chatPages.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2093
|
+
for (const deferred of this.#chatReads.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2094
|
+
this.#reactions.clear();
|
|
2095
|
+
this.#chatSends.clear();
|
|
2096
|
+
this.#chatPages.clear();
|
|
2097
|
+
this.#chatReads.clear();
|
|
2098
|
+
}
|
|
2099
|
+
#sendTrackedRequest(requestId, frame, pending) {
|
|
2100
|
+
encodeV1ClientFrame(frame);
|
|
2101
|
+
const promise = new Promise((resolve, reject) => pending.set(requestId, { resolve, reject, settled: false }));
|
|
2102
|
+
this.#options.send(frame);
|
|
2103
|
+
return promise;
|
|
2104
|
+
}
|
|
2105
|
+
#observeChatMessage(frame) {
|
|
2106
|
+
this.#assertChatMessageVersion(frame);
|
|
2107
|
+
const message = chatMessageFromFrame(frame);
|
|
2108
|
+
this.#rememberChatSequence(message.sequence);
|
|
2109
|
+
this.#emit({ type: "chat_message", message });
|
|
2110
|
+
}
|
|
2111
|
+
#assertChatMessageVersion(frame) {
|
|
2112
|
+
if (this.#state.version === 1 && !("attachments" in frame)) throw new V1ReplicaError("collaboration_v1 received a chat message without attachments");
|
|
2113
|
+
}
|
|
2114
|
+
#rememberChatReadReceipt(receipt) {
|
|
2115
|
+
const existing = this.#state.readReceipts.find((candidate) => candidate.participantId === receipt.participantId && candidate.participantGeneration === receipt.participantGeneration);
|
|
2116
|
+
if (existing && compareUnsignedDecimals(existing.readThroughSequence, receipt.readThroughSequence) >= 0) return;
|
|
2117
|
+
this.#state = {
|
|
2118
|
+
...this.#state,
|
|
2119
|
+
readReceipts: [...this.#state.readReceipts.filter((candidate) => candidate.participantId !== receipt.participantId || candidate.participantGeneration !== receipt.participantGeneration), receipt]
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
#rememberChatSequence(sequence) {
|
|
2123
|
+
if (this.#chatAfterSequence === null || compareUnsignedDecimals(sequence, this.#chatAfterSequence) > 0) this.#chatAfterSequence = sequence;
|
|
2124
|
+
if (this.#state.chatHeadSequence === null || compareUnsignedDecimals(sequence, this.#state.chatHeadSequence) > 0) this.#state = { ...this.#state, chatHeadSequence: sequence };
|
|
2125
|
+
}
|
|
2126
|
+
#assertReady(capability) {
|
|
2127
|
+
if (!this.#state.negotiated) throw new V1SyncError("collaboration is unavailable", "collaboration_unavailable");
|
|
2128
|
+
if (!this.#options.isLive()) throw new V1SyncError("collaboration requires a live connection", "not_live");
|
|
2129
|
+
if (capability && !this.#state.capabilities.includes(capability)) throw new V1SyncError("collaboration capability denied", "capability_denied");
|
|
2130
|
+
}
|
|
2131
|
+
#assertCapacity() {
|
|
2132
|
+
const maximum = Math.min(this.#options.maxPendingRequests ?? MAX_COLLABORATION_REQUESTS_IN_FLIGHT, MAX_COLLABORATION_REQUESTS_IN_FLIGHT);
|
|
2133
|
+
if (this.#reactions.size + this.#chatSends.size + this.#chatPages.size + this.#chatReads.size >= maximum) throw new V1SyncError("collaboration in-flight capacity exceeded", "capacity");
|
|
2134
|
+
}
|
|
2135
|
+
#assertRequestIdAvailable(id) {
|
|
2136
|
+
if (this.#reactions.has(id) || this.#chatSends.has(id) || this.#chatPages.has(id) || this.#chatReads.has(id)) throw new V1SyncError("collaboration request ID is already pending", "request_id_conflict");
|
|
2137
|
+
}
|
|
2138
|
+
#nextRequestId() {
|
|
2139
|
+
const id = this.#options.requestIds?.next() ?? crypto.randomUUID();
|
|
2140
|
+
this.#assertRequestIdAvailable(id);
|
|
2141
|
+
return id;
|
|
2142
|
+
}
|
|
2143
|
+
#requireLive() {
|
|
2144
|
+
if (!this.#options.isLive()) throw new V1ReplicaError("live frame arrived before four-stream recovery completed");
|
|
2145
|
+
}
|
|
2146
|
+
#requireNegotiated() {
|
|
2147
|
+
if (!this.#state.negotiated) throw new V1ReplicaError("collaboration frame arrived without a negotiated extension");
|
|
2148
|
+
}
|
|
2149
|
+
#requireVersion(version) {
|
|
2150
|
+
this.#requireNegotiated();
|
|
2151
|
+
if (this.#state.version !== version) throw new V1ReplicaError("collaboration_v1 frame arrived on a different extension version");
|
|
2152
|
+
}
|
|
2153
|
+
#emit(event) {
|
|
2154
|
+
for (const listener of this.#listeners) listener(event);
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
function copyCapabilities(capabilities) {
|
|
2158
|
+
return Object.fromEntries(Object.entries(capabilities).map(([participantId, values]) => [participantId, [...values]]));
|
|
2159
|
+
}
|
|
2160
|
+
function chatReadReceiptFromWire(receipt) {
|
|
2161
|
+
return {
|
|
2162
|
+
participantId: receipt.participant_id,
|
|
2163
|
+
participantGeneration: receipt.participant_generation,
|
|
2164
|
+
readThroughSequence: receipt.sequence,
|
|
2165
|
+
readAt: receipt.read_at
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
function compareUnsignedDecimals(left, right) {
|
|
2169
|
+
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
2170
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/sync/v1-persistence.ts
|
|
2174
|
+
var InMemoryV1PendingTargetStore = class {
|
|
2175
|
+
#commands = /* @__PURE__ */ new Map();
|
|
2176
|
+
constructor(commands = []) {
|
|
2177
|
+
for (const command of commands) this.#commands.set(command.commandId, structuredClone(command));
|
|
2178
|
+
}
|
|
2179
|
+
async load() {
|
|
2180
|
+
return [...this.#commands.values()].map((command) => structuredClone(command)).sort(comparePending);
|
|
2181
|
+
}
|
|
2182
|
+
async put(command) {
|
|
2183
|
+
this.#commands.set(command.commandId, structuredClone(command));
|
|
2184
|
+
}
|
|
2185
|
+
async remove(commandId) {
|
|
2186
|
+
this.#commands.delete(commandId);
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
function compareV1PendingTargets(left, right) {
|
|
2190
|
+
return comparePending(left, right);
|
|
2191
|
+
}
|
|
2192
|
+
function comparePending(left, right) {
|
|
2193
|
+
return left.createdAt - right.createdAt || left.commandId.localeCompare(right.commandId);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/sync/v1-command-scheduler.ts
|
|
2197
|
+
var MAX_IN_FLIGHT = 256;
|
|
2198
|
+
var MAX_PENDING_BYTES = 1024 * 1024;
|
|
2199
|
+
var MAX_PENDING_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
2200
|
+
var MAX_RETRIES = 3;
|
|
2201
|
+
var OPERATION_PENDING_POLL_INTERVAL_MS = 1e3;
|
|
2202
|
+
var V1CommandScheduler = class {
|
|
2203
|
+
#options;
|
|
2204
|
+
#pendingTargets = /* @__PURE__ */ new Map();
|
|
2205
|
+
#reservedCommandIds = /* @__PURE__ */ new Set();
|
|
2206
|
+
#commands = /* @__PURE__ */ new Map();
|
|
2207
|
+
#acknowledgements = /* @__PURE__ */ new Map();
|
|
2208
|
+
#pendingRemovals = /* @__PURE__ */ new Map();
|
|
2209
|
+
#commandRetryTimers = /* @__PURE__ */ new Map();
|
|
2210
|
+
#pendingRemovalRetryTimers = /* @__PURE__ */ new Map();
|
|
2211
|
+
#stopGeneration = 0;
|
|
2212
|
+
constructor(options) {
|
|
2213
|
+
this.#options = options;
|
|
2214
|
+
}
|
|
2215
|
+
get pendingCount() {
|
|
2216
|
+
return this.#pendingTargets.size;
|
|
2217
|
+
}
|
|
2218
|
+
get inFlightCount() {
|
|
2219
|
+
return this.#commands.size + this.#pendingRemovals.size + this.#reservedCommandIds.size;
|
|
2220
|
+
}
|
|
2221
|
+
get pendingCommands() {
|
|
2222
|
+
return [...this.#pendingTargets.values()].sort(compareV1PendingTargets).map((pending) => pending.command);
|
|
2223
|
+
}
|
|
2224
|
+
load() {
|
|
2225
|
+
return this.#options.store.load();
|
|
2226
|
+
}
|
|
2227
|
+
reconcileStored(stored) {
|
|
2228
|
+
const storedCommandIds = new Set(stored.map((pending) => pending.commandId));
|
|
2229
|
+
for (const commandId of this.#pendingTargets.keys()) {
|
|
2230
|
+
if (storedCommandIds.has(commandId) || this.#commands.has(commandId)) continue;
|
|
2231
|
+
this.#pendingTargets.delete(commandId);
|
|
2232
|
+
}
|
|
2233
|
+
for (const commandId of this.#pendingRemovals.keys()) {
|
|
2234
|
+
if (storedCommandIds.has(commandId)) continue;
|
|
2235
|
+
this.#pendingRemovals.delete(commandId);
|
|
2236
|
+
this.#clearPendingRemovalRetryTimer(commandId);
|
|
2237
|
+
}
|
|
2238
|
+
for (const pending of [...stored].sort(compareV1PendingTargets)) {
|
|
2239
|
+
if (this.#pendingRemovals.has(pending.commandId)) {
|
|
2240
|
+
void this.#removePersistedTarget(pending.commandId, pending);
|
|
2241
|
+
} else if (this.#commands.has(pending.commandId)) {
|
|
2242
|
+
this.#pendingTargets.set(pending.commandId, pending);
|
|
2243
|
+
} else {
|
|
2244
|
+
this.#pendingTargets.delete(pending.commandId);
|
|
2245
|
+
this.#restorePending(pending);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
stop(code) {
|
|
2250
|
+
this.#stopGeneration += 1;
|
|
2251
|
+
this.#clearCommandRetryTimers();
|
|
2252
|
+
this.#clearPendingRemovalRetryTimers();
|
|
2253
|
+
for (const deferred of this.#commands.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2254
|
+
this.#commands.clear();
|
|
2255
|
+
this.#acknowledgements.clear();
|
|
2256
|
+
}
|
|
2257
|
+
disconnect() {
|
|
2258
|
+
this.#clearCommandRetryTimers();
|
|
2259
|
+
}
|
|
2260
|
+
assertCapacity(externalInFlight) {
|
|
2261
|
+
if (this.inFlightCount + externalInFlight >= this.#maxPending()) throw new V1SyncError("in-flight request capacity exceeded", "capacity");
|
|
2262
|
+
}
|
|
2263
|
+
async sendTarget(command, externalInFlight, options) {
|
|
2264
|
+
this.assertCapacity(externalInFlight);
|
|
2265
|
+
const commandId = options?.commandId ?? this.#nextCommandId();
|
|
2266
|
+
this.#assertCommandIdAvailable(commandId);
|
|
2267
|
+
const frame = targetFrame(commandId, command);
|
|
2268
|
+
encodeV1ClientFrame(frame);
|
|
2269
|
+
const pending = { commandId, command, createdAt: this.#now(), bytes: new TextEncoder().encode(JSON.stringify(frame)).byteLength };
|
|
2270
|
+
if (this.#pendingBytes() + pending.bytes > (this.#options.maxPendingBytes ?? MAX_PENDING_BYTES)) throw new V1SyncError("pending target byte capacity exceeded", "capacity");
|
|
2271
|
+
this.#reservedCommandIds.add(commandId);
|
|
2272
|
+
const stopGeneration = this.#stopGeneration;
|
|
2273
|
+
try {
|
|
2274
|
+
await this.#options.store.put(pending);
|
|
2275
|
+
if (stopGeneration !== this.#stopGeneration) {
|
|
2276
|
+
this.#pendingTargets.set(commandId, pending);
|
|
2277
|
+
this.#commands.set(commandId, { resolve: () => void 0, reject: () => void 0, settled: true, frame, retries: 0, durableTarget: true, createdAt: pending.createdAt });
|
|
2278
|
+
this.#options.sendIfLive(frame);
|
|
2279
|
+
this.#options.stateChanged();
|
|
2280
|
+
throw new V1SyncError("client_stopped", "client_stopped");
|
|
2281
|
+
}
|
|
2282
|
+
this.#pendingTargets.set(commandId, pending);
|
|
2283
|
+
const promise = this.#registerCommand(commandId, frame, true, pending.createdAt);
|
|
2284
|
+
this.#options.sendIfLive(frame);
|
|
2285
|
+
this.#options.stateChanged();
|
|
2286
|
+
return promise;
|
|
2287
|
+
} finally {
|
|
2288
|
+
this.#reservedCommandIds.delete(commandId);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
sendOperation(createFrame, externalInFlight, options) {
|
|
2292
|
+
this.assertCapacity(externalInFlight);
|
|
2293
|
+
const commandId = options?.commandId ?? this.#nextCommandId();
|
|
2294
|
+
this.#assertCommandIdAvailable(commandId);
|
|
2295
|
+
const frame = createFrame(commandId);
|
|
2296
|
+
encodeV1ClientFrame(frame);
|
|
2297
|
+
const promise = this.#registerCommand(commandId, frame, false, this.#now());
|
|
2298
|
+
this.#options.sendIfLive(frame);
|
|
2299
|
+
return promise;
|
|
2300
|
+
}
|
|
2301
|
+
async acknowledge(frame) {
|
|
2302
|
+
const deferred = this.#commands.get(frame.command_id);
|
|
2303
|
+
if (!deferred) return;
|
|
2304
|
+
if (frame.outcome === "rejected" || frame.outcome === "command_id_conflict") {
|
|
2305
|
+
await this.#finishCommand(frame.command_id, frame);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
if (isTerminalLifecycleOperation(deferred.frame)) {
|
|
2309
|
+
await this.#finishCommand(frame.command_id, frame);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
if (!this.#options.ackHeadIsProven(frame)) {
|
|
2313
|
+
const previous = this.#acknowledgements.get(frame.command_id);
|
|
2314
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(frame)) throw new V1ReplicaError("conflicting duplicate command ACK");
|
|
2315
|
+
this.#acknowledgements.set(frame.command_id, frame);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
await this.#finishCommand(frame.command_id, frame);
|
|
2319
|
+
}
|
|
2320
|
+
async settleProven() {
|
|
2321
|
+
for (const commandId of this.#commands.keys()) {
|
|
2322
|
+
const ack = this.#acknowledgements.get(commandId);
|
|
2323
|
+
if (ack && this.#options.ackHeadIsProven(ack)) await this.#finishCommand(commandId, ack);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
async settleTerminalLifecycle(frame, participantId) {
|
|
2327
|
+
const operationName = frame.name === "episode_ended" ? "end_episode" : frame.name === "participant_left" && frame.payload.participant_id === participantId ? "participant_leave" : null;
|
|
2328
|
+
if (!operationName) return;
|
|
2329
|
+
for (const [commandId, deferred] of this.#commands) {
|
|
2330
|
+
if (deferred.frame.type !== "operation" || deferred.frame.name !== operationName) continue;
|
|
2331
|
+
await this.#finishCommand(commandId, {
|
|
2332
|
+
type: "ack",
|
|
2333
|
+
command_id: commandId,
|
|
2334
|
+
delivery: "duplicate",
|
|
2335
|
+
outcome: "satisfied",
|
|
2336
|
+
revision: frame.revision,
|
|
2337
|
+
state_digest: frame.resulting_state_digest
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
async settleTerminalRecovery(frame) {
|
|
2342
|
+
const satisfiedOperations = /* @__PURE__ */ new Set();
|
|
2343
|
+
if (frame.reason === "episode_ended") {
|
|
2344
|
+
satisfiedOperations.add("end_episode");
|
|
2345
|
+
satisfiedOperations.add("participant_leave");
|
|
2346
|
+
} else if (frame.reason === "participant_inactive") {
|
|
2347
|
+
satisfiedOperations.add("participant_leave");
|
|
2348
|
+
}
|
|
2349
|
+
for (const [commandId, deferred] of this.#commands) {
|
|
2350
|
+
if (deferred.frame.type !== "operation" || !satisfiedOperations.has(deferred.frame.name)) continue;
|
|
2351
|
+
await this.#finishCommand(commandId, {
|
|
2352
|
+
type: "ack",
|
|
2353
|
+
command_id: commandId,
|
|
2354
|
+
delivery: "duplicate",
|
|
2355
|
+
outcome: "satisfied",
|
|
2356
|
+
revision: frame.head.revision,
|
|
2357
|
+
state_digest: frame.head.state_digest
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
retry(commandId, code) {
|
|
2362
|
+
const deferred = this.#commands.get(commandId);
|
|
2363
|
+
if (!deferred || this.#commandRetryTimers.has(commandId)) return;
|
|
2364
|
+
if (code === "external_operation_pending") {
|
|
2365
|
+
this.#pollPendingOperation(commandId, deferred);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (deferred.retries >= MAX_RETRIES) {
|
|
2369
|
+
rejectV1Deferred(deferred, new V1SyncError(code, "retry_exhausted"));
|
|
2370
|
+
if (!deferred.durableTarget) this.#commands.delete(commandId);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
deferred.retries += 1;
|
|
2374
|
+
const timer = this.#options.clock().setTimeout(() => {
|
|
2375
|
+
this.#commandRetryTimers.delete(commandId);
|
|
2376
|
+
if (this.#commands.get(commandId) === deferred) this.#options.sendIfLive(deferred.frame);
|
|
2377
|
+
}, this.#options.retryDelayMs ?? 100);
|
|
2378
|
+
this.#commandRetryTimers.set(commandId, timer);
|
|
2379
|
+
}
|
|
2380
|
+
enterLive() {
|
|
2381
|
+
for (const pending of this.#commands.values()) {
|
|
2382
|
+
pending.retries = 0;
|
|
2383
|
+
this.#options.sendIfLive(pending.frame);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
#registerCommand(commandId, frame, durableTarget, createdAt) {
|
|
2387
|
+
if (this.#commands.has(commandId)) throw new V1SyncError("command ID is already pending", "command_id_conflict");
|
|
2388
|
+
return new Promise((resolve, reject) => this.#commands.set(commandId, { resolve, reject, settled: false, frame, retries: 0, durableTarget, createdAt }));
|
|
2389
|
+
}
|
|
2390
|
+
#pollPendingOperation(commandId, deferred) {
|
|
2391
|
+
const remainingAge = (this.#options.maxOperationPendingAgeMs ?? MAX_PENDING_AGE_MS) - (this.#now() - deferred.createdAt);
|
|
2392
|
+
if (remainingAge <= 0) {
|
|
2393
|
+
this.#expirePendingOperation(commandId, deferred);
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const timer = this.#options.clock().setTimeout(
|
|
2397
|
+
() => {
|
|
2398
|
+
this.#commandRetryTimers.delete(commandId);
|
|
2399
|
+
if (this.#commands.get(commandId) !== deferred) return;
|
|
2400
|
+
if (this.#now() - deferred.createdAt >= (this.#options.maxOperationPendingAgeMs ?? MAX_PENDING_AGE_MS)) {
|
|
2401
|
+
this.#expirePendingOperation(commandId, deferred);
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
this.#options.sendIfLive(deferred.frame);
|
|
2405
|
+
},
|
|
2406
|
+
Math.min(OPERATION_PENDING_POLL_INTERVAL_MS, remainingAge)
|
|
2407
|
+
);
|
|
2408
|
+
this.#commandRetryTimers.set(commandId, timer);
|
|
2409
|
+
}
|
|
2410
|
+
#expirePendingOperation(commandId, deferred) {
|
|
2411
|
+
if (this.#commands.get(commandId) !== deferred) return;
|
|
2412
|
+
this.#commands.delete(commandId);
|
|
2413
|
+
this.#acknowledgements.delete(commandId);
|
|
2414
|
+
this.#clearCommandRetryTimer(commandId);
|
|
2415
|
+
let pending;
|
|
2416
|
+
if (deferred.durableTarget) {
|
|
2417
|
+
pending = this.#pendingTargets.get(commandId);
|
|
2418
|
+
this.#pendingTargets.delete(commandId);
|
|
2419
|
+
if (pending) this.#pendingRemovals.set(commandId, pending);
|
|
2420
|
+
}
|
|
2421
|
+
rejectV1Deferred(deferred, new V1SyncError("external operation remained pending beyond its maximum age", "operation_pending_timeout"));
|
|
2422
|
+
this.#options.stateChanged();
|
|
2423
|
+
if (pending) void this.#removePersistedTarget(commandId, pending);
|
|
2424
|
+
}
|
|
2425
|
+
async #finishCommand(commandId, result) {
|
|
2426
|
+
const deferred = this.#commands.get(commandId);
|
|
2427
|
+
if (!deferred) return;
|
|
2428
|
+
this.#commands.delete(commandId);
|
|
2429
|
+
this.#clearCommandRetryTimer(commandId);
|
|
2430
|
+
this.#acknowledgements.delete(commandId);
|
|
2431
|
+
let pending;
|
|
2432
|
+
if (deferred.durableTarget) {
|
|
2433
|
+
pending = this.#pendingTargets.get(commandId);
|
|
2434
|
+
this.#pendingTargets.delete(commandId);
|
|
2435
|
+
if (pending) this.#pendingRemovals.set(commandId, pending);
|
|
2436
|
+
}
|
|
2437
|
+
if (result.outcome === "rejected" || result.outcome === "command_id_conflict") rejectV1Deferred(deferred, new V1SyncError(result.reason, result.outcome));
|
|
2438
|
+
else resolveV1Deferred(deferred, result);
|
|
2439
|
+
this.#options.stateChanged();
|
|
2440
|
+
if (pending) void this.#removePersistedTarget(commandId, pending);
|
|
2441
|
+
}
|
|
2442
|
+
#restorePending(pending) {
|
|
2443
|
+
if (this.#pendingTargets.size + this.#pendingRemovals.size >= this.#maxPending() || this.#now() - pending.createdAt > (this.#options.maxPendingAgeMs ?? MAX_PENDING_AGE_MS) || this.#pendingBytes() + pending.bytes > (this.#options.maxPendingBytes ?? MAX_PENDING_BYTES)) {
|
|
2444
|
+
void this.#removePersistedTarget(pending.commandId, pending);
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const frame = targetFrame(pending.commandId, pending.command);
|
|
2448
|
+
encodeV1ClientFrame(frame);
|
|
2449
|
+
this.#pendingTargets.set(pending.commandId, pending);
|
|
2450
|
+
this.#commands.set(pending.commandId, { resolve: () => void 0, reject: () => void 0, settled: true, frame, retries: 0, durableTarget: true, createdAt: pending.createdAt });
|
|
2451
|
+
}
|
|
2452
|
+
async #removePersistedTarget(commandId, pending) {
|
|
2453
|
+
this.#pendingRemovals.set(commandId, pending);
|
|
2454
|
+
try {
|
|
2455
|
+
await this.#options.store.remove(commandId);
|
|
2456
|
+
if (this.#pendingRemovals.get(commandId) !== pending) return;
|
|
2457
|
+
this.#pendingRemovals.delete(commandId);
|
|
2458
|
+
this.#clearPendingRemovalRetryTimer(commandId);
|
|
2459
|
+
} catch {
|
|
2460
|
+
if (this.#pendingRemovals.get(commandId) !== pending) return;
|
|
2461
|
+
this.#schedulePendingRemovalRetry(commandId, pending);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
#schedulePendingRemovalRetry(commandId, pending) {
|
|
2465
|
+
if (!this.#options.isStarted() || this.#pendingRemovalRetryTimers.has(commandId)) return;
|
|
2466
|
+
const timer = this.#options.clock().setTimeout(() => {
|
|
2467
|
+
this.#pendingRemovalRetryTimers.delete(commandId);
|
|
2468
|
+
void this.#removePersistedTarget(commandId, pending);
|
|
2469
|
+
}, this.#options.retryDelayMs ?? 100);
|
|
2470
|
+
this.#pendingRemovalRetryTimers.set(commandId, timer);
|
|
2471
|
+
}
|
|
2472
|
+
#assertCommandIdAvailable(commandId) {
|
|
2473
|
+
if (this.#commands.has(commandId) || this.#pendingTargets.has(commandId) || this.#pendingRemovals.has(commandId) || this.#reservedCommandIds.has(commandId)) throw new V1SyncError("command ID is already pending", "command_id_conflict");
|
|
2474
|
+
}
|
|
2475
|
+
#pendingBytes() {
|
|
2476
|
+
return [...this.#pendingTargets.values(), ...this.#pendingRemovals.values()].reduce((total, pending) => total + pending.bytes, 0);
|
|
2477
|
+
}
|
|
2478
|
+
#maxPending() {
|
|
2479
|
+
return Math.min(this.#options.maxPendingCommands ?? MAX_IN_FLIGHT, MAX_IN_FLIGHT);
|
|
2480
|
+
}
|
|
2481
|
+
#nextCommandId() {
|
|
2482
|
+
return this.#options.ids?.next() ?? crypto.randomUUID();
|
|
2483
|
+
}
|
|
2484
|
+
#now() {
|
|
2485
|
+
return this.#options.clock().now();
|
|
2486
|
+
}
|
|
2487
|
+
#clearCommandRetryTimers() {
|
|
2488
|
+
for (const timer of this.#commandRetryTimers.values()) this.#options.clock().clearTimeout(timer);
|
|
2489
|
+
this.#commandRetryTimers.clear();
|
|
2490
|
+
}
|
|
2491
|
+
#clearPendingRemovalRetryTimers() {
|
|
2492
|
+
for (const timer of this.#pendingRemovalRetryTimers.values()) this.#options.clock().clearTimeout(timer);
|
|
2493
|
+
this.#pendingRemovalRetryTimers.clear();
|
|
2494
|
+
}
|
|
2495
|
+
#clearPendingRemovalRetryTimer(commandId) {
|
|
2496
|
+
const timer = this.#pendingRemovalRetryTimers.get(commandId);
|
|
2497
|
+
if (timer === void 0) return;
|
|
2498
|
+
this.#options.clock().clearTimeout(timer);
|
|
2499
|
+
this.#pendingRemovalRetryTimers.delete(commandId);
|
|
2500
|
+
}
|
|
2501
|
+
#clearCommandRetryTimer(commandId) {
|
|
2502
|
+
const timer = this.#commandRetryTimers.get(commandId);
|
|
2503
|
+
if (timer === void 0) return;
|
|
2504
|
+
this.#options.clock().clearTimeout(timer);
|
|
2505
|
+
this.#commandRetryTimers.delete(commandId);
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
function targetFrame(commandId, command) {
|
|
2509
|
+
switch (command.name) {
|
|
2510
|
+
case "set_hand_raised":
|
|
2511
|
+
return { type: "command", command_id: commandId, name: command.name, payload: command.payload };
|
|
2512
|
+
case "set_display_name":
|
|
2513
|
+
return { type: "command", command_id: commandId, name: command.name, payload: command.payload };
|
|
2514
|
+
case "set_admission_policy":
|
|
2515
|
+
return { type: "command", command_id: commandId, name: command.name, payload: command.payload };
|
|
2516
|
+
case "assign_roles":
|
|
2517
|
+
return { type: "command", command_id: commandId, name: command.name, payload: command.payload };
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
function isTerminalLifecycleOperation(frame) {
|
|
2521
|
+
return frame.type === "operation" && (frame.name === "participant_leave" || frame.name === "end_episode");
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// src/sync/v1-frame-signature.ts
|
|
2525
|
+
function frameSignature(frame) {
|
|
2526
|
+
return new TextDecoder().decode(canonicalJsonBytesFromUnknown(frame));
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/sync/v1-live-target-coordinator.ts
|
|
2530
|
+
var MAX_RETRIES2 = 3;
|
|
2531
|
+
var MAX_LIVE_SERVER_RETRIES = 16;
|
|
2532
|
+
var LIVE_TARGET_RETRY_BUDGET_MS = 15e3;
|
|
2533
|
+
var LOCAL_MEDIA_TIMEOUT_MS = 45e3;
|
|
2534
|
+
var MAX_LIVE_TARGET_RETRY_DELAY_MS = 1e3;
|
|
2535
|
+
var V1LiveTargetCoordinator = class {
|
|
2536
|
+
#options;
|
|
2537
|
+
#targets = /* @__PURE__ */ new Map();
|
|
2538
|
+
#retryTimers = /* @__PURE__ */ new Map();
|
|
2539
|
+
#deadlineTimers = /* @__PURE__ */ new Map();
|
|
2540
|
+
#localMedia = { microphone: "unknown", camera: "unknown", screen: "unknown" };
|
|
2541
|
+
constructor(options) {
|
|
2542
|
+
this.#options = options;
|
|
2543
|
+
}
|
|
2544
|
+
get pendingCount() {
|
|
2545
|
+
return this.#targets.size;
|
|
2546
|
+
}
|
|
2547
|
+
has(requestId) {
|
|
2548
|
+
return this.#targets.has(requestId);
|
|
2549
|
+
}
|
|
2550
|
+
getLocalMedia() {
|
|
2551
|
+
return { ...this.#localMedia };
|
|
2552
|
+
}
|
|
2553
|
+
updateFromPublications(publications) {
|
|
2554
|
+
const participantId = this.#options.participantId();
|
|
2555
|
+
if (!participantId) return;
|
|
2556
|
+
for (const source of ["microphone", "camera", "screen"]) {
|
|
2557
|
+
const publication = publications.find((candidate) => candidate.source === source && candidate.participantId === participantId);
|
|
2558
|
+
this.#localMedia[source] = publication?.enabled ? "enabled" : "disabled";
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
resetLocalMedia() {
|
|
2562
|
+
for (const source of ["microphone", "camera", "screen"]) this.#localMedia[source] = "unknown";
|
|
2563
|
+
}
|
|
2564
|
+
send(name, source, enabled, options) {
|
|
2565
|
+
this.#options.assertCapacity();
|
|
2566
|
+
const participantId = this.#options.participantId();
|
|
2567
|
+
if (!this.#options.isLive() || !participantId) return Promise.reject(new V1SyncError("self-media targets require a live participant", "not_live"));
|
|
2568
|
+
if (!this.#options.mediaPlane) return Promise.reject(new V1SyncError("a client MediaPlane adapter is required for self-media targets", "media_plane_unavailable"));
|
|
2569
|
+
const operationId = options?.requestId ?? this.#nextRequestId();
|
|
2570
|
+
if (this.#targets.has(operationId) || this.#options.requestIdInUse(operationId)) throw new V1SyncError("request ID is already pending", "request_id_conflict");
|
|
2571
|
+
const frame = { type: "live_target", operation_id: operationId, name, enabled };
|
|
2572
|
+
encodeV1ClientFrame(frame);
|
|
2573
|
+
const { deferred, promise } = createLiveDeferred(frame, source, enabled, this.#now());
|
|
2574
|
+
this.#targets.set(operationId, deferred);
|
|
2575
|
+
const deadlineTimer = this.#options.clock().setTimeout(() => {
|
|
2576
|
+
this.#deadlineTimers.delete(operationId);
|
|
2577
|
+
if (this.#targets.get(operationId) === deferred) this.#fail(operationId, deferred, new V1SyncError("self-media target confirmation timed out", "retry_exhausted"));
|
|
2578
|
+
}, LIVE_TARGET_RETRY_BUDGET_MS);
|
|
2579
|
+
this.#deadlineTimers.set(operationId, deadlineTimer);
|
|
2580
|
+
this.#localMedia[source] = "requesting";
|
|
2581
|
+
this.#options.sendIfLive(frame);
|
|
2582
|
+
this.#options.stateChanged();
|
|
2583
|
+
return promise;
|
|
2584
|
+
}
|
|
2585
|
+
receive(frame) {
|
|
2586
|
+
this.#requireLive();
|
|
2587
|
+
const deferred = this.#matchingTarget(frame);
|
|
2588
|
+
if (!deferred || this.#acceptDuplicateServerResult(deferred, frame)) return;
|
|
2589
|
+
this.#applyServerResult(deferred, frame);
|
|
2590
|
+
}
|
|
2591
|
+
#matchingTarget(frame) {
|
|
2592
|
+
const deferred = this.#targets.get(frame.operation_id);
|
|
2593
|
+
return deferred?.frame.name === frame.name ? deferred : void 0;
|
|
2594
|
+
}
|
|
2595
|
+
#acceptDuplicateServerResult(deferred, frame) {
|
|
2596
|
+
if (!deferred.serverResultSignature) return false;
|
|
2597
|
+
if (deferred.serverResultSignature !== frameSignature(frame)) throw new V1ReplicaError("conflicting duplicate live-target result");
|
|
2598
|
+
return true;
|
|
2599
|
+
}
|
|
2600
|
+
#applyServerResult(deferred, frame) {
|
|
2601
|
+
if (frame.outcome === "retryable_failure") {
|
|
2602
|
+
this.#retryServer(frame.operation_id, deferred, frame.error_code ?? frame.outcome);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (!isSuccessfulLiveTargetResult(frame)) {
|
|
2606
|
+
this.#fail(frame.operation_id, deferred, new V1SyncError(frame.error_code ?? frame.outcome, frame.outcome));
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
deferred.serverResult = frame;
|
|
2610
|
+
deferred.serverResultSignature = frameSignature(frame);
|
|
2611
|
+
this.#clearRetryTimer(frame.operation_id);
|
|
2612
|
+
this.#clearDeadlineTimer(frame.operation_id);
|
|
2613
|
+
const localDeadline = this.#options.clock().setTimeout(() => {
|
|
2614
|
+
this.#deadlineTimers.delete(frame.operation_id);
|
|
2615
|
+
if (this.#targets.get(frame.operation_id) === deferred) this.#fail(frame.operation_id, deferred, new V1SyncError("Media did not respond in time. Try again.", "media_timeout"));
|
|
2616
|
+
}, LOCAL_MEDIA_TIMEOUT_MS);
|
|
2617
|
+
this.#deadlineTimers.set(frame.operation_id, localDeadline);
|
|
2618
|
+
this.#executeLocal(frame.operation_id, deferred);
|
|
2619
|
+
}
|
|
2620
|
+
enterLive() {
|
|
2621
|
+
for (const pending of this.#targets.values()) {
|
|
2622
|
+
pending.serverRetries = 0;
|
|
2623
|
+
if (pending.serverResult) this.#executeLocal(pending.frame.operation_id, pending);
|
|
2624
|
+
else this.#options.sendIfLive(pending.frame);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
disconnect(code) {
|
|
2628
|
+
this.#clearTimers();
|
|
2629
|
+
for (const deferred of this.#targets.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2630
|
+
this.#targets.clear();
|
|
2631
|
+
}
|
|
2632
|
+
#retryServer(operationId, deferred, errorCode) {
|
|
2633
|
+
if (this.#retryTimers.has(operationId)) return;
|
|
2634
|
+
const remainingBudget = LIVE_TARGET_RETRY_BUDGET_MS - (this.#now() - deferred.createdAt);
|
|
2635
|
+
if (deferred.serverRetries >= MAX_LIVE_SERVER_RETRIES || remainingBudget <= 0) {
|
|
2636
|
+
this.#fail(operationId, deferred, new V1SyncError(errorCode, "retry_exhausted"));
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
deferred.serverRetries += 1;
|
|
2640
|
+
const baseDelay = this.#options.retryDelayMs ?? 100;
|
|
2641
|
+
const delay = Math.min(remainingBudget, MAX_LIVE_TARGET_RETRY_DELAY_MS, baseDelay * 2 ** Math.min(deferred.serverRetries - 1, 4));
|
|
2642
|
+
this.#scheduleRetry(operationId, () => this.#options.sendIfLive(deferred.frame), delay);
|
|
2643
|
+
}
|
|
2644
|
+
#executeLocal(operationId, deferred) {
|
|
2645
|
+
const mediaPlane = this.#options.mediaPlane;
|
|
2646
|
+
const participantId = this.#options.participantId();
|
|
2647
|
+
if (!mediaPlane || !participantId || deferred.localInFlight || this.#targets.get(operationId) !== deferred) return;
|
|
2648
|
+
deferred.localInFlight = true;
|
|
2649
|
+
let result;
|
|
2650
|
+
try {
|
|
2651
|
+
result = mediaPlane.setLocalPublicationTarget({ operationId, participantId, source: deferred.source, enabled: deferred.enabled });
|
|
2652
|
+
} catch {
|
|
2653
|
+
this.#localResult(operationId, deferred, { outcome: "ambiguous", errorCode: "media_plane_exception" });
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
void result.then((outcome) => this.#localResult(operationId, deferred, normalizeMediaPlaneResult(outcome))).catch(() => this.#localResult(operationId, deferred, { outcome: "ambiguous", errorCode: "media_plane_exception" }));
|
|
2657
|
+
}
|
|
2658
|
+
#localResult(operationId, deferred, result) {
|
|
2659
|
+
if (this.#targets.get(operationId) !== deferred) return;
|
|
2660
|
+
deferred.localInFlight = false;
|
|
2661
|
+
if (result.outcome === "retryable_failure") {
|
|
2662
|
+
this.#retryLocal(operationId, deferred, result);
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
this.#settleLocal(operationId, deferred, result);
|
|
2666
|
+
}
|
|
2667
|
+
#retryLocal(operationId, deferred, result) {
|
|
2668
|
+
if (deferred.localRetries >= MAX_RETRIES2) {
|
|
2669
|
+
this.#fail(operationId, deferred, new V1SyncError(result.errorCode ?? result.outcome, "retry_exhausted"));
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
deferred.localRetries += 1;
|
|
2673
|
+
this.#scheduleRetry(operationId, () => this.#executeLocal(operationId, deferred));
|
|
2674
|
+
}
|
|
2675
|
+
#settleLocal(operationId, deferred, result) {
|
|
2676
|
+
if (!isSuccessfulMediaPlaneResult(result)) {
|
|
2677
|
+
if (result.errorCode === "signaling_timeout" || result.errorCode === "negotiation_timeout") {
|
|
2678
|
+
this.#fail(operationId, deferred, new V1SyncError("Media did not respond in time. Try again.", result.errorCode));
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
this.#fail(operationId, deferred, new V1SyncError(result.errorCode ?? result.outcome, result.outcome));
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const serverResult = deferred.serverResult;
|
|
2685
|
+
if (!serverResult) throw new V1ReplicaError("local MediaPlane completed before server authorization");
|
|
2686
|
+
this.#targets.delete(operationId);
|
|
2687
|
+
this.#clearRetryTimer(operationId);
|
|
2688
|
+
this.#clearDeadlineTimer(operationId);
|
|
2689
|
+
this.#localMedia[deferred.source] = deferred.enabled ? "enabled" : "disabled";
|
|
2690
|
+
resolveV1Deferred(deferred, { operationId, name: deferred.frame.name, serverOutcome: serverResult.outcome, mediaPlaneOutcome: result.outcome });
|
|
2691
|
+
this.#options.stateChanged();
|
|
2692
|
+
}
|
|
2693
|
+
#scheduleRetry(operationId, retry, delay = this.#options.retryDelayMs ?? 100) {
|
|
2694
|
+
const timer = this.#options.clock().setTimeout(() => {
|
|
2695
|
+
this.#retryTimers.delete(operationId);
|
|
2696
|
+
retry();
|
|
2697
|
+
}, delay);
|
|
2698
|
+
this.#retryTimers.set(operationId, timer);
|
|
2699
|
+
}
|
|
2700
|
+
#fail(operationId, deferred, error) {
|
|
2701
|
+
this.#targets.delete(operationId);
|
|
2702
|
+
this.#clearRetryTimer(operationId);
|
|
2703
|
+
this.#clearDeadlineTimer(operationId);
|
|
2704
|
+
this.#localMedia[deferred.source] = "failed";
|
|
2705
|
+
rejectV1Deferred(deferred, error);
|
|
2706
|
+
this.#options.stateChanged();
|
|
2707
|
+
}
|
|
2708
|
+
#nextRequestId() {
|
|
2709
|
+
return this.#options.requestIds?.next() ?? crypto.randomUUID();
|
|
2710
|
+
}
|
|
2711
|
+
#now() {
|
|
2712
|
+
return this.#options.clock().now();
|
|
2713
|
+
}
|
|
2714
|
+
#requireLive() {
|
|
2715
|
+
if (!this.#options.isLive()) throw new V1ReplicaError("live frame arrived before four-stream recovery completed");
|
|
2716
|
+
}
|
|
2717
|
+
#clearTimers() {
|
|
2718
|
+
for (const timer of this.#retryTimers.values()) this.#options.clock().clearTimeout(timer);
|
|
2719
|
+
for (const timer of this.#deadlineTimers.values()) this.#options.clock().clearTimeout(timer);
|
|
2720
|
+
this.#retryTimers.clear();
|
|
2721
|
+
this.#deadlineTimers.clear();
|
|
2722
|
+
}
|
|
2723
|
+
#clearRetryTimer(operationId) {
|
|
2724
|
+
const timer = this.#retryTimers.get(operationId);
|
|
2725
|
+
if (timer === void 0) return;
|
|
2726
|
+
this.#options.clock().clearTimeout(timer);
|
|
2727
|
+
this.#retryTimers.delete(operationId);
|
|
2728
|
+
}
|
|
2729
|
+
#clearDeadlineTimer(operationId) {
|
|
2730
|
+
const timer = this.#deadlineTimers.get(operationId);
|
|
2731
|
+
if (timer === void 0) return;
|
|
2732
|
+
this.#options.clock().clearTimeout(timer);
|
|
2733
|
+
this.#deadlineTimers.delete(operationId);
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
function validMediaPlaneResult(result) {
|
|
2737
|
+
return (result.outcome === "confirmed" || result.outcome === "satisfied" || result.outcome === "retryable_failure" || result.outcome === "terminal_failure" || result.outcome === "ambiguous") && (result.errorCode === null || typeof result.errorCode === "string");
|
|
2738
|
+
}
|
|
2739
|
+
function normalizeMediaPlaneResult(result) {
|
|
2740
|
+
return validMediaPlaneResult(result) ? result : { outcome: "ambiguous", errorCode: "invalid_media_plane_result" };
|
|
2741
|
+
}
|
|
2742
|
+
function isSuccessfulLiveTargetResult(result) {
|
|
2743
|
+
return result.outcome === "confirmed" || result.outcome === "satisfied";
|
|
2744
|
+
}
|
|
2745
|
+
function isSuccessfulMediaPlaneResult(result) {
|
|
2746
|
+
return result.outcome === "confirmed" || result.outcome === "satisfied";
|
|
2747
|
+
}
|
|
2748
|
+
function createLiveDeferred(frame, source, enabled, createdAt) {
|
|
2749
|
+
let resolvePromise = (_value) => void 0;
|
|
2750
|
+
let rejectPromise = (_error) => void 0;
|
|
2751
|
+
const promise = new Promise((resolve, reject) => {
|
|
2752
|
+
resolvePromise = resolve;
|
|
2753
|
+
rejectPromise = reject;
|
|
2754
|
+
});
|
|
2755
|
+
return {
|
|
2756
|
+
deferred: {
|
|
2757
|
+
resolve: resolvePromise,
|
|
2758
|
+
reject: rejectPromise,
|
|
2759
|
+
settled: false,
|
|
2760
|
+
frame,
|
|
2761
|
+
createdAt,
|
|
2762
|
+
serverRetries: 0,
|
|
2763
|
+
localRetries: 0,
|
|
2764
|
+
localInFlight: false,
|
|
2765
|
+
source,
|
|
2766
|
+
enabled
|
|
2767
|
+
},
|
|
2768
|
+
promise
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/sync/v1-client.ts
|
|
2773
|
+
var encoder3 = new TextEncoder();
|
|
2774
|
+
var MAX_REPLAY_EVENTS = SyncProtocolLimits.completeReplayMaxEvents;
|
|
2775
|
+
var MAX_REPLAY_BYTES = SyncProtocolLimits.completeReplayEncodedBytes;
|
|
2776
|
+
var MAX_PROJECTION_EVENT_EVIDENCE = 256;
|
|
2777
|
+
var CLIENT_RESTART_CLOSE_CODE = 4e3;
|
|
2778
|
+
var DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
2779
|
+
var MAX_RECONNECT_DELAY_MS = 5e3;
|
|
2780
|
+
var FAST_RECONNECT_ATTEMPTS = 8;
|
|
2781
|
+
var NOTICE_PROBE_INTERVAL_MS = 500;
|
|
2782
|
+
var NOTICE_SILENCE_MS = 750;
|
|
2783
|
+
var V1SyncClient = class {
|
|
2784
|
+
#options;
|
|
2785
|
+
#commandScheduler;
|
|
2786
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
2787
|
+
#requestListeners = /* @__PURE__ */ new Set();
|
|
2788
|
+
#collaborationState;
|
|
2789
|
+
#controlHeads = /* @__PURE__ */ new Map();
|
|
2790
|
+
#controlEvents = /* @__PURE__ */ new Map();
|
|
2791
|
+
#liveTargets;
|
|
2792
|
+
#requests = /* @__PURE__ */ new Map();
|
|
2793
|
+
#mediaEventEvidence = /* @__PURE__ */ new Map();
|
|
2794
|
+
#presenceEventEvidence = /* @__PURE__ */ new Map();
|
|
2795
|
+
#phase = { phase: "idle" };
|
|
2796
|
+
#socket = null;
|
|
2797
|
+
#control = null;
|
|
2798
|
+
#media = null;
|
|
2799
|
+
#presence = null;
|
|
2800
|
+
#participantId = null;
|
|
2801
|
+
#participantGeneration = null;
|
|
2802
|
+
#recovery = null;
|
|
2803
|
+
#started = false;
|
|
2804
|
+
#startupGeneration = 0;
|
|
2805
|
+
#connectionGeneration = 0;
|
|
2806
|
+
#reconnectTimer;
|
|
2807
|
+
#reconnectAttempt = 0;
|
|
2808
|
+
#heartbeatTimer;
|
|
2809
|
+
#missedHeartbeats = 0;
|
|
2810
|
+
#noticeTimer;
|
|
2811
|
+
#noticeProbeTimer;
|
|
2812
|
+
#noticeLastInboundAt = 0;
|
|
2813
|
+
#noticeUnresponsive = false;
|
|
2814
|
+
#unsubscribeLifecycle;
|
|
2815
|
+
#inbound = Promise.resolve();
|
|
2816
|
+
#transportAvailable = true;
|
|
2817
|
+
#online = true;
|
|
2818
|
+
#active = true;
|
|
2819
|
+
#unsubscribeLocalMedia;
|
|
2820
|
+
#unsubscribeRemoteMedia;
|
|
2821
|
+
#localPublications = [];
|
|
2822
|
+
#remotePublications = [];
|
|
2823
|
+
constructor(options) {
|
|
2824
|
+
assertV1Url(options.url);
|
|
2825
|
+
this.#options = options;
|
|
2826
|
+
this.#commandScheduler = new V1CommandScheduler({
|
|
2827
|
+
store: options.pendingStore ?? new InMemoryV1PendingTargetStore(),
|
|
2828
|
+
ids: options.ids,
|
|
2829
|
+
maxPendingCommands: options.maxPendingCommands,
|
|
2830
|
+
maxPendingBytes: options.maxPendingBytes,
|
|
2831
|
+
maxPendingAgeMs: options.maxPendingAgeMs,
|
|
2832
|
+
maxOperationPendingAgeMs: options.maxOperationPendingAgeMs,
|
|
2833
|
+
retryDelayMs: options.retryDelayMs,
|
|
2834
|
+
clock: () => this.#clock(),
|
|
2835
|
+
isStarted: () => this.#started,
|
|
2836
|
+
sendIfLive: (frame) => this.#sendIfLive(frame),
|
|
2837
|
+
stateChanged: () => this.#emit(),
|
|
2838
|
+
ackHeadIsProven: (ack) => this.#ackHeadIsProven(ack)
|
|
2839
|
+
});
|
|
2840
|
+
this.#liveTargets = new V1LiveTargetCoordinator({
|
|
2841
|
+
mediaPlane: options.mediaPlane,
|
|
2842
|
+
requestIds: options.requestIds,
|
|
2843
|
+
retryDelayMs: options.retryDelayMs,
|
|
2844
|
+
clock: () => this.#clock(),
|
|
2845
|
+
participantId: () => this.#participantId,
|
|
2846
|
+
isLive: () => this.#phase.phase === "live",
|
|
2847
|
+
requestIdInUse: (requestId) => this.#requests.has(requestId),
|
|
2848
|
+
assertCapacity: () => this.#assertCapacity(),
|
|
2849
|
+
sendIfLive: (frame) => this.#sendIfLive(frame),
|
|
2850
|
+
stateChanged: () => this.#emit()
|
|
2851
|
+
});
|
|
2852
|
+
this.#collaborationState = new V1CollaborationState({
|
|
2853
|
+
request: options.collaboration,
|
|
2854
|
+
requestIds: options.requestIds,
|
|
2855
|
+
maxPendingRequests: options.maxPendingCollaborationRequests,
|
|
2856
|
+
isLive: () => this.#phase.phase === "live",
|
|
2857
|
+
send: (frame) => this.#send(frame),
|
|
2858
|
+
stateChanged: () => this.#emit()
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
async start() {
|
|
2862
|
+
if (this.#started) return;
|
|
2863
|
+
this.#started = true;
|
|
2864
|
+
const startupGeneration = ++this.#startupGeneration;
|
|
2865
|
+
let stored;
|
|
2866
|
+
try {
|
|
2867
|
+
stored = await this.#commandScheduler.load();
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
if (startupGeneration === this.#startupGeneration) {
|
|
2870
|
+
this.#started = false;
|
|
2871
|
+
}
|
|
2872
|
+
throw error;
|
|
2873
|
+
}
|
|
2874
|
+
if (!this.#started || startupGeneration !== this.#startupGeneration) return;
|
|
2875
|
+
this.#commandScheduler.reconcileStored(stored);
|
|
2876
|
+
this.#subscribeMediaPlane(this.#options.mediaPlane);
|
|
2877
|
+
this.#unsubscribeLifecycle = this.#options.lifecycle?.subscribe((event) => this.#handleLifecycle(event));
|
|
2878
|
+
this.#connect();
|
|
2879
|
+
}
|
|
2880
|
+
stop() {
|
|
2881
|
+
this.#started = false;
|
|
2882
|
+
this.#startupGeneration += 1;
|
|
2883
|
+
this.#unsubscribeLifecycle?.();
|
|
2884
|
+
this.#unsubscribeLifecycle = void 0;
|
|
2885
|
+
this.#clearReconnect();
|
|
2886
|
+
this.#clearHeartbeat();
|
|
2887
|
+
this.#clearNoticeProbe();
|
|
2888
|
+
this.#commandScheduler.stop("client_stopped");
|
|
2889
|
+
this.#unsubscribeMediaPlane();
|
|
2890
|
+
this.#socket?.close(1e3, "client stopped");
|
|
2891
|
+
this.#socket = null;
|
|
2892
|
+
this.#phase = { phase: "stopped" };
|
|
2893
|
+
this.#rejectEphemeral("client_stopped");
|
|
2894
|
+
this.#collaborationState.disconnect("client_stopped");
|
|
2895
|
+
this.#localPublications = [];
|
|
2896
|
+
this.#remotePublications = [];
|
|
2897
|
+
this.#liveTargets.resetLocalMedia();
|
|
2898
|
+
this.#emit();
|
|
2899
|
+
}
|
|
2900
|
+
getSnapshot() {
|
|
2901
|
+
const pendingCommands = this.#commandScheduler.pendingCommands;
|
|
2902
|
+
const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
|
|
2903
|
+
return {
|
|
2904
|
+
connection: this.#phase.phase === "live" ? { ...this.#phase, noticeUnresponsive: this.#noticeUnresponsive } : { ...this.#phase },
|
|
2905
|
+
participantId: this.#participantId,
|
|
2906
|
+
participantGeneration: this.#participantGeneration,
|
|
2907
|
+
control: this.#control,
|
|
2908
|
+
optimisticControl,
|
|
2909
|
+
media: copyProjection(this.#media),
|
|
2910
|
+
presence: copyProjection(this.#presence),
|
|
2911
|
+
mediaPlane: { local: this.#localPublications.map(copyPublication), remote: this.#remotePublications.map(copyPublication) },
|
|
2912
|
+
localMedia: this.#liveTargets.getLocalMedia(),
|
|
2913
|
+
pendingCommandCount: this.#commandScheduler.pendingCount
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
subscribe(listener) {
|
|
2917
|
+
this.#listeners.add(listener);
|
|
2918
|
+
listener(this.getSnapshot());
|
|
2919
|
+
return () => this.#listeners.delete(listener);
|
|
2920
|
+
}
|
|
2921
|
+
onDirectedRequest(listener) {
|
|
2922
|
+
this.#requestListeners.add(listener);
|
|
2923
|
+
return () => this.#requestListeners.delete(listener);
|
|
2924
|
+
}
|
|
2925
|
+
getCollaborationExtensionState() {
|
|
2926
|
+
return this.#collaborationState.getState();
|
|
2927
|
+
}
|
|
2928
|
+
getParticipantCollaborationCapabilities() {
|
|
2929
|
+
return this.#collaborationState.getParticipantCapabilities();
|
|
2930
|
+
}
|
|
2931
|
+
subscribeCollaboration(listener) {
|
|
2932
|
+
return this.#collaborationState.subscribe(listener);
|
|
2933
|
+
}
|
|
2934
|
+
sendReaction(reaction) {
|
|
2935
|
+
return this.#collaborationState.sendReaction(reaction);
|
|
2936
|
+
}
|
|
2937
|
+
sendChatMessage(input) {
|
|
2938
|
+
return this.#collaborationState.sendChatMessage(input);
|
|
2939
|
+
}
|
|
2940
|
+
markChatRead(sequence) {
|
|
2941
|
+
return this.#collaborationState.markChatRead(sequence);
|
|
2942
|
+
}
|
|
2943
|
+
readChatPage(input) {
|
|
2944
|
+
return this.#collaborationState.readChatPage(input);
|
|
2945
|
+
}
|
|
2946
|
+
setHandRaised(raised, options) {
|
|
2947
|
+
return this.#sendTarget({ name: "set_hand_raised", payload: { raised } }, options);
|
|
2948
|
+
}
|
|
2949
|
+
setDisplayName(displayName, options) {
|
|
2950
|
+
return this.#sendTarget({ name: "set_display_name", payload: { display_name: displayName } }, options);
|
|
2951
|
+
}
|
|
2952
|
+
setAdmissionPolicy(policy, options) {
|
|
2953
|
+
return this.#sendTarget({ name: "set_admission_policy", payload: { policy } }, options);
|
|
2954
|
+
}
|
|
2955
|
+
assignRole(participantId, role, options) {
|
|
2956
|
+
return this.#sendTarget({ name: "assign_roles", payload: { participant_id: participantId, role } }, options);
|
|
2957
|
+
}
|
|
2958
|
+
admit(admissionRequestId, options) {
|
|
2959
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "admit_participant", payload: { admission_request_id: admissionRequestId } }), options);
|
|
2960
|
+
}
|
|
2961
|
+
deny(admissionRequestId, options) {
|
|
2962
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "deny_admission", payload: { admission_request_id: admissionRequestId } }), options);
|
|
2963
|
+
}
|
|
2964
|
+
muteParticipant(participantId, options) {
|
|
2965
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "mute_participant", payload: { participant_id: participantId } }), options);
|
|
2168
2966
|
}
|
|
2169
2967
|
stopParticipantCamera(participantId, options) {
|
|
2170
|
-
return this.#sendOperation("stop_participant_camera", { participant_id: participantId }, options);
|
|
2968
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_participant_camera", payload: { participant_id: participantId } }), options);
|
|
2171
2969
|
}
|
|
2172
2970
|
stopParticipantScreenShare(participantId, options) {
|
|
2173
|
-
return this.#sendOperation("stop_participant_screen_share", { participant_id: participantId }, options);
|
|
2971
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_participant_screen_share", payload: { participant_id: participantId } }), options);
|
|
2174
2972
|
}
|
|
2175
2973
|
removeParticipant(participantId, options) {
|
|
2176
|
-
return this.#sendOperation("remove_participant", { participant_id: participantId }, options);
|
|
2974
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "remove_participant", payload: { participant_id: participantId } }), options);
|
|
2177
2975
|
}
|
|
2178
2976
|
startRecording(options) {
|
|
2179
2977
|
const recordingId = options?.recordingId ?? this.#nextRequestId();
|
|
2180
|
-
return this.#sendOperation("start_recording", { recording_id: recordingId }, options).then((result) => ({ recordingId, result }));
|
|
2978
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "start_recording", payload: { recording_id: recordingId } }), options).then((result) => ({ recordingId, result }));
|
|
2181
2979
|
}
|
|
2182
2980
|
stopRecording(recordingId, options) {
|
|
2183
|
-
return this.#sendOperation("stop_recording", { recording_id: recordingId }, options);
|
|
2981
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_recording", payload: { recording_id: recordingId } }), options);
|
|
2184
2982
|
}
|
|
2185
2983
|
leave(options) {
|
|
2186
|
-
return this.#sendOperation("participant_leave", {}, options);
|
|
2984
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "participant_leave", payload: {} }), options);
|
|
2187
2985
|
}
|
|
2188
2986
|
endEpisode(options) {
|
|
2189
|
-
return this.#sendOperation("end_episode", {}, options);
|
|
2987
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "end_episode", payload: {} }), options);
|
|
2190
2988
|
}
|
|
2191
2989
|
extendEpisode(minutes, options) {
|
|
2192
2990
|
if (!Number.isSafeInteger(minutes) || minutes < 1) throw new V1SyncError("episode extension must be a positive whole number of minutes", "invalid_payload");
|
|
2193
|
-
return this.#sendOperation("extend_episode", { extension_seconds: minutes * 60 }, options);
|
|
2991
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "extend_episode", payload: { extension_seconds: minutes * 60 } }), options);
|
|
2194
2992
|
}
|
|
2195
2993
|
setMicrophoneEnabled(enabled, options) {
|
|
2196
2994
|
return this.#sendLiveTarget("set_microphone_enabled", "microphone", enabled, options);
|
|
@@ -2208,68 +3006,13 @@ var V1SyncClient = class {
|
|
|
2208
3006
|
return this.#sendDirectedRequest("request_start_camera", participantId, options);
|
|
2209
3007
|
}
|
|
2210
3008
|
async #sendTarget(command, options) {
|
|
2211
|
-
this.#
|
|
2212
|
-
const commandId = options?.commandId ?? this.#nextCommandId();
|
|
2213
|
-
if (this.#commands.has(commandId) || this.#pendingTargets.has(commandId) || this.#pendingRemovals.has(commandId) || this.#reservedCommandIds.has(commandId)) throw new V1SyncError("command ID is already pending", "command_id_conflict");
|
|
2214
|
-
const frame = { type: "command", command_id: commandId, name: command.name, payload: command.payload };
|
|
2215
|
-
encodeV1ClientFrame(frame);
|
|
2216
|
-
const pending = { commandId, command, createdAt: this.#now(), bytes: encoder3.encode(JSON.stringify(frame)).byteLength };
|
|
2217
|
-
if (this.#pendingBytes() + pending.bytes > (this.#options.maxPendingBytes ?? MAX_PENDING_BYTES)) throw new V1SyncError("pending target byte capacity exceeded", "capacity");
|
|
2218
|
-
this.#reservedCommandIds.add(commandId);
|
|
2219
|
-
try {
|
|
2220
|
-
await this.#store.put(pending);
|
|
2221
|
-
this.#pendingTargets.set(commandId, pending);
|
|
2222
|
-
const promise = this.#registerCommand(commandId, frame, true, pending.createdAt);
|
|
2223
|
-
this.#sendIfLive(frame);
|
|
2224
|
-
this.#emit();
|
|
2225
|
-
return promise;
|
|
2226
|
-
} finally {
|
|
2227
|
-
this.#reservedCommandIds.delete(commandId);
|
|
2228
|
-
}
|
|
3009
|
+
return this.#commandScheduler.sendTarget(command, this.#liveTargets.pendingCount + this.#requests.size, options);
|
|
2229
3010
|
}
|
|
2230
|
-
#sendOperation(
|
|
2231
|
-
this.#
|
|
2232
|
-
const commandId = options?.commandId ?? this.#nextCommandId();
|
|
2233
|
-
if (this.#commands.has(commandId) || this.#pendingTargets.has(commandId) || this.#pendingRemovals.has(commandId) || this.#reservedCommandIds.has(commandId)) throw new V1SyncError("command ID is already pending", "command_id_conflict");
|
|
2234
|
-
const frame = { type: "operation", command_id: commandId, name, payload };
|
|
2235
|
-
encodeV1ClientFrame(frame);
|
|
2236
|
-
const promise = this.#registerCommand(commandId, frame, false, this.#now());
|
|
2237
|
-
this.#sendIfLive(frame);
|
|
2238
|
-
return promise;
|
|
3011
|
+
#sendOperation(createFrame, options) {
|
|
3012
|
+
return this.#commandScheduler.sendOperation(createFrame, this.#liveTargets.pendingCount + this.#requests.size, options);
|
|
2239
3013
|
}
|
|
2240
3014
|
#sendLiveTarget(name, source, enabled, options) {
|
|
2241
|
-
this.#
|
|
2242
|
-
if (this.#phase.phase !== "live" || !this.#participantId) return Promise.reject(new V1SyncError("self-media targets require a live participant", "not_live"));
|
|
2243
|
-
if (!this.#options.mediaPlane) return Promise.reject(new V1SyncError("a client MediaPlane adapter is required for self-media targets", "media_plane_unavailable"));
|
|
2244
|
-
const operationId = options?.requestId ?? this.#nextRequestId();
|
|
2245
|
-
if (this.#liveTargets.has(operationId) || this.#requests.has(operationId)) throw new V1SyncError("request ID is already pending", "request_id_conflict");
|
|
2246
|
-
const frame = { type: "live_target", operation_id: operationId, name, enabled };
|
|
2247
|
-
encodeV1ClientFrame(frame);
|
|
2248
|
-
let deferred;
|
|
2249
|
-
const promise = new Promise((resolve, reject) => {
|
|
2250
|
-
deferred = {
|
|
2251
|
-
resolve,
|
|
2252
|
-
reject,
|
|
2253
|
-
settled: false,
|
|
2254
|
-
frame,
|
|
2255
|
-
createdAt: this.#now(),
|
|
2256
|
-
serverRetries: 0,
|
|
2257
|
-
localRetries: 0,
|
|
2258
|
-
localInFlight: false,
|
|
2259
|
-
source,
|
|
2260
|
-
enabled
|
|
2261
|
-
};
|
|
2262
|
-
this.#liveTargets.set(operationId, deferred);
|
|
2263
|
-
});
|
|
2264
|
-
const deadlineTimer = this.#clock().setTimeout(() => {
|
|
2265
|
-
this.#liveDeadlineTimers.delete(operationId);
|
|
2266
|
-
if (this.#liveTargets.get(operationId) === deferred) this.#failLiveTarget(operationId, deferred, new V1SyncError("self-media target confirmation timed out", "retry_exhausted"));
|
|
2267
|
-
}, LIVE_TARGET_RETRY_BUDGET_MS);
|
|
2268
|
-
this.#liveDeadlineTimers.set(operationId, deadlineTimer);
|
|
2269
|
-
this.#localMedia[source] = "requesting";
|
|
2270
|
-
this.#sendIfLive(frame);
|
|
2271
|
-
this.#emit();
|
|
2272
|
-
return promise;
|
|
3015
|
+
return this.#liveTargets.send(name, source, enabled, options);
|
|
2273
3016
|
}
|
|
2274
3017
|
#sendDirectedRequest(name, participantId, options) {
|
|
2275
3018
|
this.#assertCapacity();
|
|
@@ -2282,18 +3025,18 @@ var V1SyncClient = class {
|
|
|
2282
3025
|
this.#send(frame);
|
|
2283
3026
|
return promise;
|
|
2284
3027
|
}
|
|
2285
|
-
#registerCommand(commandId, frame, durableTarget, createdAt) {
|
|
2286
|
-
if (this.#commands.has(commandId)) throw new V1SyncError("command ID is already pending", "command_id_conflict");
|
|
2287
|
-
return new Promise((resolve, reject) => this.#commands.set(commandId, { resolve, reject, settled: false, frame, retries: 0, durableTarget, createdAt }));
|
|
2288
|
-
}
|
|
2289
3028
|
#connect() {
|
|
2290
3029
|
if (!this.#started || !this.#transportAvailable || this.#socket) return;
|
|
2291
3030
|
this.#phase = { phase: "connecting" };
|
|
2292
3031
|
this.#emit();
|
|
3032
|
+
recordReconnect(this.#options.recordReconnect, "sync_attempt", { attempt: this.#reconnectAttempt });
|
|
2293
3033
|
const socket = this.#options.webSocket.connect(this.#options.url);
|
|
2294
3034
|
const connectionGeneration = ++this.#connectionGeneration;
|
|
2295
3035
|
this.#socket = socket;
|
|
2296
|
-
socket.onopen = () =>
|
|
3036
|
+
socket.onopen = () => {
|
|
3037
|
+
recordReconnect(this.#options.recordReconnect, "sync_socket_open");
|
|
3038
|
+
void this.#authenticate(socket);
|
|
3039
|
+
};
|
|
2297
3040
|
socket.onmessage = (event) => {
|
|
2298
3041
|
this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
|
|
2299
3042
|
};
|
|
@@ -2322,14 +3065,11 @@ var V1SyncClient = class {
|
|
|
2322
3065
|
},
|
|
2323
3066
|
...this.#options.telemetry ? syncTelemetryCorrelation(this.#options.telemetry) : {}
|
|
2324
3067
|
};
|
|
2325
|
-
|
|
3068
|
+
const collaborationExtension = this.#collaborationState.helloExtension;
|
|
3069
|
+
if (!collaborationExtension) {
|
|
2326
3070
|
this.#send(hello);
|
|
2327
3071
|
} else {
|
|
2328
|
-
|
|
2329
|
-
after_sequence: this.#chatAfterSequence,
|
|
2330
|
-
retained_floor_sequence: this.#collaboration.retainedFloorSequence
|
|
2331
|
-
};
|
|
2332
|
-
this.#send({ ...hello, extensions: [{ name: "collaboration_v1", chat_cursor }] });
|
|
3072
|
+
this.#send({ ...hello, extensions: [collaborationExtension] });
|
|
2333
3073
|
}
|
|
2334
3074
|
this.#emit();
|
|
2335
3075
|
} catch {
|
|
@@ -2340,7 +3080,16 @@ var V1SyncClient = class {
|
|
|
2340
3080
|
if (socket !== this.#socket) return;
|
|
2341
3081
|
try {
|
|
2342
3082
|
if (typeof data !== "string" || encoder3.encode(data).byteLength > SyncProtocolLimits.snapshotEncodedBytes) throw new V1ReplicaError("invalid inbound frame size");
|
|
2343
|
-
|
|
3083
|
+
const frame = decodeV1ServerFrame(data);
|
|
3084
|
+
if (this.#phase.phase === "live") {
|
|
3085
|
+
this.#noticeLastInboundAt = this.#now();
|
|
3086
|
+
this.#scheduleNoticeCheck();
|
|
3087
|
+
if (this.#noticeUnresponsive) {
|
|
3088
|
+
this.#noticeUnresponsive = false;
|
|
3089
|
+
this.#emit();
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
await this.#handleFrame(frame);
|
|
2344
3093
|
} catch {
|
|
2345
3094
|
this.#recover("invalid_frame");
|
|
2346
3095
|
}
|
|
@@ -2371,8 +3120,8 @@ var V1SyncClient = class {
|
|
|
2371
3120
|
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
2372
3121
|
this.#rememberControlEvent(frame);
|
|
2373
3122
|
}
|
|
2374
|
-
await this.#
|
|
2375
|
-
await this.#
|
|
3123
|
+
await this.#commandScheduler.settleTerminalLifecycle(frame, this.#participantId);
|
|
3124
|
+
await this.#commandScheduler.settleProven();
|
|
2376
3125
|
{
|
|
2377
3126
|
const control = this.#requireControl();
|
|
2378
3127
|
this.#send({ type: "delivery_ack", stream: "control", revision: control.revision, state_digest: control.stateDigest });
|
|
@@ -2381,14 +3130,14 @@ var V1SyncClient = class {
|
|
|
2381
3130
|
return;
|
|
2382
3131
|
case "ack":
|
|
2383
3132
|
this.#requireLive();
|
|
2384
|
-
await this.#
|
|
3133
|
+
await this.#commandScheduler.acknowledge(frame);
|
|
2385
3134
|
return;
|
|
2386
3135
|
case "retryable_error":
|
|
2387
3136
|
this.#requireLive();
|
|
2388
|
-
this.#
|
|
3137
|
+
this.#commandScheduler.retry(frame.command_id, frame.code);
|
|
2389
3138
|
return;
|
|
2390
3139
|
case "live_target_result":
|
|
2391
|
-
this.#
|
|
3140
|
+
this.#liveTargets.receive(frame);
|
|
2392
3141
|
return;
|
|
2393
3142
|
case "directed_request_result":
|
|
2394
3143
|
this.#requestResult(frame);
|
|
@@ -2397,43 +3146,28 @@ var V1SyncClient = class {
|
|
|
2397
3146
|
this.#directedRequest(frame);
|
|
2398
3147
|
return;
|
|
2399
3148
|
case "reaction":
|
|
2400
|
-
this.#
|
|
2401
|
-
this.#requireCollaborationNegotiated();
|
|
2402
|
-
this.#emitCollaboration({ type: "reaction", reaction: reactionFromFrame(frame) });
|
|
3149
|
+
this.#collaborationState.reaction(frame);
|
|
2403
3150
|
return;
|
|
2404
3151
|
case "reaction_result":
|
|
2405
|
-
this.#reactionResult(frame);
|
|
3152
|
+
this.#collaborationState.reactionResult(frame);
|
|
2406
3153
|
return;
|
|
2407
3154
|
case "chat_message":
|
|
2408
|
-
this.#
|
|
2409
|
-
this.#observeChatMessage(frame);
|
|
3155
|
+
this.#collaborationState.chatMessage(frame);
|
|
2410
3156
|
return;
|
|
2411
3157
|
case "chat_send_result":
|
|
2412
|
-
this.#chatSendResult(frame);
|
|
3158
|
+
this.#collaborationState.chatSendResult(frame);
|
|
2413
3159
|
return;
|
|
2414
3160
|
case "chat_page":
|
|
2415
|
-
this.#
|
|
3161
|
+
this.#collaborationState.chatPage(frame);
|
|
2416
3162
|
return;
|
|
2417
3163
|
case "chat_head":
|
|
2418
|
-
this.#
|
|
2419
|
-
this.#collaboration = {
|
|
2420
|
-
...this.#collaboration,
|
|
2421
|
-
chatHeadSequence: frame.head_sequence,
|
|
2422
|
-
retainedFloorSequence: frame.retained_floor_sequence
|
|
2423
|
-
};
|
|
2424
|
-
this.#emit();
|
|
3164
|
+
this.#collaborationState.chatHead(frame);
|
|
2425
3165
|
return;
|
|
2426
3166
|
case "chat_read_receipt":
|
|
2427
|
-
this.#
|
|
2428
|
-
this.#requireCollaborationVersion(1);
|
|
2429
|
-
{
|
|
2430
|
-
const receipt = chatReadReceiptFromFrame(frame);
|
|
2431
|
-
this.#rememberChatReadReceipt(receipt);
|
|
2432
|
-
this.#emitCollaboration({ type: "chat_read_receipt", receipt });
|
|
2433
|
-
}
|
|
3167
|
+
this.#collaborationState.chatReadReceipt(frame);
|
|
2434
3168
|
return;
|
|
2435
3169
|
case "chat_read_result":
|
|
2436
|
-
this.#chatReadResult(frame);
|
|
3170
|
+
this.#collaborationState.chatReadResult(frame);
|
|
2437
3171
|
return;
|
|
2438
3172
|
case "error":
|
|
2439
3173
|
throw new V1ReplicaError(frame.code);
|
|
@@ -2445,29 +3179,12 @@ var V1SyncClient = class {
|
|
|
2445
3179
|
}
|
|
2446
3180
|
async #welcome(frame) {
|
|
2447
3181
|
if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
|
|
2448
|
-
|
|
2449
|
-
const extension = frame.extensions[0];
|
|
2450
|
-
const version = 1;
|
|
2451
|
-
if (this.#requestedCollaborationVersion !== version) throw new V1ReplicaError("collaboration welcome version does not match the requested extension");
|
|
2452
|
-
this.#collaboration = {
|
|
2453
|
-
negotiated: true,
|
|
2454
|
-
version,
|
|
2455
|
-
capabilities: [...extension.capabilities],
|
|
2456
|
-
chatHeadSequence: extension.chat_head_sequence,
|
|
2457
|
-
retainedFloorSequence: extension.retained_floor_sequence,
|
|
2458
|
-
readReceipts: "read_receipts" in extension ? extension.read_receipts.map(chatReadReceiptFromWire) : []
|
|
2459
|
-
};
|
|
2460
|
-
this.#participantCollaborationCapabilities = copyCollaborationCapabilities(extension.participant_capabilities);
|
|
2461
|
-
} else {
|
|
2462
|
-
this.#requestedCollaborationVersion = 0;
|
|
2463
|
-
this.#collaboration = { ...this.#collaboration, negotiated: false, version: null, capabilities: [], readReceipts: [] };
|
|
2464
|
-
this.#participantCollaborationCapabilities = {};
|
|
2465
|
-
}
|
|
3182
|
+
this.#collaborationState.acceptWelcome(frame);
|
|
2466
3183
|
this.#participantId = frame.participant_id;
|
|
2467
3184
|
this.#participantGeneration = frame.participant_generation;
|
|
2468
3185
|
this.#updateLocalMediaStates();
|
|
2469
3186
|
if (frame.mode === "terminal") {
|
|
2470
|
-
await this.#
|
|
3187
|
+
await this.#commandScheduler.settleTerminalRecovery(frame);
|
|
2471
3188
|
this.#phase = { phase: "terminal", terminalReason: frame.reason };
|
|
2472
3189
|
this.#socket?.close(1e3, "terminal recovery");
|
|
2473
3190
|
this.#emit();
|
|
@@ -2482,8 +3199,9 @@ var V1SyncClient = class {
|
|
|
2482
3199
|
if (frame.mode === "replay" && (!this.#control || this.#control.revision >= frame.head.revision)) throw new V1ReplicaError("invalid replay welcome");
|
|
2483
3200
|
if (frame.mode === "up_to_date" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("up-to-date head mismatch");
|
|
2484
3201
|
if (frame.mode === "snapshot" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
|
|
3202
|
+
recordReconnect(this.#options.recordReconnect, "sync_reconnected");
|
|
2485
3203
|
this.#recovery = { id: frame.recovery_id, head: frame.head, replayEvents: 0, replayBytes: 0, controlComplete: false };
|
|
2486
|
-
if (frame.mode
|
|
3204
|
+
if (frame.mode === "snapshot") this.#ackRecovery();
|
|
2487
3205
|
this.#emit();
|
|
2488
3206
|
}
|
|
2489
3207
|
async #replay(frame) {
|
|
@@ -2495,250 +3213,66 @@ var V1SyncClient = class {
|
|
|
2495
3213
|
for (const event of frame.events) {
|
|
2496
3214
|
this.#control = await applyV1Event(this.#requireControl(), event);
|
|
2497
3215
|
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
2498
|
-
this.#rememberControlEvent(event);
|
|
2499
|
-
}
|
|
2500
|
-
this.#ackRecovery();
|
|
2501
|
-
this.#emit();
|
|
2502
|
-
}
|
|
2503
|
-
#completeRecovery(frame) {
|
|
2504
|
-
const recovery = this.#requireRecovery();
|
|
2505
|
-
if (frame.recovery_id !== recovery.id || !sameHead(this.#control, frame.head) || !sameRawHead(recovery.head, frame.head)) throw new V1ReplicaError("recovery completed at the wrong head");
|
|
2506
|
-
recovery.controlComplete = true;
|
|
2507
|
-
this.#enterLiveIfReady();
|
|
2508
|
-
}
|
|
2509
|
-
#replaceProjection(frame) {
|
|
2510
|
-
if (this.#phase.phase !== "recovering" && this.#phase.phase !== "live") throw new V1ReplicaError("projection snapshot arrived in the wrong phase");
|
|
2511
|
-
if (frame.stream === "media") {
|
|
2512
|
-
this.#media = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(mediaItem) };
|
|
2513
|
-
this.#mediaEventEvidence.clear();
|
|
2514
|
-
} else {
|
|
2515
|
-
this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
|
|
2516
|
-
this.#presenceEventEvidence.clear();
|
|
2517
|
-
}
|
|
2518
|
-
this.#enterLiveIfReady();
|
|
2519
|
-
this.#emit();
|
|
2520
|
-
}
|
|
2521
|
-
#applyProjection(frame) {
|
|
2522
|
-
this.#requireLive();
|
|
2523
|
-
if (frame.stream === "media") {
|
|
2524
|
-
if (this.#acceptProjectionDuplicate(this.#media, this.#mediaEventEvidence, frame)) return;
|
|
2525
|
-
this.#media = updateProjection(this.#media, frame.projection_id, frame.sequence, mediaItem(frame.item), mediaKey);
|
|
2526
|
-
rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
2527
|
-
} else {
|
|
2528
|
-
if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
|
|
2529
|
-
this.#presence = updateProjection(this.#presence, frame.projection_id, frame.sequence, presenceItem(frame.item), (item) => item.participantId);
|
|
2530
|
-
rememberBoundedEvidence(this.#presenceEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
2531
|
-
}
|
|
2532
|
-
this.#emit();
|
|
2533
|
-
}
|
|
2534
|
-
async #ack(frame) {
|
|
2535
|
-
const deferred = this.#commands.get(frame.command_id);
|
|
2536
|
-
if (!deferred) return;
|
|
2537
|
-
if (frame.outcome === "rejected" || frame.outcome === "command_id_conflict") {
|
|
2538
|
-
await this.#finishCommand(frame.command_id, frame);
|
|
2539
|
-
return;
|
|
2540
|
-
}
|
|
2541
|
-
if (isTerminalLifecycleOperation(deferred.frame)) {
|
|
2542
|
-
await this.#finishCommand(frame.command_id, frame);
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
|
-
if (!this.#ackHeadIsProven(frame)) {
|
|
2546
|
-
const previous = this.#acknowledgements.get(frame.command_id);
|
|
2547
|
-
if (previous && JSON.stringify(previous) !== JSON.stringify(frame)) throw new V1ReplicaError("conflicting duplicate command ACK");
|
|
2548
|
-
this.#acknowledgements.set(frame.command_id, frame);
|
|
2549
|
-
return;
|
|
2550
|
-
}
|
|
2551
|
-
await this.#finishCommand(frame.command_id, frame);
|
|
2552
|
-
}
|
|
2553
|
-
async #settleProvenCommands() {
|
|
2554
|
-
for (const commandId of this.#commands.keys()) {
|
|
2555
|
-
const ack = this.#acknowledgements.get(commandId);
|
|
2556
|
-
if (ack && this.#ackHeadIsProven(ack)) await this.#finishCommand(commandId, ack);
|
|
2557
|
-
}
|
|
2558
|
-
}
|
|
2559
|
-
async #settleTerminalLifecycleCommands(frame) {
|
|
2560
|
-
const operationName = frame.name === "episode_ended" ? "end_episode" : frame.name === "participant_left" && frame.payload.participant_id === this.#participantId ? "participant_leave" : null;
|
|
2561
|
-
if (!operationName) return;
|
|
2562
|
-
for (const [commandId, deferred] of this.#commands) {
|
|
2563
|
-
if (deferred.frame.type !== "operation" || deferred.frame.name !== operationName) continue;
|
|
2564
|
-
await this.#finishCommand(commandId, {
|
|
2565
|
-
type: "ack",
|
|
2566
|
-
command_id: commandId,
|
|
2567
|
-
delivery: "duplicate",
|
|
2568
|
-
outcome: "satisfied",
|
|
2569
|
-
revision: frame.revision,
|
|
2570
|
-
state_digest: frame.resulting_state_digest
|
|
2571
|
-
});
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
async #settleTerminalRecoveryCommands(frame) {
|
|
2575
|
-
const satisfiedOperations = /* @__PURE__ */ new Set();
|
|
2576
|
-
if (frame.reason === "episode_ended") {
|
|
2577
|
-
satisfiedOperations.add("end_episode");
|
|
2578
|
-
satisfiedOperations.add("participant_leave");
|
|
2579
|
-
} else if (frame.reason === "participant_inactive") {
|
|
2580
|
-
satisfiedOperations.add("participant_leave");
|
|
2581
|
-
}
|
|
2582
|
-
for (const [commandId, deferred] of this.#commands) {
|
|
2583
|
-
if (deferred.frame.type !== "operation" || !satisfiedOperations.has(deferred.frame.name)) continue;
|
|
2584
|
-
await this.#finishCommand(commandId, {
|
|
2585
|
-
type: "ack",
|
|
2586
|
-
command_id: commandId,
|
|
2587
|
-
delivery: "duplicate",
|
|
2588
|
-
outcome: "satisfied",
|
|
2589
|
-
revision: frame.head.revision,
|
|
2590
|
-
state_digest: frame.head.state_digest
|
|
2591
|
-
});
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
#retryCommand(commandId, code) {
|
|
2595
|
-
const deferred = this.#commands.get(commandId);
|
|
2596
|
-
if (!deferred || this.#commandRetryTimers.has(commandId)) return;
|
|
2597
|
-
if (code === "external_operation_pending") {
|
|
2598
|
-
this.#pollPendingOperation(commandId, deferred);
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
if (deferred.retries >= MAX_RETRIES) {
|
|
2602
|
-
rejectDeferred(deferred, new V1SyncError(code, "retry_exhausted"));
|
|
2603
|
-
if (!deferred.durableTarget) this.#commands.delete(commandId);
|
|
2604
|
-
return;
|
|
2605
|
-
}
|
|
2606
|
-
deferred.retries += 1;
|
|
2607
|
-
const timer = this.#clock().setTimeout(() => {
|
|
2608
|
-
this.#commandRetryTimers.delete(commandId);
|
|
2609
|
-
if (this.#commands.get(commandId) === deferred) this.#sendIfLive(deferred.frame);
|
|
2610
|
-
}, this.#options.retryDelayMs ?? 100);
|
|
2611
|
-
this.#commandRetryTimers.set(commandId, timer);
|
|
2612
|
-
}
|
|
2613
|
-
#pollPendingOperation(commandId, deferred) {
|
|
2614
|
-
const remainingAge = (this.#options.maxOperationPendingAgeMs ?? MAX_PENDING_AGE_MS) - (this.#now() - deferred.createdAt);
|
|
2615
|
-
if (remainingAge <= 0) {
|
|
2616
|
-
this.#expirePendingOperation(commandId, deferred);
|
|
2617
|
-
return;
|
|
2618
|
-
}
|
|
2619
|
-
const timer = this.#clock().setTimeout(
|
|
2620
|
-
() => {
|
|
2621
|
-
this.#commandRetryTimers.delete(commandId);
|
|
2622
|
-
if (this.#commands.get(commandId) !== deferred) return;
|
|
2623
|
-
if (this.#now() - deferred.createdAt >= (this.#options.maxOperationPendingAgeMs ?? MAX_PENDING_AGE_MS)) {
|
|
2624
|
-
this.#expirePendingOperation(commandId, deferred);
|
|
2625
|
-
return;
|
|
2626
|
-
}
|
|
2627
|
-
this.#sendIfLive(deferred.frame);
|
|
2628
|
-
},
|
|
2629
|
-
Math.min(OPERATION_PENDING_POLL_INTERVAL_MS, remainingAge)
|
|
2630
|
-
);
|
|
2631
|
-
this.#commandRetryTimers.set(commandId, timer);
|
|
2632
|
-
}
|
|
2633
|
-
#expirePendingOperation(commandId, deferred) {
|
|
2634
|
-
if (this.#commands.get(commandId) !== deferred) return;
|
|
2635
|
-
this.#commands.delete(commandId);
|
|
2636
|
-
this.#acknowledgements.delete(commandId);
|
|
2637
|
-
this.#clearCommandRetryTimer(commandId);
|
|
2638
|
-
let pending;
|
|
2639
|
-
if (deferred.durableTarget) {
|
|
2640
|
-
pending = this.#pendingTargets.get(commandId);
|
|
2641
|
-
this.#pendingTargets.delete(commandId);
|
|
2642
|
-
if (pending) this.#pendingRemovals.set(commandId, pending);
|
|
3216
|
+
this.#rememberControlEvent(event);
|
|
2643
3217
|
}
|
|
2644
|
-
|
|
3218
|
+
this.#ackRecovery();
|
|
2645
3219
|
this.#emit();
|
|
2646
|
-
if (pending) void this.#removePersistedTarget(commandId, pending);
|
|
2647
|
-
}
|
|
2648
|
-
#liveTargetResult(frame) {
|
|
2649
|
-
this.#requireLive();
|
|
2650
|
-
const deferred = this.#liveTargets.get(frame.operation_id);
|
|
2651
|
-
if (!deferred || deferred.frame.name !== frame.name) return;
|
|
2652
|
-
if (deferred.serverResultSignature) {
|
|
2653
|
-
if (deferred.serverResultSignature !== frameSignature(frame)) throw new V1ReplicaError("conflicting duplicate live-target result");
|
|
2654
|
-
return;
|
|
2655
|
-
}
|
|
2656
|
-
if (frame.outcome === "retryable_failure") {
|
|
2657
|
-
this.#retryLiveServer(frame.operation_id, deferred, frame.error_code ?? frame.outcome);
|
|
2658
|
-
return;
|
|
2659
|
-
}
|
|
2660
|
-
if (frame.outcome !== "confirmed" && frame.outcome !== "satisfied") {
|
|
2661
|
-
this.#failLiveTarget(frame.operation_id, deferred, new V1SyncError(frame.error_code ?? frame.outcome, frame.outcome));
|
|
2662
|
-
return;
|
|
2663
|
-
}
|
|
2664
|
-
deferred.serverResult = { ...frame, outcome: frame.outcome };
|
|
2665
|
-
deferred.serverResultSignature = frameSignature(frame);
|
|
2666
|
-
this.#clearLiveRetryTimer(frame.operation_id);
|
|
2667
|
-
this.#clearLiveDeadlineTimer(frame.operation_id);
|
|
2668
|
-
this.#executeLocalMediaTarget(frame.operation_id, deferred);
|
|
2669
3220
|
}
|
|
2670
|
-
#
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
return;
|
|
2676
|
-
}
|
|
2677
|
-
deferred.serverRetries += 1;
|
|
2678
|
-
const baseDelay = this.#options.retryDelayMs ?? 100;
|
|
2679
|
-
const delay = Math.min(remainingBudget, MAX_LIVE_TARGET_RETRY_DELAY_MS, baseDelay * 2 ** Math.min(deferred.serverRetries - 1, 4));
|
|
2680
|
-
this.#scheduleLiveRetry(operationId, () => this.#sendIfLive(deferred.frame), delay);
|
|
3221
|
+
#completeRecovery(frame) {
|
|
3222
|
+
const recovery = this.#requireRecovery();
|
|
3223
|
+
if (frame.recovery_id !== recovery.id || !sameHead(this.#control, frame.head) || !sameRawHead(recovery.head, frame.head)) throw new V1ReplicaError("recovery completed at the wrong head");
|
|
3224
|
+
recovery.controlComplete = true;
|
|
3225
|
+
this.#enterLiveIfReady();
|
|
2681
3226
|
}
|
|
2682
|
-
#
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3227
|
+
#replaceProjection(frame) {
|
|
3228
|
+
if (this.#phase.phase !== "recovering" && this.#phase.phase !== "live") throw new V1ReplicaError("projection snapshot arrived in the wrong phase");
|
|
3229
|
+
if (frame.stream === "media") {
|
|
3230
|
+
const previous = this.#media?.items ?? [];
|
|
3231
|
+
this.#media = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(mediaItem) };
|
|
3232
|
+
this.#mediaEventEvidence.clear();
|
|
3233
|
+
for (const item of this.#media.items)
|
|
3234
|
+
this.#notifyRemotePublicationResumed(
|
|
3235
|
+
previous.find((candidate) => mediaKey(candidate) === mediaKey(item)),
|
|
3236
|
+
item
|
|
3237
|
+
);
|
|
3238
|
+
this.#options.mediaPlane?.remotePublicationsChanged?.();
|
|
3239
|
+
} else {
|
|
3240
|
+
this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
|
|
3241
|
+
this.#presenceEventEvidence.clear();
|
|
2693
3242
|
}
|
|
2694
|
-
|
|
3243
|
+
this.#enterLiveIfReady();
|
|
3244
|
+
this.#emit();
|
|
2695
3245
|
}
|
|
2696
|
-
#
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3246
|
+
#applyProjection(frame) {
|
|
3247
|
+
this.#requireLive();
|
|
3248
|
+
if (frame.stream === "media") {
|
|
3249
|
+
if (this.#acceptProjectionDuplicate(this.#media, this.#mediaEventEvidence, frame)) return;
|
|
3250
|
+
const item = mediaItem(frame.item);
|
|
3251
|
+
const previous = this.#media?.items.find((candidate) => mediaKey(candidate) === mediaKey(item));
|
|
3252
|
+
this.#media = updateProjection(this.#media, frame.projection_id, frame.sequence, item, mediaKey);
|
|
3253
|
+
rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
3254
|
+
this.#notifyRemotePublicationResumed(previous, item);
|
|
3255
|
+
if (item.participantId !== this.#participantId && (previous?.enabled !== item.enabled || previous?.publicationId !== item.publicationId)) {
|
|
3256
|
+
this.#options.mediaPlane?.remotePublicationsChanged?.();
|
|
2703
3257
|
}
|
|
2704
|
-
|
|
2705
|
-
this.#
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
if (result.outcome !== "confirmed" && result.outcome !== "satisfied") {
|
|
2709
|
-
this.#failLiveTarget(operationId, deferred, new V1SyncError(result.errorCode ?? result.outcome, result.outcome));
|
|
2710
|
-
return;
|
|
3258
|
+
} else {
|
|
3259
|
+
if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
|
|
3260
|
+
this.#presence = updateProjection(this.#presence, frame.projection_id, frame.sequence, presenceItem(frame.item), (item) => item.participantId);
|
|
3261
|
+
rememberBoundedEvidence(this.#presenceEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
2711
3262
|
}
|
|
2712
|
-
const serverResult = deferred.serverResult;
|
|
2713
|
-
if (!serverResult) throw new V1ReplicaError("local MediaPlane completed before server authorization");
|
|
2714
|
-
this.#liveTargets.delete(operationId);
|
|
2715
|
-
this.#clearLiveRetryTimer(operationId);
|
|
2716
|
-
this.#clearLiveDeadlineTimer(operationId);
|
|
2717
|
-
this.#localMedia[deferred.source] = deferred.enabled ? "enabled" : "disabled";
|
|
2718
|
-
resolveDeferred(deferred, { operationId, name: deferred.frame.name, serverOutcome: serverResult.outcome, mediaPlaneOutcome: result.outcome });
|
|
2719
3263
|
this.#emit();
|
|
2720
3264
|
}
|
|
2721
|
-
#
|
|
2722
|
-
|
|
2723
|
-
this.#
|
|
2724
|
-
|
|
2725
|
-
}, delay);
|
|
2726
|
-
this.#liveRetryTimers.set(operationId, timer);
|
|
2727
|
-
}
|
|
2728
|
-
#failLiveTarget(operationId, deferred, error) {
|
|
2729
|
-
this.#liveTargets.delete(operationId);
|
|
2730
|
-
this.#clearLiveRetryTimer(operationId);
|
|
2731
|
-
this.#clearLiveDeadlineTimer(operationId);
|
|
2732
|
-
this.#localMedia[deferred.source] = "failed";
|
|
2733
|
-
rejectDeferred(deferred, error);
|
|
2734
|
-
this.#emit();
|
|
3265
|
+
#notifyRemotePublicationResumed(previous, current) {
|
|
3266
|
+
if (previous?.enabled === false && current.enabled && current.publicationId && previous.publicationId === current.publicationId && current.participantId !== this.#participantId) {
|
|
3267
|
+
this.#options.mediaPlane?.remotePublicationResumed?.(current.publicationId);
|
|
3268
|
+
}
|
|
2735
3269
|
}
|
|
2736
3270
|
#requestResult(frame) {
|
|
2737
3271
|
this.#requireLive();
|
|
2738
3272
|
const deferred = this.#requests.get(frame.request_id);
|
|
2739
3273
|
if (!deferred) return;
|
|
2740
3274
|
this.#requests.delete(frame.request_id);
|
|
2741
|
-
|
|
3275
|
+
resolveV1Deferred(deferred, frame);
|
|
2742
3276
|
}
|
|
2743
3277
|
#directedRequest(frame) {
|
|
2744
3278
|
this.#requireLive();
|
|
@@ -2746,130 +3280,17 @@ var V1SyncClient = class {
|
|
|
2746
3280
|
this.#send({ type: "request_ack", request_id: frame.request_id });
|
|
2747
3281
|
for (const listener of this.#requestListeners) listener(frame);
|
|
2748
3282
|
}
|
|
2749
|
-
#reactionResult(frame) {
|
|
2750
|
-
this.#requireLive();
|
|
2751
|
-
this.#requireCollaborationNegotiated();
|
|
2752
|
-
const deferred = this.#reactions.get(frame.operation_id);
|
|
2753
|
-
if (!deferred) return;
|
|
2754
|
-
this.#reactions.delete(frame.operation_id);
|
|
2755
|
-
if (frame.outcome === "accepted") {
|
|
2756
|
-
resolveDeferred(deferred, reactionFromFrame(frame.reaction));
|
|
2757
|
-
} else {
|
|
2758
|
-
rejectDeferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
#chatSendResult(frame) {
|
|
2762
|
-
this.#requireLive();
|
|
2763
|
-
this.#requireCollaborationNegotiated();
|
|
2764
|
-
const deferred = this.#chatSends.get(frame.client_message_id);
|
|
2765
|
-
if (!deferred) return;
|
|
2766
|
-
this.#chatSends.delete(frame.client_message_id);
|
|
2767
|
-
if (frame.outcome === "accepted") {
|
|
2768
|
-
this.#assertChatMessageVersion(frame.message);
|
|
2769
|
-
const message = chatMessageFromFrame(frame.message);
|
|
2770
|
-
this.#rememberChatSequence(message.sequence);
|
|
2771
|
-
resolveDeferred(deferred, message);
|
|
2772
|
-
} else {
|
|
2773
|
-
rejectDeferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
#chatPageResult(frame) {
|
|
2777
|
-
this.#requireCollaborationNegotiated();
|
|
2778
|
-
const deferred = this.#chatPages.get(frame.request_id);
|
|
2779
|
-
if (!deferred) return;
|
|
2780
|
-
this.#chatPages.delete(frame.request_id);
|
|
2781
|
-
if (frame.outcome === "cursor_reset") {
|
|
2782
|
-
this.#collaboration = { ...this.#collaboration, retainedFloorSequence: frame.retained_floor_sequence };
|
|
2783
|
-
const result = { status: "cursor_reset", retainedFloorSequence: frame.retained_floor_sequence };
|
|
2784
|
-
this.#emitCollaboration({ type: "chat_cursor_reset", retainedFloorSequence: frame.retained_floor_sequence });
|
|
2785
|
-
resolveDeferred(deferred, result);
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
this.#collaboration = {
|
|
2789
|
-
...this.#collaboration,
|
|
2790
|
-
chatHeadSequence: frame.head_sequence,
|
|
2791
|
-
retainedFloorSequence: frame.retained_floor_sequence
|
|
2792
|
-
};
|
|
2793
|
-
for (const message of frame.messages) {
|
|
2794
|
-
this.#assertChatMessageVersion(message);
|
|
2795
|
-
this.#observeChatMessage(message);
|
|
2796
|
-
}
|
|
2797
|
-
resolveDeferred(deferred, { status: "loaded", count: frame.messages.length, hasOlder: frame.has_more });
|
|
2798
|
-
}
|
|
2799
|
-
#observeChatMessage(frame) {
|
|
2800
|
-
this.#assertChatMessageVersion(frame);
|
|
2801
|
-
const message = chatMessageFromFrame(frame);
|
|
2802
|
-
this.#rememberChatSequence(message.sequence);
|
|
2803
|
-
this.#emitCollaboration({ type: "chat_message", message });
|
|
2804
|
-
}
|
|
2805
|
-
#chatReadResult(frame) {
|
|
2806
|
-
this.#requireLive();
|
|
2807
|
-
this.#requireCollaborationVersion(1);
|
|
2808
|
-
const deferred = this.#chatReads.get(frame.request_id);
|
|
2809
|
-
if (!deferred) return;
|
|
2810
|
-
this.#chatReads.delete(frame.request_id);
|
|
2811
|
-
if (frame.outcome === "accepted") {
|
|
2812
|
-
const receipt = chatReadReceiptFromFrame(frame);
|
|
2813
|
-
this.#rememberChatReadReceipt(receipt);
|
|
2814
|
-
resolveDeferred(deferred, receipt);
|
|
2815
|
-
} else {
|
|
2816
|
-
rejectDeferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
#assertChatMessageVersion(frame) {
|
|
2820
|
-
const version = this.#collaboration.version;
|
|
2821
|
-
if (version === 1 && !("attachments" in frame)) throw new V1ReplicaError("collaboration_v1 received a chat message without attachments");
|
|
2822
|
-
}
|
|
2823
|
-
#rememberChatReadReceipt(receipt) {
|
|
2824
|
-
const existing = this.#collaboration.readReceipts.find((candidate) => candidate.participantId === receipt.participantId && candidate.participantGeneration === receipt.participantGeneration);
|
|
2825
|
-
if (existing && compareUnsignedDecimals(existing.readThroughSequence, receipt.readThroughSequence) >= 0) return;
|
|
2826
|
-
this.#collaboration = {
|
|
2827
|
-
...this.#collaboration,
|
|
2828
|
-
readReceipts: [...this.#collaboration.readReceipts.filter((candidate) => candidate.participantId !== receipt.participantId || candidate.participantGeneration !== receipt.participantGeneration), receipt]
|
|
2829
|
-
};
|
|
2830
|
-
}
|
|
2831
|
-
#rememberChatSequence(sequence) {
|
|
2832
|
-
if (this.#chatAfterSequence === null || compareUnsignedDecimals(sequence, this.#chatAfterSequence) > 0) this.#chatAfterSequence = sequence;
|
|
2833
|
-
if (this.#collaboration.chatHeadSequence === null || compareUnsignedDecimals(sequence, this.#collaboration.chatHeadSequence) > 0) {
|
|
2834
|
-
this.#collaboration = { ...this.#collaboration, chatHeadSequence: sequence };
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
#emitCollaboration(event) {
|
|
2838
|
-
for (const listener of this.#collaborationListeners) listener(event);
|
|
2839
|
-
}
|
|
2840
|
-
async #finishCommand(commandId, result) {
|
|
2841
|
-
const deferred = this.#commands.get(commandId);
|
|
2842
|
-
if (!deferred) return;
|
|
2843
|
-
this.#commands.delete(commandId);
|
|
2844
|
-
this.#clearCommandRetryTimer(commandId);
|
|
2845
|
-
this.#acknowledgements.delete(commandId);
|
|
2846
|
-
let pending;
|
|
2847
|
-
if (deferred.durableTarget) {
|
|
2848
|
-
pending = this.#pendingTargets.get(commandId);
|
|
2849
|
-
this.#pendingTargets.delete(commandId);
|
|
2850
|
-
if (pending) this.#pendingRemovals.set(commandId, pending);
|
|
2851
|
-
}
|
|
2852
|
-
if (result.outcome === "rejected" || result.outcome === "command_id_conflict") rejectDeferred(deferred, new V1SyncError(result.reason, result.outcome));
|
|
2853
|
-
else resolveDeferred(deferred, result);
|
|
2854
|
-
this.#emit();
|
|
2855
|
-
if (pending) void this.#removePersistedTarget(commandId, pending);
|
|
2856
|
-
}
|
|
2857
3283
|
#enterLiveIfReady() {
|
|
2858
3284
|
if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
|
|
3285
|
+
recordReconnect(this.#options.recordReconnect, "state_resynced");
|
|
2859
3286
|
this.#recovery = null;
|
|
2860
3287
|
this.#phase = { phase: "live" };
|
|
2861
3288
|
this.#reconnectAttempt = 0;
|
|
2862
3289
|
this.#missedHeartbeats = 0;
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
this.#send(pending.frame);
|
|
2866
|
-
}
|
|
2867
|
-
for (const pending of this.#liveTargets.values()) {
|
|
2868
|
-
pending.serverRetries = 0;
|
|
2869
|
-
if (pending.serverResult) this.#executeLocalMediaTarget(pending.frame.operation_id, pending);
|
|
2870
|
-
else this.#send(pending.frame);
|
|
2871
|
-
}
|
|
3290
|
+
this.#commandScheduler.enterLive();
|
|
3291
|
+
this.#liveTargets.enterLive();
|
|
2872
3292
|
this.#startHeartbeat();
|
|
3293
|
+
this.#startNoticeProbe();
|
|
2873
3294
|
this.#emit();
|
|
2874
3295
|
}
|
|
2875
3296
|
#ackRecovery() {
|
|
@@ -2877,36 +3298,6 @@ var V1SyncClient = class {
|
|
|
2877
3298
|
const control = this.#requireControl();
|
|
2878
3299
|
this.#send({ type: "recovery_ack", recovery_id: recovery.id, revision: control.revision, state_digest: control.stateDigest });
|
|
2879
3300
|
}
|
|
2880
|
-
#restorePending(pending) {
|
|
2881
|
-
if (this.#pendingTargets.size + this.#pendingRemovals.size >= this.#maxPending() || this.#now() - pending.createdAt > (this.#options.maxPendingAgeMs ?? MAX_PENDING_AGE_MS) || this.#pendingBytes() + pending.bytes > (this.#options.maxPendingBytes ?? MAX_PENDING_BYTES)) {
|
|
2882
|
-
void this.#removePersistedTarget(pending.commandId, pending);
|
|
2883
|
-
return;
|
|
2884
|
-
}
|
|
2885
|
-
const frame = { type: "command", command_id: pending.commandId, name: pending.command.name, payload: pending.command.payload };
|
|
2886
|
-
encodeV1ClientFrame(frame);
|
|
2887
|
-
this.#pendingTargets.set(pending.commandId, pending);
|
|
2888
|
-
this.#commands.set(pending.commandId, { resolve: () => void 0, reject: () => void 0, settled: true, frame, retries: 0, durableTarget: true, createdAt: pending.createdAt });
|
|
2889
|
-
}
|
|
2890
|
-
async #removePersistedTarget(commandId, pending) {
|
|
2891
|
-
this.#pendingRemovals.set(commandId, pending);
|
|
2892
|
-
try {
|
|
2893
|
-
await this.#store.remove(commandId);
|
|
2894
|
-
if (this.#pendingRemovals.get(commandId) !== pending) return;
|
|
2895
|
-
this.#pendingRemovals.delete(commandId);
|
|
2896
|
-
this.#clearPendingRemovalRetryTimer(commandId);
|
|
2897
|
-
} catch {
|
|
2898
|
-
if (this.#pendingRemovals.get(commandId) !== pending) return;
|
|
2899
|
-
this.#schedulePendingRemovalRetry(commandId, pending);
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
#schedulePendingRemovalRetry(commandId, pending) {
|
|
2903
|
-
if (!this.#started || this.#pendingRemovalRetryTimers.has(commandId)) return;
|
|
2904
|
-
const timer = this.#clock().setTimeout(() => {
|
|
2905
|
-
this.#pendingRemovalRetryTimers.delete(commandId);
|
|
2906
|
-
void this.#removePersistedTarget(commandId, pending);
|
|
2907
|
-
}, this.#options.retryDelayMs ?? 100);
|
|
2908
|
-
this.#pendingRemovalRetryTimers.set(commandId, timer);
|
|
2909
|
-
}
|
|
2910
3301
|
#ackHeadIsProven(ack) {
|
|
2911
3302
|
return this.#controlHeads.get(ack.revision) === ack.state_digest;
|
|
2912
3303
|
}
|
|
@@ -2926,25 +3317,31 @@ var V1SyncClient = class {
|
|
|
2926
3317
|
}
|
|
2927
3318
|
#disconnected(socket) {
|
|
2928
3319
|
if (socket !== this.#socket) return;
|
|
3320
|
+
recordReconnect(this.#options.recordReconnect, "sync_loss");
|
|
2929
3321
|
this.#socket = null;
|
|
2930
3322
|
this.#recovery = null;
|
|
2931
3323
|
this.#clearHeartbeat();
|
|
2932
|
-
this.#
|
|
3324
|
+
this.#clearNoticeProbe();
|
|
3325
|
+
this.#liveTargets.disconnect("disconnected_before_delivery");
|
|
3326
|
+
this.#commandScheduler.disconnect();
|
|
2933
3327
|
this.#media = null;
|
|
2934
3328
|
this.#presence = null;
|
|
2935
3329
|
this.#rejectRequests("disconnected_before_delivery");
|
|
2936
|
-
this.#
|
|
2937
|
-
this.#collaboration = { ...this.#collaboration, negotiated: false, version: null, capabilities: [], readReceipts: [] };
|
|
2938
|
-
this.#participantCollaborationCapabilities = {};
|
|
3330
|
+
this.#collaborationState.disconnect("disconnected_before_delivery");
|
|
2939
3331
|
if (!this.#started || !this.#transportAvailable || this.#phase.phase === "terminal") return;
|
|
2940
3332
|
this.#phase = { phase: "connecting" };
|
|
2941
3333
|
this.#emit();
|
|
2942
3334
|
this.#clearReconnect();
|
|
2943
3335
|
const configuredDelay = this.#options.reconnectDelayMs;
|
|
2944
|
-
const
|
|
3336
|
+
const exponentialAttempt = configuredDelay === void 0 ? Math.max(0, this.#reconnectAttempt - FAST_RECONNECT_ATTEMPTS + 1) : this.#reconnectAttempt;
|
|
3337
|
+
const baseDelay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(exponentialAttempt, 5));
|
|
3338
|
+
const delay = configuredDelay === void 0 ? Math.round(baseDelay * (0.8 + 0.4 * Math.random())) : baseDelay;
|
|
3339
|
+
recordReconnect(this.#options.recordReconnect, "sync_backoff", { boundary: "start", delay_ms: delay, attempt: this.#reconnectAttempt });
|
|
2945
3340
|
this.#reconnectAttempt += 1;
|
|
3341
|
+
const backoffStartedAt = this.#now();
|
|
2946
3342
|
this.#reconnectTimer = this.#clock().setTimeout(() => {
|
|
2947
3343
|
this.#reconnectTimer = void 0;
|
|
3344
|
+
recordReconnect(this.#options.recordReconnect, "sync_backoff", { boundary: "end", duration_ms: Math.max(0, this.#now() - backoffStartedAt) });
|
|
2948
3345
|
this.#connect();
|
|
2949
3346
|
}, delay);
|
|
2950
3347
|
}
|
|
@@ -2955,6 +3352,7 @@ var V1SyncClient = class {
|
|
|
2955
3352
|
this.#disconnected(socket);
|
|
2956
3353
|
}
|
|
2957
3354
|
#handleLifecycle(event) {
|
|
3355
|
+
recordReconnect(this.#options.recordReconnect, "network_change", { lifecycle: event });
|
|
2958
3356
|
if (event === "online" || event === "offline") this.#online = event === "online";
|
|
2959
3357
|
else this.#active = event === "active";
|
|
2960
3358
|
this.#transportAvailable = this.#online && this.#active;
|
|
@@ -2963,21 +3361,10 @@ var V1SyncClient = class {
|
|
|
2963
3361
|
}
|
|
2964
3362
|
#rejectEphemeral(code) {
|
|
2965
3363
|
this.#rejectRequests(code);
|
|
2966
|
-
|
|
2967
|
-
this.#liveTargets.clear();
|
|
2968
|
-
}
|
|
2969
|
-
#rejectCollaboration(code) {
|
|
2970
|
-
for (const deferred of this.#reactions.values()) rejectDeferred(deferred, new V1SyncError(code, code));
|
|
2971
|
-
for (const deferred of this.#chatSends.values()) rejectDeferred(deferred, new V1SyncError(code, code));
|
|
2972
|
-
for (const deferred of this.#chatPages.values()) rejectDeferred(deferred, new V1SyncError(code, code));
|
|
2973
|
-
for (const deferred of this.#chatReads.values()) rejectDeferred(deferred, new V1SyncError(code, code));
|
|
2974
|
-
this.#reactions.clear();
|
|
2975
|
-
this.#chatSends.clear();
|
|
2976
|
-
this.#chatPages.clear();
|
|
2977
|
-
this.#chatReads.clear();
|
|
3364
|
+
this.#liveTargets.disconnect(code);
|
|
2978
3365
|
}
|
|
2979
3366
|
#rejectRequests(code) {
|
|
2980
|
-
for (const deferred of this.#requests.values())
|
|
3367
|
+
for (const deferred of this.#requests.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2981
3368
|
this.#requests.clear();
|
|
2982
3369
|
}
|
|
2983
3370
|
#sendIfLive(frame) {
|
|
@@ -2989,13 +3376,6 @@ var V1SyncClient = class {
|
|
|
2989
3376
|
#requireLive() {
|
|
2990
3377
|
if (this.#phase.phase !== "live") throw new V1ReplicaError("live frame arrived before four-stream recovery completed");
|
|
2991
3378
|
}
|
|
2992
|
-
#requireCollaborationNegotiated() {
|
|
2993
|
-
if (!this.#collaboration.negotiated) throw new V1ReplicaError("collaboration frame arrived without a negotiated extension");
|
|
2994
|
-
}
|
|
2995
|
-
#requireCollaborationVersion(version) {
|
|
2996
|
-
this.#requireCollaborationNegotiated();
|
|
2997
|
-
if (this.#collaboration.version !== version) throw new V1ReplicaError(`collaboration_v1 frame arrived on a different extension version`);
|
|
2998
|
-
}
|
|
2999
3379
|
#requireControl() {
|
|
3000
3380
|
if (!this.#control) throw new V1ReplicaError("control replica is unavailable");
|
|
3001
3381
|
return this.#control;
|
|
@@ -3005,33 +3385,7 @@ var V1SyncClient = class {
|
|
|
3005
3385
|
return this.#recovery;
|
|
3006
3386
|
}
|
|
3007
3387
|
#assertCapacity() {
|
|
3008
|
-
|
|
3009
|
-
}
|
|
3010
|
-
#assertCollaborationReady(capability) {
|
|
3011
|
-
if (!this.#collaboration.negotiated) throw new V1SyncError("collaboration is unavailable", "collaboration_unavailable");
|
|
3012
|
-
if (this.#phase.phase !== "live") throw new V1SyncError("collaboration requires a live connection", "not_live");
|
|
3013
|
-
if (capability && !this.#collaboration.capabilities.includes(capability)) throw new V1SyncError("collaboration capability denied", "capability_denied");
|
|
3014
|
-
}
|
|
3015
|
-
#assertCollaborationCapacity() {
|
|
3016
|
-
const maximum = Math.min(this.#options.maxPendingCollaborationRequests ?? MAX_COLLABORATION_REQUESTS_IN_FLIGHT, MAX_COLLABORATION_REQUESTS_IN_FLIGHT);
|
|
3017
|
-
if (this.#reactions.size + this.#chatSends.size + this.#chatPages.size + this.#chatReads.size >= maximum) throw new V1SyncError("collaboration in-flight capacity exceeded", "capacity");
|
|
3018
|
-
}
|
|
3019
|
-
#assertCollaborationRequestIdAvailable(id) {
|
|
3020
|
-
if (this.#reactions.has(id) || this.#chatSends.has(id) || this.#chatPages.has(id) || this.#chatReads.has(id)) throw new V1SyncError("collaboration request ID is already pending", "request_id_conflict");
|
|
3021
|
-
}
|
|
3022
|
-
#nextCollaborationRequestId() {
|
|
3023
|
-
const id = this.#nextRequestId();
|
|
3024
|
-
this.#assertCollaborationRequestIdAvailable(id);
|
|
3025
|
-
return id;
|
|
3026
|
-
}
|
|
3027
|
-
#maxPending() {
|
|
3028
|
-
return Math.min(this.#options.maxPendingCommands ?? MAX_IN_FLIGHT, MAX_IN_FLIGHT);
|
|
3029
|
-
}
|
|
3030
|
-
#pendingBytes() {
|
|
3031
|
-
return [...this.#pendingTargets.values(), ...this.#pendingRemovals.values()].reduce((total, pending) => total + pending.bytes, 0);
|
|
3032
|
-
}
|
|
3033
|
-
#nextCommandId() {
|
|
3034
|
-
return this.#options.ids?.next() ?? crypto.randomUUID();
|
|
3388
|
+
this.#commandScheduler.assertCapacity(this.#liveTargets.pendingCount + this.#requests.size);
|
|
3035
3389
|
}
|
|
3036
3390
|
#nextRequestId() {
|
|
3037
3391
|
return this.#options.requestIds?.next() ?? crypto.randomUUID();
|
|
@@ -3080,47 +3434,7 @@ var V1SyncClient = class {
|
|
|
3080
3434
|
this.#emit();
|
|
3081
3435
|
}
|
|
3082
3436
|
#updateLocalMediaStates() {
|
|
3083
|
-
|
|
3084
|
-
for (const source of ["microphone", "camera", "screen"]) {
|
|
3085
|
-
const publication = this.#localPublications.find((candidate) => candidate.source === source && candidate.participantId === this.#participantId);
|
|
3086
|
-
this.#localMedia[source] = publication?.enabled ? "enabled" : "disabled";
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
#clearRetryTimers() {
|
|
3090
|
-
for (const timer of this.#commandRetryTimers.values()) this.#clock().clearTimeout(timer);
|
|
3091
|
-
for (const timer of this.#liveRetryTimers.values()) this.#clock().clearTimeout(timer);
|
|
3092
|
-
for (const timer of this.#liveDeadlineTimers.values()) this.#clock().clearTimeout(timer);
|
|
3093
|
-
this.#commandRetryTimers.clear();
|
|
3094
|
-
this.#liveRetryTimers.clear();
|
|
3095
|
-
this.#liveDeadlineTimers.clear();
|
|
3096
|
-
}
|
|
3097
|
-
#clearPendingRemovalRetryTimers() {
|
|
3098
|
-
for (const timer of this.#pendingRemovalRetryTimers.values()) this.#clock().clearTimeout(timer);
|
|
3099
|
-
this.#pendingRemovalRetryTimers.clear();
|
|
3100
|
-
}
|
|
3101
|
-
#clearPendingRemovalRetryTimer(commandId) {
|
|
3102
|
-
const timer = this.#pendingRemovalRetryTimers.get(commandId);
|
|
3103
|
-
if (timer === void 0) return;
|
|
3104
|
-
this.#clock().clearTimeout(timer);
|
|
3105
|
-
this.#pendingRemovalRetryTimers.delete(commandId);
|
|
3106
|
-
}
|
|
3107
|
-
#clearLiveRetryTimer(operationId) {
|
|
3108
|
-
const timer = this.#liveRetryTimers.get(operationId);
|
|
3109
|
-
if (timer === void 0) return;
|
|
3110
|
-
this.#clock().clearTimeout(timer);
|
|
3111
|
-
this.#liveRetryTimers.delete(operationId);
|
|
3112
|
-
}
|
|
3113
|
-
#clearLiveDeadlineTimer(operationId) {
|
|
3114
|
-
const timer = this.#liveDeadlineTimers.get(operationId);
|
|
3115
|
-
if (timer === void 0) return;
|
|
3116
|
-
this.#clock().clearTimeout(timer);
|
|
3117
|
-
this.#liveDeadlineTimers.delete(operationId);
|
|
3118
|
-
}
|
|
3119
|
-
#clearCommandRetryTimer(commandId) {
|
|
3120
|
-
const timer = this.#commandRetryTimers.get(commandId);
|
|
3121
|
-
if (timer === void 0) return;
|
|
3122
|
-
this.#clock().clearTimeout(timer);
|
|
3123
|
-
this.#commandRetryTimers.delete(commandId);
|
|
3437
|
+
this.#liveTargets.updateFromPublications(this.#localPublications);
|
|
3124
3438
|
}
|
|
3125
3439
|
#startHeartbeat() {
|
|
3126
3440
|
this.#clearHeartbeat();
|
|
@@ -3141,6 +3455,36 @@ var V1SyncClient = class {
|
|
|
3141
3455
|
this.#clock().clearTimeout(this.#heartbeatTimer);
|
|
3142
3456
|
this.#heartbeatTimer = void 0;
|
|
3143
3457
|
}
|
|
3458
|
+
#startNoticeProbe() {
|
|
3459
|
+
this.#clearNoticeProbe();
|
|
3460
|
+
this.#noticeLastInboundAt = this.#now();
|
|
3461
|
+
this.#scheduleNoticeCheck();
|
|
3462
|
+
const tick = () => {
|
|
3463
|
+
this.#noticeProbeTimer = void 0;
|
|
3464
|
+
if (this.#phase.phase !== "live") return;
|
|
3465
|
+
this.#send({ type: "ping" });
|
|
3466
|
+
this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
|
|
3467
|
+
};
|
|
3468
|
+
this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
|
|
3469
|
+
}
|
|
3470
|
+
#scheduleNoticeCheck() {
|
|
3471
|
+
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3472
|
+
const remaining = Math.max(0, NOTICE_SILENCE_MS - (this.#now() - this.#noticeLastInboundAt));
|
|
3473
|
+
this.#noticeTimer = this.#clock().setTimeout(() => {
|
|
3474
|
+
this.#noticeTimer = void 0;
|
|
3475
|
+
if (this.#phase.phase !== "live" || this.#noticeUnresponsive) return;
|
|
3476
|
+
if (this.#now() - this.#noticeLastInboundAt < NOTICE_SILENCE_MS) return this.#scheduleNoticeCheck();
|
|
3477
|
+
this.#noticeUnresponsive = true;
|
|
3478
|
+
this.#emit();
|
|
3479
|
+
}, remaining);
|
|
3480
|
+
}
|
|
3481
|
+
#clearNoticeProbe() {
|
|
3482
|
+
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3483
|
+
if (this.#noticeProbeTimer !== void 0) this.#clock().clearTimeout(this.#noticeProbeTimer);
|
|
3484
|
+
this.#noticeTimer = void 0;
|
|
3485
|
+
this.#noticeProbeTimer = void 0;
|
|
3486
|
+
this.#noticeUnresponsive = false;
|
|
3487
|
+
}
|
|
3144
3488
|
#emit() {
|
|
3145
3489
|
const snapshot = this.getSnapshot();
|
|
3146
3490
|
for (const listener of this.#listeners) listener(snapshot);
|
|
@@ -3170,51 +3514,17 @@ function sameRawHead(left, right) {
|
|
|
3170
3514
|
function copyProjection(projection) {
|
|
3171
3515
|
return projection && { ...projection, items: projection.items.map((item) => ({ ...item })) };
|
|
3172
3516
|
}
|
|
3173
|
-
function frameSignature(frame) {
|
|
3174
|
-
return new TextDecoder().decode(canonicalJsonBytesFromUnknown(frame));
|
|
3175
|
-
}
|
|
3176
|
-
function copyCollaborationCapabilities(capabilities) {
|
|
3177
|
-
return Object.fromEntries(Object.entries(capabilities).map(([participantId, values]) => [participantId, [...values]]));
|
|
3178
|
-
}
|
|
3179
|
-
function chatReadReceiptFromWire(receipt) {
|
|
3180
|
-
return {
|
|
3181
|
-
participantId: receipt.participant_id,
|
|
3182
|
-
participantGeneration: receipt.participant_generation,
|
|
3183
|
-
readThroughSequence: receipt.sequence,
|
|
3184
|
-
readAt: receipt.read_at
|
|
3185
|
-
};
|
|
3186
|
-
}
|
|
3187
|
-
function compareUnsignedDecimals(left, right) {
|
|
3188
|
-
if (left.length !== right.length) return left.length - right.length;
|
|
3189
|
-
return left.localeCompare(right);
|
|
3190
|
-
}
|
|
3191
3517
|
function rememberBoundedEvidence(evidence, sequence, signature, capacity) {
|
|
3192
3518
|
evidence.set(sequence, signature);
|
|
3193
3519
|
if (evidence.size <= capacity) return;
|
|
3194
3520
|
const oldest = evidence.keys().next().value;
|
|
3195
3521
|
if (oldest !== void 0) evidence.delete(oldest);
|
|
3196
3522
|
}
|
|
3197
|
-
function resolveDeferred(deferred, value) {
|
|
3198
|
-
if (deferred.settled) return;
|
|
3199
|
-
deferred.settled = true;
|
|
3200
|
-
deferred.resolve(value);
|
|
3201
|
-
}
|
|
3202
|
-
function rejectDeferred(deferred, error) {
|
|
3203
|
-
if (deferred.settled) return;
|
|
3204
|
-
deferred.settled = true;
|
|
3205
|
-
deferred.reject(error);
|
|
3206
|
-
}
|
|
3207
3523
|
function copyPublication(publication) {
|
|
3208
3524
|
return { ...publication };
|
|
3209
3525
|
}
|
|
3210
3526
|
function validMediaPublication(publication) {
|
|
3211
|
-
return typeof publication.participantId === "string" && publication.participantId.length > 0 && (publication.source === "microphone" || publication.source === "camera" || publication.source === "screen") && typeof publication.enabled === "boolean" && (publication.publicationId === null || typeof publication.publicationId === "string") && publication.enabled
|
|
3212
|
-
}
|
|
3213
|
-
function validMediaPlaneResult(result) {
|
|
3214
|
-
return (result.outcome === "confirmed" || result.outcome === "satisfied" || result.outcome === "retryable_failure" || result.outcome === "terminal_failure" || result.outcome === "ambiguous") && (result.errorCode === null || typeof result.errorCode === "string");
|
|
3215
|
-
}
|
|
3216
|
-
function isTerminalLifecycleOperation(frame) {
|
|
3217
|
-
return frame.type === "operation" && (frame.name === "participant_leave" || frame.name === "end_episode");
|
|
3527
|
+
return typeof publication.participantId === "string" && publication.participantId.length > 0 && (publication.source === "microphone" || publication.source === "camera" || publication.source === "screen") && typeof publication.enabled === "boolean" && (publication.publicationId === null || typeof publication.publicationId === "string") && (!publication.enabled || publication.publicationId !== null);
|
|
3218
3528
|
}
|
|
3219
3529
|
function assertV1Url(value) {
|
|
3220
3530
|
const url = new URL(value);
|
|
@@ -3371,20 +3681,21 @@ export {
|
|
|
3371
3681
|
createBrowserSyncLifecycle,
|
|
3372
3682
|
createReactNativeWebSocketFactory,
|
|
3373
3683
|
createReactNativeSyncLifecycle,
|
|
3374
|
-
|
|
3375
|
-
chatMessageFromFrame,
|
|
3376
|
-
chatReadReceiptFromFrame,
|
|
3684
|
+
ChatAttachmentMimeTypeSchema,
|
|
3377
3685
|
encodeV1ClientFrame,
|
|
3378
3686
|
decodeV1ServerFrame,
|
|
3379
3687
|
decodeV1ClientFrame,
|
|
3380
|
-
|
|
3688
|
+
reactionFromFrame,
|
|
3689
|
+
chatMessageFromFrame,
|
|
3690
|
+
chatReadReceiptFromFrame,
|
|
3691
|
+
V1SyncError,
|
|
3381
3692
|
V1ReplicaError,
|
|
3382
3693
|
restoreV1Snapshot,
|
|
3383
3694
|
applyV1Event,
|
|
3384
3695
|
optimisticV1Control,
|
|
3385
3696
|
assertV1ControlSemantics,
|
|
3386
3697
|
computeV1StateDigest,
|
|
3387
|
-
|
|
3698
|
+
InMemoryV1PendingTargetStore,
|
|
3388
3699
|
V1SyncClient,
|
|
3389
3700
|
IndexedDbV1PendingTargetStore,
|
|
3390
3701
|
AsyncStorageV1PendingTargetStore,
|