@q9labsai/chalk-client 4.1.15 → 4.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +100 -5
  2. package/dist/access/grant.d.ts.map +1 -1
  3. package/dist/access/manager.d.ts.map +1 -1
  4. package/dist/{chunk-PQNSA7ZN.js → chunk-2DZUHV6I.js} +782 -122
  5. package/dist/{chunk-6ONSFCIY.js → chunk-A7BVZEFM.js} +7 -3
  6. package/dist/{chunk-LQSXEWLL.js → chunk-AUQTQOLR.js} +2685 -511
  7. package/dist/{chunk-PWUU6T3R.js → chunk-CZUJNAUI.js} +1196 -874
  8. package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
  9. package/dist/chunk-HW5TTZL2.js +99 -0
  10. package/dist/chunk-RCDXIDCN.js +33 -0
  11. package/dist/{chunk-LSDDADCN.js → chunk-ZRMGKMX4.js} +1728 -517
  12. package/dist/client.d.ts +1452 -465
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/connection/dependencies.d.ts +11 -0
  15. package/dist/connection/dependencies.d.ts.map +1 -1
  16. package/dist/connection/diagnostics.d.ts +1 -1
  17. package/dist/connection/diagnostics.d.ts.map +1 -1
  18. package/dist/connection/index.d.ts +3 -0
  19. package/dist/connection/index.d.ts.map +1 -1
  20. package/dist/connection/lifecycle.d.ts +4 -1
  21. package/dist/connection/lifecycle.d.ts.map +1 -1
  22. package/dist/connection/media-devices.d.ts.map +1 -1
  23. package/dist/connection/production.d.ts +4 -1
  24. package/dist/connection/production.d.ts.map +1 -1
  25. package/dist/connection/types.d.ts +2 -2
  26. package/dist/connection/types.d.ts.map +1 -1
  27. package/dist/effect.js +355 -130
  28. package/dist/feedback/collector.d.ts +14 -0
  29. package/dist/feedback/collector.d.ts.map +1 -0
  30. package/dist/feedback/context.d.ts +38 -0
  31. package/dist/feedback/context.d.ts.map +1 -0
  32. package/dist/feedback/controller.d.ts +4 -0
  33. package/dist/feedback/controller.d.ts.map +1 -0
  34. package/dist/feedback/index.d.ts +7 -0
  35. package/dist/feedback/index.d.ts.map +1 -0
  36. package/dist/feedback/transport.d.ts +29 -0
  37. package/dist/feedback/transport.d.ts.map +1 -0
  38. package/dist/feedback/types.d.ts +202 -0
  39. package/dist/feedback/types.d.ts.map +1 -0
  40. package/dist/feedback/validation.d.ts +30 -0
  41. package/dist/feedback/validation.d.ts.map +1 -0
  42. package/dist/foreground-deadline.d.ts +9 -0
  43. package/dist/foreground-deadline.d.ts.map +1 -0
  44. package/dist/generated/http-api.d.ts +1438 -461
  45. package/dist/generated/http-api.d.ts.map +1 -1
  46. package/dist/generated/schemas.d.ts +2667 -842
  47. package/dist/generated/schemas.d.ts.map +1 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +136 -21
  51. package/dist/media/camera-uplink.d.ts +13 -0
  52. package/dist/media/camera-uplink.d.ts.map +1 -0
  53. package/dist/media/capture-error.d.ts +27 -0
  54. package/dist/media/capture-error.d.ts.map +1 -0
  55. package/dist/media/client.d.ts +4 -0
  56. package/dist/media/client.d.ts.map +1 -1
  57. package/dist/media/index.d.ts +1 -0
  58. package/dist/media/index.d.ts.map +1 -1
  59. package/dist/media/index.js +5 -1
  60. package/dist/media/plane.d.ts +4 -0
  61. package/dist/media/plane.d.ts.map +1 -1
  62. package/dist/media/remote-track-response.d.ts +6 -0
  63. package/dist/media/remote-track-response.d.ts.map +1 -0
  64. package/dist/media/rtk.d.ts.map +1 -1
  65. package/dist/media/transport.d.ts.map +1 -1
  66. package/dist/media/types.d.ts +20 -2
  67. package/dist/media/types.d.ts.map +1 -1
  68. package/dist/public-invites/client.d.ts.map +1 -1
  69. package/dist/public-invites/index.js +4 -3
  70. package/dist/public-invites/types.d.ts +4 -2
  71. package/dist/public-invites/types.d.ts.map +1 -1
  72. package/dist/server/access-refresh.d.cts +13 -0
  73. package/dist/server/access-refresh.d.ts +14 -0
  74. package/dist/server/access-refresh.d.ts.map +1 -0
  75. package/dist/server/client.d.ts.map +1 -1
  76. package/dist/server/index.cjs +189 -23
  77. package/dist/server/index.d.cts +2 -1
  78. package/dist/server/index.d.ts +2 -1
  79. package/dist/server/index.d.ts.map +1 -1
  80. package/dist/server/index.js +187 -22
  81. package/dist/server/types.d.cts +76 -1
  82. package/dist/server/types.d.ts +76 -1
  83. package/dist/server/types.d.ts.map +1 -1
  84. package/dist/space-client/chat-controller.d.ts +3 -0
  85. package/dist/space-client/chat-controller.d.ts.map +1 -1
  86. package/dist/space-client/controller-effects.d.ts +6 -0
  87. package/dist/space-client/controller-effects.d.ts.map +1 -1
  88. package/dist/space-client/core.d.ts +12 -1
  89. package/dist/space-client/core.d.ts.map +1 -1
  90. package/dist/space-client/core.test.helpers.d.ts +2 -1
  91. package/dist/space-client/core.test.helpers.d.ts.map +1 -1
  92. package/dist/space-client/effect.d.ts +5 -0
  93. package/dist/space-client/effect.d.ts.map +1 -1
  94. package/dist/space-client/episode-diagnostic-runtime.d.ts +3 -0
  95. package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -1
  96. package/dist/space-client/errors.d.ts.map +1 -1
  97. package/dist/space-client/index.d.ts +4 -1
  98. package/dist/space-client/index.d.ts.map +1 -1
  99. package/dist/space-client/media-controller.d.ts.map +1 -1
  100. package/dist/space-client/offline-actions-storage.d.ts +66 -0
  101. package/dist/space-client/offline-actions-storage.d.ts.map +1 -0
  102. package/dist/space-client/offline-actions.d.ts +36 -0
  103. package/dist/space-client/offline-actions.d.ts.map +1 -0
  104. package/dist/space-client/participants-controller.d.ts +2 -1
  105. package/dist/space-client/participants-controller.d.ts.map +1 -1
  106. package/dist/space-client/recording-controller.d.ts +20 -0
  107. package/dist/space-client/recording-controller.d.ts.map +1 -0
  108. package/dist/space-client/reload-rejoin.d.ts +24 -0
  109. package/dist/space-client/reload-rejoin.d.ts.map +1 -0
  110. package/dist/space-client/space-client.d.ts.map +1 -1
  111. package/dist/space-client/store.d.ts +3 -1
  112. package/dist/space-client/store.d.ts.map +1 -1
  113. package/dist/space-client/terminal.test.helpers.d.ts +115 -0
  114. package/dist/space-client/terminal.test.helpers.d.ts.map +1 -0
  115. package/dist/space-client/types.d.ts +42 -4
  116. package/dist/space-client/types.d.ts.map +1 -1
  117. package/dist/sync/index.js +2 -1
  118. package/dist/sync/v1-client.d.ts +2 -5
  119. package/dist/sync/v1-client.d.ts.map +1 -1
  120. package/dist/sync/v1-collaboration-state.d.ts +65 -0
  121. package/dist/sync/v1-collaboration-state.d.ts.map +1 -0
  122. package/dist/sync/v1-command-scheduler.d.ts +54 -0
  123. package/dist/sync/v1-command-scheduler.d.ts.map +1 -0
  124. package/dist/sync/v1-deferred.d.ts +8 -0
  125. package/dist/sync/v1-deferred.d.ts.map +1 -0
  126. package/dist/sync/v1-error.d.ts +5 -0
  127. package/dist/sync/v1-error.d.ts.map +1 -0
  128. package/dist/sync/v1-frame-signature.d.ts +2 -0
  129. package/dist/sync/v1-frame-signature.d.ts.map +1 -0
  130. package/dist/sync/v1-live-target-coordinator.d.ts +32 -0
  131. package/dist/sync/v1-live-target-coordinator.d.ts.map +1 -0
  132. package/dist/sync/v1-types.d.ts +3 -0
  133. package/dist/sync/v1-types.d.ts.map +1 -1
  134. package/dist/telemetry/index.js +9 -7
  135. package/dist/telemetry/reconnect.d.ts +7 -0
  136. package/dist/telemetry/reconnect.d.ts.map +1 -0
  137. package/dist/telemetry/trace.d.ts +1 -1
  138. package/dist/telemetry/trace.d.ts.map +1 -1
  139. package/dist/webhooks/generated/event-v1.d.cts +10 -2
  140. package/dist/webhooks/generated/event-v1.d.ts +10 -2
  141. package/dist/webhooks/generated/event-v1.d.ts.map +1 -1
  142. package/dist/webhooks/generated/fixtures-v1.d.cts +2 -2
  143. package/dist/webhooks/generated/fixtures-v1.d.ts +2 -2
  144. package/dist/webhooks/index.cjs +10 -2
  145. package/dist/webhooks/index.js +10 -2
  146. package/dist/webhooks/test.cjs +2 -2
  147. package/dist/webhooks/test.js +2 -2
  148. package/dist/whiteboard/v1-client.d.ts.map +1 -1
  149. 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,989 @@ 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
