cry-synced-db-client 0.1.168 → 0.1.171

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 CHANGED
@@ -2,6 +2,102 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Runtime collection registration (`addCollectionToSync`, `replaceSyncCollection`)
6
+
7
+ Two methods to install / replace collection configs at runtime; both load the
8
+ Dexie cursor into the sync-meta cache, hydrate in-mem from Dexie, extend an
9
+ active `syncOnlyTheseCollections` filter, and fire a one-shot **targeted**
10
+ download for just that collection (`syncCollectionForFind` →
11
+ `processCollectionServerData`, cursor advances inline). Other collections
12
+ are not re-fetched.
13
+
14
+ **`addCollectionToSync(spec)` — safe, idempotent** (Promise<void>)
15
+
16
+ | Existing config | Action |
17
+ |---|---|
18
+ | permanent (`temporaryConfig !== true`) | no-op |
19
+ | temporary (`temporaryConfig === true`) | replaced |
20
+ | none | added |
21
+
22
+ **`replaceSyncCollection(spec)` — explicit re-config** (Promise<boolean>)
23
+
24
+ | Existing | New | Result |
25
+ |---|---|---|
26
+ | none | any | install, returns `true` |
27
+ | temporary | any | replace, returns `true` |
28
+ | permanent | permanent | replace, returns `true` |
29
+ | permanent | temporary | **blocked, returns `false`** (no-op) |
30
+
31
+ The only blocked transition is permanent → temporary (cannot downgrade an
32
+ established config to provisional).
33
+
34
+ ```typescript
35
+ // Boot with a provisional config so the Dexie schema includes the
36
+ // collection but the real query/conflict-resolver is deferred.
37
+ new SyncedDb({ collections: [{ name: "obravnave", temporaryConfig: true }] });
38
+
39
+ // Idempotent install after login:
40
+ await syncedDb.addCollectionToSync({
41
+ name: "obravnave",
42
+ syncConfig: { query: () => ({ lokacija_id: currentLokacijaId }) },
43
+ });
44
+
45
+ // Later, user switches lokacija — explicit re-config:
46
+ const ok = await syncedDb.replaceSyncCollection({
47
+ name: "obravnave",
48
+ syncConfig: { query: () => ({ lokacija_id: newLokacijaId }) },
49
+ });
50
+ // ok === true (replacement of permanent with permanent allowed)
51
+ ```
52
+
53
+ ### `flushToServer()` + automatic flush on `visibilitychange:hidden`
54
+
55
+ `SyncedDb.flushToServer(calledFrom?)` pushes dirty data to the server
56
+ immediately, bypassing `debounceRestWritesMs`. Flow: cancel REST upload
57
+ debounce → `flushAll()` (pending Dexie writes) → `awaitRestUpload()`
58
+ (in-flight upload) → `uploadDirtyItems(calledFrom)`. No-op when offline,
59
+ forced offline, or pre-`init`.
60
+
61
+ A `visibilitychange` listener fires `flushToServer("visibility-hidden")`
62
+ automatically when the tab becomes `hidden`. Browser may throttle or
63
+ suspend a hidden tab shortly after the event, so the debounced upload
64
+ could otherwise never run.
65
+
66
+ ```typescript
67
+ // Manual flush (e.g. before navigation)
68
+ await syncedDb.flushToServer("pre-navigate");
69
+
70
+ // Automatic: no caller action required. Listener registered in init(),
71
+ // cleaned up in close().
72
+ ```
73
+
74
+ Upload itself is **not gated on `isLeader`** — `PendingChangesManager`
75
+ guards only on `canSync()` — so followers also drain their dirty queue
76
+ on hidden. Cancelling the timer cancels only the *current* pending burst:
77
+ subsequent writes schedule a new timer and the auto-sync interval still
78
+ fires. To permanently stop uploads, call `close()` or `forceOffline(true)`.
79
+
80
+ ### Leadership transition timestamps (`leaderSince` / `followerSince`)
81
+
82
+ Two new getters on `SyncedDb` (and `I_LeaderElectionManager`):
83
+
84
+ - `leaderSince(): Date | undefined` — when this tab acquired the Web Locks
85
+ leader lock; `undefined` while a follower
86
+ - `followerSince(): Date | undefined` — when this tab transitioned to
87
+ follower (or, for tabs that never claimed leadership, the construction
88
+ timestamp); `undefined` while the leader
89
+
90
+ Exactly one of the two is defined at any time after construction. Useful
91
+ for heartbeat payloads / debug overlays (`Date.now() - leaderSince()` =
92
+ "time in current leadership state").
93
+
94
+ ```typescript
95
+ const isLeader = syncedDb.isLeaderTab();
96
+ const sinceMs = isLeader
97
+ ? Date.now() - syncedDb.leaderSince()!.getTime()
98
+ : Date.now() - syncedDb.followerSince()!.getTime();
99
+ ```
100
+
5
101
  ### `onServerSyncWrite` callback
