cry-synced-db-client 0.1.67 → 0.1.71
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 +338 -31
- package/dist/src/db/SyncedDb.d.ts +25 -5
- package/dist/src/db/sync/SyncEngine.d.ts +8 -0
- package/dist/src/db/types/internal.d.ts +2 -0
- package/dist/src/db/types/managers.d.ts +4 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/types/DbEntity.d.ts +1 -0
- package/dist/src/types/I_RestInterface.d.ts +5 -1
- package/dist/src/types/I_SyncedDb.d.ts +37 -5
- package/dist/src/utils/localQuery.d.ts +20 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -121,6 +121,103 @@ function filterByQuery(items, query) {
|
|
|
121
121
|
}
|
|
122
122
|
return items.filter((item) => matchesQuery(item, query));
|
|
123
123
|
}
|
|
124
|
+
function sortItems(items, sort) {
|
|
125
|
+
const sortEntries = Object.entries(sort);
|
|
126
|
+
if (sortEntries.length === 0)
|
|
127
|
+
return items;
|
|
128
|
+
return [...items].sort((a, b) => {
|
|
129
|
+
for (const [field, direction] of sortEntries) {
|
|
130
|
+
const aVal = getNestedValue(a, field);
|
|
131
|
+
const bVal = getNestedValue(b, field);
|
|
132
|
+
const cmp = compareValues(aVal, bVal);
|
|
133
|
+
if (cmp !== 0)
|
|
134
|
+
return cmp * direction;
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function compareValues(a, b) {
|
|
140
|
+
if (a === b)
|
|
141
|
+
return 0;
|
|
142
|
+
if (a == null && b == null)
|
|
143
|
+
return 0;
|
|
144
|
+
if (a == null)
|
|
145
|
+
return -1;
|
|
146
|
+
if (b == null)
|
|
147
|
+
return 1;
|
|
148
|
+
if (a instanceof Date && b instanceof Date) {
|
|
149
|
+
return a.getTime() - b.getTime();
|
|
150
|
+
}
|
|
151
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
152
|
+
return a - b;
|
|
153
|
+
}
|
|
154
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
155
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
156
|
+
}
|
|
157
|
+
if (typeof a === "boolean" && typeof b === "boolean") {
|
|
158
|
+
return (a ? 1 : 0) - (b ? 1 : 0);
|
|
159
|
+
}
|
|
160
|
+
const sa = String(a);
|
|
161
|
+
const sb = String(b);
|
|
162
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
163
|
+
}
|
|
164
|
+
function applySkipLimit(items, skip, limit) {
|
|
165
|
+
let result = items;
|
|
166
|
+
if (skip && skip > 0) {
|
|
167
|
+
result = result.slice(skip);
|
|
168
|
+
}
|
|
169
|
+
if (limit != null && limit >= 0) {
|
|
170
|
+
result = result.slice(0, limit);
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
function projectItem(item, project) {
|
|
175
|
+
const entries = Object.entries(project);
|
|
176
|
+
if (entries.length === 0)
|
|
177
|
+
return item;
|
|
178
|
+
const hasIncludes = entries.some(([, v]) => v === true || v === 1);
|
|
179
|
+
if (hasIncludes) {
|
|
180
|
+
const result = {};
|
|
181
|
+
const excludeId = project._id === false || project._id === 0;
|
|
182
|
+
if (!excludeId && item._id !== undefined) {
|
|
183
|
+
result._id = item._id;
|
|
184
|
+
}
|
|
185
|
+
for (const [field, value] of entries) {
|
|
186
|
+
if (field === "_id")
|
|
187
|
+
continue;
|
|
188
|
+
if (value === true || value === 1) {
|
|
189
|
+
const val = getNestedValue(item, field);
|
|
190
|
+
if (val !== undefined) {
|
|
191
|
+
result[field] = val;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
} else {
|
|
197
|
+
const result = { ...item };
|
|
198
|
+
for (const [field, value] of entries) {
|
|
199
|
+
if (value === false || value === 0) {
|
|
200
|
+
delete result[field];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function applyQueryOpts(items, opts) {
|
|
207
|
+
if (!opts)
|
|
208
|
+
return items;
|
|
209
|
+
let result = items;
|
|
210
|
+
if (opts.sort && Object.keys(opts.sort).length > 0) {
|
|
211
|
+
result = sortItems(result, opts.sort);
|
|
212
|
+
}
|
|
213
|
+
if (opts.skip || opts.limit != null) {
|
|
214
|
+
result = applySkipLimit(result, opts.skip, opts.limit);
|
|
215
|
+
}
|
|
216
|
+
if (opts.project) {
|
|
217
|
+
result = result.map((item) => projectItem(item, opts.project));
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
124
221
|
|
|
125
222
|
// src/db/managers/InMemManager.ts
|
|
126
223
|
class InMemManager {
|
|
@@ -2032,6 +2129,7 @@ class SyncEngine {
|
|
|
2032
2129
|
let receivedCount = 0;
|
|
2033
2130
|
let sentCount = 0;
|
|
2034
2131
|
let conflictsResolved = 0;
|
|
2132
|
+
const collectionStats = {};
|
|
2035
2133
|
try {
|
|
2036
2134
|
this.deps.cancelRestUploadTimer();
|
|
2037
2135
|
await this.deps.awaitRestUpload();
|
|
@@ -2069,6 +2167,11 @@ class SyncEngine {
|
|
|
2069
2167
|
for (const [collectionName, config] of configMap) {
|
|
2070
2168
|
const serverData = allServerData[collectionName] || [];
|
|
2071
2169
|
receivedCount += serverData.length;
|
|
2170
|
+
collectionStats[collectionName] = {
|
|
2171
|
+
receivedCount: serverData.length,
|
|
2172
|
+
sentCount: 0,
|
|
2173
|
+
receivedItems: serverData
|
|
2174
|
+
};
|
|
2072
2175
|
const stats = await this.processIncomingServerData(collectionName, config, serverData);
|
|
2073
2176
|
conflictsResolved += stats.conflictsResolved;
|
|
2074
2177
|
if (stats.updatedIds.length > 0) {
|
|
@@ -2080,13 +2183,25 @@ class SyncEngine {
|
|
|
2080
2183
|
}
|
|
2081
2184
|
const uploadStats = await this.uploadDirtyItems(calledFrom);
|
|
2082
2185
|
sentCount = uploadStats.sentCount;
|
|
2186
|
+
for (const [collectionName, stats] of Object.entries(uploadStats.collectionSentCounts || {})) {
|
|
2187
|
+
if (collectionStats[collectionName]) {
|
|
2188
|
+
collectionStats[collectionName].sentCount = stats;
|
|
2189
|
+
} else {
|
|
2190
|
+
collectionStats[collectionName] = {
|
|
2191
|
+
receivedCount: 0,
|
|
2192
|
+
sentCount: stats,
|
|
2193
|
+
receivedItems: []
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2083
2197
|
this.callOnSync({
|
|
2084
2198
|
durationMs: Date.now() - startTime,
|
|
2085
2199
|
receivedCount,
|
|
2086
2200
|
sentCount,
|
|
2087
2201
|
conflictsResolved,
|
|
2088
2202
|
success: true,
|
|
2089
|
-
calledFrom
|
|
2203
|
+
calledFrom,
|
|
2204
|
+
collections: collectionStats
|
|
2090
2205
|
});
|
|
2091
2206
|
} catch (err) {
|
|
2092
2207
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -2099,7 +2214,8 @@ class SyncEngine {
|
|
|
2099
2214
|
conflictsResolved,
|
|
2100
2215
|
success: false,
|
|
2101
2216
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
2102
|
-
calledFrom
|
|
2217
|
+
calledFrom,
|
|
2218
|
+
collections: collectionStats
|
|
2103
2219
|
});
|
|
2104
2220
|
throw err;
|
|
2105
2221
|
}
|
|
@@ -2158,6 +2274,7 @@ class SyncEngine {
|
|
|
2158
2274
|
throw err;
|
|
2159
2275
|
}
|
|
2160
2276
|
let sentCount = 0;
|
|
2277
|
+
const collectionSentCounts = {};
|
|
2161
2278
|
for (const result of results) {
|
|
2162
2279
|
const { collection, results: { inserted, updated, deleted, errors } } = result;
|
|
2163
2280
|
const allSuccessIds = [
|
|
@@ -2168,6 +2285,7 @@ class SyncEngine {
|
|
|
2168
2285
|
if (allSuccessIds.length > 0) {
|
|
2169
2286
|
await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
|
|
2170
2287
|
}
|
|
2288
|
+
let collectionSentCount = 0;
|
|
2171
2289
|
const insertedAndUpdated = [...inserted, ...updated];
|
|
2172
2290
|
if (insertedAndUpdated.length > 0) {
|
|
2173
2291
|
const idsToCheck = insertedAndUpdated.map((e) => e._id);
|
|
@@ -2202,12 +2320,17 @@ class SyncEngine {
|
|
|
2202
2320
|
this.deps.writeToInMemBatch(collection, inMemUpdateBatch, "upsert");
|
|
2203
2321
|
}
|
|
2204
2322
|
sentCount += insertedAndUpdated.length;
|
|
2323
|
+
collectionSentCount += insertedAndUpdated.length;
|
|
2205
2324
|
}
|
|
2206
2325
|
if (deleted.length > 0) {
|
|
2207
2326
|
const deleteIds = deleted.map((e) => e._id);
|
|
2208
2327
|
await this.dexieDb.deleteMany(collection, deleteIds);
|
|
2209
2328
|
this.deps.writeToInMemBatch(collection, deleteIds.map((id) => ({ _id: id })), "delete");
|
|
2210
2329
|
sentCount += deleted.length;
|
|
2330
|
+
collectionSentCount += deleted.length;
|
|
2331
|
+
}
|
|
2332
|
+
if (collectionSentCount > 0) {
|
|
2333
|
+
collectionSentCounts[collection] = collectionSentCount;
|
|
2211
2334
|
}
|
|
2212
2335
|
const allItems = [...inserted, ...updated, ...deleted];
|
|
2213
2336
|
let maxTs = undefined;
|
|
@@ -2234,7 +2357,7 @@ class SyncEngine {
|
|
|
2234
2357
|
console.error(`Sync errors for ${collection}:`, errors);
|
|
2235
2358
|
}
|
|
2236
2359
|
}
|
|
2237
|
-
return { sentCount };
|
|
2360
|
+
return { sentCount, collectionSentCounts };
|
|
2238
2361
|
}
|
|
2239
2362
|
async uploadDirtyItemsForCollection(collection) {
|
|
2240
2363
|
const dirtyItems = await this.dexieDb.getDirty(collection);
|
|
@@ -2287,6 +2410,16 @@ class SyncEngine {
|
|
|
2287
2410
|
}
|
|
2288
2411
|
return { sentCount };
|
|
2289
2412
|
}
|
|
2413
|
+
async processCollectionServerData(collectionName, serverData) {
|
|
2414
|
+
const config = this.collections.get(collectionName);
|
|
2415
|
+
if (!config)
|
|
2416
|
+
return { updatedIds: [] };
|
|
2417
|
+
const result = await this.processIncomingServerData(collectionName, config, serverData);
|
|
2418
|
+
if (result.updatedIds.length > 0) {
|
|
2419
|
+
this.deps.broadcastUpdates({ [collectionName]: result.updatedIds });
|
|
2420
|
+
}
|
|
2421
|
+
return { updatedIds: result.updatedIds };
|
|
2422
|
+
}
|
|
2290
2423
|
async processIncomingServerData(collectionName, config, serverData) {
|
|
2291
2424
|
if (serverData.length === 0) {
|
|
2292
2425
|
return { conflictsResolved: 0, maxTs: undefined, updatedIds: [] };
|
|
@@ -2313,14 +2446,14 @@ class SyncEngine {
|
|
|
2313
2446
|
conflictsResolved++;
|
|
2314
2447
|
const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
|
|
2315
2448
|
dexieBatch.push(resolved);
|
|
2316
|
-
if (!resolved._deleted) {
|
|
2449
|
+
if (!resolved._deleted && !resolved._archived) {
|
|
2317
2450
|
inMemSaveBatch.push(resolved);
|
|
2318
2451
|
} else {
|
|
2319
2452
|
inMemDeleteIds.push(serverItem._id);
|
|
2320
2453
|
}
|
|
2321
2454
|
} else {
|
|
2322
2455
|
dexieBatch.push(serverItem);
|
|
2323
|
-
if (!serverItem._deleted) {
|
|
2456
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2324
2457
|
inMemSaveBatch.push(serverItem);
|
|
2325
2458
|
} else {
|
|
2326
2459
|
inMemDeleteIds.push(serverItem._id);
|
|
@@ -2328,7 +2461,7 @@ class SyncEngine {
|
|
|
2328
2461
|
}
|
|
2329
2462
|
} else {
|
|
2330
2463
|
dexieBatch.push(serverItem);
|
|
2331
|
-
if (!serverItem._deleted) {
|
|
2464
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2332
2465
|
inMemSaveBatch.push(serverItem);
|
|
2333
2466
|
}
|
|
2334
2467
|
}
|
|
@@ -2571,12 +2704,12 @@ class ServerUpdateHandler {
|
|
|
2571
2704
|
if (dirtyChange && !metaChanged) {
|
|
2572
2705
|
await this.dexieDb.clearDirtyChange(collection, serverItem._id);
|
|
2573
2706
|
}
|
|
2574
|
-
if (!serverItem._deleted) {
|
|
2707
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2575
2708
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(serverItem)], "upsert");
|
|
2576
2709
|
}
|
|
2577
2710
|
} else {
|
|
2578
2711
|
await this.dexieDb.insert(collection, serverItem);
|
|
2579
|
-
if (!serverItem._deleted) {
|
|
2712
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2580
2713
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(serverItem)], "upsert");
|
|
2581
2714
|
}
|
|
2582
2715
|
}
|
|
@@ -2602,7 +2735,7 @@ class ServerUpdateHandler {
|
|
|
2602
2735
|
}
|
|
2603
2736
|
const currentInMemState = { ...localItem, ...pendingChange.data };
|
|
2604
2737
|
const merged = this.mergeLocalWithDelta(currentInMemState, serverDelta);
|
|
2605
|
-
if (!merged._deleted) {
|
|
2738
|
+
if (!merged._deleted && !merged._archived) {
|
|
2606
2739
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
|
|
2607
2740
|
}
|
|
2608
2741
|
return;
|
|
@@ -2614,7 +2747,7 @@ class ServerUpdateHandler {
|
|
|
2614
2747
|
if (metaChanged) {
|
|
2615
2748
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
2616
2749
|
}
|
|
2617
|
-
if (!merged._deleted) {
|
|
2750
|
+
if (!merged._deleted && !merged._archived) {
|
|
2618
2751
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
|
|
2619
2752
|
}
|
|
2620
2753
|
} else {
|
|
@@ -2622,7 +2755,7 @@ class ServerUpdateHandler {
|
|
|
2622
2755
|
return;
|
|
2623
2756
|
const merged = this.mergeLocalWithDelta(localItem, serverDelta);
|
|
2624
2757
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
2625
|
-
if (!merged._deleted) {
|
|
2758
|
+
if (!merged._deleted && !merged._archived) {
|
|
2626
2759
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
|
|
2627
2760
|
} else {
|
|
2628
2761
|
this.deps.writeToInMemBatch(collection, [{ _id: serverDelta._id }], "delete");
|
|
@@ -2885,12 +3018,15 @@ class SyncedDb {
|
|
|
2885
3018
|
initialized = false;
|
|
2886
3019
|
syncing = false;
|
|
2887
3020
|
syncLock = false;
|
|
3021
|
+
wsUpdateQueue = [];
|
|
2888
3022
|
updaterId;
|
|
2889
3023
|
syncedDbInstanceId;
|
|
2890
3024
|
syncMetaCache = new Map;
|
|
2891
3025
|
unsubscribeServerUpdates;
|
|
2892
3026
|
cleanupNotifierCallbacks;
|
|
2893
3027
|
beforeUnloadHandler;
|
|
3028
|
+
defaultReturnDeleted;
|
|
3029
|
+
defaultReturnArchived;
|
|
2894
3030
|
onSync;
|
|
2895
3031
|
onConflictResolved;
|
|
2896
3032
|
onWsNotification;
|
|
@@ -2905,6 +3041,8 @@ class SyncedDb {
|
|
|
2905
3041
|
this.updaterId = Math.random().toString(36).substring(2, 15);
|
|
2906
3042
|
this.syncedDbInstanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2907
3043
|
const windowId = config._testWindowId ?? this.getOrCreateWindowId();
|
|
3044
|
+
this.defaultReturnDeleted = config.returnDeleted ?? false;
|
|
3045
|
+
this.defaultReturnArchived = config.returnArchived ?? false;
|
|
2908
3046
|
this.onSync = config.onSync;
|
|
2909
3047
|
this.onConflictResolved = config.onConflictResolved;
|
|
2910
3048
|
this.onWsNotification = config.onWsNotification;
|
|
@@ -3113,7 +3251,7 @@ class SyncedDb {
|
|
|
3113
3251
|
await this.pendingChanges.recoverPendingWrites();
|
|
3114
3252
|
for (const [name] of this.collections) {
|
|
3115
3253
|
const data = await this.dexieDb.getAll(name);
|
|
3116
|
-
const activeData = data.filter((item) => !item._deleted);
|
|
3254
|
+
const activeData = data.filter((item) => !item._deleted && !item._archived);
|
|
3117
3255
|
this.inMemManager.initCollection(name, activeData);
|
|
3118
3256
|
const meta = await this.dexieDb.getSyncMeta(name);
|
|
3119
3257
|
if (meta) {
|
|
@@ -3134,7 +3272,13 @@ class SyncedDb {
|
|
|
3134
3272
|
if (cleanup)
|
|
3135
3273
|
this.cleanupNotifierCallbacks = cleanup;
|
|
3136
3274
|
}
|
|
3137
|
-
this.unsubscribeServerUpdates = this.serverUpdateNotifier.subscribe((payload) =>
|
|
3275
|
+
this.unsubscribeServerUpdates = this.serverUpdateNotifier.subscribe((payload) => {
|
|
3276
|
+
if (this.syncing) {
|
|
3277
|
+
this.wsUpdateQueue.push(payload);
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
return this.serverUpdateHandler.handleServerUpdate(payload);
|
|
3281
|
+
});
|
|
3138
3282
|
try {
|
|
3139
3283
|
await this.serverUpdateNotifier.connect();
|
|
3140
3284
|
} catch (err) {
|
|
@@ -3198,41 +3342,119 @@ class SyncedDb {
|
|
|
3198
3342
|
async setOnline(online) {
|
|
3199
3343
|
await this.connectionManager.setOnline(online);
|
|
3200
3344
|
}
|
|
3201
|
-
async findById(collection, id) {
|
|
3345
|
+
async findById(collection, id, opts) {
|
|
3202
3346
|
this.assertCollection(collection);
|
|
3347
|
+
opts = this.resolveOpts(opts);
|
|
3348
|
+
if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
|
|
3349
|
+
try {
|
|
3350
|
+
const serverItem = await this.connectionManager.withRestTimeout(this.restInterface.findById(collection, id), "findById");
|
|
3351
|
+
if (serverItem) {
|
|
3352
|
+
await this.dexieDb.saveMany(collection, [serverItem]);
|
|
3353
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
3354
|
+
this.inMemManager.writeBatch(collection, [serverItem], "upsert");
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
} catch {}
|
|
3358
|
+
}
|
|
3203
3359
|
const item = await this.dexieDb.getById(collection, id);
|
|
3204
|
-
if (!item
|
|
3360
|
+
if (!item)
|
|
3205
3361
|
return null;
|
|
3206
|
-
|
|
3362
|
+
if (!opts?.returnDeleted && item._deleted)
|
|
3363
|
+
return null;
|
|
3364
|
+
if (!opts?.returnArchived && item._archived)
|
|
3365
|
+
return null;
|
|
3366
|
+
let result = item;
|
|
3367
|
+
if (opts?.project) {
|
|
3368
|
+
const [projected] = applyQueryOpts([result], { project: opts.project });
|
|
3369
|
+
result = projected;
|
|
3370
|
+
}
|
|
3371
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3372
|
+
this.referToServerSync(collection);
|
|
3373
|
+
}
|
|
3374
|
+
return result;
|
|
3207
3375
|
}
|
|
3208
|
-
async findByIds(collection, ids) {
|
|
3376
|
+
async findByIds(collection, ids, opts) {
|
|
3209
3377
|
this.assertCollection(collection);
|
|
3378
|
+
opts = this.resolveOpts(opts);
|
|
3210
3379
|
if (ids.length === 0)
|
|
3211
3380
|
return [];
|
|
3381
|
+
if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
|
|
3382
|
+
try {
|
|
3383
|
+
const serverItems = await this.connectionManager.withRestTimeout(this.restInterface.findByIds(collection, ids), "findByIds");
|
|
3384
|
+
if (serverItems && serverItems.length > 0) {
|
|
3385
|
+
await this.dexieDb.saveMany(collection, serverItems);
|
|
3386
|
+
const toInMem = serverItems.filter((s) => !s._deleted && !s._archived);
|
|
3387
|
+
if (toInMem.length > 0) {
|
|
3388
|
+
this.inMemManager.writeBatch(collection, toInMem, "upsert");
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
} catch {}
|
|
3392
|
+
}
|
|
3212
3393
|
const items = await this.dexieDb.getByIds(collection, ids);
|
|
3213
3394
|
const results = [];
|
|
3214
3395
|
for (const item of items) {
|
|
3215
|
-
if (
|
|
3216
|
-
|
|
3217
|
-
|
|
3396
|
+
if (!item)
|
|
3397
|
+
continue;
|
|
3398
|
+
if (!opts?.returnDeleted && item._deleted)
|
|
3399
|
+
continue;
|
|
3400
|
+
if (!opts?.returnArchived && item._archived)
|
|
3401
|
+
continue;
|
|
3402
|
+
results.push(item);
|
|
3218
3403
|
}
|
|
3219
|
-
|
|
3404
|
+
const final = applyQueryOpts(results, opts);
|
|
3405
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3406
|
+
this.referToServerSync(collection);
|
|
3407
|
+
}
|
|
3408
|
+
return final;
|
|
3220
3409
|
}
|
|
3221
|
-
async findOne(collection, query) {
|
|
3410
|
+
async findOne(collection, query, opts) {
|
|
3222
3411
|
this.assertCollection(collection);
|
|
3412
|
+
opts = this.resolveOpts(opts);
|
|
3413
|
+
if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
|
|
3414
|
+
await this.syncCollectionForFind(collection, query, opts);
|
|
3415
|
+
}
|
|
3223
3416
|
const all = await this.dexieDb.getAll(collection);
|
|
3224
|
-
const active = all.filter((item) =>
|
|
3417
|
+
const active = all.filter((item) => {
|
|
3418
|
+
if (!opts?.returnDeleted && item._deleted)
|
|
3419
|
+
return false;
|
|
3420
|
+
if (!opts?.returnArchived && item._archived)
|
|
3421
|
+
return false;
|
|
3422
|
+
return true;
|
|
3423
|
+
});
|
|
3225
3424
|
const filtered = filterByQuery(active, query);
|
|
3226
|
-
if (filtered.length === 0)
|
|
3425
|
+
if (filtered.length === 0) {
|
|
3426
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3427
|
+
this.referToServerSync(collection, query);
|
|
3428
|
+
}
|
|
3227
3429
|
return null;
|
|
3228
|
-
|
|
3430
|
+
}
|
|
3431
|
+
const sorted = applyQueryOpts(filtered, { sort: opts?.sort, project: opts?.project });
|
|
3432
|
+
const result = sorted[0];
|
|
3433
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3434
|
+
this.referToServerSync(collection, query);
|
|
3435
|
+
}
|
|
3436
|
+
return result;
|
|
3229
3437
|
}
|
|
3230
|
-
async find(collection, query) {
|
|
3438
|
+
async find(collection, query, opts) {
|
|
3231
3439
|
this.assertCollection(collection);
|
|
3440
|
+
opts = this.resolveOpts(opts);
|
|
3441
|
+
if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
|
|
3442
|
+
await this.syncCollectionForFind(collection, query, opts);
|
|
3443
|
+
}
|
|
3232
3444
|
const all = await this.dexieDb.getAll(collection);
|
|
3233
|
-
const active = all.filter((item) =>
|
|
3445
|
+
const active = all.filter((item) => {
|
|
3446
|
+
if (!opts?.returnDeleted && item._deleted)
|
|
3447
|
+
return false;
|
|
3448
|
+
if (!opts?.returnArchived && item._archived)
|
|
3449
|
+
return false;
|
|
3450
|
+
return true;
|
|
3451
|
+
});
|
|
3234
3452
|
const filtered = query ? filterByQuery(active, query) : active;
|
|
3235
|
-
|
|
3453
|
+
const result = applyQueryOpts(filtered, opts);
|
|
3454
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3455
|
+
this.referToServerSync(collection, query);
|
|
3456
|
+
}
|
|
3457
|
+
return result;
|
|
3236
3458
|
}
|
|
3237
3459
|
async aggregate(collection, pipeline, opts) {
|
|
3238
3460
|
this.assertCollection(collection);
|
|
@@ -3241,6 +3463,67 @@ class SyncedDb {
|
|
|
3241
3463
|
}
|
|
3242
3464
|
return this.connectionManager.withRestTimeout(this.restInterface.aggregate(collection, pipeline, opts), "aggregate");
|
|
3243
3465
|
}
|
|
3466
|
+
async syncCollectionForFind(collection, query, opts) {
|
|
3467
|
+
const meta = this.syncMetaCache.get(collection);
|
|
3468
|
+
const timestamp = meta?.lastSyncTs || 0;
|
|
3469
|
+
try {
|
|
3470
|
+
const serverData = await this.connectionManager.withRestTimeout(this.restInterface.findNewer(collection, timestamp, query, {
|
|
3471
|
+
returnDeleted: opts?.returnDeleted || false,
|
|
3472
|
+
returnArchived: opts?.returnArchived || false
|
|
3473
|
+
}), "syncCollectionForFind");
|
|
3474
|
+
if (serverData.length > 0) {
|
|
3475
|
+
await this.syncEngine.processCollectionServerData(collection, serverData);
|
|
3476
|
+
}
|
|
3477
|
+
} catch {}
|
|
3478
|
+
}
|
|
3479
|
+
referToServerSync(collection, query) {
|
|
3480
|
+
const meta = this.syncMetaCache.get(collection);
|
|
3481
|
+
const timestamp = meta?.lastSyncTs || 0;
|
|
3482
|
+
this.connectionManager.withRestTimeout(this.restInterface.findNewer(collection, timestamp, query, { returnDeleted: true }), "referToServer").then(async (serverData) => {
|
|
3483
|
+
if (serverData.length > 0) {
|
|
3484
|
+
await this.syncEngine.processCollectionServerData(collection, serverData);
|
|
3485
|
+
}
|
|
3486
|
+
}).catch((err) => {
|
|
3487
|
+
console.error(`referToServer sync failed for ${collection}:`, err);
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
async ensureItemsAreLoaded(collection, ids, withDeleted) {
|
|
3491
|
+
this.assertCollection(collection);
|
|
3492
|
+
if (ids.length === 0)
|
|
3493
|
+
return;
|
|
3494
|
+
const localItems = await this.dexieDb.getByIds(collection, ids);
|
|
3495
|
+
const missingIds = [];
|
|
3496
|
+
for (let i = 0;i < ids.length; i++) {
|
|
3497
|
+
if (!localItems[i]) {
|
|
3498
|
+
missingIds.push(ids[i]);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
if (missingIds.length === 0)
|
|
3502
|
+
return;
|
|
3503
|
+
if (!this.isOnline())
|
|
3504
|
+
return;
|
|
3505
|
+
const serverItems = await this.connectionManager.withRestTimeout(this.restInterface.findByIds(collection, missingIds), "ensureItemsAreLoaded");
|
|
3506
|
+
if (!serverItems || serverItems.length === 0)
|
|
3507
|
+
return;
|
|
3508
|
+
const toSaveDexie = [];
|
|
3509
|
+
const toSaveInMem = [];
|
|
3510
|
+
for (const item of serverItems) {
|
|
3511
|
+
if (!item)
|
|
3512
|
+
continue;
|
|
3513
|
+
if (!withDeleted && item._deleted)
|
|
3514
|
+
continue;
|
|
3515
|
+
toSaveDexie.push(item);
|
|
3516
|
+
if (!item._deleted && !item._archived) {
|
|
3517
|
+
toSaveInMem.push(item);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
if (toSaveDexie.length > 0) {
|
|
3521
|
+
await this.dexieDb.saveMany(collection, toSaveDexie);
|
|
3522
|
+
}
|
|
3523
|
+
if (toSaveInMem.length > 0) {
|
|
3524
|
+
this.inMemManager.writeBatch(collection, toSaveInMem, "upsert");
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3244
3527
|
async save(collection, id, update) {
|
|
3245
3528
|
this.assertCollection(collection);
|
|
3246
3529
|
const existing = await this.dexieDb.getById(collection, id);
|
|
@@ -3255,7 +3538,7 @@ class SyncedDb {
|
|
|
3255
3538
|
this.pendingChanges.schedule(collection, id, newData, 0, "save");
|
|
3256
3539
|
const currentMem = this.inMemDb.getById(collection, id);
|
|
3257
3540
|
const merged = { ...currentMem || existing || { _id: id }, ...update };
|
|
3258
|
-
if (!existing?._deleted) {
|
|
3541
|
+
if (!existing?._deleted && !existing?._archived) {
|
|
3259
3542
|
this.inMemManager.writeBatch(collection, [merged], "upsert");
|
|
3260
3543
|
}
|
|
3261
3544
|
return merged;
|
|
@@ -3275,7 +3558,7 @@ class SyncedDb {
|
|
|
3275
3558
|
this.assertCollection(collection);
|
|
3276
3559
|
const id = data._id || new ObjectId2;
|
|
3277
3560
|
const existing = await this.dexieDb.getById(collection, id);
|
|
3278
|
-
if (existing && !existing._deleted) {
|
|
3561
|
+
if (existing && !existing._deleted && !existing._archived) {
|
|
3279
3562
|
console.warn(`SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`);
|
|
3280
3563
|
}
|
|
3281
3564
|
const insertChanges = { ...data, _lastUpdaterId: this.updaterId };
|
|
@@ -3416,6 +3699,16 @@ class SyncedDb {
|
|
|
3416
3699
|
} finally {
|
|
3417
3700
|
this.syncing = false;
|
|
3418
3701
|
this.syncLock = false;
|
|
3702
|
+
await this.processQueuedWsUpdates();
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
async processQueuedWsUpdates() {
|
|
3706
|
+
if (this.wsUpdateQueue.length === 0)
|
|
3707
|
+
return;
|
|
3708
|
+
const queue = this.wsUpdateQueue;
|
|
3709
|
+
this.wsUpdateQueue = [];
|
|
3710
|
+
for (const payload of queue) {
|
|
3711
|
+
await this.serverUpdateHandler.handleServerUpdate(payload);
|
|
3419
3712
|
}
|
|
3420
3713
|
}
|
|
3421
3714
|
isSyncing() {
|
|
@@ -3545,6 +3838,16 @@ class SyncedDb {
|
|
|
3545
3838
|
getOrCreateWindowId() {
|
|
3546
3839
|
return `shared-${this.tenant}`;
|
|
3547
3840
|
}
|
|
3841
|
+
resolveOpts(opts) {
|
|
3842
|
+
if (!this.defaultReturnDeleted && !this.defaultReturnArchived) {
|
|
3843
|
+
return opts;
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
...opts,
|
|
3847
|
+
returnDeleted: opts?.returnDeleted ?? this.defaultReturnDeleted,
|
|
3848
|
+
returnArchived: opts?.returnArchived ?? this.defaultReturnArchived
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3548
3851
|
assertCollection(name) {
|
|
3549
3852
|
if (!this.collections.has(name)) {
|
|
3550
3853
|
throw new Error(`Collection "${name}" not configured`);
|
|
@@ -5906,7 +6209,7 @@ var unpackr = new Unpackr({ structuredClone: true });
|
|
|
5906
6209
|
var pack2 = (x) => packr.pack(preprocessForPack(x));
|
|
5907
6210
|
var unpack2 = (x) => unpackr.unpack(x);
|
|
5908
6211
|
var DEFAULT_TIMEOUT = 5000;
|
|
5909
|
-
var DEFAULT_PROGRESS_CHUNK_SIZE =
|
|
6212
|
+
var DEFAULT_PROGRESS_CHUNK_SIZE = 16 * 1024;
|
|
5910
6213
|
|
|
5911
6214
|
class RestProxy {
|
|
5912
6215
|
endpoint;
|
|
@@ -6450,9 +6753,13 @@ class Ebus2ProxyServerUpdateNotifier {
|
|
|
6450
6753
|
}
|
|
6451
6754
|
}
|
|
6452
6755
|
export {
|
|
6756
|
+
sortItems,
|
|
6453
6757
|
resolveConflict,
|
|
6758
|
+
projectItem,
|
|
6454
6759
|
matchesQuery,
|
|
6455
6760
|
filterByQuery,
|
|
6761
|
+
applySkipLimit,
|
|
6762
|
+
applyQueryOpts,
|
|
6456
6763
|
SyncedDb,
|
|
6457
6764
|
RestProxy,
|
|
6458
6765
|
Ebus2ProxyServerUpdateNotifier,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AggregateOptions } from "mongodb";
|
|
2
2
|
import type { I_SyncedDb, SyncedDbConfig, WsNotificationInfo } from "../types/I_SyncedDb";
|
|
3
3
|
import type { MetaUpdateBroadcast } from "../types/I_DexieDb";
|
|
4
|
-
import type { QuerySpec, UpdateSpec, InsertSpec, BatchSpec } from "../types/I_RestInterface";
|
|
4
|
+
import type { QuerySpec, QueryOpts, UpdateSpec, InsertSpec, BatchSpec } from "../types/I_RestInterface";
|
|
5
5
|
import type { Id, DbEntity } from "../types/DbEntity";
|
|
6
6
|
/**
|
|
7
7
|
* Main synchronized database implementation.
|
|
@@ -26,12 +26,15 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
26
26
|
private initialized;
|
|
27
27
|
private syncing;
|
|
28
28
|
private syncLock;
|
|
29
|
+
private wsUpdateQueue;
|
|
29
30
|
private readonly updaterId;
|
|
30
31
|
private readonly syncedDbInstanceId;
|
|
31
32
|
private syncMetaCache;
|
|
32
33
|
private unsubscribeServerUpdates?;
|
|
33
34
|
private cleanupNotifierCallbacks?;
|
|
34
35
|
private beforeUnloadHandler?;
|
|
36
|
+
private readonly defaultReturnDeleted;
|
|
37
|
+
private readonly defaultReturnArchived;
|
|
35
38
|
private readonly onSync?;
|
|
36
39
|
private readonly onConflictResolved?;
|
|
37
40
|
private readonly onWsNotification?;
|
|
@@ -51,11 +54,22 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
51
54
|
forceOffline(forced: boolean): void;
|
|
52
55
|
isForcedOffline(): boolean;
|
|
53
56
|
setOnline(online: boolean): Promise<void>;
|
|
54
|
-
findById<T extends DbEntity>(collection: string, id: Id): Promise<T | null>;
|
|
55
|
-
findByIds<T extends DbEntity>(collection: string, ids: Id[]): Promise<T[]>;
|
|
56
|
-
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T
|
|
57
|
-
find<T extends DbEntity>(collection: string, query?: QuerySpec<T
|
|
57
|
+
findById<T extends DbEntity>(collection: string, id: Id, opts?: QueryOpts): Promise<T | null>;
|
|
58
|
+
findByIds<T extends DbEntity>(collection: string, ids: Id[], opts?: QueryOpts): Promise<T[]>;
|
|
59
|
+
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T>, opts?: QueryOpts): Promise<T | null>;
|
|
60
|
+
find<T extends DbEntity>(collection: string, query?: QuerySpec<T>, opts?: QueryOpts): Promise<T[]>;
|
|
58
61
|
aggregate<T>(collection: string, pipeline: object[], opts?: AggregateOptions): Promise<T[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Sync a collection from server before querying locally.
|
|
64
|
+
* Used when returnDeleted/returnArchived is specified on find/findOne.
|
|
65
|
+
*/
|
|
66
|
+
private syncCollectionForFind;
|
|
67
|
+
/**
|
|
68
|
+
* Fire-and-forget background sync for a single collection.
|
|
69
|
+
* Calls findNewer with the last sync timestamp and processes results.
|
|
70
|
+
*/
|
|
71
|
+
private referToServerSync;
|
|
72
|
+
ensureItemsAreLoaded(collection: string, ids: string[], withDeleted?: boolean): Promise<void>;
|
|
59
73
|
save<T extends DbEntity>(collection: string, id: Id, update: Partial<T>): Promise<T>;
|
|
60
74
|
upsert<T extends DbEntity>(collection: string, query: QuerySpec<T>, update: UpdateSpec<T>): Promise<T>;
|
|
61
75
|
insert<T extends DbEntity>(collection: string, data: InsertSpec<T>): Promise<T>;
|
|
@@ -65,6 +79,7 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
65
79
|
hardDelete<T extends DbEntity>(collection: string, query: QuerySpec<T>): Promise<number>;
|
|
66
80
|
ping(timeoutMs?: number): Promise<boolean>;
|
|
67
81
|
sync(calledFrom?: string): Promise<void>;
|
|
82
|
+
private processQueuedWsUpdates;
|
|
68
83
|
isSyncing(): boolean;
|
|
69
84
|
upsertBatch<T extends DbEntity>(collection: string, batch: BatchSpec<T>): Promise<T[]>;
|
|
70
85
|
getMemoryCollection<T extends DbEntity>(collection: string): T[];
|
|
@@ -94,5 +109,10 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
94
109
|
* so we use a constant value to ensure all tabs compete for the same lock.
|
|
95
110
|
*/
|
|
96
111
|
private getOrCreateWindowId;
|
|
112
|
+
/**
|
|
113
|
+
* Resolve effective QueryOpts by merging global defaults with per-call opts.
|
|
114
|
+
* Per-call values take precedence over global defaults.
|
|
115
|
+
*/
|
|
116
|
+
private resolveOpts;
|
|
97
117
|
private assertCollection;
|
|
98
118
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Processing incoming data with conflict resolution
|
|
7
7
|
* - Uploading dirty items to server
|
|
8
8
|
*/
|
|
9
|
+
import type { LocalDbEntity } from "../../types/DbEntity";
|
|
9
10
|
import type { I_SyncEngine, SyncEngineConfig } from "../types/managers";
|
|
10
11
|
import type { UploadResult } from "../types/internal";
|
|
11
12
|
export declare class SyncEngine implements I_SyncEngine {
|
|
@@ -30,6 +31,13 @@ export declare class SyncEngine implements I_SyncEngine {
|
|
|
30
31
|
* Upload dirty items for a specific collection.
|
|
31
32
|
*/
|
|
32
33
|
uploadDirtyItemsForCollection(collection: string): Promise<UploadResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Process incoming server data for a single collection.
|
|
36
|
+
* Used by referToServer to process findNewer results.
|
|
37
|
+
*/
|
|
38
|
+
processCollectionServerData(collectionName: string, serverData: LocalDbEntity[]): Promise<{
|
|
39
|
+
updatedIds: string[];
|
|
40
|
+
}>;
|
|
33
41
|
private processIncomingServerData;
|
|
34
42
|
private compareTimestamps;
|
|
35
43
|
private resolveCollectionConflict;
|
|
@@ -27,6 +27,8 @@ export interface SyncResult {
|
|
|
27
27
|
*/
|
|
28
28
|
export interface UploadResult {
|
|
29
29
|
sentCount: number;
|
|
30
|
+
/** Per-collection sent counts (collection name -> count) */
|
|
31
|
+
collectionSentCounts?: Record<string, number>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Result from processing incoming server data.
|
|
@@ -242,6 +242,10 @@ export interface I_SyncEngine {
|
|
|
242
242
|
uploadDirtyItems(calledFrom?: string): Promise<UploadResult>;
|
|
243
243
|
/** Upload dirty items for a specific collection. */
|
|
244
244
|
uploadDirtyItemsForCollection(collection: string): Promise<UploadResult>;
|
|
245
|
+
/** Process incoming server data for a single collection (used by referToServer). */
|
|
246
|
+
processCollectionServerData(collectionName: string, serverData: import("../../types/DbEntity").LocalDbEntity[]): Promise<{
|
|
247
|
+
updatedIds: string[];
|
|
248
|
+
}>;
|
|
245
249
|
}
|
|
246
250
|
export interface ServerUpdateHandlerCallbacks {
|
|
247
251
|
onWsNotification?: (info: WsNotificationInfo) => void;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export type { RestProxyConfig } from "./db/RestProxy";
|
|
|
8
8
|
export { Ebus2ProxyServerUpdateNotifier } from "./db/Ebus2ProxyServerUpdateNotifier";
|
|
9
9
|
export type { Ebus2ProxyServerUpdateNotifierConfig } from "./db/Ebus2ProxyServerUpdateNotifier";
|
|
10
10
|
export { resolveConflict } from "./utils/conflictResolution";
|
|
11
|
-
export { filterByQuery, matchesQuery } from "./utils/localQuery";
|
|
11
|
+
export { filterByQuery, matchesQuery, sortItems, applySkipLimit, projectItem, applyQueryOpts } from "./utils/localQuery";
|
|
@@ -5,7 +5,7 @@ export type CollectionUpdateResult = Types.CollectionUpdateResult;
|
|
|
5
5
|
export type Obj = {
|
|
6
6
|
[key: string]: any;
|
|
7
7
|
};
|
|
8
|
-
export type QuerySpec<T> = Partial<Record<keyof T | "_deleted" | "_blocked" | "_ts" | "_id", any>>;
|
|
8
|
+
export type QuerySpec<T> = Partial<Record<keyof T | "_deleted" | "_archived" | "_blocked" | "_ts" | "_id", any>>;
|
|
9
9
|
export type Projection = SchemaMember<any, Document | number | boolean | any>;
|
|
10
10
|
export type QueryOpts = Partial<{
|
|
11
11
|
project: Projection;
|
|
@@ -16,6 +16,10 @@ export type QueryOpts = Partial<{
|
|
|
16
16
|
readPreference: ReadPreference;
|
|
17
17
|
/** Če je true, vrne tudi soft-deleted objekte (z _deleted poljem) */
|
|
18
18
|
returnDeleted: boolean;
|
|
19
|
+
/** Če je true, vrne tudi archived objekte (z _archived poljem) */
|
|
20
|
+
returnArchived: boolean;
|
|
21
|
+
/** Če je true, vrne lokalne rezultate in nato v ozadju sinhronizira s serverjem */
|
|
22
|
+
referToServer: boolean;
|
|
19
23
|
}>;
|
|
20
24
|
export type KeyOf<T> = keyof T | "$bit" | "$set" | "$inc" | "$currentDate" | "$min" | "$max" | "$mul" | "$rename" | "$setOnInsert" | "$unset" | "$pull" | "$push" | "$pop" | "$addToSet" | "$pushAll" | "_rev" | "_ts" | "_csq" | "_deleted";
|
|
21
25
|
export type InsertKeyOf<T> = keyof T | "_rev" | "_ts" | "_csq" | "_deleted";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AggregateOptions } from "mongodb";
|
|
2
2
|
import type { Id, DbEntity, LocalDbEntity } from "./DbEntity";
|
|
3
|
-
import type { QuerySpec, UpdateSpec, InsertSpec, BatchSpec, I_RestInterface, CollectionUpdateRequest, CollectionUpdateResult, GetNewerSpec } from "./I_RestInterface";
|
|
3
|
+
import type { QuerySpec, QueryOpts, UpdateSpec, InsertSpec, BatchSpec, I_RestInterface, CollectionUpdateRequest, CollectionUpdateResult, GetNewerSpec } from "./I_RestInterface";
|
|
4
4
|
import type { I_DexieDb } from "./I_DexieDb";
|
|
5
5
|
import type { I_InMemDb } from "./I_InMemDb";
|
|
6
6
|
import type { I_ServerUpdateNotifier } from "./I_ServerUpdateNotifier";
|
|
@@ -198,6 +198,17 @@ export interface ConflictResolutionReport {
|
|
|
198
198
|
/** Timestamp when conflict was resolved */
|
|
199
199
|
timestamp: Date;
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Per-collection sync statistics
|
|
203
|
+
*/
|
|
204
|
+
export interface CollectionSyncStats {
|
|
205
|
+
/** Number of items received from server for this collection */
|
|
206
|
+
receivedCount: number;
|
|
207
|
+
/** Number of dirty items sent to server for this collection */
|
|
208
|
+
sentCount: number;
|
|
209
|
+
/** The actual items received from server (for debugging/logging) */
|
|
210
|
+
receivedItems: LocalDbEntity[];
|
|
211
|
+
}
|
|
201
212
|
/**
|
|
202
213
|
* Informacije o sinhronizaciji za debugging/logging
|
|
203
214
|
*/
|
|
@@ -216,6 +227,8 @@ export interface SyncInfo {
|
|
|
216
227
|
error?: Error;
|
|
217
228
|
/** Where sync was called from (for debugging) */
|
|
218
229
|
calledFrom?: string;
|
|
230
|
+
/** Per-collection sync statistics (collection name -> stats) */
|
|
231
|
+
collections?: Record<string, CollectionSyncStats>;
|
|
219
232
|
}
|
|
220
233
|
/**
|
|
221
234
|
* Configuration for collection sync behavior (used in sync() method only, not uploadDirtyItems)
|
|
@@ -338,6 +351,20 @@ export interface SyncedDbConfig {
|
|
|
338
351
|
* whenever objects are written to in-mem. Default: false.
|
|
339
352
|
*/
|
|
340
353
|
useObjectMetadata?: boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Global default for returning soft-deleted items in find operations.
|
|
356
|
+
* When true, all find* methods include deleted items unless overridden per-call.
|
|
357
|
+
* Per-call opts.returnDeleted takes precedence over this setting.
|
|
358
|
+
* Default: false
|
|
359
|
+
*/
|
|
360
|
+
returnDeleted?: boolean;
|
|
361
|
+
/**
|
|
362
|
+
* Global default for returning archived items in find operations.
|
|
363
|
+
* When true, all find* methods include archived items unless overridden per-call.
|
|
364
|
+
* Per-call opts.returnArchived takes precedence over this setting.
|
|
365
|
+
* Default: false
|
|
366
|
+
*/
|
|
367
|
+
returnArchived?: boolean;
|
|
341
368
|
/**
|
|
342
369
|
* Enable sync on wake from sleep for leader tab.
|
|
343
370
|
* When enabled, detects wake via pageshow/focus/visibilitychange events
|
|
@@ -423,13 +450,18 @@ export interface I_SyncedDb {
|
|
|
423
450
|
*/
|
|
424
451
|
isForcedOffline(): boolean;
|
|
425
452
|
/** Poišče objekt po ID-ju */
|
|
426
|
-
findById<T extends DbEntity>(collection: string, id: Id): Promise<T | null>;
|
|
453
|
+
findById<T extends DbEntity>(collection: string, id: Id, opts?: QueryOpts): Promise<T | null>;
|
|
427
454
|
/** Poišče objekte po ID-jih */
|
|
428
|
-
findByIds<T extends DbEntity>(collection: string, ids: Id[]): Promise<T[]>;
|
|
455
|
+
findByIds<T extends DbEntity>(collection: string, ids: Id[], opts?: QueryOpts): Promise<T[]>;
|
|
429
456
|
/** Poišče prvi objekt, ki ustreza poizvedbi */
|
|
430
|
-
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T
|
|
457
|
+
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T>, opts?: QueryOpts): Promise<T | null>;
|
|
431
458
|
/** Poišče vse objekte, ki ustrezajo poizvedbi */
|
|
432
|
-
find<T extends DbEntity>(collection: string, query?: QuerySpec<T
|
|
459
|
+
find<T extends DbEntity>(collection: string, query?: QuerySpec<T>, opts?: QueryOpts): Promise<T[]>;
|
|
460
|
+
/**
|
|
461
|
+
* Preveri prisotnost podanih ID-jev v lokalni bazi.
|
|
462
|
+
* Manjkajoče naloži s serverja (če je online).
|
|
463
|
+
*/
|
|
464
|
+
ensureItemsAreLoaded(collection: string, ids: string[], withDeleted?: boolean): Promise<void>;
|
|
433
465
|
/** Izvede agregacijo na serverju (offline vrne []) */
|
|
434
466
|
aggregate<T>(collection: string, pipeline: object[], opts?: AggregateOptions): Promise<T[]>;
|
|
435
467
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuerySpec } from "../types/I_RestInterface";
|
|
1
|
+
import type { QuerySpec, QueryOpts, Projection } from "../types/I_RestInterface";
|
|
2
2
|
import type { DbEntity } from "../types/DbEntity";
|
|
3
3
|
/**
|
|
4
4
|
* Preveri, ali objekt ustreza MongoDB-style query specifikaciji
|
|
@@ -9,3 +9,22 @@ export declare function matchesQuery<T extends DbEntity>(item: T, query: QuerySp
|
|
|
9
9
|
* Filtriraj array objektov po query
|
|
10
10
|
*/
|
|
11
11
|
export declare function filterByQuery<T extends DbEntity>(items: T[], query?: QuerySpec<T>): T[];
|
|
12
|
+
/**
|
|
13
|
+
* Sortiraj array objektov po MongoDB-style sort specifikaciji.
|
|
14
|
+
* Podpira multi-field sort: { name: 1, age: -1 }
|
|
15
|
+
*/
|
|
16
|
+
export declare function sortItems<T>(items: T[], sort: Record<string, -1 | 1>): T[];
|
|
17
|
+
/**
|
|
18
|
+
* Uporabi skip in limit na array.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applySkipLimit<T>(items: T[], skip?: number, limit?: number): T[];
|
|
21
|
+
/**
|
|
22
|
+
* Uporabi projekcijo na en objekt.
|
|
23
|
+
* Podpira include mode ({ field: true }) in exclude mode ({ field: false }).
|
|
24
|
+
* _id je vedno vključen, razen če je eksplicitno izključen.
|
|
25
|
+
*/
|
|
26
|
+
export declare function projectItem<T>(item: T, project: Projection): Partial<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Uporabi QueryOpts na array: sort → skip → limit → project
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyQueryOpts<T>(items: T[], opts?: QueryOpts): T[];
|