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 +97 -17
- package/dist/src/db/DexieDb.d.ts +2 -0
- package/dist/src/db/SyncedDb.d.ts +1 -0
- package/dist/src/db/managers/CrossTabSyncManager.d.ts +20 -0
- package/dist/src/db/types/managers.d.ts +12 -0
- package/dist/src/types/I_DexieDb.d.ts +26 -7
- package/dist/src/types/I_SyncedDb.d.ts +2 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/src/db/DexieDb.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|