cry-synced-db-client 0.1.69 → 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 +291 -27
- package/dist/src/db/SyncedDb.d.ts +23 -5
- package/dist/src/db/sync/SyncEngine.d.ts +8 -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 +24 -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 {
|
|
@@ -2313,6 +2410,16 @@ class SyncEngine {
|
|
|
2313
2410
|
}
|
|
2314
2411
|
return { sentCount };
|
|
2315
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
|
+
}
|
|
2316
2423
|
async processIncomingServerData(collectionName, config, serverData) {
|
|
2317
2424
|
if (serverData.length === 0) {
|
|
2318
2425
|
return { conflictsResolved: 0, maxTs: undefined, updatedIds: [] };
|
|
@@ -2339,14 +2446,14 @@ class SyncEngine {
|
|
|
2339
2446
|
conflictsResolved++;
|
|
2340
2447
|
const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
|
|
2341
2448
|
dexieBatch.push(resolved);
|
|
2342
|
-
if (!resolved._deleted) {
|
|
2449
|
+
if (!resolved._deleted && !resolved._archived) {
|
|
2343
2450
|
inMemSaveBatch.push(resolved);
|
|
2344
2451
|
} else {
|
|
2345
2452
|
inMemDeleteIds.push(serverItem._id);
|
|
2346
2453
|
}
|
|
2347
2454
|
} else {
|
|
2348
2455
|
dexieBatch.push(serverItem);
|
|
2349
|
-
if (!serverItem._deleted) {
|
|
2456
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2350
2457
|
inMemSaveBatch.push(serverItem);
|
|
2351
2458
|
} else {
|
|
2352
2459
|
inMemDeleteIds.push(serverItem._id);
|
|
@@ -2354,7 +2461,7 @@ class SyncEngine {
|
|
|
2354
2461
|
}
|
|
2355
2462
|
} else {
|
|
2356
2463
|
dexieBatch.push(serverItem);
|
|
2357
|
-
if (!serverItem._deleted) {
|
|
2464
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2358
2465
|
inMemSaveBatch.push(serverItem);
|
|
2359
2466
|
}
|
|
2360
2467
|
}
|
|
@@ -2597,12 +2704,12 @@ class ServerUpdateHandler {
|
|
|
2597
2704
|
if (dirtyChange && !metaChanged) {
|
|
2598
2705
|
await this.dexieDb.clearDirtyChange(collection, serverItem._id);
|
|
2599
2706
|
}
|
|
2600
|
-
if (!serverItem._deleted) {
|
|
2707
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2601
2708
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(serverItem)], "upsert");
|
|
2602
2709
|
}
|
|
2603
2710
|
} else {
|
|
2604
2711
|
await this.dexieDb.insert(collection, serverItem);
|
|
2605
|
-
if (!serverItem._deleted) {
|
|
2712
|
+
if (!serverItem._deleted && !serverItem._archived) {
|
|
2606
2713
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(serverItem)], "upsert");
|
|
2607
2714
|
}
|
|
2608
2715
|
}
|
|
@@ -2628,7 +2735,7 @@ class ServerUpdateHandler {
|
|
|
2628
2735
|
}
|
|
2629
2736
|
const currentInMemState = { ...localItem, ...pendingChange.data };
|
|
2630
2737
|
const merged = this.mergeLocalWithDelta(currentInMemState, serverDelta);
|
|
2631
|
-
if (!merged._deleted) {
|
|
2738
|
+
if (!merged._deleted && !merged._archived) {
|
|
2632
2739
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
|
|
2633
2740
|
}
|
|
2634
2741
|
return;
|
|
@@ -2640,7 +2747,7 @@ class ServerUpdateHandler {
|
|
|
2640
2747
|
if (metaChanged) {
|
|
2641
2748
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
2642
2749
|
}
|
|
2643
|
-
if (!merged._deleted) {
|
|
2750
|
+
if (!merged._deleted && !merged._archived) {
|
|
2644
2751
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
|
|
2645
2752
|
}
|
|
2646
2753
|
} else {
|
|
@@ -2648,7 +2755,7 @@ class ServerUpdateHandler {
|
|
|
2648
2755
|
return;
|
|
2649
2756
|
const merged = this.mergeLocalWithDelta(localItem, serverDelta);
|
|
2650
2757
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
2651
|
-
if (!merged._deleted) {
|
|
2758
|
+
if (!merged._deleted && !merged._archived) {
|
|
2652
2759
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
|
|
2653
2760
|
} else {
|
|
2654
2761
|
this.deps.writeToInMemBatch(collection, [{ _id: serverDelta._id }], "delete");
|
|
@@ -2918,6 +3025,8 @@ class SyncedDb {
|
|
|
2918
3025
|
unsubscribeServerUpdates;
|
|
2919
3026
|
cleanupNotifierCallbacks;
|
|
2920
3027
|
beforeUnloadHandler;
|
|
3028
|
+
defaultReturnDeleted;
|
|
3029
|
+
defaultReturnArchived;
|
|
2921
3030
|
onSync;
|
|
2922
3031
|
onConflictResolved;
|
|
2923
3032
|
onWsNotification;
|
|
@@ -2932,6 +3041,8 @@ class SyncedDb {
|
|
|
2932
3041
|
this.updaterId = Math.random().toString(36).substring(2, 15);
|
|
2933
3042
|
this.syncedDbInstanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2934
3043
|
const windowId = config._testWindowId ?? this.getOrCreateWindowId();
|
|
3044
|
+
this.defaultReturnDeleted = config.returnDeleted ?? false;
|
|
3045
|
+
this.defaultReturnArchived = config.returnArchived ?? false;
|
|
2935
3046
|
this.onSync = config.onSync;
|
|
2936
3047
|
this.onConflictResolved = config.onConflictResolved;
|
|
2937
3048
|
this.onWsNotification = config.onWsNotification;
|
|
@@ -3140,7 +3251,7 @@ class SyncedDb {
|
|
|
3140
3251
|
await this.pendingChanges.recoverPendingWrites();
|
|
3141
3252
|
for (const [name] of this.collections) {
|
|
3142
3253
|
const data = await this.dexieDb.getAll(name);
|
|
3143
|
-
const activeData = data.filter((item) => !item._deleted);
|
|
3254
|
+
const activeData = data.filter((item) => !item._deleted && !item._archived);
|
|
3144
3255
|
this.inMemManager.initCollection(name, activeData);
|
|
3145
3256
|
const meta = await this.dexieDb.getSyncMeta(name);
|
|
3146
3257
|
if (meta) {
|
|
@@ -3231,41 +3342,119 @@ class SyncedDb {
|
|
|
3231
3342
|
async setOnline(online) {
|
|
3232
3343
|
await this.connectionManager.setOnline(online);
|
|
3233
3344
|
}
|
|
3234
|
-
async findById(collection, id) {
|
|
3345
|
+
async findById(collection, id, opts) {
|
|
3235
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
|
+
}
|
|
3236
3359
|
const item = await this.dexieDb.getById(collection, id);
|
|
3237
|
-
if (!item
|
|
3360
|
+
if (!item)
|
|
3238
3361
|
return null;
|
|
3239
|
-
|
|
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;
|
|
3240
3375
|
}
|
|
3241
|
-
async findByIds(collection, ids) {
|
|
3376
|
+
async findByIds(collection, ids, opts) {
|
|
3242
3377
|
this.assertCollection(collection);
|
|
3378
|
+
opts = this.resolveOpts(opts);
|
|
3243
3379
|
if (ids.length === 0)
|
|
3244
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
|
+
}
|
|
3245
3393
|
const items = await this.dexieDb.getByIds(collection, ids);
|
|
3246
3394
|
const results = [];
|
|
3247
3395
|
for (const item of items) {
|
|
3248
|
-
if (
|
|
3249
|
-
|
|
3250
|
-
|
|
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);
|
|
3403
|
+
}
|
|
3404
|
+
const final = applyQueryOpts(results, opts);
|
|
3405
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3406
|
+
this.referToServerSync(collection);
|
|
3251
3407
|
}
|
|
3252
|
-
return
|
|
3408
|
+
return final;
|
|
3253
3409
|
}
|
|
3254
|
-
async findOne(collection, query) {
|
|
3410
|
+
async findOne(collection, query, opts) {
|
|
3255
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
|
+
}
|
|
3256
3416
|
const all = await this.dexieDb.getAll(collection);
|
|
3257
|
-
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
|
+
});
|
|
3258
3424
|
const filtered = filterByQuery(active, query);
|
|
3259
|
-
if (filtered.length === 0)
|
|
3425
|
+
if (filtered.length === 0) {
|
|
3426
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3427
|
+
this.referToServerSync(collection, query);
|
|
3428
|
+
}
|
|
3260
3429
|
return null;
|
|
3261
|
-
|
|
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;
|
|
3262
3437
|
}
|
|
3263
|
-
async find(collection, query) {
|
|
3438
|
+
async find(collection, query, opts) {
|
|
3264
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
|
+
}
|
|
3265
3444
|
const all = await this.dexieDb.getAll(collection);
|
|
3266
|
-
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
|
+
});
|
|
3267
3452
|
const filtered = query ? filterByQuery(active, query) : active;
|
|
3268
|
-
|
|
3453
|
+
const result = applyQueryOpts(filtered, opts);
|
|
3454
|
+
if (opts?.referToServer && this.isOnline()) {
|
|
3455
|
+
this.referToServerSync(collection, query);
|
|
3456
|
+
}
|
|
3457
|
+
return result;
|
|
3269
3458
|
}
|
|
3270
3459
|
async aggregate(collection, pipeline, opts) {
|
|
3271
3460
|
this.assertCollection(collection);
|
|
@@ -3274,6 +3463,67 @@ class SyncedDb {
|
|
|
3274
3463
|
}
|
|
3275
3464
|
return this.connectionManager.withRestTimeout(this.restInterface.aggregate(collection, pipeline, opts), "aggregate");
|
|
3276
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
|
+
}
|
|
3277
3527
|
async save(collection, id, update) {
|
|
3278
3528
|
this.assertCollection(collection);
|
|
3279
3529
|
const existing = await this.dexieDb.getById(collection, id);
|
|
@@ -3288,7 +3538,7 @@ class SyncedDb {
|
|
|
3288
3538
|
this.pendingChanges.schedule(collection, id, newData, 0, "save");
|
|
3289
3539
|
const currentMem = this.inMemDb.getById(collection, id);
|
|
3290
3540
|
const merged = { ...currentMem || existing || { _id: id }, ...update };
|
|
3291
|
-
if (!existing?._deleted) {
|
|
3541
|
+
if (!existing?._deleted && !existing?._archived) {
|
|
3292
3542
|
this.inMemManager.writeBatch(collection, [merged], "upsert");
|
|
3293
3543
|
}
|
|
3294
3544
|
return merged;
|
|
@@ -3308,7 +3558,7 @@ class SyncedDb {
|
|
|
3308
3558
|
this.assertCollection(collection);
|
|
3309
3559
|
const id = data._id || new ObjectId2;
|
|
3310
3560
|
const existing = await this.dexieDb.getById(collection, id);
|
|
3311
|
-
if (existing && !existing._deleted) {
|
|
3561
|
+
if (existing && !existing._deleted && !existing._archived) {
|
|
3312
3562
|
console.warn(`SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`);
|
|
3313
3563
|
}
|
|
3314
3564
|
const insertChanges = { ...data, _lastUpdaterId: this.updaterId };
|
|
@@ -3588,6 +3838,16 @@ class SyncedDb {
|
|
|
3588
3838
|
getOrCreateWindowId() {
|
|
3589
3839
|
return `shared-${this.tenant}`;
|
|
3590
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
|
+
}
|
|
3591
3851
|
assertCollection(name) {
|
|
3592
3852
|
if (!this.collections.has(name)) {
|
|
3593
3853
|
throw new Error(`Collection "${name}" not configured`);
|
|
@@ -5949,7 +6209,7 @@ var unpackr = new Unpackr({ structuredClone: true });
|
|
|
5949
6209
|
var pack2 = (x) => packr.pack(preprocessForPack(x));
|
|
5950
6210
|
var unpack2 = (x) => unpackr.unpack(x);
|
|
5951
6211
|
var DEFAULT_TIMEOUT = 5000;
|
|
5952
|
-
var DEFAULT_PROGRESS_CHUNK_SIZE =
|
|
6212
|
+
var DEFAULT_PROGRESS_CHUNK_SIZE = 16 * 1024;
|
|
5953
6213
|
|
|
5954
6214
|
class RestProxy {
|
|
5955
6215
|
endpoint;
|
|
@@ -6493,9 +6753,13 @@ class Ebus2ProxyServerUpdateNotifier {
|
|
|
6493
6753
|
}
|
|
6494
6754
|
}
|
|
6495
6755
|
export {
|
|
6756
|
+
sortItems,
|
|
6496
6757
|
resolveConflict,
|
|
6758
|
+
projectItem,
|
|
6497
6759
|
matchesQuery,
|
|
6498
6760
|
filterByQuery,
|
|
6761
|
+
applySkipLimit,
|
|
6762
|
+
applyQueryOpts,
|
|
6499
6763
|
SyncedDb,
|
|
6500
6764
|
RestProxy,
|
|
6501
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.
|
|
@@ -33,6 +33,8 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
33
33
|
private unsubscribeServerUpdates?;
|
|
34
34
|
private cleanupNotifierCallbacks?;
|
|
35
35
|
private beforeUnloadHandler?;
|
|
36
|
+
private readonly defaultReturnDeleted;
|
|
37
|
+
private readonly defaultReturnArchived;
|
|
36
38
|
private readonly onSync?;
|
|
37
39
|
private readonly onConflictResolved?;
|
|
38
40
|
private readonly onWsNotification?;
|
|
@@ -52,11 +54,22 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
52
54
|
forceOffline(forced: boolean): void;
|
|
53
55
|
isForcedOffline(): boolean;
|
|
54
56
|
setOnline(online: boolean): Promise<void>;
|
|
55
|
-
findById<T extends DbEntity>(collection: string, id: Id): Promise<T | null>;
|
|
56
|
-
findByIds<T extends DbEntity>(collection: string, ids: Id[]): Promise<T[]>;
|
|
57
|
-
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T
|
|
58
|
-
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[]>;
|
|
59
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>;
|
|
60
73
|
save<T extends DbEntity>(collection: string, id: Id, update: Partial<T>): Promise<T>;
|
|
61
74
|
upsert<T extends DbEntity>(collection: string, query: QuerySpec<T>, update: UpdateSpec<T>): Promise<T>;
|
|
62
75
|
insert<T extends DbEntity>(collection: string, data: InsertSpec<T>): Promise<T>;
|
|
@@ -96,5 +109,10 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
96
109
|
* so we use a constant value to ensure all tabs compete for the same lock.
|
|
97
110
|
*/
|
|
98
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;
|
|
99
117
|
private assertCollection;
|
|
100
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;
|
|
@@ -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";
|
|
@@ -351,6 +351,20 @@ export interface SyncedDbConfig {
|
|
|
351
351
|
* whenever objects are written to in-mem. Default: false.
|
|
352
352
|
*/
|
|
353
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;
|
|
354
368
|
/**
|
|
355
369
|
* Enable sync on wake from sleep for leader tab.
|
|
356
370
|
* When enabled, detects wake via pageshow/focus/visibilitychange events
|
|
@@ -436,13 +450,18 @@ export interface I_SyncedDb {
|
|
|
436
450
|
*/
|
|
437
451
|
isForcedOffline(): boolean;
|
|
438
452
|
/** Poišče objekt po ID-ju */
|
|
439
|
-
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>;
|
|
440
454
|
/** Poišče objekte po ID-jih */
|
|
441
|
-
findByIds<T extends DbEntity>(collection: string, ids: Id[]): Promise<T[]>;
|
|
455
|
+
findByIds<T extends DbEntity>(collection: string, ids: Id[], opts?: QueryOpts): Promise<T[]>;
|
|
442
456
|
/** Poišče prvi objekt, ki ustreza poizvedbi */
|
|
443
|
-
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T
|
|
457
|
+
findOne<T extends DbEntity>(collection: string, query: QuerySpec<T>, opts?: QueryOpts): Promise<T | null>;
|
|
444
458
|
/** Poišče vse objekte, ki ustrezajo poizvedbi */
|
|
445
|
-
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>;
|
|
446
465
|
/** Izvede agregacijo na serverju (offline vrne []) */
|
|
447
466
|
aggregate<T>(collection: string, pipeline: object[], opts?: AggregateOptions): Promise<T[]>;
|
|
448
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[];
|