@q9labsai/chalk-client 4.1.15 → 4.1.17
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 +100 -5
- package/dist/access/grant.d.ts.map +1 -1
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-PQNSA7ZN.js → chunk-2DZUHV6I.js} +782 -122
- package/dist/{chunk-6ONSFCIY.js → chunk-A7BVZEFM.js} +7 -3
- package/dist/{chunk-LQSXEWLL.js → chunk-AUQTQOLR.js} +2685 -511
- package/dist/{chunk-PWUU6T3R.js → chunk-CZUJNAUI.js} +1196 -874
- package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
- package/dist/chunk-HW5TTZL2.js +99 -0
- package/dist/chunk-RCDXIDCN.js +33 -0
- package/dist/{chunk-LSDDADCN.js → chunk-ZRMGKMX4.js} +1728 -517
- package/dist/client.d.ts +1452 -465
- package/dist/client.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +11 -0
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/diagnostics.d.ts +1 -1
- package/dist/connection/diagnostics.d.ts.map +1 -1
- package/dist/connection/index.d.ts +3 -0
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.d.ts +4 -1
- package/dist/connection/lifecycle.d.ts.map +1 -1
- package/dist/connection/media-devices.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 +355 -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/foreground-deadline.d.ts +9 -0
- package/dist/foreground-deadline.d.ts.map +1 -0
- package/dist/generated/http-api.d.ts +1438 -461
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +2667 -842
- 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 +136 -21
- 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/rtk.d.ts.map +1 -1
- 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 +13 -0
- package/dist/server/access-refresh.d.ts +14 -0
- package/dist/server/access-refresh.d.ts.map +1 -0
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/index.cjs +189 -23
- package/dist/server/index.d.cts +2 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +187 -22
- package/dist/server/types.d.cts +76 -1
- package/dist/server/types.d.ts +76 -1
- 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 +2 -1
- 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/terminal.test.helpers.d.ts +115 -0
- package/dist/space-client/terminal.test.helpers.d.ts.map +1 -0
- package/dist/space-client/types.d.ts +42 -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 +2 -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/webhooks/generated/event-v1.d.cts +10 -2
- package/dist/webhooks/generated/event-v1.d.ts +10 -2
- package/dist/webhooks/generated/event-v1.d.ts.map +1 -1
- package/dist/webhooks/generated/fixtures-v1.d.cts +2 -2
- package/dist/webhooks/generated/fixtures-v1.d.ts +2 -2
- package/dist/webhooks/index.cjs +10 -2
- package/dist/webhooks/index.js +10 -2
- package/dist/webhooks/test.cjs +2 -2
- package/dist/webhooks/test.js +2 -2
- 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,989 @@ 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
|
+
restartTransport() {
|
|
2862
|
+
this.#recover("lifecycle transport deadline");
|
|
2863
|
+
}
|
|
2864
|
+
async start() {
|
|
2865
|
+
if (this.#started) return;
|
|
2866
|
+
this.#started = true;
|
|
2867
|
+
const startupGeneration = ++this.#startupGeneration;
|
|
2868
|
+
let stored;
|
|
2869
|
+
try {
|
|
2870
|
+
stored = await this.#commandScheduler.load();
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
if (startupGeneration === this.#startupGeneration) {
|
|
2873
|
+
this.#started = false;
|
|
2874
|
+
}
|
|
2875
|
+
throw error;
|
|
2876
|
+
}
|
|
2877
|
+
if (!this.#started || startupGeneration !== this.#startupGeneration) return;
|
|
2878
|
+
this.#commandScheduler.reconcileStored(stored);
|
|
2879
|
+
this.#subscribeMediaPlane(this.#options.mediaPlane);
|
|
2880
|
+
this.#unsubscribeLifecycle = this.#options.lifecycle?.subscribe((event) => this.#handleLifecycle(event));
|
|
2881
|
+
this.#connect();
|
|
2882
|
+
}
|
|
2883
|
+
stop() {
|
|
2884
|
+
this.#started = false;
|
|
2885
|
+
this.#startupGeneration += 1;
|
|
2886
|
+
this.#unsubscribeLifecycle?.();
|
|
2887
|
+
this.#unsubscribeLifecycle = void 0;
|
|
2888
|
+
this.#clearReconnect();
|
|
2889
|
+
this.#clearHeartbeat();
|
|
2890
|
+
this.#clearNoticeProbe();
|
|
2891
|
+
this.#commandScheduler.stop("client_stopped");
|
|
2892
|
+
this.#unsubscribeMediaPlane();
|
|
2893
|
+
this.#socket?.close(1e3, "client stopped");
|
|
2894
|
+
this.#socket = null;
|
|
2895
|
+
this.#phase = { phase: "stopped" };
|
|
2896
|
+
this.#rejectEphemeral("client_stopped");
|
|
2897
|
+
this.#collaborationState.disconnect("client_stopped");
|
|
2898
|
+
this.#localPublications = [];
|
|
2899
|
+
this.#remotePublications = [];
|
|
2900
|
+
this.#liveTargets.resetLocalMedia();
|
|
2901
|
+
this.#emit();
|
|
2902
|
+
}
|
|
2903
|
+
getSnapshot() {
|
|
2904
|
+
const pendingCommands = this.#commandScheduler.pendingCommands;
|
|
2905
|
+
const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
|
|
2906
|
+
return {
|
|
2907
|
+
connection: this.#phase.phase === "live" ? { ...this.#phase, noticeUnresponsive: this.#noticeUnresponsive } : { ...this.#phase },
|
|
2908
|
+
participantId: this.#participantId,
|
|
2909
|
+
participantGeneration: this.#participantGeneration,
|
|
2910
|
+
control: this.#control,
|
|
2911
|
+
optimisticControl,
|
|
2912
|
+
media: copyProjection(this.#media),
|
|
2913
|
+
presence: copyProjection(this.#presence),
|
|
2914
|
+
mediaPlane: { local: this.#localPublications.map(copyPublication), remote: this.#remotePublications.map(copyPublication) },
|
|
2915
|
+
localMedia: this.#liveTargets.getLocalMedia(),
|
|
2916
|
+
pendingCommandCount: this.#commandScheduler.pendingCount
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
subscribe(listener) {
|
|
2920
|
+
this.#listeners.add(listener);
|
|
2921
|
+
listener(this.getSnapshot());
|
|
2922
|
+
return () => this.#listeners.delete(listener);
|
|
2923
|
+
}
|
|
2924
|
+
onDirectedRequest(listener) {
|
|
2925
|
+
this.#requestListeners.add(listener);
|
|
2926
|
+
return () => this.#requestListeners.delete(listener);
|
|
2927
|
+
}
|
|
2928
|
+
getCollaborationExtensionState() {
|
|
2929
|
+
return this.#collaborationState.getState();
|
|
2930
|
+
}
|
|
2931
|
+
getParticipantCollaborationCapabilities() {
|
|
2932
|
+
return this.#collaborationState.getParticipantCapabilities();
|
|
2933
|
+
}
|
|
2934
|
+
subscribeCollaboration(listener) {
|
|
2935
|
+
return this.#collaborationState.subscribe(listener);
|
|
2936
|
+
}
|
|
2937
|
+
sendReaction(reaction) {
|
|
2938
|
+
return this.#collaborationState.sendReaction(reaction);
|
|
2939
|
+
}
|
|
2940
|
+
sendChatMessage(input) {
|
|
2941
|
+
return this.#collaborationState.sendChatMessage(input);
|
|
2942
|
+
}
|
|
2943
|
+
markChatRead(sequence) {
|
|
2944
|
+
return this.#collaborationState.markChatRead(sequence);
|
|
2945
|
+
}
|
|
2946
|
+
readChatPage(input) {
|
|
2947
|
+
return this.#collaborationState.readChatPage(input);
|
|
2948
|
+
}
|
|
2949
|
+
setHandRaised(raised, options) {
|
|
2950
|
+
return this.#sendTarget({ name: "set_hand_raised", payload: { raised } }, options);
|
|
2951
|
+
}
|
|
2952
|
+
setDisplayName(displayName, options) {
|
|
2953
|
+
return this.#sendTarget({ name: "set_display_name", payload: { display_name: displayName } }, options);
|
|
2954
|
+
}
|
|
2955
|
+
setAdmissionPolicy(policy, options) {
|
|
2956
|
+
return this.#sendTarget({ name: "set_admission_policy", payload: { policy } }, options);
|
|
2957
|
+
}
|
|
2958
|
+
assignRole(participantId, role, options) {
|
|
2959
|
+
return this.#sendTarget({ name: "assign_roles", payload: { participant_id: participantId, role } }, options);
|
|
2960
|
+
}
|
|
2961
|
+
admit(admissionRequestId, options) {
|
|
2962
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "admit_participant", payload: { admission_request_id: admissionRequestId } }), options);
|
|
2963
|
+
}
|
|
2964
|
+
deny(admissionRequestId, options) {
|
|
2965
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "deny_admission", payload: { admission_request_id: admissionRequestId } }), options);
|
|
2966
|
+
}
|
|
2967
|
+
muteParticipant(participantId, options) {
|
|
2968
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "mute_participant", payload: { participant_id: participantId } }), options);
|
|
2168
2969
|
}
|
|
2169
2970
|
stopParticipantCamera(participantId, options) {
|
|
2170
|
-
return this.#sendOperation("stop_participant_camera", { participant_id: participantId }, options);
|
|
2971
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_participant_camera", payload: { participant_id: participantId } }), options);
|
|
2171
2972
|
}
|
|
2172
2973
|
stopParticipantScreenShare(participantId, options) {
|
|
2173
|
-
return this.#sendOperation("stop_participant_screen_share", { participant_id: participantId }, options);
|
|
2974
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_participant_screen_share", payload: { participant_id: participantId } }), options);
|
|
2174
2975
|
}
|
|
2175
2976
|
removeParticipant(participantId, options) {
|
|
2176
|
-
return this.#sendOperation("remove_participant", { participant_id: participantId }, options);
|
|
2977
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "remove_participant", payload: { participant_id: participantId } }), options);
|
|
2177
2978
|
}
|
|
2178
2979
|
startRecording(options) {
|
|
2179
2980
|
const recordingId = options?.recordingId ?? this.#nextRequestId();
|
|
2180
|
-
return this.#sendOperation("start_recording", { recording_id: recordingId }, options).then((result) => ({ recordingId, result }));
|
|
2981
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "start_recording", payload: { recording_id: recordingId } }), options).then((result) => ({ recordingId, result }));
|
|
2181
2982
|
}
|
|
2182
2983
|
stopRecording(recordingId, options) {
|
|
2183
|
-
return this.#sendOperation("stop_recording", { recording_id: recordingId }, options);
|
|
2984
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_recording", payload: { recording_id: recordingId } }), options);
|
|
2184
2985
|
}
|
|
2185
2986
|
leave(options) {
|
|
2186
|
-
return this.#sendOperation("participant_leave", {}, options);
|
|
2987
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "participant_leave", payload: {} }), options);
|
|
2187
2988
|
}
|
|
2188
2989
|
endEpisode(options) {
|
|
2189
|
-
return this.#sendOperation("end_episode", {}, options);
|
|
2990
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "end_episode", payload: {} }), options);
|
|
2190
2991
|
}
|
|
2191
2992
|
extendEpisode(minutes, options) {
|
|
2192
2993
|
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);
|
|
2994
|
+
return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "extend_episode", payload: { extension_seconds: minutes * 60 } }), options);
|
|
2194
2995
|
}
|
|
2195
2996
|
setMicrophoneEnabled(enabled, options) {
|
|
2196
2997
|
return this.#sendLiveTarget("set_microphone_enabled", "microphone", enabled, options);
|
|
@@ -2208,68 +3009,13 @@ var V1SyncClient = class {
|
|
|
2208
3009
|
return this.#sendDirectedRequest("request_start_camera", participantId, options);
|
|
2209
3010
|
}
|
|
2210
3011
|
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
|
-
}
|
|
3012
|
+
return this.#commandScheduler.sendTarget(command, this.#liveTargets.pendingCount + this.#requests.size, options);
|
|
2229
3013
|
}
|
|
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;
|
|
3014
|
+
#sendOperation(createFrame, options) {
|
|
3015
|
+
return this.#commandScheduler.sendOperation(createFrame, this.#liveTargets.pendingCount + this.#requests.size, options);
|
|
2239
3016
|
}
|
|
2240
3017
|
#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;
|
|
3018
|
+
return this.#liveTargets.send(name, source, enabled, options);
|
|
2273
3019
|
}
|
|
2274
3020
|
#sendDirectedRequest(name, participantId, options) {
|
|
2275
3021
|
this.#assertCapacity();
|
|
@@ -2282,18 +3028,18 @@ var V1SyncClient = class {
|
|
|
2282
3028
|
this.#send(frame);
|
|
2283
3029
|
return promise;
|
|
2284
3030
|
}
|
|
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
3031
|
#connect() {
|
|
2290
3032
|
if (!this.#started || !this.#transportAvailable || this.#socket) return;
|
|
2291
3033
|
this.#phase = { phase: "connecting" };
|
|
2292
3034
|
this.#emit();
|
|
3035
|
+
recordReconnect(this.#options.recordReconnect, "sync_attempt", { attempt: this.#reconnectAttempt });
|
|
2293
3036
|
const socket = this.#options.webSocket.connect(this.#options.url);
|
|
2294
3037
|
const connectionGeneration = ++this.#connectionGeneration;
|
|
2295
3038
|
this.#socket = socket;
|
|
2296
|
-
socket.onopen = () =>
|
|
3039
|
+
socket.onopen = () => {
|
|
3040
|
+
recordReconnect(this.#options.recordReconnect, "sync_socket_open");
|
|
3041
|
+
void this.#authenticate(socket);
|
|
3042
|
+
};
|
|
2297
3043
|
socket.onmessage = (event) => {
|
|
2298
3044
|
this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
|
|
2299
3045
|
};
|
|
@@ -2322,14 +3068,11 @@ var V1SyncClient = class {
|
|
|
2322
3068
|
},
|
|
2323
3069
|
...this.#options.telemetry ? syncTelemetryCorrelation(this.#options.telemetry) : {}
|
|
2324
3070
|
};
|
|
2325
|
-
|
|
3071
|
+
const collaborationExtension = this.#collaborationState.helloExtension;
|
|
3072
|
+
if (!collaborationExtension) {
|
|
2326
3073
|
this.#send(hello);
|
|
2327
3074
|
} 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 }] });
|
|
3075
|
+
this.#send({ ...hello, extensions: [collaborationExtension] });
|
|
2333
3076
|
}
|
|
2334
3077
|
this.#emit();
|
|
2335
3078
|
} catch {
|
|
@@ -2340,7 +3083,16 @@ var V1SyncClient = class {
|
|
|
2340
3083
|
if (socket !== this.#socket) return;
|
|
2341
3084
|
try {
|
|
2342
3085
|
if (typeof data !== "string" || encoder3.encode(data).byteLength > SyncProtocolLimits.snapshotEncodedBytes) throw new V1ReplicaError("invalid inbound frame size");
|
|
2343
|
-
|
|
3086
|
+
const frame = decodeV1ServerFrame(data);
|
|
3087
|
+
if (this.#phase.phase === "live") {
|
|
3088
|
+
this.#noticeLastInboundAt = this.#now();
|
|
3089
|
+
this.#scheduleNoticeCheck();
|
|
3090
|
+
if (this.#noticeUnresponsive) {
|
|
3091
|
+
this.#noticeUnresponsive = false;
|
|
3092
|
+
this.#emit();
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
await this.#handleFrame(frame);
|
|
2344
3096
|
} catch {
|
|
2345
3097
|
this.#recover("invalid_frame");
|
|
2346
3098
|
}
|
|
@@ -2371,24 +3123,28 @@ var V1SyncClient = class {
|
|
|
2371
3123
|
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
2372
3124
|
this.#rememberControlEvent(frame);
|
|
2373
3125
|
}
|
|
2374
|
-
await this.#
|
|
2375
|
-
await this.#
|
|
3126
|
+
await this.#commandScheduler.settleTerminalLifecycle(frame, this.#participantId);
|
|
3127
|
+
await this.#commandScheduler.settleProven();
|
|
2376
3128
|
{
|
|
2377
3129
|
const control = this.#requireControl();
|
|
2378
3130
|
this.#send({ type: "delivery_ack", stream: "control", revision: control.revision, state_digest: control.stateDigest });
|
|
2379
3131
|
}
|
|
3132
|
+
if (frame.name === "participant_left" && frame.payload.participant_id === this.#participantId) {
|
|
3133
|
+
this.#phase = { phase: "terminal", terminalReason: "participant_inactive" };
|
|
3134
|
+
this.#socket?.close(1e3, "participant inactive");
|
|
3135
|
+
}
|
|
2380
3136
|
this.#emit();
|
|
2381
3137
|
return;
|
|
2382
3138
|
case "ack":
|
|
2383
3139
|
this.#requireLive();
|
|
2384
|
-
await this.#
|
|
3140
|
+
await this.#commandScheduler.acknowledge(frame);
|
|
2385
3141
|
return;
|
|
2386
3142
|
case "retryable_error":
|
|
2387
3143
|
this.#requireLive();
|
|
2388
|
-
this.#
|
|
3144
|
+
this.#commandScheduler.retry(frame.command_id, frame.code);
|
|
2389
3145
|
return;
|
|
2390
3146
|
case "live_target_result":
|
|
2391
|
-
this.#
|
|
3147
|
+
this.#liveTargets.receive(frame);
|
|
2392
3148
|
return;
|
|
2393
3149
|
case "directed_request_result":
|
|
2394
3150
|
this.#requestResult(frame);
|
|
@@ -2397,43 +3153,28 @@ var V1SyncClient = class {
|
|
|
2397
3153
|
this.#directedRequest(frame);
|
|
2398
3154
|
return;
|
|
2399
3155
|
case "reaction":
|
|
2400
|
-
this.#
|
|
2401
|
-
this.#requireCollaborationNegotiated();
|
|
2402
|
-
this.#emitCollaboration({ type: "reaction", reaction: reactionFromFrame(frame) });
|
|
3156
|
+
this.#collaborationState.reaction(frame);
|
|
2403
3157
|
return;
|
|
2404
3158
|
case "reaction_result":
|
|
2405
|
-
this.#reactionResult(frame);
|
|
3159
|
+
this.#collaborationState.reactionResult(frame);
|
|
2406
3160
|
return;
|
|
2407
3161
|
case "chat_message":
|
|
2408
|
-
this.#
|
|
2409
|
-
this.#observeChatMessage(frame);
|
|
3162
|
+
this.#collaborationState.chatMessage(frame);
|
|
2410
3163
|
return;
|
|
2411
3164
|
case "chat_send_result":
|
|
2412
|
-
this.#chatSendResult(frame);
|
|
3165
|
+
this.#collaborationState.chatSendResult(frame);
|
|
2413
3166
|
return;
|
|
2414
3167
|
case "chat_page":
|
|
2415
|
-
this.#
|
|
3168
|
+
this.#collaborationState.chatPage(frame);
|
|
2416
3169
|
return;
|
|
2417
3170
|
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();
|
|
3171
|
+
this.#collaborationState.chatHead(frame);
|
|
2425
3172
|
return;
|
|
2426
3173
|
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
|
-
}
|
|
3174
|
+
this.#collaborationState.chatReadReceipt(frame);
|
|
2434
3175
|
return;
|
|
2435
3176
|
case "chat_read_result":
|
|
2436
|
-
this.#chatReadResult(frame);
|
|
3177
|
+
this.#collaborationState.chatReadResult(frame);
|
|
2437
3178
|
return;
|
|
2438
3179
|
case "error":
|
|
2439
3180
|
throw new V1ReplicaError(frame.code);
|
|
@@ -2445,29 +3186,12 @@ var V1SyncClient = class {
|
|
|
2445
3186
|
}
|
|
2446
3187
|
async #welcome(frame) {
|
|
2447
3188
|
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
|
-
}
|
|
3189
|
+
this.#collaborationState.acceptWelcome(frame);
|
|
2466
3190
|
this.#participantId = frame.participant_id;
|
|
2467
3191
|
this.#participantGeneration = frame.participant_generation;
|
|
2468
3192
|
this.#updateLocalMediaStates();
|
|
2469
3193
|
if (frame.mode === "terminal") {
|
|
2470
|
-
await this.#
|
|
3194
|
+
await this.#commandScheduler.settleTerminalRecovery(frame);
|
|
2471
3195
|
this.#phase = { phase: "terminal", terminalReason: frame.reason };
|
|
2472
3196
|
this.#socket?.close(1e3, "terminal recovery");
|
|
2473
3197
|
this.#emit();
|
|
@@ -2482,8 +3206,9 @@ var V1SyncClient = class {
|
|
|
2482
3206
|
if (frame.mode === "replay" && (!this.#control || this.#control.revision >= frame.head.revision)) throw new V1ReplicaError("invalid replay welcome");
|
|
2483
3207
|
if (frame.mode === "up_to_date" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("up-to-date head mismatch");
|
|
2484
3208
|
if (frame.mode === "snapshot" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
|
|
3209
|
+
recordReconnect(this.#options.recordReconnect, "sync_reconnected");
|
|
2485
3210
|
this.#recovery = { id: frame.recovery_id, head: frame.head, replayEvents: 0, replayBytes: 0, controlComplete: false };
|
|
2486
|
-
if (frame.mode
|
|
3211
|
+
if (frame.mode === "snapshot") this.#ackRecovery();
|
|
2487
3212
|
this.#emit();
|
|
2488
3213
|
}
|
|
2489
3214
|
async #replay(frame) {
|
|
@@ -2494,251 +3219,71 @@ var V1SyncClient = class {
|
|
|
2494
3219
|
if (recovery.replayEvents > MAX_REPLAY_EVENTS || recovery.replayBytes > MAX_REPLAY_BYTES) throw new V1ReplicaError("replay exceeds client bounds");
|
|
2495
3220
|
for (const event of frame.events) {
|
|
2496
3221
|
this.#control = await applyV1Event(this.#requireControl(), event);
|
|
2497
|
-
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);
|
|
3222
|
+
this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
|
|
3223
|
+
this.#rememberControlEvent(event);
|
|
2643
3224
|
}
|
|
2644
|
-
|
|
3225
|
+
this.#ackRecovery();
|
|
2645
3226
|
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
3227
|
}
|
|
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);
|
|
3228
|
+
#completeRecovery(frame) {
|
|
3229
|
+
const recovery = this.#requireRecovery();
|
|
3230
|
+
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");
|
|
3231
|
+
recovery.controlComplete = true;
|
|
3232
|
+
this.#enterLiveIfReady();
|
|
2681
3233
|
}
|
|
2682
|
-
#
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3234
|
+
#replaceProjection(frame) {
|
|
3235
|
+
if (this.#phase.phase !== "recovering" && this.#phase.phase !== "live") throw new V1ReplicaError("projection snapshot arrived in the wrong phase");
|
|
3236
|
+
if (frame.stream === "media") {
|
|
3237
|
+
const previous = this.#media?.items ?? [];
|
|
3238
|
+
this.#media = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(mediaItem) };
|
|
3239
|
+
this.#mediaEventEvidence.clear();
|
|
3240
|
+
for (const item of this.#media.items)
|
|
3241
|
+
this.#notifyRemotePublicationResumed(
|
|
3242
|
+
previous.find((candidate) => mediaKey(candidate) === mediaKey(item)),
|
|
3243
|
+
item
|
|
3244
|
+
);
|
|
3245
|
+
if (!this.#mediaEnded()) this.#options.mediaPlane?.remotePublicationsChanged?.();
|
|
3246
|
+
} else {
|
|
3247
|
+
this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
|
|
3248
|
+
this.#presenceEventEvidence.clear();
|
|
2693
3249
|
}
|
|
2694
|
-
|
|
3250
|
+
this.#enterLiveIfReady();
|
|
3251
|
+
this.#emit();
|
|
2695
3252
|
}
|
|
2696
|
-
#
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3253
|
+
#applyProjection(frame) {
|
|
3254
|
+
this.#requireLive();
|
|
3255
|
+
if (frame.stream === "media") {
|
|
3256
|
+
if (this.#acceptProjectionDuplicate(this.#media, this.#mediaEventEvidence, frame)) return;
|
|
3257
|
+
const item = mediaItem(frame.item);
|
|
3258
|
+
const previous = this.#media?.items.find((candidate) => mediaKey(candidate) === mediaKey(item));
|
|
3259
|
+
this.#media = updateProjection(this.#media, frame.projection_id, frame.sequence, item, mediaKey);
|
|
3260
|
+
rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
3261
|
+
this.#notifyRemotePublicationResumed(previous, item);
|
|
3262
|
+
if (!this.#mediaEnded() && item.participantId !== this.#participantId && (previous?.enabled !== item.enabled || previous?.publicationId !== item.publicationId)) {
|
|
3263
|
+
this.#options.mediaPlane?.remotePublicationsChanged?.();
|
|
2703
3264
|
}
|
|
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;
|
|
3265
|
+
} else {
|
|
3266
|
+
if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
|
|
3267
|
+
this.#presence = updateProjection(this.#presence, frame.projection_id, frame.sequence, presenceItem(frame.item), (item) => item.participantId);
|
|
3268
|
+
rememberBoundedEvidence(this.#presenceEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
2711
3269
|
}
|
|
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
3270
|
this.#emit();
|
|
2720
3271
|
}
|
|
2721
|
-
#
|
|
2722
|
-
|
|
2723
|
-
this.#liveRetryTimers.delete(operationId);
|
|
2724
|
-
retry();
|
|
2725
|
-
}, delay);
|
|
2726
|
-
this.#liveRetryTimers.set(operationId, timer);
|
|
3272
|
+
#mediaEnded() {
|
|
3273
|
+
return this.#control?.status === "ended" || this.#phase.phase === "terminal";
|
|
2727
3274
|
}
|
|
2728
|
-
#
|
|
2729
|
-
this.#
|
|
2730
|
-
this.#
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
rejectDeferred(deferred, error);
|
|
2734
|
-
this.#emit();
|
|
3275
|
+
#notifyRemotePublicationResumed(previous, current) {
|
|
3276
|
+
if (this.#mediaEnded()) return;
|
|
3277
|
+
if (previous?.enabled === false && current.enabled && current.publicationId && previous.publicationId === current.publicationId && current.participantId !== this.#participantId) {
|
|
3278
|
+
this.#options.mediaPlane?.remotePublicationResumed?.(current.publicationId);
|
|
3279
|
+
}
|
|
2735
3280
|
}
|
|
2736
3281
|
#requestResult(frame) {
|
|
2737
3282
|
this.#requireLive();
|
|
2738
3283
|
const deferred = this.#requests.get(frame.request_id);
|
|
2739
3284
|
if (!deferred) return;
|
|
2740
3285
|
this.#requests.delete(frame.request_id);
|
|
2741
|
-
|
|
3286
|
+
resolveV1Deferred(deferred, frame);
|
|
2742
3287
|
}
|
|
2743
3288
|
#directedRequest(frame) {
|
|
2744
3289
|
this.#requireLive();
|
|
@@ -2746,130 +3291,17 @@ var V1SyncClient = class {
|
|
|
2746
3291
|
this.#send({ type: "request_ack", request_id: frame.request_id });
|
|
2747
3292
|
for (const listener of this.#requestListeners) listener(frame);
|
|
2748
3293
|
}
|
|
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
3294
|
#enterLiveIfReady() {
|
|
2858
3295
|
if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
|
|
3296
|
+
recordReconnect(this.#options.recordReconnect, "state_resynced");
|
|
2859
3297
|
this.#recovery = null;
|
|
2860
3298
|
this.#phase = { phase: "live" };
|
|
2861
3299
|
this.#reconnectAttempt = 0;
|
|
2862
3300
|
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
|
-
}
|
|
3301
|
+
this.#commandScheduler.enterLive();
|
|
3302
|
+
this.#liveTargets.enterLive();
|
|
2872
3303
|
this.#startHeartbeat();
|
|
3304
|
+
this.#startNoticeProbe();
|
|
2873
3305
|
this.#emit();
|
|
2874
3306
|
}
|
|
2875
3307
|
#ackRecovery() {
|
|
@@ -2877,36 +3309,6 @@ var V1SyncClient = class {
|
|
|
2877
3309
|
const control = this.#requireControl();
|
|
2878
3310
|
this.#send({ type: "recovery_ack", recovery_id: recovery.id, revision: control.revision, state_digest: control.stateDigest });
|
|
2879
3311
|
}
|
|
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
3312
|
#ackHeadIsProven(ack) {
|
|
2911
3313
|
return this.#controlHeads.get(ack.revision) === ack.state_digest;
|
|
2912
3314
|
}
|
|
@@ -2926,25 +3328,31 @@ var V1SyncClient = class {
|
|
|
2926
3328
|
}
|
|
2927
3329
|
#disconnected(socket) {
|
|
2928
3330
|
if (socket !== this.#socket) return;
|
|
3331
|
+
recordReconnect(this.#options.recordReconnect, "sync_loss");
|
|
2929
3332
|
this.#socket = null;
|
|
2930
3333
|
this.#recovery = null;
|
|
2931
3334
|
this.#clearHeartbeat();
|
|
2932
|
-
this.#
|
|
3335
|
+
this.#clearNoticeProbe();
|
|
3336
|
+
this.#liveTargets.disconnect("disconnected_before_delivery");
|
|
3337
|
+
this.#commandScheduler.disconnect();
|
|
2933
3338
|
this.#media = null;
|
|
2934
3339
|
this.#presence = null;
|
|
2935
3340
|
this.#rejectRequests("disconnected_before_delivery");
|
|
2936
|
-
this.#
|
|
2937
|
-
this.#collaboration = { ...this.#collaboration, negotiated: false, version: null, capabilities: [], readReceipts: [] };
|
|
2938
|
-
this.#participantCollaborationCapabilities = {};
|
|
3341
|
+
this.#collaborationState.disconnect("disconnected_before_delivery");
|
|
2939
3342
|
if (!this.#started || !this.#transportAvailable || this.#phase.phase === "terminal") return;
|
|
2940
3343
|
this.#phase = { phase: "connecting" };
|
|
2941
3344
|
this.#emit();
|
|
2942
3345
|
this.#clearReconnect();
|
|
2943
3346
|
const configuredDelay = this.#options.reconnectDelayMs;
|
|
2944
|
-
const
|
|
3347
|
+
const exponentialAttempt = configuredDelay === void 0 ? Math.max(0, this.#reconnectAttempt - FAST_RECONNECT_ATTEMPTS + 1) : this.#reconnectAttempt;
|
|
3348
|
+
const baseDelay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(exponentialAttempt, 5));
|
|
3349
|
+
const delay = configuredDelay === void 0 ? Math.round(baseDelay * (0.8 + 0.4 * Math.random())) : baseDelay;
|
|
3350
|
+
recordReconnect(this.#options.recordReconnect, "sync_backoff", { boundary: "start", delay_ms: delay, attempt: this.#reconnectAttempt });
|
|
2945
3351
|
this.#reconnectAttempt += 1;
|
|
3352
|
+
const backoffStartedAt = this.#now();
|
|
2946
3353
|
this.#reconnectTimer = this.#clock().setTimeout(() => {
|
|
2947
3354
|
this.#reconnectTimer = void 0;
|
|
3355
|
+
recordReconnect(this.#options.recordReconnect, "sync_backoff", { boundary: "end", duration_ms: Math.max(0, this.#now() - backoffStartedAt) });
|
|
2948
3356
|
this.#connect();
|
|
2949
3357
|
}, delay);
|
|
2950
3358
|
}
|
|
@@ -2955,6 +3363,7 @@ var V1SyncClient = class {
|
|
|
2955
3363
|
this.#disconnected(socket);
|
|
2956
3364
|
}
|
|
2957
3365
|
#handleLifecycle(event) {
|
|
3366
|
+
recordReconnect(this.#options.recordReconnect, "network_change", { lifecycle: event });
|
|
2958
3367
|
if (event === "online" || event === "offline") this.#online = event === "online";
|
|
2959
3368
|
else this.#active = event === "active";
|
|
2960
3369
|
this.#transportAvailable = this.#online && this.#active;
|
|
@@ -2963,21 +3372,10 @@ var V1SyncClient = class {
|
|
|
2963
3372
|
}
|
|
2964
3373
|
#rejectEphemeral(code) {
|
|
2965
3374
|
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();
|
|
3375
|
+
this.#liveTargets.disconnect(code);
|
|
2978
3376
|
}
|
|
2979
3377
|
#rejectRequests(code) {
|
|
2980
|
-
for (const deferred of this.#requests.values())
|
|
3378
|
+
for (const deferred of this.#requests.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
|
|
2981
3379
|
this.#requests.clear();
|
|
2982
3380
|
}
|
|
2983
3381
|
#sendIfLive(frame) {
|
|
@@ -2989,13 +3387,6 @@ var V1SyncClient = class {
|
|
|
2989
3387
|
#requireLive() {
|
|
2990
3388
|
if (this.#phase.phase !== "live") throw new V1ReplicaError("live frame arrived before four-stream recovery completed");
|
|
2991
3389
|
}
|
|
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
3390
|
#requireControl() {
|
|
3000
3391
|
if (!this.#control) throw new V1ReplicaError("control replica is unavailable");
|
|
3001
3392
|
return this.#control;
|
|
@@ -3005,33 +3396,7 @@ var V1SyncClient = class {
|
|
|
3005
3396
|
return this.#recovery;
|
|
3006
3397
|
}
|
|
3007
3398
|
#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();
|
|
3399
|
+
this.#commandScheduler.assertCapacity(this.#liveTargets.pendingCount + this.#requests.size);
|
|
3035
3400
|
}
|
|
3036
3401
|
#nextRequestId() {
|
|
3037
3402
|
return this.#options.requestIds?.next() ?? crypto.randomUUID();
|
|
@@ -3080,47 +3445,7 @@ var V1SyncClient = class {
|
|
|
3080
3445
|
this.#emit();
|
|
3081
3446
|
}
|
|
3082
3447
|
#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);
|
|
3448
|
+
this.#liveTargets.updateFromPublications(this.#localPublications);
|
|
3124
3449
|
}
|
|
3125
3450
|
#startHeartbeat() {
|
|
3126
3451
|
this.#clearHeartbeat();
|
|
@@ -3141,6 +3466,36 @@ var V1SyncClient = class {
|
|
|
3141
3466
|
this.#clock().clearTimeout(this.#heartbeatTimer);
|
|
3142
3467
|
this.#heartbeatTimer = void 0;
|
|
3143
3468
|
}
|
|
3469
|
+
#startNoticeProbe() {
|
|
3470
|
+
this.#clearNoticeProbe();
|
|
3471
|
+
this.#noticeLastInboundAt = this.#now();
|
|
3472
|
+
this.#scheduleNoticeCheck();
|
|
3473
|
+
const tick = () => {
|
|
3474
|
+
this.#noticeProbeTimer = void 0;
|
|
3475
|
+
if (this.#phase.phase !== "live") return;
|
|
3476
|
+
this.#send({ type: "ping" });
|
|
3477
|
+
this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
|
|
3478
|
+
};
|
|
3479
|
+
this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
|
|
3480
|
+
}
|
|
3481
|
+
#scheduleNoticeCheck() {
|
|
3482
|
+
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3483
|
+
const remaining = Math.max(0, NOTICE_SILENCE_MS - (this.#now() - this.#noticeLastInboundAt));
|
|
3484
|
+
this.#noticeTimer = this.#clock().setTimeout(() => {
|
|
3485
|
+
this.#noticeTimer = void 0;
|
|
3486
|
+
if (this.#phase.phase !== "live" || this.#noticeUnresponsive) return;
|
|
3487
|
+
if (this.#now() - this.#noticeLastInboundAt < NOTICE_SILENCE_MS) return this.#scheduleNoticeCheck();
|
|
3488
|
+
this.#noticeUnresponsive = true;
|
|
3489
|
+
this.#emit();
|
|
3490
|
+
}, remaining);
|
|
3491
|
+
}
|
|
3492
|
+
#clearNoticeProbe() {
|
|
3493
|
+
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3494
|
+
if (this.#noticeProbeTimer !== void 0) this.#clock().clearTimeout(this.#noticeProbeTimer);
|
|
3495
|
+
this.#noticeTimer = void 0;
|
|
3496
|
+
this.#noticeProbeTimer = void 0;
|
|
3497
|
+
this.#noticeUnresponsive = false;
|
|
3498
|
+
}
|
|
3144
3499
|
#emit() {
|
|
3145
3500
|
const snapshot = this.getSnapshot();
|
|
3146
3501
|
for (const listener of this.#listeners) listener(snapshot);
|
|
@@ -3170,51 +3525,17 @@ function sameRawHead(left, right) {
|
|
|
3170
3525
|
function copyProjection(projection) {
|
|
3171
3526
|
return projection && { ...projection, items: projection.items.map((item) => ({ ...item })) };
|
|
3172
3527
|
}
|
|
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
3528
|
function rememberBoundedEvidence(evidence, sequence, signature, capacity) {
|
|
3192
3529
|
evidence.set(sequence, signature);
|
|
3193
3530
|
if (evidence.size <= capacity) return;
|
|
3194
3531
|
const oldest = evidence.keys().next().value;
|
|
3195
3532
|
if (oldest !== void 0) evidence.delete(oldest);
|
|
3196
3533
|
}
|
|
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
3534
|
function copyPublication(publication) {
|
|
3208
3535
|
return { ...publication };
|
|
3209
3536
|
}
|
|
3210
3537
|
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");
|
|
3538
|
+
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
3539
|
}
|
|
3219
3540
|
function assertV1Url(value) {
|
|
3220
3541
|
const url = new URL(value);
|
|
@@ -3371,20 +3692,21 @@ export {
|
|
|
3371
3692
|
createBrowserSyncLifecycle,
|
|
3372
3693
|
createReactNativeWebSocketFactory,
|
|
3373
3694
|
createReactNativeSyncLifecycle,
|
|
3374
|
-
|
|
3375
|
-
chatMessageFromFrame,
|
|
3376
|
-
chatReadReceiptFromFrame,
|
|
3695
|
+
ChatAttachmentMimeTypeSchema,
|
|
3377
3696
|
encodeV1ClientFrame,
|
|
3378
3697
|
decodeV1ServerFrame,
|
|
3379
3698
|
decodeV1ClientFrame,
|
|
3380
|
-
|
|
3699
|
+
reactionFromFrame,
|
|
3700
|
+
chatMessageFromFrame,
|
|
3701
|
+
chatReadReceiptFromFrame,
|
|
3702
|
+
V1SyncError,
|
|
3381
3703
|
V1ReplicaError,
|
|
3382
3704
|
restoreV1Snapshot,
|
|
3383
3705
|
applyV1Event,
|
|
3384
3706
|
optimisticV1Control,
|
|
3385
3707
|
assertV1ControlSemantics,
|
|
3386
3708
|
computeV1StateDigest,
|
|
3387
|
-
|
|
3709
|
+
InMemoryV1PendingTargetStore,
|
|
3388
3710
|
V1SyncClient,
|
|
3389
3711
|
IndexedDbV1PendingTargetStore,
|
|
3390
3712
|
AsyncStorageV1PendingTargetStore,
|