cry-synced-db-client 0.1.112 → 0.1.114

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/dist/index.js CHANGED
@@ -599,6 +599,10 @@ var LeaderElectionManager = class {
599
599
  var CrossTabSyncManager = class {
600
600
  constructor(config) {
601
601
  this.pendingBroadcasts = /* @__PURE__ */ new Map();
602
+ /** True while a full server sync is in progress — suppresses delta broadcasts. */
603
+ this.serverSyncInProgress = false;
604
+ /** Collections that received updates while serverSyncInProgress was true. */
605
+ this.syncAffectedCollections = /* @__PURE__ */ new Set();
602
606
  this.tenant = config.tenant;
603
607
  this.instanceId = config.instanceId;
604
608
  this.windowId = config.windowId;
@@ -631,10 +635,18 @@ var CrossTabSyncManager = class {
631
635
  /**
632
636
  * Broadcast updated IDs to other tabs (debounced).
633
637
  * Only the leader should broadcast.
638
+ * While a server sync is in progress, suppresses delta broadcasts and only
639
+ * records which collections were affected (for the post-sync reload broadcast).
634
640
  */
635
641
  broadcastMetaUpdate(updates) {
636
642
  if (!this.metaUpdateChannel) return;
637
643
  if (!this.deps.isLeader()) return;
644
+ if (this.serverSyncInProgress) {
645
+ for (const collection of Object.keys(updates)) {
646
+ this.syncAffectedCollections.add(collection);
647
+ }
648
+ return;
649
+ }
638
650
  for (const [collection, ids] of Object.entries(updates)) {
639
651
  let existing = this.pendingBroadcasts.get(collection);
640
652
  if (!existing) {
@@ -652,6 +664,31 @@ var CrossTabSyncManager = class {
652
664
  this.flushBroadcasts();
653
665
  }, this.debounceMs);
654
666
  }
667
+ /**
668
+ * Called when a full server sync starts.
669
+ * Cancels any pending incremental broadcasts and enables suppression mode.
670
+ */
671
+ startServerSync() {
672
+ this.serverSyncInProgress = true;
673
+ this.syncAffectedCollections.clear();
674
+ if (this.broadcastDebounceTimer) {
675
+ clearTimeout(this.broadcastDebounceTimer);
676
+ this.broadcastDebounceTimer = void 0;
677
+ }
678
+ this.pendingBroadcasts.clear();
679
+ }
680
+ /**
681
+ * Called when a full server sync ends.
682
+ * Emits a single reload broadcast for all collections touched during the sync,
683
+ * replacing the many incremental delta broadcasts that were suppressed.
684
+ */
685
+ endServerSync() {
686
+ this.serverSyncInProgress = false;
687
+ if (this.syncAffectedCollections.size === 0) return;
688
+ const collections = Array.from(this.syncAffectedCollections);
689
+ this.syncAffectedCollections.clear();
690
+ this.broadcastReload(collections);
691
+ }
655
692
  /**
656
693
  * Flush pending broadcasts immediately.
657
694
  */
@@ -667,6 +704,7 @@ var CrossTabSyncManager = class {
667
704
  }
668
705
  if (Object.keys(updates).length > 0) {
669
706
  const payload = {
707
+ type: "delta",
670
708
  updates,
671
709
  instanceId: this.instanceId,
672
710
  isLeader: this.deps.isLeader(),
@@ -705,23 +743,32 @@ var CrossTabSyncManager = class {
705
743
  // Private Methods
706
744
  // ============================================================
707
745
  async handleCrossTabMetaUpdate(payload) {
708
- if (!this.deps.isInitialized()) {
709
- return;
746
+ if (!this.deps.isInitialized()) return;
747
+ if (payload.instanceId === this.instanceId) return;
748
+ if (payload.type === "reload") {
749
+ await this.handleReloadBroadcast(payload);
750
+ } else {
751
+ await this.handleDeltaBroadcast(payload);
710
752
  }
711
- if (payload.instanceId === this.instanceId) {
712
- return;
753
+ }
754
+ async handleReloadBroadcast(payload) {
755
+ const collections = this.deps.getCollections();
756
+ for (const collection of payload.collections) {
757
+ if (!collections.has(collection)) continue;
758
+ if (!this.deps.isSyncAllowed(collection)) continue;
759
+ try {
760
+ await this.deps.reloadCollectionFromDexie(collection);
761
+ } catch (err) {
762
+ console.error(`Error reloading collection ${collection} from Dexie:`, err);
763
+ }
713
764
  }
765
+ }
766
+ async handleDeltaBroadcast(payload) {
714
767
  const collections = this.deps.getCollections();
715
768
  for (const [collection, ids] of Object.entries(payload.updates)) {
716
- if (!collections.has(collection)) {
717
- continue;
718
- }
719
- if (!this.deps.isSyncAllowed(collection)) {
720
- continue;
721
- }
722
- if (ids.length === 0) {
723
- continue;
724
- }
769
+ if (!collections.has(collection)) continue;
770
+ if (!this.deps.isSyncAllowed(collection)) continue;
771
+ if (ids.length === 0) continue;
725
772
  try {
726
773
  const items = await this.deps.dexieDb.getByIds(collection, ids);
727
774
  const upsertBatch = [];
@@ -764,10 +811,25 @@ var CrossTabSyncManager = class {
764
811
  }
765
812
  }
766
813
  } catch (err) {
767
- console.error(`Error handling cross-tab meta update for ${collection}:`, err);
814
+ console.error(`Error handling cross-tab delta update for ${collection}:`, err);
768
815
  }
769
816
  }
770
817
  }
818
+ broadcastReload(collections) {
819
+ if (!this.metaUpdateChannel) return;
820
+ const payload = {
821
+ type: "reload",
822
+ collections,
823
+ instanceId: this.instanceId,
824
+ isLeader: this.deps.isLeader(),
825
+ windowId: this.windowId
826
+ };
827
+ try {
828
+ this.metaUpdateChannel.postMessage(payload);
829
+ } catch (err) {
830
+ console.error("Failed to broadcast reload:", err);
831
+ }
832
+ }
771
833
  callOnInfrastructureError(type, message, error) {
772
834
  if (this.callbacks.onInfrastructureError) {
773
835
  try {
@@ -3376,6 +3438,7 @@ var SyncedDb = class _SyncedDb {
3376
3438
  const windowId = (_a = config._testWindowId) != null ? _a : this.getOrCreateWindowId();
3377
3439
  this.defaultReturnDeleted = (_b = config.returnDeleted) != null ? _b : false;
3378
3440
  this.defaultReturnArchived = (_c = config.returnArchived) != null ? _c : false;
3441
+ this.onDatabaseCreated = config.onDatabaseCreated;
3379
3442
  this.onSyncStart = config.onSyncStart;
3380
3443
  this.onSyncEnd = config.onSyncEnd;
3381
3444
  this.onDexieSyncStart = config.onDexieSyncStart;
@@ -3435,7 +3498,8 @@ var SyncedDb = class _SyncedDb {
3435
3498
  writeToInMemBatch: (collection, items, operation) => {
3436
3499
  this.inMemManager.writeBatch(collection, items, operation);
3437
3500
  },
3438
- isSyncAllowed: (collection) => this.isSyncAllowed(collection)
3501
+ isSyncAllowed: (collection) => this.isSyncAllowed(collection),
3502
+ reloadCollectionFromDexie: (collection) => this.loadCollectionToInMem(collection).then(() => void 0)
3439
3503
  }
3440
3504
  });
3441
3505
  this.connectionManager = new ConnectionManager({
@@ -3651,6 +3715,13 @@ var SyncedDb = class _SyncedDb {
3651
3715
  }
3652
3716
  await this._loadLastFullSync();
3653
3717
  await this._loadLastInitialSync();
3718
+ if (this.dexieDb.isNewDatabase() && this.onDatabaseCreated) {
3719
+ try {
3720
+ this.onDatabaseCreated();
3721
+ } catch (err) {
3722
+ console.error("onDatabaseCreated callback failed:", err);
3723
+ }
3724
+ }
3654
3725
  await this.pendingChanges.recoverPendingWrites();
3655
3726
  const allowedColls = [...this.collections.keys()].filter((n) => this.isSyncAllowed(n));
3656
3727
  const dexieStart = Date.now();
@@ -4264,6 +4335,7 @@ var SyncedDb = class _SyncedDb {
4264
4335
  if (this.syncLock) return;
4265
4336
  this.syncLock = true;
4266
4337
  this.syncing = true;
4338
+ this.crossTabSync.startServerSync();
4267
4339
  try {
4268
4340
  await this.syncEngine.sync(calledFrom);
4269
4341
  if (!this.syncOnlyCollections) {
@@ -4280,6 +4352,7 @@ var SyncedDb = class _SyncedDb {
4280
4352
  } finally {
4281
4353
  this.syncing = false;
4282
4354
  this.syncLock = false;
4355
+ this.crossTabSync.endServerSync();
4283
4356
  await this.processQueuedWsUpdates();
4284
4357
  }
4285
4358
  }
@@ -4512,7 +4585,7 @@ var SyncedDb = class _SyncedDb {
4512
4585
  }
4513
4586
  assertCollection(name) {
4514
4587
  if (!this.collections.has(name)) {
4515
- throw new Error(`Collection "${name}" not configured`);
4588
+ throw new Error(`SyncedDb: Collection "${(name == null ? void 0 : name.toString()) || "?"}" not configured`);
4516
4589
  }
4517
4590
  }
4518
4591
  /** Stringify an Id parameter (ObjectId → hex string). */
@@ -4593,6 +4666,7 @@ var DexieDb = class extends Dexie {
4593
4666
  constructor(tenant, collectionConfigs) {
4594
4667
  super(`synced-db-${tenant}`);
4595
4668
  this.collections = /* @__PURE__ */ new Map();
4669
+ this._isNewDatabase = false;
4596
4670
  this.tenant = tenant;
4597
4671
  const schema = {};
4598
4672
  schema[SYNC_META_TABLE] = "[tenant+collection]";
@@ -4601,6 +4675,9 @@ var DexieDb = class extends Dexie {
4601
4675
  schema[config.name] = "_id";
4602
4676
  }
4603
4677
  this.version(1).stores(schema);
4678
+ this.on("populate", () => {
4679
+ this._isNewDatabase = true;
4680
+ });
4604
4681
  this.syncMeta = this.table(SYNC_META_TABLE);
4605
4682
  this.dirtyChanges = this.table(DIRTY_CHANGES_TABLE);
4606
4683
  for (const config of collectionConfigs) {
@@ -4610,7 +4687,7 @@ var DexieDb = class extends Dexie {
4610
4687
  getTable(collection) {
4611
4688
  const table = this.collections.get(collection);
4612
4689
  if (!table) {
4613
- throw new Error(`Collection "${collection}" not configured`);
4690
+ throw new Error(`DexieDb: Collection "${(collection == null ? void 0 : collection.toString()) || "?"}" not configured`);
4614
4691
  }
4615
4692
  return table;
4616
4693
  }
@@ -4809,6 +4886,9 @@ var DexieDb = class extends Dexie {
4809
4886
  getTenant() {
4810
4887
  return this.tenant;
4811
4888
  }
4889
+ isNewDatabase() {
4890
+ return this._isNewDatabase;
4891
+ }
4812
4892
  };
4813
4893
 
4814
4894
  // node_modules/msgpackr/unpack.js
@@ -12,6 +12,7 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
12
12
  private collections;
13
13
  private syncMeta;
14
14
  private dirtyChanges;
15
+ private _isNewDatabase;
15
16
  constructor(tenant: string, collectionConfigs: CollectionConfig<any>[]);
16
17
  private getTable;
17
18
  private idToString;
@@ -51,4 +52,5 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
51
52
  setSyncMeta(collection: string, lastSyncTs: any): Promise<void>;
52
53
  deleteSyncMeta(collection: string): Promise<void>;
53
54
  getTenant(): string;
55
+ isNewDatabase(): boolean;
54
56
  }
@@ -37,6 +37,7 @@ export declare class SyncedDb implements I_SyncedDb {
37
37
  private beforeUnloadHandler?;
38
38
  private readonly defaultReturnDeleted;
39
39
  private readonly defaultReturnArchived;
40
+ private readonly onDatabaseCreated?;
40
41
  private readonly onSyncStart?;
41
42
  private readonly onSyncEnd?;
42
43
  private readonly onDexieSyncStart?;
@@ -16,6 +16,10 @@ export declare class CrossTabSyncManager implements I_CrossTabSyncManager {
16
16
  private metaUpdateChannel?;
17
17
  private pendingBroadcasts;
18
18
  private broadcastDebounceTimer?;
19
+ /** True while a full server sync is in progress — suppresses delta broadcasts. */
20
+ private serverSyncInProgress;
21
+ /** Collections that received updates while serverSyncInProgress was true. */
22
+ private syncAffectedCollections;
19
23
  constructor(config: CrossTabSyncConfig);
20
24
  /**
21
25
  * Get debounce time in ms.
@@ -28,8 +32,21 @@ export declare class CrossTabSyncManager implements I_CrossTabSyncManager {
28
32
  /**
29
33
  * Broadcast updated IDs to other tabs (debounced).
30
34
  * Only the leader should broadcast.
35
+ * While a server sync is in progress, suppresses delta broadcasts and only
36
+ * records which collections were affected (for the post-sync reload broadcast).
31
37
  */
32
38
  broadcastMetaUpdate(updates: Record<string, string[]>): void;
39
+ /**
40
+ * Called when a full server sync starts.
41
+ * Cancels any pending incremental broadcasts and enables suppression mode.
42
+ */
43
+ startServerSync(): void;
44
+ /**
45
+ * Called when a full server sync ends.
46
+ * Emits a single reload broadcast for all collections touched during the sync,
47
+ * replacing the many incremental delta broadcasts that were suppressed.
48
+ */
49
+ endServerSync(): void;
33
50
  /**
34
51
  * Flush pending broadcasts immediately.
35
52
  */
@@ -43,5 +60,8 @@ export declare class CrossTabSyncManager implements I_CrossTabSyncManager {
43
60
  */
44
61
  handleExternalBroadcast(payload: MetaUpdateBroadcast): void;
45
62
  private handleCrossTabMetaUpdate;
63
+ private handleReloadBroadcast;
64
+ private handleDeltaBroadcast;
65
+ private broadcastReload;
46
66
  private callOnInfrastructureError;
47
67
  }
@@ -52,6 +52,8 @@ export interface CrossTabSyncDeps {
52
52
  writeToInMemBatch: <T extends DbEntity>(collection: string, items: T[], operation: "upsert" | "delete") => void;
53
53
  /** Whether a collection participates in sync (not writeOnly, not filtered out). */
54
54
  isSyncAllowed: (collection: string) => boolean;
55
+ /** Reload a collection fully from Dexie into in-mem (called on reload broadcast). */
56
+ reloadCollectionFromDexie: (collection: string) => Promise<void>;
55
57
  }
56
58
  export interface CrossTabSyncConfig {
57
59
  tenant: string;
@@ -70,6 +72,16 @@ export interface I_CrossTabSyncManager {
70
72
  broadcastMetaUpdate(updates: Record<string, string[]>): void;
71
73
  /** Flush pending broadcasts immediately. */
72
74
  flushBroadcasts(): void;
75
+ /**
76
+ * Called when a full server sync starts.
77
+ * Suppresses incremental delta broadcasts and accumulates affected collections.
78
+ */
79
+ startServerSync(): void;
80
+ /**
81
+ * Called when a full server sync ends.
82
+ * Emits a single reload broadcast for all collections touched during the sync.
83
+ */
84
+ endServerSync(): void;
73
85
  /** Cleanup resources. */
74
86
  dispose(): void;
75
87
  /** Handle external broadcast (for testing). */
@@ -27,13 +27,8 @@ export interface DirtyChange {
27
27
  /** When last change was accumulated */
28
28
  updatedAt: number;
29
29
  }
30
- /**
31
- * Payload for cross-tab meta update broadcasts
32
- * Maps collection names to arrays of _id strings that were updated
33
- */
34
- export interface MetaUpdateBroadcast {
35
- /** Map of collection name -> array of _id strings that were updated */
36
- updates: Record<string, string[]>;
30
+ /** Shared fields for all cross-tab broadcast messages */
31
+ interface BroadcastBase {
37
32
  /** Unique ID of the SyncedDb instance that sent this broadcast */
38
33
  instanceId: string;
39
34
  /** Whether the sender is the leader tab */
@@ -41,6 +36,27 @@ export interface MetaUpdateBroadcast {
41
36
  /** Window ID of the sender (for debugging) */
42
37
  windowId: string;
43
38
  }
39
+ /**
40
+ * Incremental cross-tab broadcast: specific IDs updated per collection.
41
+ * Sent after individual writes (user edits, WebSocket updates).
42
+ */
43
+ export interface DeltaBroadcast extends BroadcastBase {
44
+ type: "delta";
45
+ /** Map of collection name -> array of _id strings that were updated */
46
+ updates: Record<string, string[]>;
47
+ }
48
+ /**
49
+ * Full-reload cross-tab broadcast: receiving tab must reload listed collections
50
+ * from Dexie into in-mem. Sent once after a full server sync completes instead
51
+ * of emitting many incremental delta broadcasts during the sync.
52
+ */
53
+ export interface ReloadBroadcast extends BroadcastBase {
54
+ type: "reload";
55
+ /** Collections that were affected by the sync and need a full reload */
56
+ collections: string[];
57
+ }
58
+ /** Discriminated union of all cross-tab broadcast message types */
59
+ export type MetaUpdateBroadcast = DeltaBroadcast | ReloadBroadcast;
44
60
  /**
45
61
  * Interface za Dexie (IndexedDB) bazo podatkov
46
62
  * Vse metode so async ker IndexedDB je asinhrona
@@ -100,4 +116,7 @@ export interface I_DexieDb {
100
116
  deleteSyncMeta(collection: string): Promise<void>;
101
117
  /** Vrne tenant */
102
118
  getTenant(): string;
119
+ /** Returns true if the IndexedDB database was created fresh during this session (first ever open). */
120
+ isNewDatabase(): boolean;
103
121
  }
122
+ export {};
@@ -293,6 +293,8 @@ export interface SyncedDbConfig {
293
293
  debounceRestWritesMs?: number;
294
294
  /** Callback ki se pokliče, ko SyncedDb sam preide v offline stanje (npr. ob sync napaki) */
295
295
  onForcedOffline?: (reason: string) => void;
296
+ /** Callback fired once during init() when the IndexedDB database was created fresh (first ever open). */
297
+ onDatabaseCreated?: () => void;
296
298
  /** Callback at the start of each sync cycle. initialSync=true if no full sync has completed yet. */
297
299
  onSyncStart?: (info: {
298
300
  calledFrom?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.112",
3
+ "version": "0.1.114",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",