@q9labsai/chalk-client 4.1.14 → 4.1.16

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