@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.
- package/README.md +79 -56
- package/dist/backup/index.d.ts +44 -45
- package/dist/backup/index.mjs +7 -7
- package/dist/backup-scheduler/index.d.ts +42 -41
- package/dist/backup-scheduler/index.mjs +4 -4
- package/dist/baseline-Co3iIsN_.d.ts +19 -0
- package/dist/change-tracker-DqCdeEyx.d.ts +128 -0
- package/dist/{chunk-VOSJBZ6Q.mjs → chunk-2ES3DT4A.mjs} +71 -33
- package/dist/{chunk-6CMCO7S4.mjs → chunk-2O3PP4CF.mjs} +1 -1
- package/dist/{chunk-E7Q7HE46.mjs → chunk-3BZLZ47R.mjs} +26 -26
- package/dist/chunk-4KASJKKK.mjs +2501 -0
- package/dist/{chunk-MYNI2WFS.mjs → chunk-52XW3Z76.mjs} +40 -8
- package/dist/chunk-55YRLSM2.mjs +57 -0
- package/dist/{chunk-KDAHYFW7.mjs → chunk-6AOJCNRU.mjs} +5 -5
- package/dist/{chunk-KXTYR2OQ.mjs → chunk-A3PHCO6W.mjs} +39 -111
- package/dist/chunk-ADNWEHGZ.mjs +299 -0
- package/dist/{chunk-2QLXDHAP.mjs → chunk-BLMRNSUG.mjs} +11 -2
- package/dist/chunk-BVMZJOVM.mjs +5 -0
- package/dist/{chunk-IWGIYDMZ.mjs → chunk-BZWDPF4G.mjs} +1 -1
- package/dist/{chunk-YVF2ZJTE.mjs → chunk-CZL4D3BM.mjs} +1 -1
- package/dist/{chunk-7SC2TZAS.mjs → chunk-DX4WGJL7.mjs} +2 -2
- package/dist/{chunk-45MKELYA.mjs → chunk-ESTZZPT5.mjs} +132 -13
- package/dist/{chunk-QP5L5DFJ.mjs → chunk-FJPR2CJX.mjs} +3 -3
- package/dist/chunk-HR7K3GZW.mjs +36 -0
- package/dist/{chunk-LXCGPZPA.mjs → chunk-IEKIYO4V.mjs} +1 -1
- package/dist/{chunk-TUD5CJ76.mjs → chunk-KB4OLJAD.mjs} +26 -19
- package/dist/{chunk-PBRXXISQ.mjs → chunk-KINDFBQP.mjs} +3 -2
- package/dist/{chunk-E6IMX7RS.mjs → chunk-KJLNYOOL.mjs} +2 -2
- package/dist/{chunk-BTTFW4Z4.mjs → chunk-KU32OT4H.mjs} +2 -10
- package/dist/{chunk-JQWGHSEH.mjs → chunk-NVSQ2T5Q.mjs} +71 -70
- package/dist/{chunk-W4EUTJNT.mjs → chunk-NZGSDALX.mjs} +94 -96
- package/dist/{chunk-U445A3C4.mjs → chunk-QGVRRKB2.mjs} +1 -1
- package/dist/{chunk-GYCEJNU4.mjs → chunk-R7GBXNTO.mjs} +1 -1
- package/dist/{chunk-7C36BCSN.mjs → chunk-VDN6FHU4.mjs} +1 -1
- package/dist/{chunk-6IYS4WLH.mjs → chunk-VO3ZWA2G.mjs} +142 -115
- package/dist/chunk-WOMADPM4.mjs +12 -0
- package/dist/{chunk-OUSWVNWT.mjs → chunk-YCMB6GR4.mjs} +1 -1
- package/dist/{chunk-YNTDOFTN.mjs → chunk-YW7IKQ7T.mjs} +1 -1
- package/dist/{chunk-M2BEAH5I.mjs → chunk-ZN4HI5FK.mjs} +126 -72
- package/dist/{chunk-SBL6GN43.mjs → chunk-ZYWI45EN.mjs} +1 -1
- package/dist/client/index.d.ts +135 -137
- package/dist/client/index.mjs +177 -82
- package/dist/client/topology.d.ts +31 -31
- package/dist/client/topology.mjs +42 -23
- package/dist/client-base-BxTocU1x.d.ts +398 -0
- package/dist/client-streams-YKANXMWO.mjs +112 -0
- package/dist/codegen/cli.mjs +4 -4
- package/dist/codegen/index.d.ts +36 -29
- package/dist/codegen/index.mjs +4 -4
- package/dist/core/index.d.ts +130 -115
- package/dist/core/index.mjs +387 -297
- package/dist/core/writer-worker.mjs +3 -3
- package/dist/database-CGDsXW1K.d.ts +959 -0
- package/dist/database-backup-DcLhxoa1.d.ts +143 -0
- package/dist/driver/better-sqlite3.d.ts +11 -10
- package/dist/driver/better-sqlite3.mjs +11 -11
- package/dist/driver/bun.d.ts +9 -8
- package/dist/driver/expo.d.ts +6 -7
- package/dist/driver/node.d.ts +11 -10
- package/dist/driver/node.mjs +13 -11
- package/dist/driver/wa-sqlite.d.ts +6 -6
- package/dist/errors-CJUkQ-KL.d.ts +199 -0
- package/dist/file-migrations/index.d.ts +20 -18
- package/dist/file-migrations/index.mjs +3 -3
- package/dist/forward-rpc-VRWFZXHN.mjs +44 -0
- package/dist/operation-registry-CGhNT-Sn.d.ts +96 -0
- package/dist/primary-wins-CHNOYuN7.d.ts +81 -0
- package/dist/protocol-DFpNNsIT.d.ts +153 -0
- package/dist/query-types-5B02ixH0.d.ts +129 -0
- package/dist/react/index.d.ts +22 -22
- package/dist/replication/coordinator/etcd.d.ts +37 -31
- package/dist/replication/coordinator/etcd.mjs +296 -131
- package/dist/replication/index.d.ts +201 -161
- package/dist/replication/index.mjs +630 -529
- package/dist/restore-options-CH8mE-Tb.d.ts +84 -0
- package/dist/server/index.d.ts +201 -191
- package/dist/server/index.mjs +736 -619
- package/dist/server-options-aYtCkefX.d.ts +317 -0
- package/dist/server-streams-24XL4T43.mjs +254 -0
- package/dist/sirannon-J3EgPVD7.d.ts +179 -0
- package/dist/transport/grpc.d.ts +37 -24
- package/dist/transport/grpc.mjs +83 -3199
- package/dist/transport/memory.d.ts +25 -26
- package/dist/transport/memory.mjs +18 -9
- package/dist/types-BrAiDGOb.d.ts +505 -0
- package/dist/types-CEJG0eFS.d.ts +88 -0
- package/dist/types-Dc8oWlxj.d.ts +1580 -0
- package/dist/types-DqODKeOq.d.ts +123 -0
- package/dist/types-guEKSzMr.d.ts +367 -0
- package/package.json +7 -7
- package/dist/baseline-D93hcIEE.d.ts +0 -17
- package/dist/change-tracker-CjRFuZdB.d.ts +0 -144
- package/dist/client-base-Cmd9EVme.d.ts +0 -397
- package/dist/database-BLQvqjsw.d.ts +0 -917
- package/dist/database-backup-B7oszQ2n.d.ts +0 -141
- package/dist/errors-Dei4GdBb.d.ts +0 -196
- package/dist/operation-registry-oiPuSYI6.d.ts +0 -94
- package/dist/primary-wins-B0np8JS3.d.ts +0 -85
- package/dist/protocol-HI-nY8DD.d.ts +0 -152
- package/dist/query-types-p1liOP-b.d.ts +0 -127
- package/dist/restore-options-CksYsH1l.d.ts +0 -83
- package/dist/server-options-Bj5iJnjx.d.ts +0 -292
- package/dist/sirannon-BPxC5tC2.d.ts +0 -178
- package/dist/types-B0EhgDbr.d.ts +0 -340
- package/dist/types-CbjfRwrK.d.ts +0 -499
- package/dist/types-CjhxcjhA.d.ts +0 -123
- package/dist/types-DPSmjU54.d.ts +0 -1518
- package/dist/types-DTO4CYhW.d.ts +0 -88
package/dist/client/index.mjs
CHANGED
|
@@ -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-
|
|
2
|
-
export { HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, ServerCapabilities, WebSocketTransport } from '../chunk-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
138
|
+
* Returns the string form of a clock reading, padded so that string order matches clock order.
|
|
139
139
|
*
|
|
140
|
-
* @param wallMs -
|
|
141
|
-
* @param logical -
|
|
142
|
-
* @param nodeId -
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
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 =
|
|
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
|
|
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(
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
this.
|
|
1215
|
-
this.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
1485
|
-
* it because this device is behind. A device
|
|
1486
|
-
*
|
|
1487
|
-
* server has migrated
|
|
1488
|
-
* subscription
|
|
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
|
-
|
|
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-
|
|
2
|
-
import { C as ClientOptions } from '../server-options-
|
|
3
|
-
import { T as Transport, D as DatabaseClient } from '../client-base-
|
|
4
|
-
import { H as ClusterStatusInfo } from '../types-
|
|
5
|
-
import '../operation-registry-
|
|
6
|
-
import '../types-
|
|
7
|
-
import '../types-
|
|
8
|
-
import '../protocol-
|
|
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
|
-
|
|
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
|
-
*
|
|
21
|
+
* Parses the body of `GET /db/{id}/cluster` and returns the cluster status that the server reports.
|
|
22
22
|
*
|
|
23
|
-
* The route
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
31
|
-
* @returns The status the server
|
|
32
|
-
* @throws A
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
48
|
+
* The address of the primary, for a group that you list by hand.
|
|
50
49
|
*/
|
|
51
50
|
primary?: string;
|
|
52
51
|
/**
|
|
53
|
-
*
|
|
52
|
+
* The addresses of the replicas, for a group that you list by hand.
|
|
54
53
|
*/
|
|
55
54
|
replicas?: string[];
|
|
56
55
|
/**
|
|
57
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
86
|
+
private readonly endpointsSetAside;
|
|
88
87
|
constructor(options: TopologyAwareClientOptions);
|
|
89
88
|
/**
|
|
90
|
-
* Builds the routing transport
|
|
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
|
|
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
|
|
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
|
-
|
|
114
|
+
_setEndpointAside(url: string): void;
|
|
115
|
+
private isReachable;
|
|
116
116
|
/** @internal */
|
|
117
117
|
_refreshClusterRouting(databaseId: string): Promise<void>;
|
|
118
118
|
private fetchClusterRouting;
|