+ restartTransport() {
2862
+ this.#recover("lifecycle transport deadline");
2863
+ }
2864
+ async start() {
2865
+ if (this.#started) return;
2866
+ this.#started = true;
2867
+ const startupGeneration = ++this.#startupGeneration;
2868
+ let stored;
2869
+ try {
2870
+ stored = await this.#commandScheduler.load();
2871
+ } catch (error) {
2872
+ if (startupGeneration === this.#startupGeneration) {
2873
+ this.#started = false;
2874
+ }
2875
+ throw error;
2876
+ }
2877
+ if (!this.#started || startupGeneration !== this.#startupGeneration) return;
2878
+ this.#commandScheduler.reconcileStored(stored);
2879
+ this.#subscribeMediaPlane(this.#options.mediaPlane);
2880
+ this.#unsubscribeLifecycle = this.#options.lifecycle?.subscribe((event) => this.#handleLifecycle(event));
2881
+ this.#connect();
2882
+ }
2883
+ stop() {
2884
+ this.#started = false;
2885
+ this.#startupGeneration += 1;
2886
+ this.#unsubscribeLifecycle?.();
2887
+ this.#unsubscribeLifecycle = void 0;
2888
+ this.#clearReconnect();
2889
+ this.#clearHeartbeat();
2890
+ this.#clearNoticeProbe();
2891
+ this.#commandScheduler.stop("client_stopped");
2892
+ this.#unsubscribeMediaPlane();
2893
+ this.#socket?.close(1e3, "client stopped");
2894
+ this.#socket = null;
2895
+ this.#phase = { phase: "stopped" };
2896
+ this.#rejectEphemeral("client_stopped");
2897
+ this.#collaborationState.disconnect("client_stopped");
2898
+ this.#localPublications = [];
2899
+ this.#remotePublications = [];
2900
+ this.#liveTargets.resetLocalMedia();
2901
+ this.#emit();
2902
+ }
2903
+ getSnapshot() {
2904
+ const pendingCommands = this.#commandScheduler.pendingCommands;
2905
+ const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
2906
+ return {
2907
+ connection: this.#phase.phase === "live" ? { ...this.#phase, noticeUnresponsive: this.#noticeUnresponsive } : { ...this.#phase },
2908
+ participantId: this.#participantId,
2909
+ participantGeneration: this.#participantGeneration,
2910
+ control: this.#control,
2911
+ optimisticControl,
2912
+ media: copyProjection(this.#media),
2913
+ presence: copyProjection(this.#presence),
2914
+ mediaPlane: { local: this.#localPublications.map(copyPublication), remote: this.#remotePublications.map(copyPublication) },
2915
+ localMedia: this.#liveTargets.getLocalMedia(),
2916
+ pendingCommandCount: this.#commandScheduler.pendingCount
2917
+ };
2918
+ }
2919
+ subscribe(listener) {
2920
+ this.#listeners.add(listener);
2921
+ listener(this.getSnapshot());
2922
+ return () => this.#listeners.delete(listener);
2923
+ }
2924
+ onDirectedRequest(listener) {
2925
+ this.#requestListeners.add(listener);
2926
+ return () => this.#requestListeners.delete(listener);
2927
+ }
2928
+ getCollaborationExtensionState() {
2929
+ return this.#collaborationState.getState();
2930
+ }
2931
+ getParticipantCollaborationCapabilities() {
2932
+ return this.#collaborationState.getParticipantCapabilities();
2933
+ }
2934
+ subscribeCollaboration(listener) {
2935
+ return this.#collaborationState.subscribe(listener);
2936
+ }
2937
+ sendReaction(reaction) {
2938
+ return this.#collaborationState.sendReaction(reaction);
2939
+ }
2940
+ sendChatMessage(input) {
2941
+ return this.#collaborationState.sendChatMessage(input);
2942
+ }
2943
+ markChatRead(sequence) {
2944
+ return this.#collaborationState.markChatRead(sequence);
2945
+ }
2946
+ readChatPage(input) {
2947
+ return this.#collaborationState.readChatPage(input);
2948
+ }
2949
+ setHandRaised(raised, options) {
2950
+ return this.#sendTarget({ name: "set_hand_raised", payload: { raised } }, options);
2951
+ }
2952
+ setDisplayName(displayName, options) {
2953
+ return this.#sendTarget({ name: "set_display_name", payload: { display_name: displayName } }, options);
2954
+ }
2955
+ setAdmissionPolicy(policy, options) {
2956
+ return this.#sendTarget({ name: "set_admission_policy", payload: { policy } }, options);
2957
+ }
2958
+ assignRole(participantId, role, options) {
2959
+ return this.#sendTarget({ name: "assign_roles", payload: { participant_id: participantId, role } }, options);
2960
+ }
2961
+ admit(admissionRequestId, options) {
2962
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "admit_participant", payload: { admission_request_id: admissionRequestId } }), options);
2963
+ }
2964
+ deny(admissionRequestId, options) {
2965
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "deny_admission", payload: { admission_request_id: admissionRequestId } }), options);
2966
+ }
2967
+ muteParticipant(participantId, options) {
2968
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "mute_participant", payload: { participant_id: participantId } }), options);
2168
2969
  }
2169
2970
  stopParticipantCamera(participantId, options) {
2170
- return this.#sendOperation("stop_participant_camera", { participant_id: participantId }, options);
2971
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_participant_camera", payload: { participant_id: participantId } }), options);
2171
2972
  }
