@delali/sirannon-db 0.3.4-next.62 → 0.3.4

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 (108) hide show
  1. package/README.md +79 -56
  2. package/dist/backup/index.d.ts +44 -45
  3. package/dist/backup/index.mjs +7 -7
  4. package/dist/backup-scheduler/index.d.ts +42 -41
  5. package/dist/backup-scheduler/index.mjs +4 -4
  6. package/dist/baseline-Co3iIsN_.d.ts +19 -0
  7. package/dist/change-tracker-DqCdeEyx.d.ts +128 -0
  8. package/dist/{chunk-VOSJBZ6Q.mjs → chunk-2ES3DT4A.mjs} +71 -33
  9. package/dist/{chunk-6CMCO7S4.mjs → chunk-2O3PP4CF.mjs} +1 -1
  10. package/dist/{chunk-E7Q7HE46.mjs → chunk-3BZLZ47R.mjs} +26 -26
  11. package/dist/chunk-4KASJKKK.mjs +2501 -0
  12. package/dist/{chunk-MYNI2WFS.mjs → chunk-52XW3Z76.mjs} +40 -8
  13. package/dist/chunk-55YRLSM2.mjs +57 -0
  14. package/dist/{chunk-KDAHYFW7.mjs → chunk-6AOJCNRU.mjs} +5 -5
  15. package/dist/{chunk-KXTYR2OQ.mjs → chunk-A3PHCO6W.mjs} +39 -111
  16. package/dist/chunk-ADNWEHGZ.mjs +299 -0
  17. package/dist/{chunk-2QLXDHAP.mjs → chunk-BLMRNSUG.mjs} +11 -2
  18. package/dist/chunk-BVMZJOVM.mjs +5 -0
  19. package/dist/{chunk-IWGIYDMZ.mjs → chunk-BZWDPF4G.mjs} +1 -1
  20. package/dist/{chunk-YVF2ZJTE.mjs → chunk-CZL4D3BM.mjs} +1 -1
  21. package/dist/{chunk-7SC2TZAS.mjs → chunk-DX4WGJL7.mjs} +2 -2
  22. package/dist/{chunk-45MKELYA.mjs → chunk-ESTZZPT5.mjs} +132 -13
  23. package/dist/{chunk-QP5L5DFJ.mjs → chunk-FJPR2CJX.mjs} +3 -3
  24. package/dist/chunk-HR7K3GZW.mjs +36 -0
  25. package/dist/{chunk-LXCGPZPA.mjs → chunk-IEKIYO4V.mjs} +1 -1
  26. package/dist/{chunk-TUD5CJ76.mjs → chunk-KB4OLJAD.mjs} +26 -19
  27. package/dist/{chunk-PBRXXISQ.mjs → chunk-KINDFBQP.mjs} +3 -2
  28. package/dist/{chunk-E6IMX7RS.mjs → chunk-KJLNYOOL.mjs} +2 -2
  29. package/dist/{chunk-BTTFW4Z4.mjs → chunk-KU32OT4H.mjs} +2 -10
  30. package/dist/{chunk-JQWGHSEH.mjs → chunk-NVSQ2T5Q.mjs} +71 -70
  31. package/dist/{chunk-W4EUTJNT.mjs → chunk-NZGSDALX.mjs} +94 -96
  32. package/dist/{chunk-U445A3C4.mjs → chunk-QGVRRKB2.mjs} +1 -1
  33. package/dist/{chunk-GYCEJNU4.mjs → chunk-R7GBXNTO.mjs} +1 -1
  34. package/dist/{chunk-7C36BCSN.mjs → chunk-VDN6FHU4.mjs} +1 -1
  35. package/dist/{chunk-6IYS4WLH.mjs → chunk-VO3ZWA2G.mjs} +142 -115
  36. package/dist/chunk-WOMADPM4.mjs +12 -0
  37. package/dist/{chunk-OUSWVNWT.mjs → chunk-YCMB6GR4.mjs} +1 -1
  38. package/dist/{chunk-YNTDOFTN.mjs → chunk-YW7IKQ7T.mjs} +1 -1
  39. package/dist/{chunk-M2BEAH5I.mjs → chunk-ZN4HI5FK.mjs} +126 -72
  40. package/dist/{chunk-SBL6GN43.mjs → chunk-ZYWI45EN.mjs} +1 -1
  41. package/dist/client/index.d.ts +135 -137
  42. package/dist/client/index.mjs +177 -82
  43. package/dist/client/topology.d.ts +31 -31
  44. package/dist/client/topology.mjs +42 -23
  45. package/dist/client-base-BxTocU1x.d.ts +398 -0
  46. package/dist/client-streams-YKANXMWO.mjs +112 -0
  47. package/dist/codegen/cli.mjs +4 -4
  48. package/dist/codegen/index.d.ts +36 -29
  49. package/dist/codegen/index.mjs +4 -4
  50. package/dist/core/index.d.ts +130 -115
  51. package/dist/core/index.mjs +387 -297
  52. package/dist/core/writer-worker.mjs +3 -3
  53. package/dist/database-CGDsXW1K.d.ts +959 -0
  54. package/dist/database-backup-DcLhxoa1.d.ts +143 -0
  55. package/dist/driver/better-sqlite3.d.ts +11 -10
  56. package/dist/driver/better-sqlite3.mjs +11 -11
  57. package/dist/driver/bun.d.ts +9 -8
  58. package/dist/driver/expo.d.ts +6 -7
  59. package/dist/driver/node.d.ts +11 -10
  60. package/dist/driver/node.mjs +13 -11
  61. package/dist/driver/wa-sqlite.d.ts +6 -6
  62. package/dist/errors-CJUkQ-KL.d.ts +199 -0
  63. package/dist/file-migrations/index.d.ts +20 -18
  64. package/dist/file-migrations/index.mjs +3 -3
  65. package/dist/forward-rpc-VRWFZXHN.mjs +44 -0
  66. package/dist/operation-registry-CGhNT-Sn.d.ts +96 -0
  67. package/dist/primary-wins-CHNOYuN7.d.ts +81 -0
  68. package/dist/protocol-DFpNNsIT.d.ts +153 -0
  69. package/dist/query-types-5B02ixH0.d.ts +129 -0
  70. package/dist/react/index.d.ts +22 -22
  71. package/dist/replication/coordinator/etcd.d.ts +37 -31
  72. package/dist/replication/coordinator/etcd.mjs +296 -131
  73. package/dist/replication/index.d.ts +201 -161
  74. package/dist/replication/index.mjs +630 -529
  75. package/dist/restore-options-CH8mE-Tb.d.ts +84 -0
  76. package/dist/server/index.d.ts +201 -191
  77. package/dist/server/index.mjs +736 -619
  78. package/dist/server-options-aYtCkefX.d.ts +317 -0
  79. package/dist/server-streams-24XL4T43.mjs +254 -0
  80. package/dist/sirannon-J3EgPVD7.d.ts +179 -0
  81. package/dist/transport/grpc.d.ts +37 -24
  82. package/dist/transport/grpc.mjs +83 -3199
  83. package/dist/transport/memory.d.ts +25 -26
  84. package/dist/transport/memory.mjs +18 -9
  85. package/dist/types-BrAiDGOb.d.ts +505 -0
  86. package/dist/types-CEJG0eFS.d.ts +88 -0
  87. package/dist/types-Dc8oWlxj.d.ts +1580 -0
  88. package/dist/types-DqODKeOq.d.ts +123 -0
  89. package/dist/types-guEKSzMr.d.ts +367 -0
  90. package/package.json +7 -7
  91. package/dist/baseline-D93hcIEE.d.ts +0 -17
  92. package/dist/change-tracker-CjRFuZdB.d.ts +0 -144
  93. package/dist/client-base-Cmd9EVme.d.ts +0 -397
  94. package/dist/database-BLQvqjsw.d.ts +0 -917
  95. package/dist/database-backup-B7oszQ2n.d.ts +0 -141
  96. package/dist/errors-Dei4GdBb.d.ts +0 -196
  97. package/dist/operation-registry-oiPuSYI6.d.ts +0 -94
  98. package/dist/primary-wins-B0np8JS3.d.ts +0 -85
  99. package/dist/protocol-HI-nY8DD.d.ts +0 -152
  100. package/dist/query-types-p1liOP-b.d.ts +0 -127
  101. package/dist/restore-options-CksYsH1l.d.ts +0 -83
  102. package/dist/server-options-Bj5iJnjx.d.ts +0 -292
  103. package/dist/sirannon-BPxC5tC2.d.ts +0 -178
  104. package/dist/types-B0EhgDbr.d.ts +0 -340
  105. package/dist/types-CbjfRwrK.d.ts +0 -499
  106. package/dist/types-CjhxcjhA.d.ts +0 -123
  107. package/dist/types-DPSmjU54.d.ts +0 -1518
  108. package/dist/types-DTO4CYhW.d.ts +0 -88
