cry-synced-db-client 0.1.141 → 0.1.143

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,36 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### `getDirtyMeta()` for lightweight dirty-state inspection
6
+
7
+ - New `SyncedDb.getDirtyMeta()` returns dirty-entry meta (everything except the
8
+ `changes` payload) grouped per collection, only for collections with ≥1 dirty
9
+ record. Mirrors `getDirty()` shape but avoids loading change payloads —
10
+ useful for counts, timestamps, and indicator UIs.
11
+ - New `I_DexieDb.getDirtyMeta(collection)` returning `DirtyMeta[]`.
12
+ - New exported `DirtyMeta` type (`Omit<DirtyChange, "changes">`) and
13
+ `DirtyChange` type surfaced from the package entry.
14
+
15
+ ### Fix: multi-tab divergence when offline edits cross leader/follower
16
+
17
+ Fixes a bug where, after both tabs edited different records offline and came
18
+ online with leader-first, the leader ended up with stale record content
19
+ carrying the new server `_rev`. Because `resolveConflict` ignores server
20
+ echoes with equal-or-lower `_rev`, the divergence was permanent until page
21
+ reload. Follower-first came out clean; leader-first did not.
22
+
23
+ Two contributing causes, both fixed:
24
+
25
+ - `SyncEngine` post-upload in-mem patch no longer spreads stale `getInMemById`
26
+ result over server-returned `_rev`/`_ts`. In-mem is now fed the freshly
27
+ patched Dexie item (authoritative content + server meta), so the tab that
28
+ uploaded on behalf of another tab's dirty write ends up with matching
29
+ content and `_rev` in-mem.
30
+ - `CrossTabSyncManager.broadcastMetaUpdate` no longer gated by `isLeader()`.
31
+ Non-leader tabs now broadcast their local writes so the leader's in-mem
32
+ cache learns of them via the existing shared-Dexie reload path. Reload
33
+ broadcasts (post-full-sync) remain leader-only.
34
+
5
35
  ### BREAKING: Self-healing sync/reconnect lifecycle
6
36
 
7
37
  Fixes a class of bugs where the 60s auto-sync scheduler silently died after a
package/dist/index.js CHANGED
@@ -634,13 +634,14 @@ var CrossTabSyncManager = class {
634
634
  }
635
635
  /**
636
636
  * Broadcast updated IDs to other tabs (debounced).
637
- * Only the leader should broadcast.
637
+ * Any tab with local writes broadcasts so other tabs refresh their in-mem
638
+ * from shared Dexie. Otherwise non-leader writes stay invisible to the leader's
639
+ * in-mem cache, and a later upload patches new _rev onto stale content.
638
640
  * While a server sync is in progress, suppresses delta broadcasts and only
639
641
  * records which collections were affected (for the post-sync reload broadcast).
640
642
  */
641
643
  broadcastMetaUpdate(updates) {
642
644
  if (!this.metaUpdateChannel) return;
643
- if (!this.deps.isLeader()) return;
644
645
  if (this.serverSyncInProgress) {
645
646
  for (const collection of Object.keys(updates)) {
646
647
  this.syncAffectedCollections.add(collection);
@@ -2668,13 +2669,7 @@ var _SyncEngine = class _SyncEngine {
2668
2669
  dexieDeleteIds.push(entity._id);
2669
2670
  } else {
2670
2671
  dexieSaveBatch.push(dexieItem);
2671
- const inMemItem = this.deps.getInMemById(collection, entity._id);
2672
- if (inMemItem) {
2673
- inMemUpdateBatch.push(__spreadProps(__spreadValues({}, inMemItem), {
2674
- _rev: entity._rev,
2675
- _ts: entity._ts
2676
- }));
2677
- }
2672
+ inMemUpdateBatch.push(dexieItem);
2678
2673
  }
2679
2674
  }
2680
2675
  }
@@ -4550,6 +4545,16 @@ var _SyncedDb = class _SyncedDb {
4550
4545
  }
4551
4546
  return result;
4552
4547
  }
4548
+ async getDirtyMeta() {
4549
+ const result = {};
4550
+ for (const [collectionName] of this.collections) {
4551
+ const metas = await this.dexieDb.getDirtyMeta(collectionName);
4552
+ if (metas.length > 0) {
4553
+ result[collectionName] = metas;
4554
+ }
4555
+ }
4556
+ return result;
4557
+ }
4553
4558
  // ==================== Data Deletion ====================
4554
4559
  async dropCollection(collection, force = false) {
4555
4560
  this.assertCollection(collection);
@@ -5170,6 +5175,21 @@ var DexieDb = class extends Dexie {
5170
5175
  }
5171
5176
  return result;
5172
5177
  }