2172
2973
  stopParticipantScreenShare(participantId, options) {
2173
- return this.#sendOperation("stop_participant_screen_share", { participant_id: participantId }, options);
2974
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_participant_screen_share", payload: { participant_id: participantId } }), options);
2174
2975
  }
2175
2976
  removeParticipant(participantId, options) {
2176
- return this.#sendOperation("remove_participant", { participant_id: participantId }, options);
2977
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "remove_participant", payload: { participant_id: participantId } }), options);
2177
2978
  }
2178
2979
  startRecording(options) {
2179
2980
  const recordingId = options?.recordingId ?? this.#nextRequestId();
2180
- return this.#sendOperation("start_recording", { recording_id: recordingId }, options).then((result) => ({ recordingId, result }));
2981
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "start_recording", payload: { recording_id: recordingId } }), options).then((result) => ({ recordingId, result }));
2181
2982
  }
2182
2983
  stopRecording(recordingId, options) {
2183
- return this.#sendOperation("stop_recording", { recording_id: recordingId }, options);
2984
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "stop_recording", payload: { recording_id: recordingId } }), options);
2184
2985
  }
2185
2986
  leave(options) {
2186
- return this.#sendOperation("participant_leave", {}, options);
2987
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "participant_leave", payload: {} }), options);
2187
2988
  }
2188
2989
  endEpisode(options) {
2189
- return this.#sendOperation("end_episode", {}, options);
2990
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "end_episode", payload: {} }), options);
2190
2991
  }
2191
2992
  extendEpisode(minutes, options) {
2192
2993
  if (!Number.isSafeInteger(minutes) || minutes < 1) throw new V1SyncError("episode extension must be a positive whole number of minutes", "invalid_payload");
2193
- return this.#sendOperation("extend_episode", { extension_seconds: minutes * 60 }, options);
2994
+ return this.#sendOperation((commandId) => ({ type: "operation", command_id: commandId, name: "extend_episode", payload: { extension_seconds: minutes * 60 } }), options);
2194
2995
  }
2195
2996
  setMicrophoneEnabled(enabled, options) {
2196
2997
  return this.#sendLiveTarget("set_microphone_enabled", "microphone", enabled, options);
@@ -2208,68 +3009,13 @@ var V1SyncClient = class {
2208
3009
  return this.#sendDirectedRequest("request_start_camera", participantId, options);
2209
3010
  }
2210
3011
  async #sendTarget(command, options) {
2211
- this.#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
- }
3012
+ return this.#commandScheduler.sendTarget(command, this.#liveTargets.pendingCount + this.#requests.size, options);
2229
3013
  }
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;
3014
+ #sendOperation(createFrame, options) {
3015
+ return this.#commandScheduler.sendOperation(createFrame, this.#liveTargets.pendingCount + this.#requests.size, options);
2239
3016
  }
2240
3017
  #sendLiveTarget(name, source, enabled, options) {
2241
- this.#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;
3018
+ return this.#liveTargets.send(name, source, enabled, options);
2273
3019
  }
2274
3020
  #sendDirectedRequest(name, participantId, options) {
2275
3021
  this.#assertCapacity();
@@ -2282,18 +3028,18 @@ var V1SyncClient = class {
2282
3028
  this.#send(frame);
2283
3029
  return promise;
2284
3030
  }
2285
- #registerCommand(commandId, frame, durableTarget, createdAt) {
2286
- if (this.#commands.has(commandId)) throw new V1SyncError("command ID is already pending", "command_id_conflict");
2287
- return new Promise((resolve, reject) => this.#commands.set(commandId, { resolve, reject, settled: false, frame, retries: 0, durableTarget, createdAt }));
2288
- }
2289
3031
  #connect() {
2290
3032
  if (!this.#started || !this.#transportAvailable || this.#socket) return;
2291
3033
  this.#phase = { phase: "connecting" };
2292
3034
  this.#emit();
3035
+ recordReconnect(this.#options.recordReconnect, "sync_attempt", { attempt: this.#reconnectAttempt });
2293
3036
  const socket = this.#options.webSocket.connect(this.#options.url);
2294
3037
  const connectionGeneration = ++this.#connectionGeneration;
2295
3038
  this.#socket = socket;
2296
- socket.onopen = () => void this.#authenticate(socket);
3039
+ socket.onopen = () => {
3040
+ recordReconnect(this.#options.recordReconnect, "sync_socket_open");
3041
+ void this.#authenticate(socket);
3042
+ };
2297
3043
  socket.onmessage = (event) => {
2298
3044
  this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
2299
3045
  };
@@ -2322,14 +3068,11 @@ var V1SyncClient = class {
2322
3068
  },
2323
3069
  ...this.#options.telemetry ? syncTelemetryCorrelation(this.#options.telemetry) : {}
2324
3070
  };
2325
- if (this.#requestedCollaborationVersion === 0) {
3071
+ const collaborationExtension = this.#collaborationState.helloExtension;
3072
+ if (!collaborationExtension) {
2326
3073
  this.#send(hello);
2327
3074
  } 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 }] });
3075
+ this.#send({ ...hello, extensions: [collaborationExtension] });
2333
3076
  }
2334
3077
  this.#emit();