6
102
 
7
103
  Single-shot callback that fires once per `restInterface.updateCollections`
package/dist/index.js CHANGED
@@ -876,6 +876,7 @@ var LeaderElectionManager = class {
876
876
  this.isLeaderFlag = false;
877
877
  this.closing = false;
878
878
  this.releasingDueToVisibility = false;
879
+ this._followerSince = /* @__PURE__ */ new Date();
879
880
  this.tenant = config.tenant;
880
881
  this.windowId = config.windowId;
881
882
  this.callbacks = config.callbacks;
@@ -973,6 +974,21 @@ var LeaderElectionManager = class {
973
974
  isLeader() {
974
975
  return this.isLeaderFlag;
975
976
  }
977
+ /**
978
+ * Returns the timestamp when this tab became leader, or undefined if currently a follower.
979
+ * Cleared on transition to follower.
980
+ */
981
+ leaderSince() {
982
+ return this._leaderSince;
983
+ }
984
+ /**
985
+ * Returns the timestamp when this tab became follower, or undefined if currently a leader.
986
+ * Initialized at construction (every tab starts as a follower until it claims the lock).
987
+ * Cleared on transition to leader.
988
+ */
989
+ followerSince() {
990
+ return this._followerSince;
991
+ }
976
992
  /**
977
993
  * Cleanup resources.
978
994
  */
@@ -1024,6 +1040,8 @@ var LeaderElectionManager = class {
1024
1040
  }
1025
1041
  }
1026
1042
  callOnBecameLeader() {
1043
+ this._leaderSince = /* @__PURE__ */ new Date();
1044
+ this._followerSince = void 0;
1027
1045
  if (this.callbacks.onBecameLeader) {
1028
1046
  try {
1029
1047
  this.callbacks.onBecameLeader();
@@ -1033,6 +1051,8 @@ var LeaderElectionManager = class {
1033
1051
  }
1034
1052
  }
1035
1053
  callOnLostLeadership() {
1054
+ this._followerSince = /* @__PURE__ */ new Date();
1055
+ this._leaderSince = void 0;
1036
1056
  if (this.callbacks.onLostLeadership) {
1037
1057
  try {
1038
1058
  this.callbacks.onLostLeadership();
@@ -4381,8 +4401,8 @@ var _SyncedDb = class _SyncedDb {
4381
4401
  });
4382
4402
  this.pendingChanges = new PendingChangesManager({
4383
4403
  tenant: this.tenant,
4384
- debounceDexieWritesMs: (_l = config.debounceDexieWritesMs) != null ? _l : 500,
4385
- debounceRestWritesMs: (_m = config.debounceRestWritesMs) != null ? _m : 100,
4404
+ debounceDexieWritesMs: (_l = config.debounceDexieWritesMs) != null ? _l : 200,
4405
+ debounceRestWritesMs: (_m = config.debounceRestWritesMs) != null ? _m : 1e3,
4386
4406
  callbacks: {
4387
4407
  onDexieWriteRequest: config.onDexieWriteRequest,
4388
4408
  onDexieWriteResult: config.onDexieWriteResult,
@@ -4499,6 +4519,55 @@ var _SyncedDb = class _SyncedDb {
4499
4519
  isLeaderTab() {
4500
4520
  return this.leaderElection.isLeader();
4501
4521
  }
4522
+ leaderSince() {
4523
+ return this.leaderElection.leaderSince();
4524
+ }
4525
+ followerSince() {
4526
+ return this.leaderElection.followerSince();
4527
+ }
4528
+ /**
4529
+ * Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
4530
+ */
4531
+ async addCollectionToSync(spec) {
4532
+ const existing = this.collections.get(spec.name);
4533
+ if (existing && !existing.temporaryConfig) {
4534
+ return;
4535
+ }
4536
+ await this._installCollectionConfig(spec);
4537
+ }
4538
+ /**
4539
+ * Replace the collection config and re-sync. See `I_SyncedDb.replaceSyncCollection`.
4540
+ */
4541
+ async replaceSyncCollection(spec) {
4542
+ const existing = this.collections.get(spec.name);
4543
+ if (existing && !existing.temporaryConfig && spec.temporaryConfig) {
4544
+ return false;
4545
+ }
4546
+ await this._installCollectionConfig(spec);
4547
+ return true;
4548
+ }
4549
+ /**
4550
+ * Shared install path for addCollectionToSync / replaceSyncCollection:
4551
+ * write config, extend active sync filter, load Dexie cursor + in-mem,
4552
+ * fire a targeted one-shot download for just this collection.
4553
+ */
4554
+ async _installCollectionConfig(spec) {
4555
+ var _a;
4556
+ this.collections.set(spec.name, spec);
4557
+ if (this.syncOnlyCollections) {
4558
+ this.syncOnlyCollections.add(spec.name);
4559
+ }
4560
+ const meta = await this.dexieDb.getSyncMeta(spec.name);
4561
+ if (meta) this.syncMetaCache.set(spec.name, meta);
4562
+ if (!spec.writeOnly) {
4563
+ await this.loadCollectionToInMem(spec.name);
4564
+ }
4565
+ if (!spec.writeOnly && this.connectionManager.canSync()) {
4566
+ const rawQuery = (_a = spec.syncConfig) == null ? void 0 : _a.query;
4567
+ const query = typeof rawQuery === "function" ? rawQuery() : rawQuery;
4568
+ await this.syncCollectionForFind(spec.name, query, { returnDeleted: true });
4569
+ }
4570
+ }
4502
4571
  /**
4503
4572
  * Restrict sync to only these collections. When non-empty, only the listed
4504
4573
  * collections load from Dexie→in-mem and download from server. Pass an empty
@@ -4616,6 +4685,15 @@ var _SyncedDb = class _SyncedDb {
4616
4685
  };
4617
4686
  window.addEventListener("beforeunload", this.beforeUnloadHandler);
4618
4687
  }
4688
+ if (typeof document !== "undefined") {
4689
+ this.visibilityFlushHandler = () => {
4690
+ if (document.visibilityState !== "hidden") return;
4691
+ this.flushToServer("visibility-hidden").catch((err) => {
4692
+ console.warn("flushToServer on visibility-hidden failed:", err == null ? void 0 : err.message);
4693
+ });
4694
+ };
4695
+ document.addEventListener("visibilitychange", this.visibilityFlushHandler);
4696
+ }
4619
4697
  this.initialized = true;
4620
4698
  }
4621
4699
  /**
@@ -4626,6 +4704,25 @@ var _SyncedDb = class _SyncedDb {
4626
4704
  async flush() {
4627
4705
  await this.pendingChanges.flushAll();
4628
4706
  }
4707
+ /**
4708
+ * Push dirty data to the server immediately, bypassing the upload debounce.
4709
+ *
4710
+ * Flushes pending Dexie writes, awaits any in-flight REST upload, then fires
4711
+ * a fresh `uploadDirtyItems()`. Called automatically when the tab becomes
4712
+ * hidden so data is not stranded by browser tab-throttling / suspension.
4713
+ *
4714
+ * No-op when offline, forced offline, or not yet initialized.
4715
+ *
4716
+ * @param calledFrom Diagnostic tag threaded through to upload callbacks
4717
+ */
4718
+ async flushToServer(calledFrom) {
4719
+ if (!this.initialized) return;
4720
+ if (!this.connectionManager.canSync()) return;
4721
+ this.pendingChanges.cancelRestUploadTimer();
4722
+ await this.pendingChanges.flushAll();
4723
+ await this.pendingChanges.awaitRestUpload();
4724
+ await this.syncEngine.uploadDirtyItems(calledFrom);
4725
+ }
4629
4726
  /**
4630
4727
  * Returns when all collections were last successfully synced
4631
4728
  * from the server, or undefined if never.
@@ -4706,6 +4803,10 @@ var _SyncedDb = class _SyncedDb {
4706
4803
  window.removeEventListener("beforeunload", this.beforeUnloadHandler);
4707
4804
  this.beforeUnloadHandler = void 0;
4708
4805
  }
4806
+ if (typeof document !== "undefined" && this.visibilityFlushHandler) {
4807
+ document.removeEventListener("visibilitychange", this.visibilityFlushHandler);
4808
+ this.visibilityFlushHandler = void 0;
4809
+ }
4709
4810
  this.syncMetaCache.clear();
4710
4811
  for (const collectionName of this.collections.keys()) {
4711
4812
  this.inMemManager.clearCollection(collectionName);
@@ -6423,6 +6524,18 @@ var SyncedDb = _SyncedDb;
6423
6524
  import Dexie from "dexie";
6424
6525
  var SYNC_META_TABLE = "_sync_meta";
6425
6526
  var DIRTY_CHANGES_TABLE = "_dirty_changes";
6527
+ var META_ONLY_DIRTY_KEYS = /* @__PURE__ */ new Set([
6528
+ "_id",
6529
+ "_ts",
6530
+ "_rev",
6531
+ "_lastUpdaterId"
6532
+ ]);
6533
+ function isMetaOnlyChanges(changes) {
6534
+ for (const k of Object.keys(changes)) {
6535
+ if (!META_ONLY_DIRTY_KEYS.has(k)) return false;
6536
+ }
6537
+ return true;
6538
+ }
6426
6539
  var DexieDb = class extends Dexie {
6427
6540
  constructor(tenant, collectionConfigs) {
6428
6541
  super(`synced-db-${tenant}`);
@@ -6587,6 +6700,7 @@ var DexieDb = class extends Dexie {
6587
6700
  return result;
6588
6701
  }
6589
6702
  async addDirtyChange(collection, id, changes, baseMeta) {
6703
+ if (isMetaOnlyChanges(changes)) return;
6590
6704
  const stringId = this.idToString(id);
6591
6705
  const existing = await this.dirtyChanges.get([collection, stringId]);
6592
6706
  const now = Date.now();
@@ -6609,13 +6723,15 @@ var DexieDb = class extends Dexie {
6609
6723
  async addDirtyChangesBatch(collection, changesList) {
6610
6724
  var _a, _b;
6611
6725
  if (changesList.length === 0) return;
6726
+ const filtered = changesList.filter((c) => !isMetaOnlyChanges(c.changes));
6727
+ if (filtered.length === 0) return;
6612
6728
  const now = Date.now();
6613
6729
  const keys = [];
6614
- for (const c of changesList) keys.push([collection, this.idToString(c.id)]);
6730
+ for (const c of filtered) keys.push([collection, this.idToString(c.id)]);
6615
6731
  const existingEntries = await this.dirtyChanges.bulkGet(keys);
6616
6732
  const toWrite = [];
6617
- for (let i = 0; i < changesList.length; i++) {
6618
- const changeItem = changesList[i];
6733
+ for (let i = 0; i < filtered.length; i++) {
6734
+ const changeItem = filtered[i];
6619
6735
  const stringId = this.idToString(changeItem.id);
6620
6736
  const existing = existingEntries[i];
6621
6737
  if (existing) {
@@ -1,5 +1,5 @@
1
1
  import type { AggregateOptions } from "mongodb";
2
- import type { I_SyncedDb, SyncedDbConfig, WsNotificationInfo, EvictionInfo, EvictionCollectionInfo } from "../types/I_SyncedDb";
2
+ import type { I_SyncedDb, SyncedDbConfig, CollectionConfig, WsNotificationInfo, EvictionInfo, EvictionCollectionInfo } from "../types/I_SyncedDb";
3
3
  import type { DirtyMeta, MetaUpdateBroadcast } from "../types/I_DexieDb";
4
4
  import type { QuerySpec, QueryOpts, UpdateSpec, InsertSpec, BatchSpec } from "../types/I_RestInterface";
5
5
  import type { Id, DbEntity } from "../types/DbEntity";
@@ -36,6 +36,7 @@ export declare class SyncedDb implements I_SyncedDb {
36
36
  private unsubscribeServerUpdates?;
37
37
  private cleanupNotifierCallbacks?;
38
38
  private beforeUnloadHandler?;
39
+ private visibilityFlushHandler?;
39
40
  private readonly defaultReturnDeleted;
40
41
  private readonly defaultReturnArchived;
41
42
  private readonly onDatabaseCreated?;
@@ -62,6 +63,22 @@ export declare class SyncedDb implements I_SyncedDb {
62
63
  getInstanceId(): string;
63
64
  getCrossTabSyncDebounceMs(): number;
64
65
  isLeaderTab(): boolean;
66
+ leaderSince(): Date | undefined;
67
+ followerSince(): Date | undefined;
68
+ /**
69
+ * Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
70
+ */
71
+ addCollectionToSync(spec: CollectionConfig): Promise<void>;
72
+ /**
73
+ * Replace the collection config and re-sync. See `I_SyncedDb.replaceSyncCollection`.
74
+ */
75
+ replaceSyncCollection(spec: CollectionConfig): Promise<boolean>;
76
+ /**
77
+ * Shared install path for addCollectionToSync / replaceSyncCollection:
78
+ * write config, extend active sync filter, load Dexie cursor + in-mem,
79
+ * fire a targeted one-shot download for just this collection.
80
+ */
81
+ private _installCollectionConfig;
65
82
  /**
66
83
  * Restrict sync to only these collections. When non-empty, only the listed
67
84
  * collections load from Dexie→in-mem and download from server. Pass an empty
@@ -87,6 +104,18 @@ export declare class SyncedDb implements I_SyncedDb {
87
104
  * Does NOT upload to server — call sync() for that.
88
105
  */
89
106
  flush(): Promise<void>;
107
+ /**
108
+ * Push dirty data to the server immediately, bypassing the upload debounce.
109
+ *
110
+ * Flushes pending Dexie writes, awaits any in-flight REST upload, then fires
111
+ * a fresh `uploadDirtyItems()`. Called automatically when the tab becomes
112
+ * hidden so data is not stranded by browser tab-throttling / suspension.
113
+ *
114
+ * No-op when offline, forced offline, or not yet initialized.
115
+ *
116
+ * @param calledFrom Diagnostic tag threaded through to upload callbacks
117
+ */
118
+ flushToServer(calledFrom?: string): Promise<void>;
90
119
  private _lastFullSyncDate?;
91
120
  private _lastInitialSyncDate?;
92
121
  /**
@@ -12,6 +12,8 @@ export declare class LeaderElectionManager implements I_LeaderElectionManager {
12
12
  private isLeaderFlag;
13
13
  private closing;
14
14
  private releasingDueToVisibility;
15
+ private _leaderSince?;
16
+ private _followerSince?;
15
17
  private releaseLeaderLockResolve?;
16
18
  private becameLeaderPromise?;
17
19
  private becameLeaderResolve?;
@@ -36,6 +38,17 @@ export declare class LeaderElectionManager implements I_LeaderElectionManager {
36
38
  * Check if this instance is currently the leader.
37
39
  */
38
40
  isLeader(): boolean;
41
+ /**
42
+ * Returns the timestamp when this tab became leader, or undefined if currently a follower.
43
+ * Cleared on transition to follower.
44
+ */
45
+ leaderSince(): Date | undefined;
46
+ /**
47
+ * Returns the timestamp when this tab became follower, or undefined if currently a leader.
48
+ * Initialized at construction (every tab starts as a follower until it claims the lock).
49
+ * Cleared on transition to leader.
50
+ */
51
+ followerSince(): Date | undefined;
39
52
  /**
40
53
  * Cleanup resources.
41
54
  */
@@ -26,6 +26,10 @@ export interface I_LeaderElectionManager {
26
26
  releaseLeaderLock(): void;
27
27
  /** Check if this instance is currently the leader. */
28
28
  isLeader(): boolean;
29
+ /** Timestamp when this tab became leader, or undefined if currently a follower. */
30
+ leaderSince(): Date | undefined;
31
+ /** Timestamp when this tab became follower, or undefined if currently a leader. */
32
+ followerSince(): Date | undefined;
29
33
  /** Initialize (setup visibility listeners, BroadcastChannel). */
30
34
  init(): void;
31
35
  /** Cleanup resources. */
@@ -402,6 +402,13 @@ export interface CollectionConfig<T extends DbEntity = any, M = any> {
402
402
  * Read operations throw. Server sync and WS notifications are skipped.
403
403
  */
404
404
  writeOnly?: boolean;
405
+ /**
406
+ * Marks this config as provisional — a later `addCollectionToSync()` with
407
+ * a permanent (non-temporary) config will replace it. Permanent configs
408
+ * (the default) are immutable: subsequent `addCollectionToSync()` calls
409
+ * for the same collection are no-ops once a permanent config is in place.
410
+ */
411
+ temporaryConfig?: boolean;
405
412
  /** Whether this collection uses in-memory metadata */
406
413
  hasMetadata?: boolean;
407
414
  /** Callback called when a single object is written to in-mem. Returns metadata to store. */
@@ -429,9 +436,9 @@ export interface SyncedDbConfig {
429
436
  restTimeoutMs?: number;
430
437
  /** Timeout za sync REST klice v ms (default: 120000) - daljši ker sync prenaša več podatkov */
431
438
  syncTimeoutMs?: number;
432
- /** Debounce čas za zapis v Dexie v ms (default: 500) */
439
+ /** Debounce čas za zapis v Dexie v ms (default: 200) */
433
440
  debounceDexieWritesMs?: number;
434
- /** Debounce čas za pošiljanje na REST v ms (default: 100) - po uspešnem zapisu v Dexie */
441
+ /** Debounce čas za pošiljanje na REST v ms (default: 1000) - po uspešnem zapisu v Dexie */
435
442
  debounceRestWritesMs?: number;
436
443
  /**
437
444
  * Callback fired on each sync failure. Unlike the removed `onForcedOffline`,
@@ -737,6 +744,53 @@ export interface I_SyncedDb {
737
744
  * Does NOT upload to server — call sync() for that.
738
745
  */
739
746
  flush(): Promise<void>;
747
+ /**
748
+ * Push dirty data to the server immediately, bypassing the upload debounce.
749
+ * Flushes pending Dexie writes, awaits any in-flight REST upload, then fires
750
+ * a fresh upload. Called automatically on `visibilitychange` to `hidden` so
751
+ * data is not stranded by browser tab-throttling / suspension.
752
+ * No-op when offline, forced offline, or not yet initialized.
753
+ * @param calledFrom Diagnostic tag threaded through to upload callbacks
754
+ */
755
+ flushToServer(calledFrom?: string): Promise<void>;
756
+ /**
757
+ * Register a collection for sync at runtime.
758
+ *
759
+ * Behavior depends on whether a config already exists for `spec.name`:
760
+ * - permanent (existing config has `temporaryConfig !== true`): no-op
761
+ * - temporary (existing config has `temporaryConfig === true`): replaced by `spec`
762
+ * - none: `spec` is added
763
+ *
764
+ * On add or replace: loads the Dexie cursor for this collection into the
765
+ * sync-meta cache, hydrates in-mem from Dexie, and (when online + not
766
+ * `writeOnly`) fires a one-shot download sync for just this collection.
767
+ * If a `syncOnlyTheseCollections` filter is active, the new collection is
768
+ * added to it so future syncs include it.
769
+ *
770
+ * @param spec Collection config to register
771
+ */
772
+ addCollectionToSync(spec: CollectionConfig): Promise<void>;
773
+ /**
774
+ * Replace the collection config and re-sync.
775
+ *
776
+ * Like `addCollectionToSync` but more aggressive — overrides even a
777
+ * permanent existing config. The only blocked transition is
778
+ * **permanent → temporary** (cannot downgrade an already-established
779
+ * config to provisional):
780
+ *
781
+ * - no existing config: install `spec`, return `true`
782
+ * - existing temporary: replace with `spec` (any type), return `true`
783
+ * - existing permanent, `spec.temporaryConfig === true`: return `false`, no-op
784
+ * - existing permanent, `spec` permanent: replace with `spec`, return `true`
785
+ *
786
+ * On replace: extends active `syncOnlyTheseCollections` filter, reloads
787
+ * the Dexie cursor, re-hydrates in-mem, and (when online + not `writeOnly`)
788
+ * fires a one-shot targeted download.
789
+ *
790
+ * @param spec Collection config to install
791
+ * @returns `true` if installed, `false` if blocked (permanent → temporary)
792
+ */
793
+ replaceSyncCollection(spec: CollectionConfig): Promise<boolean>;
740
794
  /**
741
795
  * Returns when all collections were last successfully synced
742
796
  * from the server, or undefined if never synced.
@@ -954,6 +1008,19 @@ export interface I_SyncedDb {
954
1008
  * @returns true if this instance holds the leader lock
955
1009
  */
956
1010
  isLeaderTab(): boolean;
1011
+ /**
1012
+ * Timestamp when this tab became the leader.
1013
+ * Set on transition from follower to leader; cleared when leadership is lost.
1014
+ * @returns Date of leader transition, or undefined if currently a follower
1015
+ */
1016
+ leaderSince(): Date | undefined;
1017
+ /**
1018
+ * Timestamp when this tab became a follower.
1019
+ * Initialized at construction (every tab starts as a follower until it claims
1020
+ * the lock); cleared when leadership is gained, set again on transition to follower.
1021
+ * @returns Date of follower transition, or undefined if currently the leader
1022
+ */
1023
+ followerSince(): Date | undefined;
957
1024
  /**
958
1025
  * Get metadata for a single object.
959
1026
  * @param collection Collection name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.168",
3
+ "version": "0.1.171",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",