5178
+ async getDirtyMeta(collection) {
5179
+ const dirtyEntries = await this.dirtyChanges.where("[collection+id]").between([collection, Dexie.minKey], [collection, Dexie.maxKey]).toArray();
5180
+ const result = [];
5181
+ for (const entry of dirtyEntries) {
5182
+ result.push({
5183
+ collection: entry.collection,
5184
+ id: entry.id,
5185
+ baseTs: entry.baseTs,
5186
+ baseRev: entry.baseRev,
5187
+ createdAt: entry.createdAt,
5188
+ updatedAt: entry.updatedAt
5189
+ });
5190
+ }
5191
+ return result;
5192
+ }
5173
5193
  async addDirtyChange(collection, id, changes, baseMeta) {
5174
5194
  const stringId = this.idToString(id);
5175
5195
  const existing = await this.dirtyChanges.get([collection, stringId]);
@@ -1,5 +1,5 @@
1
1
  import Dexie from "dexie";
2
- import type { DirtyChange, I_DexieDb, SyncMeta } from "../types/I_DexieDb";
2
+ import type { DirtyChange, DirtyMeta, I_DexieDb, SyncMeta } from "../types/I_DexieDb";
3
3
  import type { CollectionConfig } from "../types/CollectionConfig";
4
4
  import type { Id, LocalDbEntity } from "../types/DbEntity";
5
5
  /**
@@ -31,6 +31,7 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
31
31
  forEachBatch<T extends LocalDbEntity>(collection: string, batchSize: number, callback: (items: T[]) => Promise<void>): Promise<void>;
32
32
  count(collection: string): Promise<number>;
33
33
  getDirty<T extends LocalDbEntity>(collection: string): Promise<Partial<T>[]>;
34
+ getDirtyMeta(collection: string): Promise<DirtyMeta[]>;
34
35
  addDirtyChange(collection: string, id: Id, changes: Record<string, any>, baseMeta?: {
35
36
  _ts?: any;
36
37
  _rev?: number;
@@ -1,6 +1,6 @@
1
1
  import type { AggregateOptions } from "mongodb";
2
2
  import type { I_SyncedDb, SyncedDbConfig, WsNotificationInfo, EvictionInfo, EvictionCollectionInfo } from "../types/I_SyncedDb";
3
- import type { MetaUpdateBroadcast } from "../types/I_DexieDb";
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";
6
6
  /**
@@ -161,6 +161,7 @@ export declare class SyncedDb implements I_SyncedDb {
161
161
  getOnWsNotification(): ((info: WsNotificationInfo) => void) | undefined;
162
162
  getOnWakeSync(): ((info: import("./types/managers").WakeSyncInfo) => void) | undefined;
163
163
  getDirty<T extends DbEntity>(): Promise<Readonly<Record<string, readonly T[]>>>;
164
+ getDirtyMeta(): Promise<Readonly<Record<string, readonly DirtyMeta[]>>>;
164
165
  dropCollection(collection: string, force?: boolean): Promise<void>;
165
166
  dropDatabase(force?: boolean): Promise<void>;
166
167
  /**
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * CrossTabSyncManager - Manages cross-tab synchronization via BroadcastChannel.
3
3
  *
4
- * When the leader tab syncs data or receives WebSocket updates, it broadcasts
5
- * the IDs of updated records so other tabs can refresh their in-memory state.
4
+ * Any tab with local writes (leader or follower) broadcasts the IDs of updated
5
+ * records so other tabs refresh their in-memory state from shared Dexie.
6
+ * Reload broadcasts (post-full-sync) remain leader-only.
6
7
  */
7
8
  import type { MetaUpdateBroadcast } from "../../types/I_DexieDb";
8
9
  import type { I_CrossTabSyncManager, CrossTabSyncConfig } from "../types/managers";
@@ -31,7 +32,9 @@ export declare class CrossTabSyncManager implements I_CrossTabSyncManager {
31
32
  init(): void;
32
33
  /**
33
34
  * Broadcast updated IDs to other tabs (debounced).
34
- * Only the leader should broadcast.
35
+ * Any tab with local writes broadcasts so other tabs refresh their in-mem
36
+ * from shared Dexie. Otherwise non-leader writes stay invisible to the leader's
37
+ * in-mem cache, and a later upload patches new _rev onto stale content.
35
38
  * While a server sync is in progress, suppresses delta broadcasts and only
36
39
  * records which collections were affected (for the post-sync reload broadcast).
37
40
  */
@@ -27,6 +27,12 @@ export interface DirtyChange {
27
27
  /** When last change was accumulated */
28
28
  updatedAt: number;
29
29
  }
30
+ /**
31
+ * Meta fields of a DirtyChange entry, without the `changes` payload.
32
+ * Used by `getDirtyMeta` for lightweight dirty-state inspection
33
+ * (counts, timestamps) without loading change payloads into memory.
34
+ */
35
+ export type DirtyMeta = Omit<DirtyChange, "changes">;
30
36
  /** Shared fields for all cross-tab broadcast messages */
31
37
  interface BroadcastBase {
32
38
  /** Unique ID of the SyncedDb instance that sent this broadcast */
@@ -84,6 +90,8 @@ export interface I_DexieDb {
84
90
  forEachBatch<T extends LocalDbEntity>(collection: string, batchSize: number, callback: (items: T[]) => Promise<void>): Promise<void>;
85
91
  /** Vrne vse dirty objekte (z lokalnimi spremembami) - returns only changed fields + _id + metadata */
86
92
  getDirty<T extends LocalDbEntity>(collection: string): Promise<Partial<T>[]>;
93
+ /** Vrne meta podatke vseh dirty vnosov za kolekcijo (brez `changes` payloada) */
94
+ getDirtyMeta(collection: string): Promise<DirtyMeta[]>;
87
95
  /** Add or accumulate changes for a record */
88
96
  addDirtyChange(collection: string, id: Id, changes: Record<string, any>, baseMeta?: {
89
97
  _ts?: any;
@@ -1,7 +1,7 @@
1
1
  import type { AggregateOptions } from "mongodb";
2
2
  import type { Id, DbEntity, LocalDbEntity } from "./DbEntity";
3
3
  import type { QuerySpec, QueryOpts, UpdateSpec, InsertSpec, BatchSpec, I_RestInterface, CollectionUpdateRequest, CollectionUpdateResult, GetNewerSpec } from "./I_RestInterface";
4
- import type { I_DexieDb } from "./I_DexieDb";
4
+ import type { DirtyMeta, I_DexieDb } from "./I_DexieDb";
5
5
  import type { I_InMemDb } from "./I_InMemDb";
6
6
  import type { I_ServerUpdateNotifier } from "./I_ServerUpdateNotifier";
7
7
  import type { WakeSyncInfo, NetworkStatusChangeInfo } from "../db/types/managers";
@@ -700,6 +700,11 @@ export interface I_SyncedDb {
700
700
  getDebounceRestWritesMs(): number;
701
701
  /** Vrne vse dirty objekte iz vseh kolekcij */
702
702
  getDirty<T extends DbEntity>(): Promise<Readonly<Record<string, readonly T[]>>>;
703
+ /**
704
+ * Vrne meta podatke dirty vnosov (brez `changes` payloada) po kolekcijah,
705
+ * ki imajo vsaj en dirty zapis. Kolekcije brez dirty vnosov niso vključene.
706
+ */
707
+ getDirtyMeta(): Promise<Readonly<Record<string, readonly DirtyMeta[]>>>;
703
708
  /**
704
709
  * Drops a collection, ensuring no data loss.
705
710
  * - Throws if offline or forcedOffline (unless force=true)
@@ -2,7 +2,7 @@ export type { Id, Entity, IdOrEntity, DbEntity, LocalDbEntity } from "./DbEntity
2
2
  export type { PublishableOperation, PublishRevsPayloadInsert, PublishRevsPayloadUpdate, PublishRevsPayloadDelete, PublishRevsPayloadUpdateMany, PublishRevsPayloadDeleteMany, PublishRevsPayloadBatchItem, PublishRevsPayloadBatch, PublishRevsPayload, PublishRevsSpec, PublishDataPayloadBase, PublishDataPayloadInsert, PublishDataPayloadUpdate, PublishDataPayloadDelete, PublishDataPayloadBatch, PublishDataPayload, PublishDataSpec, PublishSpec, } from "./PublishRevsPayload";
3
3
  export type { Obj, QuerySpec, Projection, QueryOpts, KeyOf, InsertKeyOf, InsertSpec, UpdateSpec, BatchSpec, UpsertOptions, GetNewerSpec, I_RestInterface as RestInterface, } from "./I_RestInterface";
4
4
  export type { I_InMemDb as InMemDb } from "./I_InMemDb";
5
- export type { I_DexieDb as DexieDb, SyncMeta } from "./I_DexieDb";
5
+ export type { I_DexieDb as DexieDb, SyncMeta, DirtyChange, DirtyMeta } from "./I_DexieDb";
6
6
  export type { I_ServerUpdateNotifier as ServerUpdateNotifier, ServerUpdateCallback, ServerUpdateNotifierCallbacks } from "./I_ServerUpdateNotifier";
7
7
  export type { I_SyncedDb as SyncedDb, SyncedDbConfig, CollectionConfig, CollectionSyncConfig, SyncInfo, ServerWriteRequestInfo, ServerWriteResultInfo, FindNewerManyCallInfo, FindNewerManyResultInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, InfrastructureErrorType, InfrastructureErrorInfo, ConflictSource, ConflictResolutionReport, CrossTabSyncInfo, EvictionInfo, EvictionCollectionInfo, } from "./I_SyncedDb";
8
8
  export type { NetworkStatusChangeInfo } from "../db/types/managers";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.141",
3
+ "version": "0.1.143",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",