2335
3078
  } catch {
@@ -2340,7 +3083,16 @@ var V1SyncClient = class {
2340
3083
  if (socket !== this.#socket) return;
2341
3084
  try {
2342
3085
  if (typeof data !== "string" || encoder3.encode(data).byteLength > SyncProtocolLimits.snapshotEncodedBytes) throw new V1ReplicaError("invalid inbound frame size");
2343
- await this.#handleFrame(decodeV1ServerFrame(data));
3086
+ const frame = decodeV1ServerFrame(data);
3087
+ if (this.#phase.phase === "live") {
3088
+ this.#noticeLastInboundAt = this.#now();
3089
+ this.#scheduleNoticeCheck();
3090
+ if (this.#noticeUnresponsive) {
3091
+ this.#noticeUnresponsive = false;
3092
+ this.#emit();
3093
+ }
3094
+ }
3095
+ await this.#handleFrame(frame);
2344
3096
  } catch {
2345
3097
  this.#recover("invalid_frame");
2346
3098
  }
@@ -2371,24 +3123,28 @@ var V1SyncClient = class {
2371
3123
  this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
2372
3124
  this.#rememberControlEvent(frame);
2373
3125
  }
2374
- await this.#settleTerminalLifecycleCommands(frame);
2375
- await this.#settleProvenCommands();
3126
+ await this.#commandScheduler.settleTerminalLifecycle(frame, this.#participantId);
3127
+ await this.#commandScheduler.settleProven();
2376
3128
  {
2377
3129
  const control = this.#requireControl();
2378
3130
  this.#send({ type: "delivery_ack", stream: "control", revision: control.revision, state_digest: control.stateDigest });
2379
3131
  }
3132
+ if (frame.name === "participant_left" && frame.payload.participant_id === this.#participantId) {
3133
+ this.#phase = { phase: "terminal", terminalReason: "participant_inactive" };
3134
+ this.#socket?.close(1e3, "participant inactive");
3135
+ }
2380
3136
  this.#emit();
2381
3137
  return;
2382
3138
  case "ack":
2383
3139
  this.#requireLive();
2384
- await this.#ack(frame);
3140
+ await this.#commandScheduler.acknowledge(frame);
2385
3141
  return;
2386
3142
  case "retryable_error":
2387
3143
  this.#requireLive();
2388
- this.#retryCommand(frame.command_id, frame.code);
3144
+ this.#commandScheduler.retry(frame.command_id, frame.code);
2389
3145
  return;
2390
3146
  case "live_target_result":
2391
- this.#liveTargetResult(frame);
3147
+ this.#liveTargets.receive(frame);
2392
3148
  return;
2393
3149
  case "directed_request_result":
2394
3150
  this.#requestResult(frame);
@@ -2397,43 +3153,28 @@ var V1SyncClient = class {
2397
3153
  this.#directedRequest(frame);
2398
3154
  return;
2399
3155
  case "reaction":
2400
- this.#requireLive();
2401
- this.#requireCollaborationNegotiated();
2402
- this.#emitCollaboration({ type: "reaction", reaction: reactionFromFrame(frame) });
3156
+ this.#collaborationState.reaction(frame);
2403
3157
  return;
2404
3158
  case "reaction_result":
2405
- this.#reactionResult(frame);
3159
+ this.#collaborationState.reactionResult(frame);
2406
3160
  return;
2407
3161
  case "chat_message":
2408
- this.#requireCollaborationNegotiated();
2409
- this.#observeChatMessage(frame);
3162
+ this.#collaborationState.chatMessage(frame);
2410
3163
  return;
2411
3164
  case "chat_send_result":
2412
- this.#chatSendResult(frame);
3165
+ this.#collaborationState.chatSendResult(frame);
2413
3166
  return;
2414
3167
  case "chat_page":
2415
- this.#chatPageResult(frame);
3168
+ this.#collaborationState.chatPage(frame);
2416
3169
  return;
2417
3170
  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();
3171
+ this.#collaborationState.chatHead(frame);
2425
3172
  return;
2426
3173
  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
- }
3174
+ this.#collaborationState.chatReadReceipt(frame);
2434
3175
  return;
2435
3176
  case "chat_read_result":
2436
- this.#chatReadResult(frame);
3177
+ this.#collaborationState.chatReadResult(frame);
2437
3178
  return;
2438
3179
  case "error":
2439
3180
  throw new V1ReplicaError(frame.code);
@@ -2445,29 +3186,12 @@ var V1SyncClient = class {
2445
3186
  }
2446
3187
  async #welcome(frame) {
2447
3188
  if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
2448
- 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
- }
3189
+ this.#collaborationState.acceptWelcome(frame);
2466
3190
  this.#participantId = frame.participant_id;
2467
3191
  this.#participantGeneration = frame.participant_generation;
2468
3192
  this.#updateLocalMediaStates();
2469
3193
  if (frame.mode === "terminal") {
2470
- await this.#settleTerminalRecoveryCommands(frame);
3194
+ await this.#commandScheduler.settleTerminalRecovery(frame);
2471
3195
  this.#phase = { phase: "terminal", terminalReason: frame.reason };
2472
3196
  this.#socket?.close(1e3, "terminal recovery");
2473
3197
  this.#emit();
@@ -2482,8 +3206,9 @@ var V1SyncClient = class {
2482
3206
  if (frame.mode === "replay" && (!this.#control || this.#control.revision >= frame.head.revision)) throw new V1ReplicaError("invalid replay welcome");
2483
3207
  if (frame.mode === "up_to_date" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("up-to-date head mismatch");
2484
3208
  if (frame.mode === "snapshot" && !sameHead(this.#control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
3209
+ recordReconnect(this.#options.recordReconnect, "sync_reconnected");
2485
3210
  this.#recovery = { id: frame.recovery_id, head: frame.head, replayEvents: 0, replayBytes: 0, controlComplete: false };
2486
- if (frame.mode !== "replay") this.#ackRecovery();
3211
+ if (frame.mode === "snapshot") this.#ackRecovery();
2487
3212
  this.#emit();
2488
3213
  }
2489
3214
  async #replay(frame) {
@@ -2494,251 +3219,71 @@ var V1SyncClient = class {
2494
3219
  if (recovery.replayEvents > MAX_REPLAY_EVENTS || recovery.replayBytes > MAX_REPLAY_BYTES) throw new V1ReplicaError("replay exceeds client bounds");
2495
3220
  for (const event of frame.events) {
2496
3221
  this.#control = await applyV1Event(this.#requireControl(), event);
2497
- this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
2498
- this.#rememberControlEvent(event);
2499
- }
2500
- this.#ackRecovery();
2501
- this.#emit();
2502
- }
2503
- #completeRecovery(frame) {
2504
- const recovery = this.#requireRecovery();
2505
- if (frame.recovery_id !== recovery.id || !sameHead(this.#control, frame.head) || !sameRawHead(recovery.head, frame.head)) throw new V1ReplicaError("recovery completed at the wrong head");
2506
- recovery.controlComplete = true;
2507
- this.#enterLiveIfReady();
2508
- }
2509
- #replaceProjection(frame) {
2510
- if (this.#phase.phase !== "recovering" && this.#phase.phase !== "live") throw new V1ReplicaError("projection snapshot arrived in the wrong phase");
2511
- if (frame.stream === "media") {
2512
- this.#media = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(mediaItem) };
2513
- this.#mediaEventEvidence.clear();
2514
- } else {
2515
- this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
2516
- this.#presenceEventEvidence.clear();
2517
- }
2518
- this.#enterLiveIfReady();
2519
- this.#emit();
2520
- }
2521
- #applyProjection(frame) {
2522
- this.#requireLive();
2523
- if (frame.stream === "media") {
2524
- if (this.#acceptProjectionDuplicate(this.#media, this.#mediaEventEvidence, frame)) return;
2525
- this.#media = updateProjection(this.#media, frame.projection_id, frame.sequence, mediaItem(frame.item), mediaKey);
2526
- rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
2527
- } else {
2528
- if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
2529
- this.#presence = updateProjection(this.#presence, frame.projection_id, frame.sequence, presenceItem(frame.item), (item) => item.participantId);
2530
- rememberBoundedEvidence(this.#presenceEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
2531
- }
2532
- this.#emit();
2533
- }
2534
- async #ack(frame) {
2535
- const deferred = this.#commands.get(frame.command_id);
2536
- if (!deferred) return;
2537
- if (frame.outcome === "rejected" || frame.outcome === "command_id_conflict") {
2538
- await this.#finishCommand(frame.command_id, frame);
2539
- return;
2540
- }
2541
- if (isTerminalLifecycleOperation(deferred.frame)) {
2542
- await this.#finishCommand(frame.command_id, frame);
2543
- return;
2544
- }
2545
- if (!this.#ackHeadIsProven(frame)) {
2546
- const previous = this.#acknowledgements.get(frame.command_id);
2547
- if (previous && JSON.stringify(previous) !== JSON.stringify(frame)) throw new V1ReplicaError("conflicting duplicate command ACK");
2548
- this.#acknowledgements.set(frame.command_id, frame);
2549
- return;
2550
- }
2551
- await this.#finishCommand(frame.command_id, frame);
2552
- }
2553
- async #settleProvenCommands() {
2554
- for (const commandId of this.#commands.keys()) {
2555
- const ack = this.#acknowledgements.get(commandId);
2556
- if (ack && this.#ackHeadIsProven(ack)) await this.#finishCommand(commandId, ack);
2557
- }
2558
- }
2559
- async #settleTerminalLifecycleCommands(frame) {
2560
- const operationName = frame.name === "episode_ended" ? "end_episode" : frame.name === "participant_left" && frame.payload.participant_id === this.#participantId ? "participant_leave" : null;
2561
- if (!operationName) return;
2562
- for (const [commandId, deferred] of this.#commands) {
2563
- if (deferred.frame.type !== "operation" || deferred.frame.name !== operationName) continue;
2564
- await this.#finishCommand(commandId, {
2565
- type: "ack",
2566
- command_id: commandId,
2567
- delivery: "duplicate",
2568
- outcome: "satisfied",
2569
- revision: frame.revision,
2570
- state_digest: frame.resulting_state_digest
2571
- });
2572
- }
2573
- }
2574
- async #settleTerminalRecoveryCommands(frame) {
2575
- const satisfiedOperations = /* @__PURE__ */ new Set();
2576
- if (frame.reason === "episode_ended") {
2577
- satisfiedOperations.add("end_episode");
2578
- satisfiedOperations.add("participant_leave");
2579
- } else if (frame.reason === "participant_inactive") {
2580
- satisfiedOperations.add("participant_leave");
2581
- }
2582
- for (const [commandId, deferred] of this.#commands) {
2583
- if (deferred.frame.type !== "operation" || !satisfiedOperations.has(deferred.frame.name)) continue;
2584
- await this.#finishCommand(commandId, {
2585
- type: "ack",
2586
- command_id: commandId,
2587
- delivery: "duplicate",
2588
- outcome: "satisfied",
2589
- revision: frame.head.revision,
2590
- state_digest: frame.head.state_digest
2591
- });
2592
- }
2593
- }
2594
- #retryCommand(commandId, code) {
2595
- const deferred = this.#commands.get(commandId);
2596
- if (!deferred || this.#commandRetryTimers.has(commandId)) return;
2597
- if (code === "external_operation_pending") {
2598
- this.#pollPendingOperation(commandId, deferred);
2599
- return;
2600
- }
2601
- if (deferred.retries >= MAX_RETRIES) {
2602
- rejectDeferred(deferred, new V1SyncError(code, "retry_exhausted"));
2603
- if (!deferred.durableTarget) this.#commands.delete(commandId);
2604
- return;
2605
- }
2606
- deferred.retries += 1;
2607
- const timer = this.#clock().setTimeout(() => {
2608
- this.#commandRetryTimers.delete(commandId);
2609
- if (this.#commands.get(commandId) === deferred) this.#sendIfLive(deferred.frame);
2610
- }, this.#options.retryDelayMs ?? 100);
2611
- this.#commandRetryTimers.set(commandId, timer);
2612
- }
2613
- #pollPendingOperation(commandId, deferred) {
2614
- const remainingAge = (this.#options.maxOperationPendingAgeMs ?? MAX_PENDING_AGE_MS) - (this.#now() - deferred.createdAt);
2615
- if (remainingAge <= 0) {
2616
- this.#expirePendingOperation(commandId, deferred);
2617
- return;
2618
- }
2619
- const timer = this.#clock().setTimeout(
2620
- () => {
2621
- this.#commandRetryTimers.delete(commandId);
2622
- if (this.#commands.get(commandId) !== deferred) return;
2623
- if (this.#now() - deferred.createdAt >= (this.#options.maxOperationPendingAgeMs ?? MAX_PENDING_AGE_MS)) {
2624
- this.#expirePendingOperation(commandId, deferred);
2625
- return;
2626
- }
2627
- this.#sendIfLive(deferred.frame);
2628
- },
2629
- Math.min(OPERATION_PENDING_POLL_INTERVAL_MS, remainingAge)
2630
- );
2631
- this.#commandRetryTimers.set(commandId, timer);
2632
- }
2633
- #expirePendingOperation(commandId, deferred) {
2634
- if (this.#commands.get(commandId) !== deferred) return;
2635
- this.#commands.delete(commandId);
2636
- this.#acknowledgements.delete(commandId);
2637
- this.#clearCommandRetryTimer(commandId);
2638
- let pending;
2639
- if (deferred.durableTarget) {
2640
- pending = this.#pendingTargets.get(commandId);
2641
- this.#pendingTargets.delete(commandId);
2642
- if (pending) this.#pendingRemovals.set(commandId, pending);
3222
+ this.#rememberControlHead(this.#control.revision, this.#control.stateDigest);
3223
+ this.#rememberControlEvent(event);
2643
3224
  }
2644
- rejectDeferred(deferred, new V1SyncError("external operation remained pending beyond its maximum age", "operation_pending_timeout"));
3225
+ this.#ackRecovery();
2645
3226
  this.#emit();
2646
- if (pending) void this.#removePersistedTarget(commandId, pending);
2647
- }
2648
- #liveTargetResult(frame) {
2649
- this.#requireLive();
2650
- const deferred = this.#liveTargets.get(frame.operation_id);
2651
- if (!deferred || deferred.frame.name !== frame.name) return;
2652
- if (deferred.serverResultSignature) {
2653
- if (deferred.serverResultSignature !== frameSignature(frame)) throw new V1ReplicaError("conflicting duplicate live-target result");
2654
- return;
2655
- }
2656
- if (frame.outcome === "retryable_failure") {
2657
- this.#retryLiveServer(frame.operation_id, deferred, frame.error_code ?? frame.outcome);
2658
- return;
2659
- }
2660
- if (frame.outcome !== "confirmed" && frame.outcome !== "satisfied") {
2661
- this.#failLiveTarget(frame.operation_id, deferred, new V1SyncError(frame.error_code ?? frame.outcome, frame.outcome));
2662
- return;
2663
- }
2664
- deferred.serverResult = { ...frame, outcome: frame.outcome };
2665
- deferred.serverResultSignature = frameSignature(frame);
2666
- this.#clearLiveRetryTimer(frame.operation_id);
2667
- this.#clearLiveDeadlineTimer(frame.operation_id);
2668
- this.#executeLocalMediaTarget(frame.operation_id, deferred);
2669
3227
  }
2670
- #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);
3228
+ #completeRecovery(frame) {
3229
+ const recovery = this.#requireRecovery();
3230
+ if (frame.recovery_id !== recovery.id || !sameHead(this.#control, frame.head) || !sameRawHead(recovery.head, frame.head)) throw new V1ReplicaError("recovery completed at the wrong head");
3231
+ recovery.controlComplete = true;
3232
+ this.#enterLiveIfReady();
2681
3233
  }
2682
- #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;
3234
+ #replaceProjection(frame) {
3235
+ if (this.#phase.phase !== "recovering" && this.#phase.phase !== "live") throw new V1ReplicaError("projection snapshot arrived in the wrong phase");
3236
+ if (frame.stream === "media") {
3237
+ const previous = this.#media?.items ?? [];
3238
+ this.#media = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(mediaItem) };
3239
+ this.#mediaEventEvidence.clear();
3240
+ for (const item of this.#media.items)
3241
+ this.#notifyRemotePublicationResumed(
3242
+ previous.find((candidate) => mediaKey(candidate) === mediaKey(item)),
3243
+ item
3244
+ );
3245
+ if (!this.#mediaEnded()) this.#options.mediaPlane?.remotePublicationsChanged?.();
3246
+ } else {
3247
+ this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
3248
+ this.#presenceEventEvidence.clear();
2693
3249
  }
2694
- 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" }));
3250
+ this.#enterLiveIfReady();
3251
+ this.#emit();
2695
3252
  }
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;
3253
+ #applyProjection(frame) {
3254
+ this.#requireLive();
3255
+ if (frame.stream === "media") {
3256
+ if (this.#acceptProjectionDuplicate(this.#media, this.#mediaEventEvidence, frame)) return;
3257
+ const item = mediaItem(frame.item);
3258
+ const previous = this.#media?.items.find((candidate) => mediaKey(candidate) === mediaKey(item));
3259
+ this.#media = updateProjection(this.#media, frame.projection_id, frame.sequence, item, mediaKey);
3260
+ rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
3261
+ this.#notifyRemotePublicationResumed(previous, item);
3262
+ if (!this.#mediaEnded() && item.participantId !== this.#participantId && (previous?.enabled !== item.enabled || previous?.publicationId !== item.publicationId)) {
3263
+ this.#options.mediaPlane?.remotePublicationsChanged?.();
2703
3264
  }
2704
- 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;
3265
+ } else {
3266
+ if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
3267
+ this.#presence = updateProjection(this.#presence, frame.projection_id, frame.sequence, presenceItem(frame.item), (item) => item.participantId);
3268
+ rememberBoundedEvidence(this.#presenceEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
2711
3269
  }
2712
- const serverResult = deferred.serverResult;
2713
- if (!serverResult) throw new V1ReplicaError("local MediaPlane completed before server authorization");
2714
- this.#liveTargets.delete(operationId);
2715
- this.#clearLiveRetryTimer(operationId);
2716
- this.#clearLiveDeadlineTimer(operationId);
2717
- this.#localMedia[deferred.source] = deferred.enabled ? "enabled" : "disabled";
2718
- resolveDeferred(deferred, { operationId, name: deferred.frame.name, serverOutcome: serverResult.outcome, mediaPlaneOutcome: result.outcome });
2719
3270
  this.#emit();
2720
3271
  }
2721
- #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);
3272
+ #mediaEnded() {
3273
+ return this.#control?.status === "ended" || this.#phase.phase === "terminal";
2727
3274
  }
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();
3275
+ #notifyRemotePublicationResumed(previous, current) {
3276
+ if (this.#mediaEnded()) return;
3277
+ if (previous?.enabled === false && current.enabled && current.publicationId && previous.publicationId === current.publicationId && current.participantId !== this.#participantId) {
3278
+ this.#options.mediaPlane?.remotePublicationResumed?.(current.publicationId);
3279
+ }
2735
3280
  }
2736
3281
  #requestResult(frame) {
2737
3282
  this.#requireLive();
2738
3283
  const deferred = this.#requests.get(frame.request_id);
2739
3284
  if (!deferred) return;
2740
3285
  this.#requests.delete(frame.request_id);
2741
- resolveDeferred(deferred, frame);
3286
+ resolveV1Deferred(deferred, frame);
2742
3287
  }
2743
3288
  #directedRequest(frame) {
2744
3289
  this.#requireLive();
@@ -2746,130 +3291,17 @@ var V1SyncClient = class {
2746
3291
  this.#send({ type: "request_ack", request_id: frame.request_id });
2747
3292
  for (const listener of this.#requestListeners) listener(frame);
2748
3293
  }
2749
- #reactionResult(frame) {
2750
- this.#requireLive();
2751
- this.#requireCollaborationNegotiated();
2752
- const deferred = this.#reactions.get(frame.operation_id);
2753
- if (!deferred) return;
2754
- this.#reactions.delete(frame.operation_id);
2755
- if (frame.outcome === "accepted") {
2756
- resolveDeferred(deferred, reactionFromFrame(frame.reaction));
2757
- } else {
2758
- rejectDeferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
2759
- }
2760
- }
2761
- #chatSendResult(frame) {
2762
- this.#requireLive();
2763
- this.#requireCollaborationNegotiated();
2764
- const deferred = this.#chatSends.get(frame.client_message_id);
2765
- if (!deferred) return;
2766
- this.#chatSends.delete(frame.client_message_id);
2767
- if (frame.outcome === "accepted") {
2768
- this.#assertChatMessageVersion(frame.message);
2769
- const message = chatMessageFromFrame(frame.message);
2770
- this.#rememberChatSequence(message.sequence);
2771
- resolveDeferred(deferred, message);
2772
- } else {
2773
- rejectDeferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
2774
- }
2775
- }
2776
- #chatPageResult(frame) {
2777
- this.#requireCollaborationNegotiated();
2778
- const deferred = this.#chatPages.get(frame.request_id);
2779
- if (!deferred) return;
2780
- this.#chatPages.delete(frame.request_id);
2781
- if (frame.outcome === "cursor_reset") {
2782
- this.#collaboration = { ...this.#collaboration, retainedFloorSequence: frame.retained_floor_sequence };
2783
- const result = { status: "cursor_reset", retainedFloorSequence: frame.retained_floor_sequence };
2784
- this.#emitCollaboration({ type: "chat_cursor_reset", retainedFloorSequence: frame.retained_floor_sequence });
2785
- resolveDeferred(deferred, result);
2786
- return;
2787
- }
2788
- this.#collaboration = {
2789
- ...this.#collaboration,
2790
- chatHeadSequence: frame.head_sequence,
2791
- retainedFloorSequence: frame.retained_floor_sequence
2792
- };
2793
- for (const message of frame.messages) {
2794
- this.#assertChatMessageVersion(message);
2795
- this.#observeChatMessage(message);
2796
- }
2797
- resolveDeferred(deferred, { status: "loaded", count: frame.messages.length, hasOlder: frame.has_more });
2798
- }
2799
- #observeChatMessage(frame) {
2800
- this.#assertChatMessageVersion(frame);
2801
- const message = chatMessageFromFrame(frame);
2802
- this.#rememberChatSequence(message.sequence);
2803
- this.#emitCollaboration({ type: "chat_message", message });
2804
- }
2805
- #chatReadResult(frame) {
2806
- this.#requireLive();
2807
- this.#requireCollaborationVersion(1);
2808
- const deferred = this.#chatReads.get(frame.request_id);
2809
- if (!deferred) return;
2810
- this.#chatReads.delete(frame.request_id);
2811
- if (frame.outcome === "accepted") {
2812
- const receipt = chatReadReceiptFromFrame(frame);
2813
- this.#rememberChatReadReceipt(receipt);
2814
- resolveDeferred(deferred, receipt);
2815
- } else {
2816
- rejectDeferred(deferred, new V1SyncError(frame.error_code, frame.error_code));
2817
- }
2818
- }
2819
- #assertChatMessageVersion(frame) {
2820
- const version = this.#collaboration.version;
2821
- if (version === 1 && !("attachments" in frame)) throw new V1ReplicaError("collaboration_v1 received a chat message without attachments");
2822
- }
2823
- #rememberChatReadReceipt(receipt) {
2824
- const existing = this.#collaboration.readReceipts.find((candidate) => candidate.participantId === receipt.participantId && candidate.participantGeneration === receipt.participantGeneration);
2825
- if (existing && compareUnsignedDecimals(existing.readThroughSequence, receipt.readThroughSequence) >= 0) return;
2826
- this.#collaboration = {
2827
- ...this.#collaboration,
2828
- readReceipts: [...this.#collaboration.readReceipts.filter((candidate) => candidate.participantId !== receipt.participantId || candidate.participantGeneration !== receipt.participantGeneration), receipt]
2829
- };
2830
- }
2831
- #rememberChatSequence(sequence) {
2832
- if (this.#chatAfterSequence === null || compareUnsignedDecimals(sequence, this.#chatAfterSequence) > 0) this.#chatAfterSequence = sequence;
2833
- if (this.#collaboration.chatHeadSequence === null || compareUnsignedDecimals(sequence, this.#collaboration.chatHeadSequence) > 0) {
2834
- this.#collaboration = { ...this.#collaboration, chatHeadSequence: sequence };
2835
- }
2836
- }
2837
- #emitCollaboration(event) {
2838
- for (const listener of this.#collaborationListeners) listener(event);
2839
- }
2840
- async #finishCommand(commandId, result) {
2841
- const deferred = this.#commands.get(commandId);
2842
- if (!deferred) return;
2843
- this.#commands.delete(commandId);
2844
- this.#clearCommandRetryTimer(commandId);
2845
- this.#acknowledgements.delete(commandId);
2846
- let pending;
2847
- if (deferred.durableTarget) {
2848
- pending = this.#pendingTargets.get(commandId);
2849
- this.#pendingTargets.delete(commandId);
2850
- if (pending) this.#pendingRemovals.set(commandId, pending);
2851
- }
2852
- if (result.outcome === "rejected" || result.outcome === "command_id_conflict") rejectDeferred(deferred, new V1SyncError(result.reason, result.outcome));
2853
- else resolveDeferred(deferred, result);
2854
- this.#emit();
2855
- if (pending) void this.#removePersistedTarget(commandId, pending);
2856
- }
2857
3294
  #enterLiveIfReady() {