@@ -1,5 +1,5 @@
1
- import { DatabaseClient, createEndpointTransport, toBaseUrl, DEFAULT_HTTP_REQUEST_TIMEOUT_MS, postJson, decodeTaggedValues, RemoteError, invokeCallerCallback, encodeTaggedValues, assertWebSocketCredentials, STAGED_STREAM_CAPABILITY, verifyDeviceSyncCapabilities, unrefTimer, toWsUrl, WebSocketTransport } from '../chunk-6IYS4WLH.mjs';
2
- export { HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, ServerCapabilities, WebSocketTransport } from '../chunk-6IYS4WLH.mjs';
1
+ import { DatabaseClient, createEndpointTransport, toBaseUrl, DEFAULT_HTTP_REQUEST_TIMEOUT_MS, postJson, decodeTaggedValues, RemoteError, invokeCallerCallback, encodeTaggedValues, assertWebSocketCredentials, STAGED_STREAM_CAPABILITY, verifyDeviceSyncCapabilities, unrefTimer, toWsUrl, WebSocketTransport } from '../chunk-VO3ZWA2G.mjs';
2
+ export { HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, ServerCapabilities, WebSocketTransport } from '../chunk-VO3ZWA2G.mjs';
3
3
 
4
4
  // src/core/sync/canonicalise.ts
