cry-synced-db-client 0.1.47 → 0.1.50
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 +183 -195
- package/dist/src/db/DexieDb.d.ts +3 -16
- package/dist/src/db/SyncedDb.d.ts +23 -14
- package/dist/src/types/CollectionConfig.d.ts +1 -2
- package/dist/src/types/I_DexieDb.d.ts +3 -17
- package/dist/src/types/I_SyncedDb.d.ts +22 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1097,8 +1097,7 @@ class SyncedDb {
|
|
|
1097
1097
|
onWsNotification;
|
|
1098
1098
|
onOnlineStatusChange;
|
|
1099
1099
|
crossTabSyncDebounceMs;
|
|
1100
|
-
|
|
1101
|
-
onStaleMetaConflict;
|
|
1100
|
+
onCrossTabSync;
|
|
1102
1101
|
onBecameLeader;
|
|
1103
1102
|
onLostLeadership;
|
|
1104
1103
|
onInfrastructureError;
|
|
@@ -1117,8 +1116,9 @@ class SyncedDb {
|
|
|
1117
1116
|
updaterId;
|
|
1118
1117
|
syncedDbInstanceId;
|
|
1119
1118
|
syncMetaCache = new Map;
|
|
1120
|
-
|
|
1121
|
-
|
|
1119
|
+
metaUpdateChannel;
|
|
1120
|
+
pendingBroadcasts = new Map;
|
|
1121
|
+
broadcastDebounceTimer;
|
|
1122
1122
|
isLeader = false;
|
|
1123
1123
|
releaseLeaderLockResolve;
|
|
1124
1124
|
leaderReelectionChannel;
|
|
@@ -1154,8 +1154,7 @@ class SyncedDb {
|
|
|
1154
1154
|
this.onWsNotification = config.onWsNotification;
|
|
1155
1155
|
this.onOnlineStatusChange = config.onOnlineStatusChange;
|
|
1156
1156
|
this.crossTabSyncDebounceMs = config.crossTabSyncDebounceMs ?? DEFAULT_CROSS_TAB_SYNC_DEBOUNCE_MS;
|
|
1157
|
-
this.
|
|
1158
|
-
this.onStaleMetaConflict = config.onStaleMetaConflict;
|
|
1157
|
+
this.onCrossTabSync = config.onCrossTabSync;
|
|
1159
1158
|
this.onBecameLeader = config.onBecameLeader;
|
|
1160
1159
|
this.onLostLeadership = config.onLostLeadership;
|
|
1161
1160
|
this.onInfrastructureError = config.onInfrastructureError;
|
|
@@ -1174,6 +1173,9 @@ class SyncedDb {
|
|
|
1174
1173
|
isLeaderTab() {
|
|
1175
1174
|
return this.isLeader;
|
|
1176
1175
|
}
|
|
1176
|
+
simulateExternalBroadcast(payload) {
|
|
1177
|
+
this.handleCrossTabMetaUpdate(payload);
|
|
1178
|
+
}
|
|
1177
1179
|
tryBecomeLeader() {
|
|
1178
1180
|
if (this.closing) {
|
|
1179
1181
|
return Promise.resolve();
|
|
@@ -1343,9 +1345,6 @@ class SyncedDb {
|
|
|
1343
1345
|
this.syncMetaCache.set(name, meta);
|
|
1344
1346
|
}
|
|
1345
1347
|
}
|
|
1346
|
-
this.unsubscribeMetaUpdates = this.dexieDb.onMetaUpdated((payload) => {
|
|
1347
|
-
this.handleCrossTabMetaUpdate(payload);
|
|
1348
|
-
});
|
|
1349
1348
|
if (typeof BroadcastChannel !== "undefined") {
|
|
1350
1349
|
this.leaderReelectionChannel = new BroadcastChannel(`synced-db-leader-reelection-${this.tenant}`);
|
|
1351
1350
|
this.leaderReelectionChannel.onmessage = (event) => {
|
|
@@ -1353,6 +1352,10 @@ class SyncedDb {
|
|
|
1353
1352
|
this.handleLeaderReelection();
|
|
1354
1353
|
}
|
|
1355
1354
|
};
|
|
1355
|
+
this.metaUpdateChannel = new BroadcastChannel(`synced-db-meta-${this.tenant}`);
|
|
1356
|
+
this.metaUpdateChannel.onmessage = (event) => {
|
|
1357
|
+
this.handleCrossTabMetaUpdate(event.data);
|
|
1358
|
+
};
|
|
1356
1359
|
}
|
|
1357
1360
|
if (typeof document === "undefined" || document.visibilityState === "visible") {
|
|
1358
1361
|
this.tryBecomeLeader();
|
|
@@ -1402,9 +1405,10 @@ class SyncedDb {
|
|
|
1402
1405
|
}
|
|
1403
1406
|
this.stopAutoSync();
|
|
1404
1407
|
await this.flushAllPendingChanges();
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
this.
|
|
1408
|
+
this.flushBroadcasts();
|
|
1409
|
+
if (this.metaUpdateChannel) {
|
|
1410
|
+
this.metaUpdateChannel.close();
|
|
1411
|
+
this.metaUpdateChannel = undefined;
|
|
1408
1412
|
}
|
|
1409
1413
|
this.releaseLeaderLock();
|
|
1410
1414
|
if (this.leaderReelectionChannel) {
|
|
@@ -1803,7 +1807,7 @@ class SyncedDb {
|
|
|
1803
1807
|
const syncSpecs = [];
|
|
1804
1808
|
const configMap = new Map;
|
|
1805
1809
|
for (const [collectionName, config] of this.collections) {
|
|
1806
|
-
const meta =
|
|
1810
|
+
const meta = this.syncMetaCache.get(collectionName);
|
|
1807
1811
|
syncSpecs.push({
|
|
1808
1812
|
collection: collectionName,
|
|
1809
1813
|
timestamp: meta?.lastSyncTs || 0,
|
|
@@ -1944,59 +1948,68 @@ class SyncedDb {
|
|
|
1944
1948
|
}
|
|
1945
1949
|
return result;
|
|
1946
1950
|
}
|
|
1947
|
-
async
|
|
1951
|
+
async dropCollection(collection, force = false) {
|
|
1948
1952
|
this.assertCollection(collection);
|
|
1953
|
+
if (!force && (this.forcedOffline || !this.online)) {
|
|
1954
|
+
throw new Error(`Cannot drop collection "${collection}": database is offline. Use force=true to drop anyway.`);
|
|
1955
|
+
}
|
|
1949
1956
|
await this.flushPendingChangesForCollection(collection);
|
|
1950
|
-
|
|
1951
|
-
if (
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
} catch (err) {
|
|
1955
|
-
console.error(`Failed to send dirty data for ${collection}:`, err);
|
|
1956
|
-
sendSuccess = false;
|
|
1957
|
+
const dirtyItems = await this.dexieDb.getDirty(collection);
|
|
1958
|
+
if (dirtyItems.length > 0) {
|
|
1959
|
+
if (!force && (this.forcedOffline || !this.online)) {
|
|
1960
|
+
throw new Error(`Cannot drop collection "${collection}": ${dirtyItems.length} unsynchronized items would be lost. Use force=true to drop anyway.`);
|
|
1957
1961
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
+
if (this.online && !this.forcedOffline) {
|
|
1963
|
+
try {
|
|
1964
|
+
await this.uploadDirtyItemsForCollection(collection);
|
|
1965
|
+
} catch (err) {
|
|
1966
|
+
if (!force) {
|
|
1967
|
+
throw new Error(`Cannot drop collection "${collection}": failed to sync dirty items to server. Use force=true to drop anyway. Original error: ${err}`);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1962
1970
|
}
|
|
1963
1971
|
}
|
|
1964
|
-
if (!sendSuccess && !force) {
|
|
1965
|
-
return false;
|
|
1966
|
-
}
|
|
1967
1972
|
await this.dexieDb.deleteCollection(collection);
|
|
1968
1973
|
this.inMemDb.deleteCollection(collection);
|
|
1969
1974
|
await this.dexieDb.deleteSyncMeta(collection);
|
|
1970
|
-
|
|
1975
|
+
await this.dexieDb.clearDirtyChanges(collection);
|
|
1976
|
+
this.syncMetaCache.delete(collection);
|
|
1971
1977
|
}
|
|
1972
|
-
async
|
|
1978
|
+
async dropDatabase(force = false) {
|
|
1979
|
+
if (!force && (this.forcedOffline || !this.online)) {
|
|
1980
|
+
throw new Error("Cannot drop database: database is offline. Use force=true to drop anyway.");
|
|
1981
|
+
}
|
|
1973
1982
|
await this.flushAllPendingChanges();
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
sendSuccess = false;
|
|
1983
|
+
const collectionNames = Array.from(this.collections.keys());
|
|
1984
|
+
const dirtyCollections = [];
|
|
1985
|
+
for (const collectionName of collectionNames) {
|
|
1986
|
+
const dirtyItems = await this.dexieDb.getDirty(collectionName);
|
|
1987
|
+
if (dirtyItems.length > 0) {
|
|
1988
|
+
dirtyCollections.push({ name: collectionName, count: dirtyItems.length });
|
|
1981
1989
|
}
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1990
|
+
}
|
|
1991
|
+
if (dirtyCollections.length > 0) {
|
|
1992
|
+
const totalDirty = dirtyCollections.reduce((sum, c) => sum + c.count, 0);
|
|
1993
|
+
if (!force && (this.forcedOffline || !this.online)) {
|
|
1994
|
+
throw new Error(`Cannot drop database: ${totalDirty} unsynchronized items in ${dirtyCollections.length} collections would be lost. Use force=true to drop anyway.`);
|
|
1995
|
+
}
|
|
1996
|
+
if (this.online && !this.forcedOffline) {
|
|
1997
|
+
try {
|
|
1998
|
+
await this.uploadDirtyItems();
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
if (!force) {
|
|
2001
|
+
throw new Error(`Cannot drop database: failed to sync dirty items to server. Use force=true to drop anyway. Original error: ${err}`);
|
|
2002
|
+
}
|
|
1988
2003
|
}
|
|
1989
2004
|
}
|
|
1990
2005
|
}
|
|
1991
|
-
|
|
1992
|
-
return false;
|
|
1993
|
-
}
|
|
1994
|
-
for (const [collectionName] of this.collections) {
|
|
2006
|
+
for (const collectionName of collectionNames) {
|
|
1995
2007
|
await this.dexieDb.deleteCollection(collectionName);
|
|
1996
2008
|
this.inMemDb.deleteCollection(collectionName);
|
|
1997
2009
|
await this.dexieDb.deleteSyncMeta(collectionName);
|
|
2010
|
+
await this.dexieDb.clearDirtyChanges(collectionName);
|
|
1998
2011
|
}
|
|
1999
|
-
|
|
2012
|
+
this.syncMetaCache.clear();
|
|
2000
2013
|
}
|
|
2001
2014
|
assertCollection(name) {
|
|
2002
2015
|
if (!this.collections.has(name)) {
|
|
@@ -2245,6 +2258,27 @@ class SyncedDb {
|
|
|
2245
2258
|
this.inMemDb.deleteManyByIds(collection, deleteIds);
|
|
2246
2259
|
sentCount += deleted.length;
|
|
2247
2260
|
}
|
|
2261
|
+
const allItems = [...inserted, ...updated, ...deleted];
|
|
2262
|
+
let maxTs = undefined;
|
|
2263
|
+
for (const item of allItems) {
|
|
2264
|
+
if (item._ts) {
|
|
2265
|
+
if (!maxTs || this.compareTimestamps(item._ts, maxTs) > 0) {
|
|
2266
|
+
maxTs = item._ts;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
if (maxTs) {
|
|
2271
|
+
const currentMeta = this.syncMetaCache.get(collection);
|
|
2272
|
+
const currentTs = currentMeta?.lastSyncTs;
|
|
2273
|
+
if (!currentTs || this.compareTimestamps(maxTs, currentTs) > 0) {
|
|
2274
|
+
await this.dexieDb.setSyncMeta(collection, maxTs);
|
|
2275
|
+
this.syncMetaCache.set(collection, {
|
|
2276
|
+
tenant: this.tenant,
|
|
2277
|
+
collection,
|
|
2278
|
+
lastSyncTs: maxTs
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2248
2282
|
}
|
|
2249
2283
|
return { sentCount };
|
|
2250
2284
|
}
|
|
@@ -2497,6 +2531,27 @@ class SyncedDb {
|
|
|
2497
2531
|
this.inMemDb.deleteManyByIds(collection, deleteIds);
|
|
2498
2532
|
sentCount += deleted.length;
|
|
2499
2533
|
}
|
|
2534
|
+
const allItems = [...inserted, ...updated, ...deleted];
|
|
2535
|
+
let maxTs = undefined;
|
|
2536
|
+
for (const item of allItems) {
|
|
2537
|
+
if (item._ts) {
|
|
2538
|
+
if (!maxTs || this.compareTimestamps(item._ts, maxTs) > 0) {
|
|
2539
|
+
maxTs = item._ts;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
if (maxTs) {
|
|
2544
|
+
const currentMeta = this.syncMetaCache.get(collection);
|
|
2545
|
+
const currentTs = currentMeta?.lastSyncTs;
|
|
2546
|
+
if (!currentTs || this.compareTimestamps(maxTs, currentTs) > 0) {
|
|
2547
|
+
await this.dexieDb.setSyncMeta(collection, maxTs);
|
|
2548
|
+
this.syncMetaCache.set(collection, {
|
|
2549
|
+
tenant: this.tenant,
|
|
2550
|
+
collection,
|
|
2551
|
+
lastSyncTs: maxTs
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2500
2555
|
if (errors) {
|
|
2501
2556
|
console.error(`Sync errors for ${collection}:`, errors);
|
|
2502
2557
|
}
|
|
@@ -2547,74 +2602,83 @@ class SyncedDb {
|
|
|
2547
2602
|
}
|
|
2548
2603
|
return false;
|
|
2549
2604
|
}
|
|
2550
|
-
|
|
2551
|
-
if (
|
|
2552
|
-
return
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2605
|
+
broadcastMetaUpdate(updates) {
|
|
2606
|
+
if (!this.metaUpdateChannel)
|
|
2607
|
+
return;
|
|
2608
|
+
for (const [collection, ids] of Object.entries(updates)) {
|
|
2609
|
+
let existing = this.pendingBroadcasts.get(collection);
|
|
2610
|
+
if (!existing) {
|
|
2611
|
+
existing = new Set;
|
|
2612
|
+
this.pendingBroadcasts.set(collection, existing);
|
|
2613
|
+
}
|
|
2614
|
+
for (const id of ids) {
|
|
2615
|
+
existing.add(id);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (this.broadcastDebounceTimer) {
|
|
2619
|
+
clearTimeout(this.broadcastDebounceTimer);
|
|
2620
|
+
}
|
|
2621
|
+
this.broadcastDebounceTimer = setTimeout(() => {
|
|
2622
|
+
this.flushBroadcasts();
|
|
2623
|
+
}, this.crossTabSyncDebounceMs);
|
|
2624
|
+
}
|
|
2625
|
+
flushBroadcasts() {
|
|
2626
|
+
if (!this.metaUpdateChannel)
|
|
2627
|
+
return;
|
|
2628
|
+
if (this.broadcastDebounceTimer) {
|
|
2629
|
+
clearTimeout(this.broadcastDebounceTimer);
|
|
2630
|
+
this.broadcastDebounceTimer = undefined;
|
|
2631
|
+
}
|
|
2632
|
+
const updates = {};
|
|
2633
|
+
for (const [collection, ids] of this.pendingBroadcasts) {
|
|
2634
|
+
updates[collection] = Array.from(ids);
|
|
2576
2635
|
}
|
|
2636
|
+
if (Object.keys(updates).length > 0) {
|
|
2637
|
+
const payload = {
|
|
2638
|
+
updates,
|
|
2639
|
+
instanceId: this.syncedDbInstanceId
|
|
2640
|
+
};
|
|
2641
|
+
this.metaUpdateChannel.postMessage(payload);
|
|
2642
|
+
}
|
|
2643
|
+
this.pendingBroadcasts.clear();
|
|
2577
2644
|
}
|
|
2578
2645
|
async handleCrossTabMetaUpdate(payload) {
|
|
2579
|
-
if (
|
|
2646
|
+
if (!this.initialized) {
|
|
2580
2647
|
return;
|
|
2581
2648
|
}
|
|
2582
|
-
|
|
2583
|
-
if (!this.collections.has(collection)) {
|
|
2649
|
+
if (payload.instanceId === this.syncedDbInstanceId) {
|
|
2584
2650
|
return;
|
|
2585
2651
|
}
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
if (newMeta) {
|
|
2590
|
-
this.syncMetaCache.set(collection, newMeta);
|
|
2591
|
-
}
|
|
2592
|
-
const newerItems = await this.dexieDb.getNewerThan(collection, lastSeenRev);
|
|
2593
|
-
let newItemsCount = 0;
|
|
2594
|
-
let maxRev = lastSeenRev;
|
|
2595
|
-
for (const item of newerItems) {
|
|
2596
|
-
if (item._rev !== undefined && item._rev > maxRev) {
|
|
2597
|
-
maxRev = item._rev;
|
|
2598
|
-
}
|
|
2599
|
-
if (item._deleted) {
|
|
2600
|
-
this.inMemDb.deleteOne(collection, item._id);
|
|
2601
|
-
} else {
|
|
2602
|
-
this.inMemDb.save(collection, item._id, this.stripLocalFields(item));
|
|
2603
|
-
}
|
|
2604
|
-
newItemsCount++;
|
|
2652
|
+
for (const [collection, ids] of Object.entries(payload.updates)) {
|
|
2653
|
+
if (!this.collections.has(collection)) {
|
|
2654
|
+
continue;
|
|
2605
2655
|
}
|
|
2606
|
-
if (
|
|
2607
|
-
|
|
2656
|
+
if (ids.length === 0) {
|
|
2657
|
+
continue;
|
|
2608
2658
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2659
|
+
try {
|
|
2660
|
+
const items = await this.dexieDb.getByIds(collection, ids);
|
|
2661
|
+
let newItemsCount = 0;
|
|
2662
|
+
for (const item of items) {
|
|
2663
|
+
if (!item)
|
|
2664
|
+
continue;
|
|
2665
|
+
if (item._deleted) {
|
|
2666
|
+
this.inMemDb.deleteOne(collection, item._id);
|
|
2667
|
+
} else {
|
|
2668
|
+
this.inMemDb.save(collection, item._id, this.stripLocalFields(item));
|
|
2669
|
+
}
|
|
2670
|
+
newItemsCount++;
|
|
2614
2671
|
}
|
|
2672
|
+
if (this.onCrossTabSync && newItemsCount > 0) {
|
|
2673
|
+
try {
|
|
2674
|
+
this.onCrossTabSync(collection, newItemsCount);
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
console.error("onCrossTabSync callback failed:", err);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
} catch (err) {
|
|
2680
|
+
console.error(`Error handling cross-tab meta update for ${collection}:`, err);
|
|
2615
2681
|
}
|
|
2616
|
-
} catch (err) {
|
|
2617
|
-
console.error(`Error handling cross-tab meta update for ${collection}:`, err);
|
|
2618
2682
|
}
|
|
2619
2683
|
}
|
|
2620
2684
|
async handleServerUpdate(payload) {
|
|
@@ -2626,14 +2690,13 @@ class SyncedDb {
|
|
|
2626
2690
|
const collectionName = payload.collection;
|
|
2627
2691
|
if (!this.collections.has(collectionName))
|
|
2628
2692
|
return;
|
|
2693
|
+
const updatedIds = [];
|
|
2629
2694
|
switch (payload.operation) {
|
|
2630
2695
|
case "insert": {
|
|
2631
2696
|
const serverItem = payload.data;
|
|
2632
2697
|
if (serverItem) {
|
|
2633
2698
|
await this.handleServerItemInsert(collectionName, serverItem);
|
|
2634
|
-
|
|
2635
|
-
await this.broadcastMetaUpdate(collectionName, serverItem._ts);
|
|
2636
|
-
}
|
|
2699
|
+
updatedIds.push(String(serverItem._id));
|
|
2637
2700
|
}
|
|
2638
2701
|
break;
|
|
2639
2702
|
}
|
|
@@ -2643,16 +2706,12 @@ class SyncedDb {
|
|
|
2643
2706
|
const localItem = await this.dexieDb.getById(collectionName, deltaData._id);
|
|
2644
2707
|
if (localItem) {
|
|
2645
2708
|
await this.handleServerItemUpdate(collectionName, localItem, deltaData);
|
|
2646
|
-
|
|
2647
|
-
await this.broadcastMetaUpdate(collectionName, deltaData._ts);
|
|
2648
|
-
}
|
|
2709
|
+
updatedIds.push(String(deltaData._id));
|
|
2649
2710
|
} else {
|
|
2650
2711
|
const fullItem = await this.restInterface.findById(collectionName, deltaData._id);
|
|
2651
2712
|
if (fullItem) {
|
|
2652
2713
|
await this.handleServerItemInsert(collectionName, fullItem);
|
|
2653
|
-
|
|
2654
|
-
await this.broadcastMetaUpdate(collectionName, fullItem._ts);
|
|
2655
|
-
}
|
|
2714
|
+
updatedIds.push(String(fullItem._id));
|
|
2656
2715
|
}
|
|
2657
2716
|
}
|
|
2658
2717
|
}
|
|
@@ -2661,7 +2720,7 @@ class SyncedDb {
|
|
|
2661
2720
|
case "delete": {
|
|
2662
2721
|
const deleteData = payload.data;
|
|
2663
2722
|
await this.handleServerItemDelete(collectionName, deleteData._id);
|
|
2664
|
-
|
|
2723
|
+
updatedIds.push(String(deleteData._id));
|
|
2665
2724
|
break;
|
|
2666
2725
|
}
|
|
2667
2726
|
case "batch": {
|
|
@@ -2679,6 +2738,7 @@ class SyncedDb {
|
|
|
2679
2738
|
}
|
|
2680
2739
|
for (const serverItem of inserts) {
|
|
2681
2740
|
await this.handleServerItemInsert(collectionName, serverItem);
|
|
2741
|
+
updatedIds.push(String(serverItem._id));
|
|
2682
2742
|
}
|
|
2683
2743
|
if (updates.length > 0) {
|
|
2684
2744
|
const updateIds = updates.map((u) => u._id);
|
|
@@ -2688,21 +2748,26 @@ class SyncedDb {
|
|
|
2688
2748
|
const localItem = localItems[i];
|
|
2689
2749
|
if (localItem) {
|
|
2690
2750
|
await this.handleServerItemUpdate(collectionName, localItem, deltaData);
|
|
2751
|
+
updatedIds.push(String(deltaData._id));
|
|
2691
2752
|
} else {
|
|
2692
2753
|
const fullItem = await this.restInterface.findById(collectionName, deltaData._id);
|
|
2693
2754
|
if (fullItem) {
|
|
2694
2755
|
await this.handleServerItemInsert(collectionName, fullItem);
|
|
2756
|
+
updatedIds.push(String(fullItem._id));
|
|
2695
2757
|
}
|
|
2696
2758
|
}
|
|
2697
2759
|
}
|
|
2698
2760
|
}
|
|
2699
2761
|
for (const deleteData of deletes) {
|
|
2700
2762
|
await this.handleServerItemDelete(collectionName, deleteData._id);
|
|
2763
|
+
updatedIds.push(String(deleteData._id));
|
|
2701
2764
|
}
|
|
2702
|
-
await this.broadcastMetaUpdate(collectionName, Date.now());
|
|
2703
2765
|
break;
|
|
2704
2766
|
}
|
|
2705
2767
|
}
|
|
2768
|
+
if (updatedIds.length > 0) {
|
|
2769
|
+
this.broadcastMetaUpdate({ [collectionName]: updatedIds });
|
|
2770
|
+
}
|
|
2706
2771
|
}
|
|
2707
2772
|
async handleServerItemInsert(collectionName, serverItem) {
|
|
2708
2773
|
const localItem = await this.dexieDb.getById(collectionName, serverItem._id);
|
|
@@ -2824,27 +2889,17 @@ var DIRTY_CHANGES_TABLE = "_dirty_changes";
|
|
|
2824
2889
|
|
|
2825
2890
|
class DexieDb extends Dexie {
|
|
2826
2891
|
tenant;
|
|
2827
|
-
instanceId;
|
|
2828
|
-
crossTabSyncDebounceMs;
|
|
2829
2892
|
collections = new Map;
|
|
2830
2893
|
syncMeta;
|
|
2831
2894
|
dirtyChanges;
|
|
2832
|
-
|
|
2833
|
-
pendingBroadcasts = new Map;
|
|
2834
|
-
debounceTimer = null;
|
|
2835
|
-
metaUpdateCallbacks = new Set;
|
|
2836
|
-
constructor(tenant, collectionConfigs, options) {
|
|
2895
|
+
constructor(tenant, collectionConfigs) {
|
|
2837
2896
|
super(`synced-db-${tenant}`);
|
|
2838
2897
|
this.tenant = tenant;
|
|
2839
|
-
this.instanceId = options?.instanceId ?? `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2840
|
-
this.crossTabSyncDebounceMs = options?.crossTabSyncDebounceMs ?? 100;
|
|
2841
2898
|
const schema = {};
|
|
2842
2899
|
schema[SYNC_META_TABLE] = "[tenant+collection]";
|
|
2843
2900
|
schema[DIRTY_CHANGES_TABLE] = "[collection+id]";
|
|
2844
2901
|
for (const config of collectionConfigs) {
|
|
2845
|
-
|
|
2846
|
-
const indexes = ["_id", "_rev", ...additionalIndexes.map(String)];
|
|
2847
|
-
schema[config.name] = indexes.join(", ");
|
|
2902
|
+
schema[config.name] = "_id";
|
|
2848
2903
|
}
|
|
2849
2904
|
this.version(1).stores(schema);
|
|
2850
2905
|
this.syncMeta = this.table(SYNC_META_TABLE);
|
|
@@ -2852,48 +2907,6 @@ class DexieDb extends Dexie {
|
|
|
2852
2907
|
for (const config of collectionConfigs) {
|
|
2853
2908
|
this.collections.set(config.name, this.table(config.name));
|
|
2854
2909
|
}
|
|
2855
|
-
this.setupBroadcastChannel();
|
|
2856
|
-
}
|
|
2857
|
-
setupBroadcastChannel() {
|
|
2858
|
-
if (typeof BroadcastChannel === "undefined") {
|
|
2859
|
-
return;
|
|
2860
|
-
}
|
|
2861
|
-
this.broadcastChannel = new BroadcastChannel(`synced-db-meta-${this.tenant}`);
|
|
2862
|
-
this.broadcastChannel.onmessage = (event) => {
|
|
2863
|
-
const payload = event.data;
|
|
2864
|
-
for (const callback of this.metaUpdateCallbacks) {
|
|
2865
|
-
try {
|
|
2866
|
-
callback(payload);
|
|
2867
|
-
} catch (err) {
|
|
2868
|
-
console.error("Error in metaUpdate callback:", err);
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
};
|
|
2872
|
-
}
|
|
2873
|
-
queueBroadcast(collection, lastSyncTs) {
|
|
2874
|
-
if (!this.broadcastChannel)
|
|
2875
|
-
return;
|
|
2876
|
-
this.pendingBroadcasts.set(collection, lastSyncTs);
|
|
2877
|
-
if (this.debounceTimer) {
|
|
2878
|
-
clearTimeout(this.debounceTimer);
|
|
2879
|
-
}
|
|
2880
|
-
this.debounceTimer = setTimeout(() => {
|
|
2881
|
-
this.flushBroadcasts();
|
|
2882
|
-
}, this.crossTabSyncDebounceMs);
|
|
2883
|
-
}
|
|
2884
|
-
flushBroadcasts() {
|
|
2885
|
-
if (!this.broadcastChannel)
|
|
2886
|
-
return;
|
|
2887
|
-
for (const [collection, lastSyncTs] of this.pendingBroadcasts) {
|
|
2888
|
-
const payload = {
|
|
2889
|
-
collection,
|
|
2890
|
-
lastSyncTs,
|
|
2891
|
-
instanceId: this.instanceId
|
|
2892
|
-
};
|
|
2893
|
-
this.broadcastChannel.postMessage(payload);
|
|
2894
|
-
}
|
|
2895
|
-
this.pendingBroadcasts.clear();
|
|
2896
|
-
this.debounceTimer = null;
|
|
2897
2910
|
}
|
|
2898
2911
|
getTable(collection) {
|
|
2899
2912
|
const table = this.collections.get(collection);
|
|
@@ -3073,7 +3086,6 @@ class DexieDb extends Dexie {
|
|
|
3073
3086
|
collection,
|
|
3074
3087
|
lastSyncTs
|
|
3075
3088
|
});
|
|
3076
|
-
this.queueBroadcast(collection, lastSyncTs);
|
|
3077
3089
|
}
|
|
3078
3090
|
async deleteSyncMeta(collection) {
|
|
3079
3091
|
await this.syncMeta.delete([this.tenant, collection]);
|
|
@@ -3081,30 +3093,6 @@ class DexieDb extends Dexie {
|
|
|
3081
3093
|
getTenant() {
|
|
3082
3094
|
return this.tenant;
|
|
3083
3095
|
}
|
|
3084
|
-
getInstanceId() {
|
|
3085
|
-
return this.instanceId;
|
|
3086
|
-
}
|
|
3087
|
-
async getNewerThan(collection, rev) {
|
|
3088
|
-
const table = this.getTable(collection);
|
|
3089
|
-
return await table.where("_rev").above(rev).toArray();
|
|
3090
|
-
}
|
|
3091
|
-
onMetaUpdated(callback) {
|
|
3092
|
-
this.metaUpdateCallbacks.add(callback);
|
|
3093
|
-
return () => {
|
|
3094
|
-
this.metaUpdateCallbacks.delete(callback);
|
|
3095
|
-
};
|
|
3096
|
-
}
|
|
3097
|
-
closeBroadcastChannel() {
|
|
3098
|
-
if (this.debounceTimer) {
|
|
3099
|
-
clearTimeout(this.debounceTimer);
|
|
3100
|
-
this.flushBroadcasts();
|
|
3101
|
-
}
|
|
3102
|
-
if (this.broadcastChannel) {
|
|
3103
|
-
this.broadcastChannel.close();
|
|
3104
|
-
this.broadcastChannel = null;
|
|
3105
|
-
}
|
|
3106
|
-
this.metaUpdateCallbacks.clear();
|
|
3107
|
-
}
|
|
3108
3096
|
}
|
|
3109
3097
|
// node_modules/msgpackr/unpack.js
|
|
3110
3098
|
var decoder;
|
package/dist/src/db/DexieDb.d.ts
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
import Dexie from "dexie";
|
|
2
|
-
import type {
|
|
2
|
+
import type { DirtyChange, 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
|
/**
|
|
6
6
|
* Implementacija DexieDb interface
|
|
7
7
|
* Uporablja Dexie za dostop do IndexedDB
|
|
8
|
+
* Note: This is pure storage - cross-tab sync is handled by SyncedDb
|
|
8
9
|
*/
|
|
9
10
|
export declare class DexieDb extends Dexie implements I_DexieDb {
|
|
10
11
|
private tenant;
|
|
11
|
-
private instanceId;
|
|
12
|
-
private crossTabSyncDebounceMs;
|
|
13
12
|
private collections;
|
|
14
13
|
private syncMeta;
|
|
15
14
|
private dirtyChanges;
|
|
16
|
-
|
|
17
|
-
private pendingBroadcasts;
|
|
18
|
-
private debounceTimer;
|
|
19
|
-
private metaUpdateCallbacks;
|
|
20
|
-
constructor(tenant: string, collectionConfigs: CollectionConfig<any>[], options?: Partial<DexieDbOptions>);
|
|
21
|
-
private setupBroadcastChannel;
|
|
22
|
-
private queueBroadcast;
|
|
23
|
-
private flushBroadcasts;
|
|
15
|
+
constructor(tenant: string, collectionConfigs: CollectionConfig<any>[]);
|
|
24
16
|
private getTable;
|
|
25
17
|
private idToString;
|
|
26
18
|
save<T extends LocalDbEntity>(collection: string, id: Id, data: Partial<T>): Promise<void>;
|
|
@@ -56,9 +48,4 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
|
|
|
56
48
|
setSyncMeta(collection: string, lastSyncTs: any): Promise<void>;
|
|
57
49
|
deleteSyncMeta(collection: string): Promise<void>;
|
|
58
50
|
getTenant(): string;
|
|
59
|
-
getInstanceId(): string;
|
|
60
|
-
getNewerThan<T extends LocalDbEntity>(collection: string, rev: number): Promise<T[]>;
|
|
61
|
-
onMetaUpdated(callback: (payload: MetaUpdateBroadcast) => void): () => void;
|
|
62
|
-
/** Close the broadcast channel when done */
|
|
63
|
-
closeBroadcastChannel(): void;
|
|
64
51
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AggregateOptions } from "mongodb";
|
|
2
2
|
import type { I_SyncedDb, SyncedDbConfig, WsNotificationInfo } from "../types/I_SyncedDb";
|
|
3
|
+
import type { MetaUpdateBroadcast } from "../types/I_DexieDb";
|
|
3
4
|
import type { QuerySpec, UpdateSpec, InsertSpec, BatchSpec } from "../types/I_RestInterface";
|
|
4
5
|
import type { Id, DbEntity } from "../types/DbEntity";
|
|
5
6
|
/**
|
|
@@ -33,8 +34,7 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
33
34
|
private onWsNotification?;
|
|
34
35
|
private onOnlineStatusChange?;
|
|
35
36
|
private crossTabSyncDebounceMs;
|
|
36
|
-
private
|
|
37
|
-
private onStaleMetaConflict?;
|
|
37
|
+
private onCrossTabSync?;
|
|
38
38
|
private onBecameLeader?;
|
|
39
39
|
private onLostLeadership?;
|
|
40
40
|
private onInfrastructureError?;
|
|
@@ -60,10 +60,12 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
60
60
|
private readonly syncedDbInstanceId;
|
|
61
61
|
/** In-memory cache of sync metadata per collection */
|
|
62
62
|
private syncMetaCache;
|
|
63
|
-
/**
|
|
64
|
-
private
|
|
65
|
-
/**
|
|
66
|
-
private
|
|
63
|
+
/** BroadcastChannel for cross-tab meta updates */
|
|
64
|
+
private metaUpdateChannel?;
|
|
65
|
+
/** Pending broadcasts to be sent (debounced) */
|
|
66
|
+
private pendingBroadcasts;
|
|
67
|
+
/** Debounce timer for broadcasts */
|
|
68
|
+
private broadcastDebounceTimer?;
|
|
67
69
|
/** Whether this instance is the leader tab */
|
|
68
70
|
private isLeader;
|
|
69
71
|
/** Resolve function to release the leader lock */
|
|
@@ -86,6 +88,11 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
86
88
|
getCrossTabSyncDebounceMs(): number;
|
|
87
89
|
/** Check if this instance is the leader tab */
|
|
88
90
|
isLeaderTab(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Test helper: simulate receiving a cross-tab broadcast from another instance.
|
|
93
|
+
* This is used in tests where BroadcastChannel is not available.
|
|
94
|
+
*/
|
|
95
|
+
simulateExternalBroadcast(payload: MetaUpdateBroadcast): void;
|
|
89
96
|
/**
|
|
90
97
|
* Try to become the leader tab using Web Locks API.
|
|
91
98
|
* Only the leader processes server notifications.
|
|
@@ -170,8 +177,8 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
170
177
|
getOnWsReconnect(): ((attempt: number) => void) | undefined;
|
|
171
178
|
getOnWsNotification(): ((info: WsNotificationInfo) => void) | undefined;
|
|
172
179
|
getDirty<T extends DbEntity>(): Promise<Readonly<Record<string, readonly T[]>>>;
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
dropCollection(collection: string, force?: boolean): Promise<void>;
|
|
181
|
+
dropDatabase(force?: boolean): Promise<void>;
|
|
175
182
|
private assertCollection;
|
|
176
183
|
private stripLocalFields;
|
|
177
184
|
private getPendingKey;
|
|
@@ -210,16 +217,18 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
210
217
|
*/
|
|
211
218
|
private timestampsEqual;
|
|
212
219
|
/**
|
|
213
|
-
*
|
|
220
|
+
* Notify other tabs about updated records via BroadcastChannel.
|
|
221
|
+
* Does NOT write to Dexie - just sends a broadcast with the updated _ids.
|
|
222
|
+
* Broadcasts are debounced to reduce message frequency.
|
|
214
223
|
*/
|
|
215
|
-
private
|
|
224
|
+
private broadcastMetaUpdate;
|
|
216
225
|
/**
|
|
217
|
-
*
|
|
218
|
-
* Updates both Dexie sync meta and the in-memory cache.
|
|
226
|
+
* Flush pending broadcasts immediately.
|
|
219
227
|
*/
|
|
220
|
-
private
|
|
228
|
+
private flushBroadcasts;
|
|
221
229
|
/**
|
|
222
|
-
* Handle cross-tab sync metadata updates from BroadcastChannel
|
|
230
|
+
* Handle cross-tab sync metadata updates from BroadcastChannel.
|
|
231
|
+
* Copies the specified objects from Dexie to in-memory and removes deleted records.
|
|
223
232
|
*/
|
|
224
233
|
private handleCrossTabMetaUpdate;
|
|
225
234
|
private handleServerUpdate;
|
|
@@ -29,20 +29,12 @@ export interface DirtyChange {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Payload for cross-tab meta update broadcasts
|
|
32
|
+
* Maps collection names to arrays of _id strings that were updated
|
|
32
33
|
*/
|
|
33
34
|
export interface MetaUpdateBroadcast {
|
|
34
|
-
collection
|
|
35
|
-
|
|
36
|
-
instanceId: string;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Options for DexieDb constructor
|
|
40
|
-
*/
|
|
41
|
-
export interface DexieDbOptions {
|
|
42
|
-
/** Unique identifier for this SyncedDb instance */
|
|
35
|
+
/** Map of collection name -> array of _id strings that were updated */
|
|
36
|
+
updates: Record<string, string[]>;
|
|
43
37
|
instanceId: string;
|
|
44
|
-
/** Debounce interval for cross-tab sync broadcasts (ms) */
|
|
45
|
-
crossTabSyncDebounceMs: number;
|
|
46
38
|
}
|
|
47
39
|
/**
|
|
48
40
|
* Interface za Dexie (IndexedDB) bazo podatkov
|
|
@@ -105,10 +97,4 @@ export interface I_DexieDb {
|
|
|
105
97
|
deleteSyncMeta(collection: string): Promise<void>;
|
|
106
98
|
/** Vrne tenant */
|
|
107
99
|
getTenant(): string;
|
|
108
|
-
/** Get records newer than revision (uses _rev index) */
|
|
109
|
-
getNewerThan<T extends LocalDbEntity>(collection: string, rev: number): Promise<T[]>;
|
|
110
|
-
/** Subscribe to metadata updates from other tabs. Returns unsubscribe function. */
|
|
111
|
-
onMetaUpdated(callback: (payload: MetaUpdateBroadcast) => void): () => void;
|
|
112
|
-
/** Get the instance ID */
|
|
113
|
-
getInstanceId(): string;
|
|
114
100
|
}
|
|
@@ -201,15 +201,13 @@ export interface SyncInfo {
|
|
|
201
201
|
/**
|
|
202
202
|
* Konfiguracija za posamezno kolekcijo
|
|
203
203
|
*/
|
|
204
|
-
export interface CollectionConfig {
|
|
204
|
+
export interface CollectionConfig<T extends DbEntity = any> {
|
|
205
205
|
/** Ime kolekcije */
|
|
206
206
|
name: string;
|
|
207
207
|
/** Opcijski filter za velike tabele - omeji obseg sinhronizacije */
|
|
208
208
|
query?: QuerySpec<any>;
|
|
209
|
-
/** Dodatni indexi poleg _id */
|
|
210
|
-
indexes?: string[];
|
|
211
209
|
/** Opcijska funkcija za razreševanje konfliktov */
|
|
212
|
-
resolveSyncConflict
|
|
210
|
+
resolveSyncConflict?(local: T, external: T): T;
|
|
213
211
|
}
|
|
214
212
|
/**
|
|
215
213
|
* Konfiguracija za SyncedDb - dependencies so podani kot parametri (DI)
|
|
@@ -275,9 +273,7 @@ export interface SyncedDbConfig {
|
|
|
275
273
|
/** Debounce interval for cross-tab sync broadcasts in ms (default: 100) */
|
|
276
274
|
crossTabSyncDebounceMs?: number;
|
|
277
275
|
/** Callback when another tab synced data and local in-memory cache was updated */
|
|
278
|
-
|
|
279
|
-
/** Callback when received metadata is older than in-memory (indicates conflict/issue) */
|
|
280
|
-
onStaleMetaConflict?: (collection: string, inMemoryTs: any, receivedTs: any) => void;
|
|
276
|
+
onCrossTabSync?: (collection: string, newItemsCount: number) => void;
|
|
281
277
|
/** Callback when this instance becomes the leader tab */
|
|
282
278
|
onBecameLeader?: () => void;
|
|
283
279
|
/** Callback when this instance loses leadership (e.g., another tab took over) */
|
|
@@ -410,20 +406,28 @@ export interface I_SyncedDb {
|
|
|
410
406
|
/** Vrne vse dirty objekte iz vseh kolekcij */
|
|
411
407
|
getDirty<T extends DbEntity>(): Promise<Readonly<Record<string, readonly T[]>>>;
|
|
412
408
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
409
|
+
* Drops a collection, ensuring no data loss.
|
|
410
|
+
* - Throws if offline or forcedOffline (unless force=true)
|
|
411
|
+
* - Flushes pending in-memory changes to Dexie
|
|
412
|
+
* - Syncs all dirty items to the server
|
|
413
|
+
* - Throws if sync fails (unless force=true)
|
|
414
|
+
* - Then deletes collection data, metadata, and dirty changes from Dexie and in-memory
|
|
415
|
+
* @param collection Name of the collection to drop
|
|
416
|
+
* @param force If true, drops even if offline or sync fails (may lose data)
|
|
417
|
+
* @throws Error if offline, forcedOffline, or sync fails (unless force=true)
|
|
418
418
|
*/
|
|
419
|
-
|
|
419
|
+
dropCollection(collection: string, force?: boolean): Promise<void>;
|
|
420
420
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
421
|
+
* Drops all collections (the entire database), ensuring no data loss.
|
|
422
|
+
* - Throws if offline or forcedOffline (unless force=true)
|
|
423
|
+
* - Flushes pending in-memory changes to Dexie
|
|
424
|
+
* - Syncs all dirty items to the server
|
|
425
|
+
* - Throws if sync fails (unless force=true)
|
|
426
|
+
* - Then deletes all collection data, metadata, and dirty changes from Dexie and in-memory
|
|
427
|
+
* @param force If true, drops even if offline or sync fails (may lose data)
|
|
428
|
+
* @throws Error if offline, forcedOffline, or sync fails (unless force=true)
|
|
425
429
|
*/
|
|
426
|
-
|
|
430
|
+
dropDatabase(force?: boolean): Promise<void>;
|
|
427
431
|
/**
|
|
428
432
|
* Check if this instance is the leader tab.
|
|
429
433
|
* Only the leader processes server notifications and writes to Dexie from server updates.
|