2858
3295
  if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
3296
+ recordReconnect(this.#options.recordReconnect, "state_resynced");
2859
3297
  this.#recovery = null;
2860
3298
  this.#phase = { phase: "live" };
2861
3299
  this.#reconnectAttempt = 0;
2862
3300
  this.#missedHeartbeats = 0;
2863
- 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
- }
3301
+ this.#commandScheduler.enterLive();
3302
+ this.#liveTargets.enterLive();
2872
3303
  this.#startHeartbeat();
3304
+ this.#startNoticeProbe();
2873
3305
  this.#emit();
2874
3306
  }
2875
3307
  #ackRecovery() {
@@ -2877,36 +3309,6 @@ var V1SyncClient = class {
2877
3309
  const control = this.#requireControl();
2878
3310
  this.#send({ type: "recovery_ack", recovery_id: recovery.id, revision: control.revision, state_digest: control.stateDigest });
2879
3311
  }
2880
- #restorePending(pending) {
2881
- if (this.#pendingTargets.size + this.#pendingRemovals.size >= this.#maxPending() || this.#now() - pending.createdAt > (this.#options.maxPendingAgeMs ?? MAX_PENDING_AGE_MS) || this.#pendingBytes() + pending.bytes > (this.#options.maxPendingBytes ?? MAX_PENDING_BYTES)) {
2882
- void this.#removePersistedTarget(pending.commandId, pending);
2883
- return;
2884
- }
2885
- const frame = { type: "command", command_id: pending.commandId, name: pending.command.name, payload: pending.command.payload };
2886
- encodeV1ClientFrame(frame);
2887
- this.#pendingTargets.set(pending.commandId, pending);
2888
- this.#commands.set(pending.commandId, { resolve: () => void 0, reject: () => void 0, settled: true, frame, retries: 0, durableTarget: true, createdAt: pending.createdAt });
2889
- }
2890
- async #removePersistedTarget(commandId, pending) {
2891
- this.#pendingRemovals.set(commandId, pending);
2892
- try {
2893
- await this.#store.remove(commandId);
2894
- if (this.#pendingRemovals.get(commandId) !== pending) return;
2895
- this.#pendingRemovals.delete(commandId);
2896
- this.#clearPendingRemovalRetryTimer(commandId);
2897
- } catch {
2898
- if (this.#pendingRemovals.get(commandId) !== pending) return;
2899
- this.#schedulePendingRemovalRetry(commandId, pending);
2900
- }
2901
- }
2902
- #schedulePendingRemovalRetry(commandId, pending) {
2903
- if (!this.#started || this.#pendingRemovalRetryTimers.has(commandId)) return;
2904
- const timer = this.#clock().setTimeout(() => {
2905
- this.#pendingRemovalRetryTimers.delete(commandId);
2906
- void this.#removePersistedTarget(commandId, pending);
2907
- }, this.#options.retryDelayMs ?? 100);
2908
- this.#pendingRemovalRetryTimers.set(commandId, timer);
2909
- }
2910
3312
  #ackHeadIsProven(ack) {