5
5
  function canonicaliseForChecksum(value) {
@@ -75,7 +75,7 @@ var HLC = class _HLC {
75
75
  this.logical = 0;
76
76
  }
77
77
  /**
78
- * Generate a new HLC timestamp, advancing the clock.
78
+ * Returns a new timestamp and advances the clock.
79
79
  *
80
80
  * @internal
81
81
  */
@@ -93,7 +93,7 @@ var HLC = class _HLC {
93
93
  return _HLC.encode(this.wallMs, this.logical, this.nodeId);
94
94
  }
95
95
  /**
96
- * Merge a remote HLC timestamp into the local clock and return the updated value.
96
+ * Merges a remote timestamp into the clock and returns the new local timestamp.
97
97
  *
98
98
  * @internal
99
99
  */
@@ -116,13 +116,13 @@ var HLC = class _HLC {
116
116
  }
117
117
  return _HLC.encode(this.wallMs, this.logical, this.nodeId);
118
118
  }
119
- /** Lexicographic comparison of two encoded HLC strings. Returns -1, 0, or 1. */
119
+ /** Compares two encoded timestamps as strings and returns -1, 0, or 1. */
120
120
  static compare(a, b) {
121
121
  if (a < b) return -1;
122
122
  if (a > b) return 1;
123
123
  return 0;
124
124
  }
