cry-synced-db-client 0.1.139 → 0.1.141
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/CHANGELOG.md +30 -0
- package/dist/index.js +144 -116
- package/dist/src/db/managers/ConnectionManager.d.ts +39 -33
- package/dist/src/db/types/managers.d.ts +16 -8
- package/dist/src/types/I_SyncedDb.d.ts +19 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### BREAKING: Self-healing sync/reconnect lifecycle
|
|
6
|
+
|
|
7
|
+
Fixes a class of bugs where the 60s auto-sync scheduler silently died after a
|
|
8
|
+
sync failure or leader flap and was never re-armed until a page reload (see
|
|
9
|
+
`/tmp/cry-synced-db-client-sync-interval-bug.md`). Reproducers spanned 5
|
|
10
|
+
tenants, 62–296 min of dead scheduler with dirty items accumulating.
|
|
11
|
+
|
|
12
|
+
- Removed `onForcedOffline` callback and `ConnectionManager.goOffline()` method.
|
|
13
|
+
- Added `onSyncFailed(reason)` callback — fires on each sync failure but does
|
|
14
|
+
**not** mutate online state. The next auto-sync / reconnect tick retries.
|
|
15
|
+
- Added `onlineRetryIntervalMs` config (default 60000, 0 = disable) — periodic
|
|
16
|
+
`tryGoOnline()` probe while offline but not forcedOffline. Always-live from
|
|
17
|
+
`init()` to `close()` so recovery does not depend on external signals.
|
|
18
|
+
- `autoSyncTimer` and the new reconnect timer are both always-live from `init()`
|
|
19
|
+
to `close()`. `setOnline(false)`, `forceOffline(true)`, and sync failure no
|
|
20
|
+
longer clear timers — only flip flags; each tick is defensive.
|
|
21
|
+
- `SyncedDb.sync()` now opportunistically calls `tryGoOnline()` when internally
|
|
22
|
+
offline (but not forcedOffline). Syncs while `forceOffline(true)` still throw
|
|
23
|
+
`Cannot sync while in forced offline mode`.
|
|
24
|
+
- `onBecameLeader` now triggers `tryGoOnline()` when offline and post-init,
|
|
25
|
+
covering visibility re-claim and leader flap after mobile browser discards
|
|
26
|
+
state.
|
|
27
|
+
- `syncLock` is now acquired before the early `tryGoOnline()` in
|
|
28
|
+
`SyncedDb.sync()` so the internal `INITIAL SYNC` that `tryGoOnline()` kicks
|
|
29
|
+
off is a no-op inside the outer call (avoids double sync).
|
|
30
|
+
|
|
31
|
+
**Migration for consumers:**
|
|
32
|
+
`onForcedOffline: (reason) => log(reason)` → `onSyncFailed: (reason) => log(reason)`.
|
|
33
|
+
Signature is identical. No other callback changes.
|
|
34
|
+
|
|
5
35
|
- Add `refreshInBackground` `QueryOpts` option for `findById` / `findByIds`
|
|
6
36
|
- Stale-while-revalidate: cache-hit returns local result immediately and
|
|
7
37
|
triggers a background fetch that updates Dexie + in-mem through conflict
|
package/dist/index.js
CHANGED
|
@@ -842,46 +842,41 @@ var CrossTabSyncManager = class {
|
|
|
842
842
|
};
|
|
843
843
|
|
|
844
844
|
// src/db/managers/ConnectionManager.ts
|
|
845
|
+
var DEFAULT_ONLINE_RETRY_INTERVAL_MS = 6e4;
|
|
845
846
|
var ConnectionManager = class {
|
|
846
847
|
constructor(config) {
|
|
847
848
|
this.online = false;
|
|
848
849
|
this.forcedOffline = false;
|
|
850
|
+
this.tryGoOnlineInFlight = false;
|
|
851
|
+
this.closed = false;
|
|
852
|
+
var _a;
|
|
849
853
|
this.restInterface = config.restInterface;
|
|
850
854
|
this.restTimeoutMs = config.restTimeoutMs;
|
|
851
855
|
this.syncTimeoutMs = config.syncTimeoutMs;
|
|
852
856
|
this.autoSyncIntervalMs = config.autoSyncIntervalMs;
|
|
857
|
+
this.onlineRetryIntervalMs = (_a = config.onlineRetryIntervalMs) != null ? _a : DEFAULT_ONLINE_RETRY_INTERVAL_MS;
|
|
853
858
|
this.callbacks = config.callbacks;
|
|
854
859
|
this.deps = config.deps;
|
|
855
860
|
}
|
|
856
|
-
/**
|
|
857
|
-
* Current online status (considering forcedOffline).
|
|
858
|
-
*/
|
|
861
|
+
/** Current online status (considering forcedOffline). */
|
|
859
862
|
isOnline() {
|
|
860
863
|
return this.online && !this.forcedOffline;
|
|
861
864
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Is forced offline mode active?
|
|
864
|
-
*/
|
|
865
865
|
isForcedOffline() {
|
|
866
866
|
return this.forcedOffline;
|
|
867
867
|
}
|
|
868
|
-
/**
|
|
869
|
-
* Can we sync with server?
|
|
870
|
-
*/
|
|
871
868
|
canSync() {
|
|
872
869
|
return this.online && !this.forcedOffline;
|
|
873
870
|
}
|
|
874
|
-
/**
|
|
875
|
-
* Can we receive server updates?
|
|
876
|
-
*/
|
|
877
871
|
canReceiveServerUpdates() {
|
|
878
872
|
return !this.forcedOffline;
|
|
879
873
|
}
|
|
880
874
|
/**
|
|
881
|
-
* Set online status.
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
875
|
+
* Set online status. Does NOT stop any timers.
|
|
876
|
+
*
|
|
877
|
+
* - `setOnline(true)` attempts `tryGoOnline` (ping → flip state).
|
|
878
|
+
* - `setOnline(false)` flips state to offline and fires `onOnlineStatusChange`.
|
|
879
|
+
* The reconnect timer will continue attempting to come back online.
|
|
885
880
|
*/
|
|
886
881
|
async setOnline(online) {
|
|
887
882
|
var _a, _b;
|
|
@@ -889,19 +884,19 @@ var ConnectionManager = class {
|
|
|
889
884
|
if (online) {
|
|
890
885
|
await this.tryGoOnline();
|
|
891
886
|
} else {
|
|
892
|
-
this.online =
|
|
893
|
-
this.stopAutoSync();
|
|
887
|
+
this.online = false;
|
|
894
888
|
(_b = (_a = this.callbacks).onOnlineStatusChange) == null ? void 0 : _b.call(_a, false);
|
|
895
889
|
}
|
|
896
890
|
}
|
|
897
891
|
/**
|
|
898
|
-
* Force offline mode.
|
|
892
|
+
* Force offline mode. Does NOT stop timers — reconnect timer will still
|
|
893
|
+
* check `forcedOffline` and skip while true. When released, `tryGoOnline`
|
|
894
|
+
* fires immediately to avoid waiting for the next tick.
|
|
899
895
|
*/
|
|
900
896
|
forceOffline(forced) {
|
|
901
897
|
if (this.forcedOffline === forced) return;
|
|
902
898
|
this.forcedOffline = forced;
|
|
903
899
|
if (forced) {
|
|
904
|
-
this.stopAutoSync();
|
|
905
900
|
this.deps.releaseLeaderLock();
|
|
906
901
|
} else {
|
|
907
902
|
this.deps.tryBecomeLeader();
|
|
@@ -911,50 +906,86 @@ var ConnectionManager = class {
|
|
|
911
906
|
}
|
|
912
907
|
}
|
|
913
908
|
/**
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
* a tab should remain leader within its window to process WebSocket notifications
|
|
917
|
-
* even while offline due to network issues.
|
|
909
|
+
* Attempt to transition from offline to online.
|
|
910
|
+
* Idempotent, guards against concurrent calls and forcedOffline.
|
|
918
911
|
*/
|
|
919
|
-
|
|
920
|
-
var _a, _b;
|
|
921
|
-
|
|
922
|
-
this.
|
|
923
|
-
this.
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
912
|
+
async tryGoOnline() {
|
|
913
|
+
var _a, _b, _c;
|
|
914
|
+
if (this.closed) return;
|
|
915
|
+
if (this.forcedOffline) return;
|
|
916
|
+
if (this.tryGoOnlineInFlight) return;
|
|
917
|
+
this.tryGoOnlineInFlight = true;
|
|
918
|
+
try {
|
|
919
|
+
const wasOffline = !this.online;
|
|
920
|
+
if (wasOffline) {
|
|
921
|
+
let pingResult;
|
|
922
|
+
try {
|
|
923
|
+
pingResult = await this.withSyncTimeout(
|
|
924
|
+
this.restInterface.ping(),
|
|
925
|
+
"ping"
|
|
926
|
+
);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
console.warn("tryGoOnline: ping failed:", err);
|
|
929
|
+
this.online = false;
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (!pingResult) {
|
|
933
|
+
const url = (_a = this.restInterface.endpoint) != null ? _a : "unknown";
|
|
934
|
+
console.warn(`Ping to ${url} failed - staying offline`);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
this.online = true;
|
|
938
|
+
(_c = (_b = this.callbacks).onOnlineStatusChange) == null ? void 0 : _c.call(_b, true);
|
|
939
|
+
if (!this.deps.isLeader()) {
|
|
940
|
+
this.deps.tryBecomeLeader();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
928
943
|
try {
|
|
929
|
-
this.
|
|
944
|
+
await this.deps.sync("INITIAL SYNC");
|
|
930
945
|
} catch (err) {
|
|
931
|
-
console.
|
|
946
|
+
console.warn("INITIAL SYNC after tryGoOnline failed (stays online):", err);
|
|
932
947
|
}
|
|
948
|
+
} finally {
|
|
949
|
+
this.tryGoOnlineInFlight = false;
|
|
933
950
|
}
|
|
934
951
|
}
|
|
935
952
|
/**
|
|
936
|
-
* Start
|
|
953
|
+
* Start both timers. Idempotent. Called by SyncedDb.init().
|
|
937
954
|
*/
|
|
938
|
-
|
|
939
|
-
this.
|
|
940
|
-
if (this.
|
|
941
|
-
|
|
955
|
+
startTimers() {
|
|
956
|
+
this.closed = false;
|
|
957
|
+
if (!this.autoSyncTimer && this.autoSyncIntervalMs && this.autoSyncIntervalMs > 0) {
|
|
958
|
+
const intervalMs = this.autoSyncIntervalMs;
|
|
959
|
+
this.autoSyncTimer = setInterval(() => {
|
|
960
|
+
if (this.forcedOffline || !this.online) return;
|
|
961
|
+
this.deps.sync(`interval ${intervalMs}ms`).catch((err) => {
|
|
962
|
+
console.error("Auto-sync failed:", err);
|
|
963
|
+
});
|
|
964
|
+
}, intervalMs);
|
|
965
|
+
}
|
|
966
|
+
if (!this.reconnectTimer && this.onlineRetryIntervalMs && this.onlineRetryIntervalMs > 0) {
|
|
967
|
+
const retryMs = this.onlineRetryIntervalMs;
|
|
968
|
+
this.reconnectTimer = setInterval(() => {
|
|
969
|
+
if (this.forcedOffline || this.online || this.tryGoOnlineInFlight) return;
|
|
970
|
+
this.tryGoOnline().catch((err) => {
|
|
971
|
+
console.error("Reconnect tryGoOnline failed:", err);
|
|
972
|
+
});
|
|
973
|
+
}, retryMs);
|
|
942
974
|
}
|
|
943
|
-
const intervalMs = this.autoSyncIntervalMs;
|
|
944
|
-
this.autoSyncTimer = setInterval(() => {
|
|
945
|
-
this.deps.sync(`interval ${intervalMs}ms`).catch((err) => {
|
|
946
|
-
console.error("Auto-sync failed:", err);
|
|
947
|
-
});
|
|
948
|
-
}, intervalMs);
|
|
949
975
|
}
|
|
950
976
|
/**
|
|
951
|
-
* Stop
|
|
977
|
+
* Stop both timers. Called by SyncedDb.close().
|
|
952
978
|
*/
|
|
953
|
-
|
|
979
|
+
stopTimers() {
|
|
980
|
+
this.closed = true;
|
|
954
981
|
if (this.autoSyncTimer) {
|
|
955
982
|
clearInterval(this.autoSyncTimer);
|
|
956
983
|
this.autoSyncTimer = void 0;
|
|
957
984
|
}
|
|
985
|
+
if (this.reconnectTimer) {
|
|
986
|
+
clearInterval(this.reconnectTimer);
|
|
987
|
+
this.reconnectTimer = void 0;
|
|
988
|
+
}
|
|
958
989
|
}
|
|
959
990
|
/**
|
|
960
991
|
* Ping server.
|
|
@@ -1025,6 +1056,19 @@ var ConnectionManager = class {
|
|
|
1025
1056
|
}
|
|
1026
1057
|
}
|
|
1027
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Notify consumers of a sync failure. Does not mutate state.
|
|
1061
|
+
* Called from SyncEngine via deps.onSyncFailed wiring.
|
|
1062
|
+
*/
|
|
1063
|
+
callOnSyncFailed(reason) {
|
|
1064
|
+
if (this.callbacks.onSyncFailed) {
|
|
1065
|
+
try {
|
|
1066
|
+
this.callbacks.onSyncFailed(reason);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
console.error("onSyncFailed callback failed:", err);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1028
1072
|
/**
|
|
1029
1073
|
* Call onWsConnect callback.
|
|
1030
1074
|
*/
|
|
@@ -1083,43 +1127,6 @@ var ConnectionManager = class {
|
|
|
1083
1127
|
getOnWsReconnect() {
|
|
1084
1128
|
return this.callbacks.onWsReconnect;
|
|
1085
1129
|
}
|
|
1086
|
-
// ============================================================
|
|
1087
|
-
// Private Methods
|
|
1088
|
-
// ============================================================
|
|
1089
|
-
async tryGoOnline() {
|
|
1090
|
-
var _a, _b, _c, _d, _e;
|
|
1091
|
-
if (this.forcedOffline) {
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
try {
|
|
1095
|
-
const pingResult = await this.withSyncTimeout(
|
|
1096
|
-
this.restInterface.ping(),
|
|
1097
|
-
"ping"
|
|
1098
|
-
);
|
|
1099
|
-
if (!pingResult) {
|
|
1100
|
-
const url = (_a = this.restInterface.endpoint) != null ? _a : "unknown";
|
|
1101
|
-
console.warn(`Ping to ${url} failed - staying offline`);
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
const wasOffline = !this.online;
|
|
1105
|
-
this.online = true;
|
|
1106
|
-
if (wasOffline) {
|
|
1107
|
-
(_c = (_b = this.callbacks).onOnlineStatusChange) == null ? void 0 : _c.call(_b, true);
|
|
1108
|
-
if (!this.deps.isLeader()) {
|
|
1109
|
-
this.deps.tryBecomeLeader();
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
this.startAutoSync();
|
|
1113
|
-
await this.deps.sync("INITIAL SYNC");
|
|
1114
|
-
} catch (err) {
|
|
1115
|
-
console.warn("Failed to go online (ping failed or timed out):", err);
|
|
1116
|
-
const wasOnline = this.online;
|
|
1117
|
-
this.online = false;
|
|
1118
|
-
if (wasOnline) {
|
|
1119
|
-
(_e = (_d = this.callbacks).onOnlineStatusChange) == null ? void 0 : _e.call(_d, false);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
1130
|
};
|
|
1124
1131
|
|
|
1125
1132
|
// node_modules/superjson/dist/double-indexed-kv.js
|
|
@@ -2541,8 +2548,8 @@ var _SyncEngine = class _SyncEngine {
|
|
|
2541
2548
|
});
|
|
2542
2549
|
} catch (err) {
|
|
2543
2550
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2544
|
-
console.error("Sync failed
|
|
2545
|
-
this.deps.
|
|
2551
|
+
console.error("Sync failed:", err);
|
|
2552
|
+
this.deps.onSyncFailed(`Sync failed: ${reason}`);
|
|
2546
2553
|
this.callOnSyncEnd({
|
|
2547
2554
|
durationMs: Date.now() - startTime,
|
|
2548
2555
|
receivedCount,
|
|
@@ -3511,7 +3518,20 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3511
3518
|
tenant: this.tenant,
|
|
3512
3519
|
windowId,
|
|
3513
3520
|
callbacks: {
|
|
3514
|
-
onBecameLeader:
|
|
3521
|
+
onBecameLeader: () => {
|
|
3522
|
+
if (this.initialized && !this.connectionManager.isOnline() && !this.connectionManager.isForcedOffline()) {
|
|
3523
|
+
this.connectionManager.tryGoOnline().catch((err) => {
|
|
3524
|
+
console.error("tryGoOnline on becameLeader failed:", err);
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
if (config.onBecameLeader) {
|
|
3528
|
+
try {
|
|
3529
|
+
config.onBecameLeader();
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
console.error("onBecameLeader callback failed:", err);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
},
|
|
3515
3535
|
onLostLeadership: config.onLostLeadership,
|
|
3516
3536
|
onInfrastructureError: config.onInfrastructureError ? (type, message, error) => {
|
|
3517
3537
|
config.onInfrastructureError({
|
|
@@ -3556,9 +3576,10 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3556
3576
|
restTimeoutMs: (_h = config.restTimeoutMs) != null ? _h : 9e4,
|
|
3557
3577
|
syncTimeoutMs: (_i = config.syncTimeoutMs) != null ? _i : 12e4,
|
|
3558
3578
|
autoSyncIntervalMs: config.autoSyncIntervalMs,
|
|
3579
|
+
onlineRetryIntervalMs: config.onlineRetryIntervalMs,
|
|
3559
3580
|
callbacks: {
|
|
3560
3581
|
onOnlineStatusChange: config.onOnlineStatusChange,
|
|
3561
|
-
|
|
3582
|
+
onSyncFailed: config.onSyncFailed,
|
|
3562
3583
|
onWsConnect: config.onWsConnect,
|
|
3563
3584
|
onWsDisconnect: config.onWsDisconnect,
|
|
3564
3585
|
onWsReconnect: config.onWsReconnect,
|
|
@@ -3628,7 +3649,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3628
3649
|
},
|
|
3629
3650
|
getInMemById: (collection, id) => this.inMemDb.getById(collection, id),
|
|
3630
3651
|
withSyncTimeout: (promise, operation) => this.connectionManager.withSyncTimeout(promise, operation),
|
|
3631
|
-
|
|
3652
|
+
onSyncFailed: (reason) => this.connectionManager.callOnSyncFailed(reason),
|
|
3632
3653
|
flushAllPendingChanges: () => this.pendingChanges.flushAll(),
|
|
3633
3654
|
cancelRestUploadTimer: () => this.pendingChanges.cancelRestUploadTimer(),
|
|
3634
3655
|
awaitRestUpload: () => this.pendingChanges.awaitRestUpload(),
|
|
@@ -3769,6 +3790,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3769
3790
|
this.crossTabSync.init();
|
|
3770
3791
|
(_a = this.wakeSync) == null ? void 0 : _a.init();
|
|
3771
3792
|
(_b = this.networkStatus) == null ? void 0 : _b.init();
|
|
3793
|
+
this.connectionManager.startTimers();
|
|
3772
3794
|
if (this.serverUpdateNotifier) {
|
|
3773
3795
|
if (this.serverUpdateNotifier.setCallbacks) {
|
|
3774
3796
|
const cleanup = this.serverUpdateNotifier.setCallbacks({
|
|
@@ -3875,7 +3897,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3875
3897
|
var _a, _b;
|
|
3876
3898
|
this.leaderElection.setClosing(true);
|
|
3877
3899
|
this.pendingChanges.cancelRestUploadTimer();
|
|
3878
|
-
this.connectionManager.
|
|
3900
|
+
this.connectionManager.stopTimers();
|
|
3879
3901
|
await this.pendingChanges.flushAll();
|
|
3880
3902
|
(_a = this.networkStatus) == null ? void 0 : _a.dispose();
|
|
3881
3903
|
(_b = this.wakeSync) == null ? void 0 : _b.dispose();
|
|
@@ -4423,39 +4445,45 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4423
4445
|
}
|
|
4424
4446
|
// ==================== Sync Operations ====================
|
|
4425
4447
|
async sync(calledFrom) {
|
|
4426
|
-
if (!this.connectionManager.canSync()) {
|
|
4427
|
-
if (this.connectionManager.isForcedOffline()) {
|
|
4428
|
-
throw new Error("Cannot sync while in forced offline mode");
|
|
4429
|
-
}
|
|
4430
|
-
return;
|
|
4431
|
-
}
|
|
4432
4448
|
if (this.syncLock) return;
|
|
4433
4449
|
this.syncLock = true;
|
|
4434
|
-
this.syncing = true;
|
|
4435
|
-
this.crossTabSync.startServerSync();
|
|
4436
4450
|
try {
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4451
|
+
if (!this.connectionManager.isOnline() && !this.connectionManager.isForcedOffline()) {
|
|
4452
|
+
await this.connectionManager.tryGoOnline();
|
|
4453
|
+
}
|
|
4454
|
+
if (!this.connectionManager.canSync()) {
|
|
4455
|
+
if (this.connectionManager.isForcedOffline()) {
|
|
4456
|
+
throw new Error("Cannot sync while in forced offline mode");
|
|
4457
|
+
}
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
this.syncing = true;
|
|
4461
|
+
this.crossTabSync.startServerSync();
|
|
4462
|
+
try {
|
|
4463
|
+
await this.syncEngine.sync(calledFrom);
|
|
4464
|
+
if (!this.syncOnlyCollections) {
|
|
4465
|
+
const now = /* @__PURE__ */ new Date();
|
|
4466
|
+
if (!this._lastFullSyncDate) {
|
|
4467
|
+
this._setLastInitialSync(now).catch((err) => {
|
|
4468
|
+
console.error("Failed to persist lastInitialSync:", err);
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
this._setLastFullSync(now).catch((err) => {
|
|
4472
|
+
console.error("Failed to persist lastFullSync:", err);
|
|
4443
4473
|
});
|
|
4444
4474
|
}
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4475
|
+
} finally {
|
|
4476
|
+
this.syncing = false;
|
|
4477
|
+
this.crossTabSync.endServerSync();
|
|
4478
|
+
await this.processQueuedWsUpdates();
|
|
4479
|
+
try {
|
|
4480
|
+
await this.maybeAutoEvict();
|
|
4481
|
+
} catch (err) {
|
|
4482
|
+
console.error("Auto-eviction failed:", err);
|
|
4483
|
+
}
|
|
4448
4484
|
}
|
|
4449
4485
|
} finally {
|
|
4450
|
-
this.syncing = false;
|
|
4451
4486
|
this.syncLock = false;
|
|
4452
|
-
this.crossTabSync.endServerSync();
|
|
4453
|
-
await this.processQueuedWsUpdates();
|
|
4454
|
-
try {
|
|
4455
|
-
await this.maybeAutoEvict();
|
|
4456
|
-
} catch (err) {
|
|
4457
|
-
console.error("Auto-eviction failed:", err);
|
|
4458
|
-
}
|
|
4459
4487
|
}
|
|
4460
4488
|
}
|
|
4461
4489
|
async processQueuedWsUpdates() {
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ConnectionManager - Manages online/offline state
|
|
2
|
+
* ConnectionManager - Manages online/offline state, auto-sync and reconnect.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
4
|
+
* Invariants:
|
|
5
|
+
* - `autoSyncTimer` and `reconnectTimer` are always-live from `startTimers()`
|
|
6
|
+
* (called by SyncedDb during init) until `stopTimers()` (called by close()).
|
|
7
|
+
* - Neither timer is cleared by `setOnline(false)`, `forceOffline(true)`, or
|
|
8
|
+
* sync failure — state changes only flip flags. Each tick is defensive and
|
|
9
|
+
* no-ops when inapplicable.
|
|
10
|
+
* - `autoSyncTimer` tick: run `sync()` iff `online && !forcedOffline`.
|
|
11
|
+
* - `reconnectTimer` tick: call `tryGoOnline()` iff `!online && !forcedOffline`
|
|
12
|
+
* and no `tryGoOnline` is in flight.
|
|
13
|
+
* - `tryGoOnline()` pings the server; on success flips `online=true` (next
|
|
14
|
+
* auto-sync tick then runs). Historical `goOffline(reason)` method and
|
|
15
|
+
* `onForcedOffline` callback are removed — use `onSyncFailed` (logging-only
|
|
16
|
+
* callback fired from SyncEngine) or explicit `forceOffline(true)`.
|
|
9
17
|
*/
|
|
10
18
|
import type { I_ConnectionManager, ConnectionManagerConfig } from "../types/managers";
|
|
11
19
|
export declare class ConnectionManager implements I_ConnectionManager {
|
|
@@ -13,54 +21,48 @@ export declare class ConnectionManager implements I_ConnectionManager {
|
|
|
13
21
|
private readonly restTimeoutMs;
|
|
14
22
|
private readonly syncTimeoutMs;
|
|
15
23
|
private readonly autoSyncIntervalMs?;
|
|
24
|
+
private readonly onlineRetryIntervalMs;
|
|
16
25
|
private readonly callbacks;
|
|
17
26
|
private readonly deps;
|
|
18
27
|
private online;
|
|
19
28
|
private forcedOffline;
|
|
20
29
|
private autoSyncTimer?;
|
|
30
|
+
private reconnectTimer?;
|
|
31
|
+
private tryGoOnlineInFlight;
|
|
32
|
+
private closed;
|
|
21
33
|
constructor(config: ConnectionManagerConfig);
|
|
22
|
-
/**
|
|
23
|
-
* Current online status (considering forcedOffline).
|
|
24
|
-
*/
|
|
34
|
+
/** Current online status (considering forcedOffline). */
|
|
25
35
|
isOnline(): boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Is forced offline mode active?
|
|
28
|
-
*/
|
|
29
36
|
isForcedOffline(): boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Can we sync with server?
|
|
32
|
-
*/
|
|
33
37
|
canSync(): boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Can we receive server updates?
|
|
36
|
-
*/
|
|
37
38
|
canReceiveServerUpdates(): boolean;
|
|
38
39
|
/**
|
|
39
|
-
* Set online status.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Set online status. Does NOT stop any timers.
|
|
41
|
+
*
|
|
42
|
+
* - `setOnline(true)` attempts `tryGoOnline` (ping → flip state).
|
|
43
|
+
* - `setOnline(false)` flips state to offline and fires `onOnlineStatusChange`.
|
|
44
|
+
* The reconnect timer will continue attempting to come back online.
|
|
43
45
|
*/
|
|
44
46
|
setOnline(online: boolean): Promise<void>;
|
|
45
47
|
/**
|
|
46
|
-
* Force offline mode.
|
|
48
|
+
* Force offline mode. Does NOT stop timers — reconnect timer will still
|
|
49
|
+
* check `forcedOffline` and skip while true. When released, `tryGoOnline`
|
|
50
|
+
* fires immediately to avoid waiting for the next tick.
|
|
47
51
|
*/
|
|
48
52
|
forceOffline(forced: boolean): void;
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* a tab should remain leader within its window to process WebSocket notifications
|
|
53
|
-
* even while offline due to network issues.
|
|
54
|
+
* Attempt to transition from offline to online.
|
|
55
|
+
* Idempotent, guards against concurrent calls and forcedOffline.
|
|
54
56
|
*/
|
|
55
|
-
|
|
57
|
+
tryGoOnline(): Promise<void>;
|
|
56
58
|
/**
|
|
57
|
-
* Start
|
|
59
|
+
* Start both timers. Idempotent. Called by SyncedDb.init().
|
|
58
60
|
*/
|
|
59
|
-
|
|
61
|
+
startTimers(): void;
|
|
60
62
|
/**
|
|
61
|
-
* Stop
|
|
63
|
+
* Stop both timers. Called by SyncedDb.close().
|
|
62
64
|
*/
|
|
63
|
-
|
|
65
|
+
stopTimers(): void;
|
|
64
66
|
/**
|
|
65
67
|
* Ping server.
|
|
66
68
|
*/
|
|
@@ -77,6 +79,11 @@ export declare class ConnectionManager implements I_ConnectionManager {
|
|
|
77
79
|
* Report infrastructure error.
|
|
78
80
|
*/
|
|
79
81
|
reportInfrastructureError(type: string, message: string, error?: Error): void;
|
|
82
|
+
/**
|
|
83
|
+
* Notify consumers of a sync failure. Does not mutate state.
|
|
84
|
+
* Called from SyncEngine via deps.onSyncFailed wiring.
|
|
85
|
+
*/
|
|
86
|
+
callOnSyncFailed(reason: string): void;
|
|
80
87
|
/**
|
|
81
88
|
* Call onWsConnect callback.
|
|
82
89
|
*/
|
|
@@ -101,5 +108,4 @@ export declare class ConnectionManager implements I_ConnectionManager {
|
|
|
101
108
|
* Get onWsReconnect callback.
|
|
102
109
|
*/
|
|
103
110
|
getOnWsReconnect(): ((attempt: number) => void) | undefined;
|
|
104
|
-
private tryGoOnline;
|
|
105
111
|
}
|
|
@@ -91,7 +91,10 @@ export interface I_CrossTabSyncManager {
|
|
|
91
91
|
}
|
|
92
92
|
export interface ConnectionCallbacks {
|
|
93
93
|
onOnlineStatusChange?: (online: boolean) => void;
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Fired on sync failure. Does NOT mutate online state. For logging only.
|
|
96
|
+
*/
|
|
97
|
+
onSyncFailed?: (reason: string) => void;
|
|
95
98
|
onWsConnect?: () => void;
|
|
96
99
|
onWsDisconnect?: (reason: string) => void;
|
|
97
100
|
onWsReconnect?: (attempt: number) => void;
|
|
@@ -112,6 +115,7 @@ export interface ConnectionManagerConfig {
|
|
|
112
115
|
restTimeoutMs: number;
|
|
113
116
|
syncTimeoutMs: number;
|
|
114
117
|
autoSyncIntervalMs?: number;
|
|
118
|
+
onlineRetryIntervalMs?: number;
|
|
115
119
|
callbacks: ConnectionCallbacks;
|
|
116
120
|
deps: ConnectionManagerDeps;
|
|
117
121
|
}
|
|
@@ -128,12 +132,15 @@ export interface I_ConnectionManager {
|
|
|
128
132
|
setOnline(online: boolean): Promise<void>;
|
|
129
133
|
/** Force offline mode. */
|
|
130
134
|
forceOffline(forced: boolean): void;
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Attempt to transition from internal-offline to online (ping + start timers
|
|
137
|
+
* if successful). No-op if already online, forcedOffline, or a try is in flight.
|
|
138
|
+
*/
|
|
139
|
+
tryGoOnline(): Promise<void>;
|
|
140
|
+
/** Start both auto-sync and reconnect timers (idempotent). */
|
|
141
|
+
startTimers(): void;
|
|
142
|
+
/** Stop both auto-sync and reconnect timers. Called from close(). */
|
|
143
|
+
stopTimers(): void;
|
|
137
144
|
/** Ping server. */
|
|
138
145
|
ping(timeoutMs?: number): Promise<boolean>;
|
|
139
146
|
/** Wrap promise with sync timeout. */
|
|
@@ -256,7 +263,8 @@ export interface SyncEngineDeps {
|
|
|
256
263
|
writeToInMemBatch: <T extends DbEntity>(collection: string, items: T[], operation: "upsert" | "delete") => void;
|
|
257
264
|
getInMemById: <T extends DbEntity>(collection: string, id: Id) => T | undefined;
|
|
258
265
|
withSyncTimeout: <T>(promise: Promise<T>, operation: string) => Promise<T>;
|
|
259
|
-
|
|
266
|
+
/** Notify consumers that a sync cycle failed. Does not mutate online state. */
|
|
267
|
+
onSyncFailed: (reason: string) => void;
|
|
260
268
|
flushAllPendingChanges: () => Promise<void>;
|
|
261
269
|
cancelRestUploadTimer: () => void;
|
|
262
270
|
awaitRestUpload: () => Promise<void>;
|
|
@@ -317,8 +317,13 @@ export interface SyncedDbConfig {
|
|
|
317
317
|
debounceDexieWritesMs?: number;
|
|
318
318
|
/** Debounce čas za pošiljanje na REST v ms (default: 100) - po uspešnem zapisu v Dexie */
|
|
319
319
|
debounceRestWritesMs?: number;
|
|
320
|
-
/**
|
|
321
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Callback fired on each sync failure. Unlike the removed `onForcedOffline`,
|
|
322
|
+
* this does NOT mutate online state — the library keeps trying on the next
|
|
323
|
+
* auto-sync tick. Use this callback for logging/telemetry only. To actually
|
|
324
|
+
* force the database offline, call {@link forceOffline}.
|
|
325
|
+
*/
|
|
326
|
+
onSyncFailed?: (reason: string) => void;
|
|
322
327
|
/** Callback fired once during init() when the IndexedDB database was created fresh (first ever open). */
|
|
323
328
|
onDatabaseCreated?: () => void;
|
|
324
329
|
/** Callback at the start of each sync cycle. initialSync=true if no full sync has completed yet. */
|
|
@@ -381,8 +386,19 @@ export interface SyncedDbConfig {
|
|
|
381
386
|
* Če je podano, se sync() kliče avtomatsko na ta interval, ko je online.
|
|
382
387
|
* Auto-sync se izvaja samo ko je online in ne bo interferiral z eksplicitnimi sync() klici
|
|
383
388
|
* (uporablja isti syncLock mehanizem).
|
|
389
|
+
*
|
|
390
|
+
* Timer je zagnan v init() in ustavljen v close(). Če je offline ali
|
|
391
|
+
* forcedOffline, tick-i so no-op (ne ubije se timer, naslednji tick bo spet
|
|
392
|
+
* poskusil). Self-healing: sync napaka ne ustavi timer-ja.
|
|
384
393
|
*/
|
|
385
394
|
autoSyncIntervalMs?: number;
|
|
395
|
+
/**
|
|
396
|
+
* Interval za periodično poskušanje preklopa iz offline v online, v ms.
|
|
397
|
+
* Vedno-živi timer: od init() do close(). Če smo offline in ne forcedOffline,
|
|
398
|
+
* vsak tick pokliče `tryGoOnline()` (ping → če uspešen, gremo online in naslednji
|
|
399
|
+
* auto-sync tick bo sinhroniziral). Default: 60000 (60 s). 0/undefined disable.
|
|
400
|
+
*/
|
|
401
|
+
onlineRetryIntervalMs?: number;
|
|
386
402
|
/** Callback when WebSocket connects */
|
|
387
403
|
onWsConnect?: () => void;
|
|
388
404
|
/** Callback when WebSocket disconnects */
|
|
@@ -391,7 +407,7 @@ export interface SyncedDbConfig {
|
|
|
391
407
|
onWsReconnect?: (attempt: number) => void;
|
|
392
408
|
/** Callback when a WebSocket notification is received */
|
|
393
409
|
onWsNotification?: (info: WsNotificationInfo) => void;
|
|
394
|
-
/** Callback when online status changes (after ping success/failure in tryGoOnline
|
|
410
|
+
/** Callback when online status changes (after ping success/failure in tryGoOnline) */
|
|
395
411
|
onOnlineStatusChange?: (online: boolean) => void;
|
|
396
412
|
/** Debounce interval for cross-tab sync broadcasts in ms (default: 100) */
|
|
397
413
|
crossTabSyncDebounceMs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cry-synced-db-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.141",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"vitest": "^4.1.2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"cry-db": "^2.4.
|
|
39
|
+
"cry-db": "^2.4.31",
|
|
40
40
|
"cry-helpers": "^2.1.193",
|
|
41
41
|
"msgpackr": "^1.11.9",
|
|
42
42
|
"notepack": "^0.0.2",
|