2911
3313
  return this.#controlHeads.get(ack.revision) === ack.state_digest;
2912
3314
  }
@@ -2926,25 +3328,31 @@ var V1SyncClient = class {
2926
3328
  }
2927
3329
  #disconnected(socket) {
2928
3330
  if (socket !== this.#socket) return;
3331
+ recordReconnect(this.#options.recordReconnect, "sync_loss");
2929
3332
  this.#socket = null;
2930
3333
  this.#recovery = null;
2931
3334
  this.#clearHeartbeat();
2932
- this.#clearRetryTimers();
3335
+ this.#clearNoticeProbe();
3336
+ this.#liveTargets.disconnect("disconnected_before_delivery");
3337
+ this.#commandScheduler.disconnect();
2933
3338
  this.#media = null;
2934
3339
  this.#presence = null;
2935
3340
  this.#rejectRequests("disconnected_before_delivery");
2936
- this.#rejectCollaboration("disconnected_before_delivery");
2937
- this.#collaboration = { ...this.#collaboration, negotiated: false, version: null, capabilities: [], readReceipts: [] };
2938
- this.#participantCollaborationCapabilities = {};
3341
+ this.#collaborationState.disconnect("disconnected_before_delivery");
2939
3342
  if (!this.#started || !this.#transportAvailable || this.#phase.phase === "terminal") return;
2940
3343
  this.#phase = { phase: "connecting" };
2941
3344
  this.#emit();
2942
3345
  this.#clearReconnect();
2943
3346
  const configuredDelay = this.#options.reconnectDelayMs;
2944
- const delay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(this.#reconnectAttempt, 5));
3347
+ const exponentialAttempt = configuredDelay === void 0 ? Math.max(0, this.#reconnectAttempt - FAST_RECONNECT_ATTEMPTS + 1) : this.#reconnectAttempt;
3348
+ const baseDelay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(exponentialAttempt, 5));
3349
+ const delay = configuredDelay === void 0 ? Math.round(baseDelay * (0.8 + 0.4 * Math.random())) : baseDelay;
3350
+ recordReconnect(this.#options.recordReconnect, "sync_backoff", { boundary: "start", delay_ms: delay, attempt: this.#reconnectAttempt });
2945
3351
  this.#reconnectAttempt += 1;
3352
+ const backoffStartedAt = this.#now();
2946
3353
  this.#reconnectTimer = this.#clock().setTimeout(() => {
2947
3354
  this.#reconnectTimer = void 0;
3355
+ recordReconnect(this.#options.recordReconnect, "sync_backoff", { boundary: "end", duration_ms: Math.max(0, this.#now() - backoffStartedAt) });
2948
3356
  this.#connect();
2949
3357
  }, delay);
2950
3358
  }
@@ -2955,6 +3363,7 @@ var V1SyncClient = class {
2955
3363
  this.#disconnected(socket);
2956
3364
  }
2957
3365
  #handleLifecycle(event) {
3366
+ recordReconnect(this.#options.recordReconnect, "network_change", { lifecycle: event });
2958
3367
  if (event === "online" || event === "offline") this.#online = event === "online";
2959
3368
  else this.#active = event === "active";
2960
3369
  this.#transportAvailable = this.#online && this.#active;
@@ -2963,21 +3372,10 @@ var V1SyncClient = class {
2963
3372
  }
2964
3373
  #rejectEphemeral(code) {
2965
3374
  this.#rejectRequests(code);
2966
- 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();
3375
+ this.#liveTargets.disconnect(code);
2978
3376
  }
2979
3377
  #rejectRequests(code) {
2980
- for (const deferred of this.#requests.values()) rejectDeferred(deferred, new V1SyncError(code, code));
3378
+ for (const deferred of this.#requests.values()) rejectV1Deferred(deferred, new V1SyncError(code, code));
2981
3379
  this.#requests.clear();
2982
3380
  }
2983
3381
  #sendIfLive(frame) {
@@ -2989,13 +3387,6 @@ var V1SyncClient = class {
2989
3387
  #requireLive() {
2990
3388
  if (this.#phase.phase !== "live") throw new V1ReplicaError("live frame arrived before four-stream recovery completed");
2991
3389
  }
2992
- #requireCollaborationNegotiated() {
2993
- if (!this.#collaboration.negotiated) throw new V1ReplicaError("collaboration frame arrived without a negotiated extension");
2994
- }
2995
- #requireCollaborationVersion(version) {
2996
- this.#requireCollaborationNegotiated();
2997
- if (this.#collaboration.version !== version) throw new V1ReplicaError(`collaboration_v1 frame arrived on a different extension version`);
2998
- }
2999
3390
  #requireControl() {
3000
3391
  if (!this.#control) throw new V1ReplicaError("control replica is unavailable");
3001
3392
  return this.#control;
@@ -3005,33 +3396,7 @@ var V1SyncClient = class {
3005
3396
  return this.#recovery;
3006
3397
  }
3007
3398
  #assertCapacity() {
3008
- 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();
3399
+ this.#commandScheduler.assertCapacity(this.#liveTargets.pendingCount + this.#requests.size);
3035
3400
  }
3036
3401
  #nextRequestId() {
3037
3402
  return this.#options.requestIds?.next() ?? crypto.randomUUID();
@@ -3080,47 +3445,7 @@ var V1SyncClient = class {
3080
3445
  this.#emit();
3081
3446
  }
3082
3447
  #updateLocalMediaStates() {
3083
- 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);
3448
+ this.#liveTargets.updateFromPublications(this.#localPublications);
3124
3449
  }
3125
3450
  #startHeartbeat() {
3126
3451
  this.#clearHeartbeat();
@@ -3141,6 +3466,36 @@ var V1SyncClient = class {
3141
3466
  this.#clock().clearTimeout(this.#heartbeatTimer);
3142
3467
  this.#heartbeatTimer = void 0;
3143
3468
  }
3469
+ #startNoticeProbe() {
3470
+ this.#clearNoticeProbe();
3471
+ this.#noticeLastInboundAt = this.#now();
3472
+ this.#scheduleNoticeCheck();
3473
+ const tick = () => {
3474
+ this.#noticeProbeTimer = void 0;
3475
+ if (this.#phase.phase !== "live") return;
3476
+ this.#send({ type: "ping" });
3477
+ this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
3478
+ };
3479
+ this.#noticeProbeTimer = this.#clock().setTimeout(tick, NOTICE_PROBE_INTERVAL_MS);
3480
+ }
3481
+ #scheduleNoticeCheck() {
3482
+ if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
3483
+ const remaining = Math.max(0, NOTICE_SILENCE_MS - (this.#now() - this.#noticeLastInboundAt));
3484
+ this.#noticeTimer = this.#clock().setTimeout(() => {
3485
+ this.#noticeTimer = void 0;
3486
+ if (this.#phase.phase !== "live" || this.#noticeUnresponsive) return;
3487
+ if (this.#now() - this.#noticeLastInboundAt < NOTICE_SILENCE_MS) return this.#scheduleNoticeCheck();
3488
+ this.#noticeUnresponsive = true;
3489
+ this.#emit();
3490
+ }, remaining);
3491
+ }
3492
+ #clearNoticeProbe() {
3493
+ if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
3494
+ if (this.#noticeProbeTimer !== void 0) this.#clock().clearTimeout(this.#noticeProbeTimer);
3495
+ this.#noticeTimer = void 0;
3496
+ this.#noticeProbeTimer = void 0;
3497
+ this.#noticeUnresponsive = false;
3498
+ }
3144
3499
  #emit() {
3145
3500
  const snapshot = this.getSnapshot();
3146
3501
  for (const listener of this.#listeners) listener(snapshot);
@@ -3170,51 +3525,17 @@ function sameRawHead(left, right) {
3170
3525
  function copyProjection(projection) {
3171
3526
  return projection && { ...projection, items: projection.items.map((item) => ({ ...item })) };
3172
3527
  }
3173
- function frameSignature(frame) {
3174
- return new TextDecoder().decode(canonicalJsonBytesFromUnknown(frame));
3175
- }
3176
- function copyCollaborationCapabilities(capabilities) {
3177
- return Object.fromEntries(Object.entries(capabilities).map(([participantId, values]) => [participantId, [...values]]));
3178
- }
3179
- function chatReadReceiptFromWire(receipt) {
3180
- return {
3181
- participantId: receipt.participant_id,
3182
- participantGeneration: receipt.participant_generation,
3183
- readThroughSequence: receipt.sequence,
3184
- readAt: receipt.read_at
3185
- };
3186
- }
3187
- function compareUnsignedDecimals(left, right) {
3188
- if (left.length !== right.length) return left.length - right.length;
3189
- return left.localeCompare(right);
3190
- }
3191
3528
  function rememberBoundedEvidence(evidence, sequence, signature, capacity) {
3192
3529
  evidence.set(sequence, signature);
3193
3530
  if (evidence.size <= capacity) return;
3194
3531
  const oldest = evidence.keys().next().value;
3195
3532
  if (oldest !== void 0) evidence.delete(oldest);
3196
3533
  }
3197
- function resolveDeferred(deferred, value) {
3198
- if (deferred.settled) return;
3199
- deferred.settled = true;
3200
- deferred.resolve(value);
3201
- }
3202
- function rejectDeferred(deferred, error) {
3203
- if (deferred.settled) return;
3204
- deferred.settled = true;
3205
- deferred.reject(error);
3206
- }
3207
3534
  function copyPublication(publication) {
3208
3535
  return { ...publication };
3209
3536
  }
3210
3537
  function validMediaPublication(publication) {
3211
- return typeof publication.participantId === "string" && publication.participantId.length > 0 && (publication.source === "microphone" || publication.source === "camera" || publication.source === "screen") && typeof publication.enabled === "boolean" && (publication.publicationId === null || typeof publication.publicationId === "string") && publication.enabled === (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");
3538
+ return typeof publication.participantId === "string" && publication.participantId.length > 0 && (publication.source === "microphone" || publication.source === "camera" || publication.source === "screen") && typeof publication.enabled === "boolean" && (publication.publicationId === null || typeof publication.publicationId === "string") && (!publication.enabled || publication.publicationId !== null);
3218
3539
  }
3219
3540
  function assertV1Url(value) {
3220
3541
  const url = new URL(value);
@@ -3371,20 +3692,21 @@ export {
3371
3692
  createBrowserSyncLifecycle,
3372
3693
  createReactNativeWebSocketFactory,
3373
3694
  createReactNativeSyncLifecycle,
3374
- reactionFromFrame,
3375
- chatMessageFromFrame,
3376
- chatReadReceiptFromFrame,
3695
+ ChatAttachmentMimeTypeSchema,
3377
3696
  encodeV1ClientFrame,
3378
3697
  decodeV1ServerFrame,
3379
3698
  decodeV1ClientFrame,
3380
- InMemoryV1PendingTargetStore,
3699
+ reactionFromFrame,
3700
+ chatMessageFromFrame,
3701
+ chatReadReceiptFromFrame,
3702
+ V1SyncError,
3381
3703
  V1ReplicaError,
3382
3704
  restoreV1Snapshot,
3383
3705
  applyV1Event,
3384
3706
  optimisticV1Control,
3385
3707
  assertV1ControlSemantics,
3386
3708
  computeV1StateDigest,
3387
- V1SyncError,
3709
+ InMemoryV1PendingTargetStore,
3388
3710
  V1SyncClient,
3389
3711
  IndexedDbV1PendingTargetStore,
3390
3712
  AsyncStorageV1PendingTargetStore,