125
- /** Parse an encoded HLC string into its wall-clock, logical, and nodeId components. */
125
+ /** Parses an encoded timestamp into its wall-clock, logical, and node ID parts. */
126
126
  static decode(hlc) {
127
127
  const parts = hlc.split("-");
128
128
  if (parts.length < 3) {
@@ -135,11 +135,11 @@ var HLC = class _HLC {
135
135
  };
136
136
  }
137
137
  /**
138
- * Builds the string form of a clock reading, which sorts in the same order as the reading itself.
138
+ * Returns the string form of a clock reading, padded so that string order matches clock order.
139
139
  *
140
- * @param wallMs - Wall-clock milliseconds since the Unix epoch.
141
- * @param logical - Counter that orders events sharing one millisecond.
142
- * @param nodeId - Identifier of the node taking the reading.
140
+ * @param wallMs - The wall-clock milliseconds since the Unix epoch.
141
+ * @param logical - The counter that orders events within one millisecond.
142
+ * @param nodeId - The ID of the node that takes the reading.
143
143
  * @returns The encoded stamp.
144
144
  */
145
145
  static encode(wallMs, logical, nodeId) {
@@ -152,7 +152,7 @@ var HLC = class _HLC {
152
152
  // src/core/sync/conflict/lww.ts
153
153
  var LWWResolver = class {
154
154
  /**
155
- * Takes the incoming row when its stamp is higher, and keeps the local row otherwise.
155
+ * Returns the incoming row when its stamp is higher, and the local row otherwise.
156
156
  *
157
157
  * @param ctx - The local and incoming versions of one row.
158
158
  * @returns Which version to write.
@@ -186,10 +186,10 @@ var FieldMergeResolver = class {
186
186
  this.getColumnVersions = getColumnVersions;
187
187
  }
188
188
  /**
189
- * Merges columns only one side changed, and settles overlapping columns by their per-column stamps.
189
+ * Returns the resolution for one conflicting row, merging changes to different columns and settling each column that both sides changed by its per-column stamp.
190
190
  *
191
191
  * @param ctx - The local and incoming versions of one row.
192
- * @returns Which version to write, or the merged row.
192
+ * @returns The version to keep, or the merged row.
193
193
  */
194
194
  async resolve(ctx) {
195
195
  const columnVersions = await this.getColumnVersions(ctx.table, ctx.rowId);
@@ -266,7 +266,7 @@ var PrimaryWinsResolver = class {
266
266
  this.primaryNodeId = primaryNodeId;
267
267
  }
268
268
  /**
269
- * Takes the version authored by the configured primary node, and falls back to last-writer-wins otherwise.
269
+ * Returns the version that the primary node wrote, or the last-writer-wins result when neither version comes from the primary.
270
270
  *
271
271
  * @param ctx - The local and incoming versions of one row.
272
272
  * @returns Which version to write.
@@ -301,7 +301,7 @@ var SirannonClient = class extends DatabaseClient {
301
301
  this.baseUrl = normaliseClientUrl(url);
302
302
  }
303
303
  /**
304
- * Builds the transport a database's requests travel over.
304
+ * Builds the transport that sends one database's requests.
305
305
  *
306
306
  * @param databaseId - Identifier of the database.
307
307
  * @returns A transport bound to that database.
@@ -310,7 +310,7 @@ var SirannonClient = class extends DatabaseClient {
310
310
  return createEndpointTransport(this.settings, this.baseUrl, databaseId);
311
311
  }
312
312
  /**
313
- * Returns the address requests are sent to.
313
+ * Returns the address that this client sends requests to.
314
314
  *
315
315
  * @returns The server's base address.
316
316
  */
@@ -478,6 +478,13 @@ function validatePage(raw) {
478
478
  }
479
479
  return record;
480
480
  }
481
+ async function snapshotGateOpen(port) {
482
+ try {
483
+ return !await port.snapshotLoadPending();
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
481
488
  async function downloadDatabaseSnapshot(port, options) {
482
489
  const baseUrl = toBaseUrl(options.url);
483
490
  const encodedId = encodeURIComponent(options.databaseId);
@@ -654,6 +661,29 @@ async function syncDeviceMigrations(db, options) {
654
661
  };
655
662
  }
656
663
 
664
+ // src/client/network-signal.ts
665
+ function onlineEventTarget() {
666
+ const candidate = globalThis;
667
+ if (typeof candidate.addEventListener !== "function" || typeof candidate.removeEventListener !== "function") {
668
+ return null;
669
+ }
670
+ return candidate;
671
+ }
672
+ function deviceNetworkSignal() {
673
+ return {
674
+ reportsOffline: () => globalThis.navigator?.onLine === false,
675
+ watchOnline(onOnline) {
676
+ const target = onlineEventTarget();
677
+ if (target === null) return () => {
678
+ };
679
+ const listener = () => onOnline();
680
+ const removeEventListener = target.removeEventListener.bind(target);
681
+ target.addEventListener("online", listener);
682
+ return () => removeEventListener("online", listener);
683
+ }
684
+ };
685
+ }
686
+
657
687
  // src/client/sync-pull-stream.ts
658
688
  var DEFAULT_IMMEDIATE_ACK_AFTER_CHANGES = 500;
659
689
  var STAGE_BATCH_EVENTS = 500;
@@ -898,6 +928,66 @@ async function pushSyncBatch(baseUrl, databaseId, batch, headers, timeoutMs, sch
898
928
  return { applied: result.applied, skipped: result.skipped, conflicts: result.conflicts };
899
929
  }
900
930
 
931
+ // src/client/sync-reconnect.ts
932
+ function jitteredBackoff(baseMs, attempt, maxMs, random = Math.random) {
933
+ const ceiling = Math.min(baseMs * 2 ** attempt, maxMs);
934
+ return Math.round(ceiling / 2 + random() * (ceiling / 2));
935
+ }
936
+ var PullReconnector = class {
937
+ constructor(config, hooks) {
938
+ this.config = config;
939
+ this.hooks = hooks;
940
+ }
941
+ timer = null;
942
+ heldForNetwork = false;
943
+ failures = 0;
944
+ stopWatching = null;
945
+ /** Begins listening for the network's return. */
946
+ start() {
947
+ if (this.stopWatching !== null) return;
948
+ this.stopWatching = this.config.network.watchOnline(() => this.handleOnline());
949
+ }
950
+ /** Clears any pending retry and stops listening for the network. */
951
+ stop() {
952
+ this.clearTimer();
953
+ this.heldForNetwork = false;
954
+ this.stopWatching?.();
955
+ this.stopWatching = null;
956
+ }
957
+ /** Records that the pull connection works, so that the next failure waits the shortest time again. */
958
+ reset() {
959
+ this.failures = 0;
960
+ }
961
+ /** Schedules one reopen, unless one is already pending or held for the network. */
962
+ schedule() {
963
+ if (this.timer !== null || this.heldForNetwork) return;
964
+ const delay = jitteredBackoff(this.config.baseDelayMs, this.failures, this.config.maxDelayMs);
965
+ this.failures += 1;
966
+ this.timer = setTimeout(() => {
967
+ this.timer = null;
968
+ if (this.config.network.reportsOffline()) {
969
+ this.heldForNetwork = true;
970
+ return;
971
+ }
972
+ this.hooks.reopen();
973
+ }, delay);
974
+ unrefTimer(this.timer);
975
+ }
976
+ handleOnline() {
977
+ this.hooks.onOnline();
978
+ if (this.timer === null && !this.heldForNetwork) return;
979
+ this.clearTimer();
980
+ this.heldForNetwork = false;
981
+ this.failures = 0;
982
+ this.hooks.reopen();
983
+ }
984
+ clearTimer() {
985
+ if (this.timer === null) return;
986
+ clearTimeout(this.timer);
987
+ this.timer = null;
988
+ }
989
+ };
990
+
901
991
  // src/client/sync-push-loop.ts
902
992
  var PushLoop = class {
903
993
  constructor(config, hooks) {
@@ -922,10 +1012,11 @@ var PushLoop = class {
922
1012
  this.timer = null;
923
1013
  }
924
1014
  /**
925
- * Empties the outbox before a snapshot replaces the database so that local work
926
- * reaches the server before the copy overwrites it. A schema refusal is
927
- * reconciled once; a device that still cannot push returns rather than
928
- * looping, leaving the snapshot to bring it back into line.
1015
+ * Pushes every change in the outbox before a snapshot download replaces the
1016
+ * database, so that the server receives local changes before the download
1017
+ * overwrites them. After a `MIGRATION_REQUIRED` refusal, it reconciles migrations
1018
+ * once, and when the device still cannot push, it returns so that the snapshot
1019
+ * brings the device's schema up to date.
929
1020
  */
930
1021
  async drainFully(port) {
931
1022
  let retriedAfterMigration = false;
@@ -942,10 +1033,15 @@ var PushLoop = class {
942
1033
  }
943
1034
  }
944
1035
  }
1036
+ retryNow() {
1037
+ this.consecutiveFailures = 0;
1038
+ this.nextAttemptAt = 0;
1039
+ void this.drain();
1040
+ }
945
1041
  async drain() {
946
1042
  const port = this.hooks.port();
947
1043
  if (this.pushing || !this.hooks.isRunning() || port === null) return;
948
- if (Date.now() < this.nextAttemptAt) return;
1044
+ if (Date.now() < this.nextAttemptAt || this.config.network.reportsOffline()) return;
949
1045
  this.pushing = true;
950
1046
  try {
951
1047
  while (this.hooks.isRunning()) {
@@ -964,7 +1060,7 @@ var PushLoop = class {
964
1060
  async handleFailure(err) {
965
1061
  this.hooks.recordError(err);
966
1062
  this.consecutiveFailures += 1;
967
- const delay = Math.min(this.config.intervalMs * 2 ** this.consecutiveFailures, this.config.maxRetryDelayMs);
1063
+ const delay = jitteredBackoff(this.config.intervalMs, this.consecutiveFailures, this.config.maxRetryDelayMs);
968
1064
  this.nextAttemptAt = Date.now() + delay;
969
1065
  if (!(err instanceof RemoteError) || err.code !== "MIGRATION_REQUIRED" || !this.hooks.isRunning()) return;
970
1066
  try {
@@ -1059,7 +1155,7 @@ var DEFAULT_ACK_INTERVAL_MS = 2e3;
1059
1155
  var DEFAULT_MAX_PUSH_RETRY_DELAY_MS = 3e4;
1060
1156
  var DEFAULT_SNAPSHOT_RETRY_DELAY_MS = 5e3;
1061
1157
  var DEFAULT_MAX_SNAPSHOT_RETRY_DELAY_MS = 3e5;
1062
- function createSyncCollaborators(baseUrl, options, host) {
1158
+ function createSyncCollaborators(baseUrl, options, host, network) {
1063
1159
  const isRunning = () => host.state() === "running";
1064
1160
  const push = new PushLoop(
1065
1161
  {
@@ -1069,7 +1165,8 @@ function createSyncCollaborators(baseUrl, options, host) {
1069
1165
  requestTimeout: options.requestTimeout,
1070
1166
  batchSize: options.batchSize ?? DEFAULT_BATCH_SIZE,
1071
1167
  intervalMs: options.pushIntervalMs ?? DEFAULT_PUSH_INTERVAL_MS,
1072
- maxRetryDelayMs: options.maxPushRetryDelayMs ?? DEFAULT_MAX_PUSH_RETRY_DELAY_MS
1168
+ maxRetryDelayMs: options.maxPushRetryDelayMs ?? DEFAULT_MAX_PUSH_RETRY_DELAY_MS,
1169
+ network
1073
1170
  },
1074
1171
  {
1075
1172
  isRunning,
@@ -1135,7 +1232,7 @@ var SyncStatusNotifier = class {
1135
1232
  countTimer = null;
1136
1233
  lastCountAt = 0;
1137
1234
  /**
1138
- * Captures the controller's status now and delivers it on a microtask.
1235
+ * Captures the controller's status now and passes it to the listener on a microtask.
1139
1236
  */
1140
1237
  notify() {
1141
1238
  if (this.listener === void 0) return;
@@ -1188,52 +1285,63 @@ var SyncStatusNotifier = class {
1188
1285
  };
1189
1286
 
1190
1287
  // src/client/sync-controller.ts
1288
+ var SERVER_REFUSES_DEVICE_SYNC_CODES = /* @__PURE__ */ new Set(["SYNC_UNSUPPORTED", "DEVICE_SYNC_NOT_ACCEPTED"]);
1289
+ var SERVER_UNREACHABLE_CODES = /* @__PURE__ */ new Set(["CONNECTION_ERROR", "TIMEOUT"]);
1191
1290
  var SyncController = class {
1192
1291
  constructor(db, options) {
1193
1292
  this.db = db;
1194
1293
  this.options = options;
1195
1294
  this.baseUrl = toBaseUrl(options.url);
1196
- this.pushIntervalMs = options.pushIntervalMs ?? DEFAULT_PUSH_INTERVAL_MS;
1197
- this.maxPushRetryDelayMs = options.maxPushRetryDelayMs ?? DEFAULT_MAX_PUSH_RETRY_DELAY_MS;
1198
1295
  assertWebSocketCredentials(options.headers, options.webSocketProtocols);
1296
+ const network = deviceNetworkSignal();
1199
1297
  this.statusChanges = new SyncStatusNotifier(
1200
1298
  options.onStatusChange,
1201
1299
  () => this.captureStatus(),
1202
1300
  () => this.refreshOutboxCount()
1203
1301
  );
1204
- const collaborators = createSyncCollaborators(this.baseUrl, options, {
1205
- state: () => this.state,
1206
- port: () => this.port,
1207
- schemaVersion: () => this.schemaVersion ?? 0,
1208
- reconcileSchema: () => this.reconcileSchema(),
1209
- recordError: (err) => this.recordError(err),
1210
- clearError: () => this.setError(null),
1211
- markResyncRequired: () => this.markResyncRequired(),
1212
- onApplyFailure: (err) => this.handleApplyFailure(err),
1213
- onApplySuccess: () => {
1214
- this.consecutivePullFailures = 0;
1215
- this.statusChanges.notify();
1302
+ const collaborators = createSyncCollaborators(
1303
+ this.baseUrl,
1304
+ options,
1305
+ {
1306
+ state: () => this.state,
1307
+ port: () => this.port,
1308
+ schemaVersion: () => this.schemaVersion ?? 0,
1309
+ reconcileSchema: () => this.reconcileSchema(),
1310
+ recordError: (err) => this.recordError(err),
1311
+ clearError: () => this.setError(null),
1312
+ markResyncRequired: () => this.markResyncRequired(),
1313
+ onApplyFailure: (err) => this.handleApplyFailure(err),
1314
+ onApplySuccess: () => {
1315
+ this.reconnect.reset();
1316
+ this.statusChanges.notify();
1317
+ },
1318
+ download: () => this.downloadSnapshot({ pageSize: options.snapshotPageSize, onProgress: options.onSnapshotProgress })
1216
1319
  },
1217
- download: () => this.downloadSnapshot({ pageSize: options.snapshotPageSize, onProgress: options.onSnapshotProgress })
1218
- });
1320
+ network
1321
+ );
1219
1322
  this.push = collaborators.push;
1220
1323
  this.pull = collaborators.pull;
1221
1324
  this.resync = collaborators.resync;
1325
+ this.reconnect = new PullReconnector(
1326
+ {
1327
+ baseDelayMs: options.pushIntervalMs ?? DEFAULT_PUSH_INTERVAL_MS,
1328
+ maxDelayMs: options.maxPushRetryDelayMs ?? DEFAULT_MAX_PUSH_RETRY_DELAY_MS,
1329
+ network
1330
+ },
1331
+ { reopen: () => void this.reopenPull(), onOnline: () => this.push.retryNow() }
1332
+ );
1222
1333
  }
1223
1334
  baseUrl;
1224
- pushIntervalMs;
1225
- maxPushRetryDelayMs;
1226
1335
  pull;
1227
1336
  push;
1228
1337
  resync;
1338
+ reconnect;
1229
1339
  statusChanges;
1230
1340
  port = null;
1231
1341
  deviceId = null;
1232
1342
  capabilities = null;
1233
1343
  schemaVersion = null;
1234
1344
  syncState = "stopped";
1235
- pullRetryTimer = null;
1236
- consecutivePullFailures = 0;
1237
1345
  pendingPushCount = 0;
1238
1346
  lastError = null;
1239
1347
  get state() {
@@ -1250,6 +1358,13 @@ var SyncController = class {
1250
1358
  }
1251
1359
  /**
1252
1360
  * Connects to the server and starts pushing and pulling changes.
1361
+ *
1362
+ * When the server is unreachable, the promise still resolves, `status()` reports `running` with the failure in
1363
+ * `lastError`, and the controller retries the connection with growing waits. It makes no attempt while the device
1364
+ * reports no network, and it retries at once when the network returns.
1365
+ *
1366
+ * @throws A `RemoteError` when the server refuses device sync, such as `DEVICE_SYNC_NOT_ACCEPTED`, `UNAUTHORIZED`, or
1367
+ * `FORBIDDEN`.
1253
1368
  */
1254
1369
  async start() {
1255
1370
  if (this.state === "running" || this.state === "starting") return;
@@ -1276,8 +1391,12 @@ var SyncController = class {
1276
1391
  }
1277
1392
  }
1278
1393
  if (!this.resync.required) {
1279
- await this.openPull();
1394
+ await this.openPull().catch((err) => {
1395
+ if (!(err instanceof RemoteError) || !SERVER_UNREACHABLE_CODES.has(err.code)) throw err;
1396
+ this.handleApplyFailure(err);
1397
+ });
1280
1398
  }
1399
+ this.reconnect.start();
1281
1400
  this.setState("running");
1282
1401
  } catch (err) {
1283
1402
  this.teardownStream();
@@ -1291,7 +1410,7 @@ var SyncController = class {
1291
1410
  }
1292
1411
  }
1293
1412
  /**
1294
- * Holds pushing and pulling without disconnecting.
1413
+ * Pauses pushing and pulling and closes the pull connection until you call {@link SyncController.resume}.
1295
1414
  */
1296
1415
  pause() {
1297
1416
  if (this.state !== "running") return;
@@ -1317,7 +1436,7 @@ var SyncController = class {
1317
1436
  await this.pull.persist();
1318
1437
  }
1319
1438
  /**
1320
- * Reports where this device stands against the server.
1439
+ * Returns this device's sync status, with the pending push count read fresh from the outbox.
1321
1440
  *
1322
1441
  * @returns The device's state, cursors, pending push count, and last failure.
1323
1442
  */
@@ -1346,7 +1465,7 @@ var SyncController = class {
1346
1465
  return changed;
1347
1466
  }
1348
1467
  /**
1349
- * Pushes local changes now instead of waiting for the next interval.
1468
+ * Pushes local changes now, ahead of the next push interval.
1350
1469
  */
1351
1470
  triggerPush() {
1352
1471
  void this.push.drain();
@@ -1360,7 +1479,7 @@ var SyncController = class {
1360
1479
  requestTimeoutMs: this.options.requestTimeout
1361
1480
  });
1362
1481
  } catch (err) {
1363
- if (err instanceof RemoteError && err.code === "SYNC_UNSUPPORTED") throw err;
1482
+ if (err instanceof RemoteError && SERVER_REFUSES_DEVICE_SYNC_CODES.has(err.code)) throw err;
1364
1483
  this.recordError(err);
1365
1484
  }
1366
1485
  }
@@ -1426,7 +1545,7 @@ var SyncController = class {
1426
1545
  const failure = describeError(err);
1427
1546
  this.setError(failure);
1428
1547
  this.resync.recordFailure();
1429
- const databaseUsable = await this.snapshotGateOpen(port);
1548
+ const databaseUsable = await snapshotGateOpen(port);
1430
1549
  this.setState("stopped");
1431
1550
  try {
1432
1551
  await this.start();
@@ -1443,34 +1562,13 @@ var SyncController = class {
1443
1562
  this.resync.complete({ ok: true, error: null, databaseUsable: true, retrying: false });
1444
1563
  }
1445
1564
  }
1446
- /**
1447
- * Answers whether the local database serves reads and writes again. A failure
1448
- * before the wipe begins leaves it intact, while one after it leaves every
1449
- * statement refused with `SNAPSHOT_IN_PROGRESS` until a later copy succeeds,
1450
- * so the application learns which of the two it is rather than assuming.
1451
- */
1452
- async snapshotGateOpen(port) {
1453
- try {
1454
- return !await port.snapshotLoadPending();
1455
- } catch {
1456
- return false;
1457
- }
1458
- }
1459
1565
  recordError(err) {
1460
1566
  this.setError(describeError(err));
1461
1567
  }
1462
1568
  handleApplyFailure(err) {
1463
1569
  this.recordError(err);
1464
1570
  this.pull.teardown();
1465
- const live = this.state === "running" || this.state === "starting";
1466
- if (!live || this.pullRetryTimer !== null) return;
1467
- const delay = Math.min(this.pushIntervalMs * 2 ** this.consecutivePullFailures, this.maxPushRetryDelayMs);
1468
- this.consecutivePullFailures += 1;
1469
- this.pullRetryTimer = setTimeout(() => {
1470
- this.pullRetryTimer = null;
1471
- void this.reopenPull();
1472
- }, delay);
1473
- unrefTimer(this.pullRetryTimer);
1571
+ if (this.state === "running" || this.state === "starting") this.reconnect.schedule();
1474
1572
  }
1475
1573
  async reopenPull() {
1476
1574
  if (this.state !== "running" || this.deviceId === null || this.resync.required) return;
@@ -1481,11 +1579,11 @@ var SyncController = class {
1481
1579
  }
1482
1580
  }
1483
1581
  /**
1484
- * Opens the pull subscription, reconciling migrations when the server refuses
1485
- * it because this device is behind. A device that only reads never pushes, so
1486
- * the subscribe refusal is the sole point at which it can learn that the
1487
- * server has migrated; without this it would retry the same refused
1488
- * subscription forever and silently receive nothing.
1582
+ * Opens the pull subscription, and reconciles migrations when the server refuses
1583
+ * it with `MIGRATION_REQUIRED` because this device's schema is behind. A device
1584
+ * that only reads never pushes, so the refused subscription is the only signal
1585
+ * that the server has migrated. Without this step, the controller would retry the
1586
+ * same refused subscription indefinitely and the device would receive no changes.
1489
1587
  */
1490
1588
  async openPull() {
1491
1589
  const deviceId = this.deviceId;
@@ -1509,10 +1607,7 @@ var SyncController = class {
1509
1607
  teardownStream() {
1510
1608
  this.push.stop();
1511
1609
  this.resync.cancel();
1512
- if (this.pullRetryTimer !== null) {
1513
- clearTimeout(this.pullRetryTimer);
1514
- this.pullRetryTimer = null;
1515
- }
1610
+ this.reconnect.stop();
1516
1611
  this.pull.teardown();
1517
1612
  }
1518
1613
  };
@@ -1,73 +1,72 @@
1
- import { a as ReadConcernLevel } from '../query-types-p1liOP-b.js';
2
- import { C as ClientOptions } from '../server-options-Bj5iJnjx.js';
3
- import { T as Transport, D as DatabaseClient } from '../client-base-Cmd9EVme.js';
4
- import { H as ClusterStatusInfo } from '../types-DPSmjU54.js';
5
- import '../operation-registry-oiPuSYI6.js';
6
- import '../types-CjhxcjhA.js';
7
- import '../types-DTO4CYhW.js';
8
- import '../protocol-HI-nY8DD.js';
1
+ import { a as ReadConcernLevel } from '../query-types-5B02ixH0.js';
2
+ import { C as ClientOptions } from '../server-options-aYtCkefX.js';
3
+ import { T as Transport, D as DatabaseClient } from '../client-base-BxTocU1x.js';
4
+ import { H as ClusterStatusInfo } from '../types-Dc8oWlxj.js';
5
+ import '../operation-registry-CGhNT-Sn.js';
6
+ import '../types-DqODKeOq.js';
7
+ import '../types-CEJG0eFS.js';
8
+ import '../protocol-DFpNNsIT.js';
9
9
 
10
10
  interface TopologyRouting {
11
11
  _getReadEndpoint(databaseId?: string, readConcern?: ReadConcernLevel): Promise<string>;
12
12
  _getWriteEndpoint(databaseId?: string): Promise<string>;
13
13
  _getReadConcern(): ReadConcernLevel | undefined;
14
14
  _usesCoordinatorDiscovery(): boolean;
15
- _removeReplica(url: string): void;
15
+ _setEndpointAside(url: string): void;
16
16
  _refreshClusterRouting(databaseId: string): Promise<void>;
17
17
  _createTransportForEndpoint(url: string, databaseId: string): Transport;
18
18
  }
19
19
 
20
20
  /**
21
- * Reads the body of `GET /db/{id}/cluster` into the status the server reported.
21
+ * Parses the body of `GET /db/{id}/cluster` and returns the cluster status that the server reports.
22
22
  *
23
- * The route answers with a decimal string for `primaryTerm`, so that a term beyond
24
- * the safe integer range survives JSON, and this returns it as a bigint. A caller
25
- * building a cluster view gets the health, the reason behind it, the node the group
26
- * names as primary, and the readable endpoints, each checked against the values the
27
- * engine reports.
23
+ * The route sends `primaryTerm` as a decimal string, so that JSON keeps a term beyond the
24
+ * safe integer range exact, and this function returns it as a bigint. It checks `health`
25
+ * and `healthReason` against the values that the engine can report, and checks the shape
26
+ * of the role, the current primary, and the read endpoints.
28
27
  *
29
28
  * @param data - The parsed JSON body of the response.
30
- * @param databaseId - The database the caller asked about.
31
- * @returns The status the server reported.
32
- * @throws A remote error with code `INVALID_RESPONSE` where the body is malformed or names another database.
29
+ * @param databaseId - The ID of the database that you asked about.
30
+ * @returns The cluster status that the server reports.
31
+ * @throws A `RemoteError` with code `INVALID_RESPONSE` when the body is malformed or its `databaseId` differs from `databaseId`.
33
32
  *
34
33
  * @public
35
34
  */
36
35
  declare function parseClusterStatus(data: unknown, databaseId: string): ClusterStatusInfo;
37
36
 
38
37
  /**
39
- * Which nodes the client holds, how it finds the rest, and where it sends each read.
38
+ * The nodes that the client starts with, how it discovers the rest, and where it sends each read.
40
39
  *
41
40
  * @public
42
41
  */
43
42
  interface TopologyAwareClientOptions extends ClientOptions {
44
43
  /**
45
- * Nodes the client starts from when it discovers the group through the coordinator.
44
+ * The nodes that the client asks for the group's routing when it discovers the group through the coordinator.
46
45
  */
47
46
  endpoints?: string[];
48
47
  /**
49
- * Address of the primary, for a group you list by hand.
48
+ * The address of the primary, for a group that you list by hand.
50
49
  */
51
50
  primary?: string;
52
51
  /**
53
- * Addresses of the replicas, for a group you list by hand.
52
+ * The addresses of the replicas, for a group that you list by hand.
54
53
  */
55
54
  replicas?: string[];
56
55
  /**
57
- * Where reads go. Default: 'primary'.
56
+ * Which kind of node the client sends each read to. Defaults to 'primary'.
58
57
  */
59
58
  readPreference?: 'primary' | 'replica' | 'nearest';
60
59
  /**
61
- * Whether the client uses the nodes you listed or asks the group for them. Default: 'static'.
60
+ * Whether the client uses the nodes that you list or asks the group for them. Defaults to 'static'.
62
61
  */
63
62
  discovery?: 'static' | 'coordinator';
64
63
  /**
65
- * Currency every read requires, which the client uses to choose a node.
64
+ * The read concern that every read requires, which the client uses to choose a node.
66
65
  */
67
66
  readConcern?: ReadConcernLevel;
68
67
  }
69
68
  /**
70
- * Connects to a replication group rather than one server: it routes each read to a node that meets its read concern, and each write to the primary.
69
+ * Connects to a replication group, and routes each read to a node that meets its read concern and each write to the primary.
71
70
  *
72
71
  * @public
73
72
  */
@@ -84,19 +83,19 @@ declare class TopologyAwareClient extends DatabaseClient implements TopologyRout
84
83
  private latencies;
85
84
  private latencyMeasuredAt;
86
85
  private latencyMeasuring;
87
- private readonly removedReplicas;
86
+ private readonly endpointsSetAside;
88
87
  constructor(options: TopologyAwareClientOptions);
89
88
  /**
90
- * Builds the routing transport a database's requests travel over.
89
+ * Builds the routing transport that sends one database's requests.
91
90
  *
92
91
  * @param databaseId - Identifier of the database.
93
- * @returns A transport that picks a node per request.
92
+ * @returns A transport that picks a node for each request.
94
93
  */
95
94
  protected createTransport(databaseId: string): Transport;
96
95
  /** @internal */
97
96
  _createTransportForEndpoint(url: string, databaseId: string): Transport;
98
97
  /**
99
- * Returns the address writes are sent to, which is the current primary.
98
+ * Returns the address that the client sends writes to, which is the current primary.
100
99
  *
101
100
  * @param databaseId - Identifier of the database.
102
101
  * @returns Address of the node that accepts writes.
@@ -112,7 +111,8 @@ declare class TopologyAwareClient extends DatabaseClient implements TopologyRout
112
111
  /** @internal */
113
112
  _usesCoordinatorDiscovery(): boolean;
114
113
  /** @internal */
115
- _removeReplica(url: string): void;
114
+ _setEndpointAside(url: string): void;
115
+ private isReachable;
116
116
  /** @internal */
117
117
  _refreshClusterRouting(databaseId: string): Promise<void>;
118
118
  private fetchClusterRouting;