cry-synced-db-client 0.1.69 → 0.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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 {
@@ -2069,11 +2166,12 @@ class SyncEngine {
2069
2166
  const allUpdatedIds = {};
2070
2167
  for (const [collectionName, config] of configMap) {
2071
2168
  const serverData = allServerData[collectionName] || [];
2169
+ delete allServerData[collectionName];
2072
2170
  receivedCount += serverData.length;
2073
2171
  collectionStats[collectionName] = {
2074
2172
  receivedCount: serverData.length,
2075
2173
  sentCount: 0,
2076
- receivedItems: serverData
2174
+ receivedItems: []
2077
2175
  };
2078
2176
  const stats = await this.processIncomingServerData(collectionName, config, serverData);
2079
2177
  conflictsResolved += stats.conflictsResolved;
@@ -2313,60 +2411,79 @@ class SyncEngine {
2313
2411
  }
2314
2412
  return { sentCount };
2315
2413
  }
2414
+ async processCollectionServerData(collectionName, serverData) {
2415
+ const config = this.collections.get(collectionName);
2416
+ if (!config)
2417
+ return { updatedIds: [] };
2418
+ const result = await this.processIncomingServerData(collectionName, config, serverData);
2419
+ if (result.updatedIds.length > 0) {
2420
+ this.deps.broadcastUpdates({ [collectionName]: result.updatedIds });
2421
+ }
2422
+ return { updatedIds: result.updatedIds };
2423
+ }
2424
+ static SYNC_BATCH_SIZE = 200;
2316
2425
  async processIncomingServerData(collectionName, config, serverData) {
2317
2426
  if (serverData.length === 0) {
2318
2427
  return { conflictsResolved: 0, maxTs: undefined, updatedIds: [] };
2319
2428
  }
2320
2429
  let maxTs;
2321
2430
  let conflictsResolved = 0;
2322
- const serverIds = serverData.map((item) => item._id);
2323
- const localItems = await this.dexieDb.getByIds(collectionName, serverIds);
2324
- const dirtyChangesMap = await this.dexieDb.getDirtyChangesBatch(collectionName, serverIds);
2325
- const dexieBatch = [];
2326
- const inMemSaveBatch = [];
2327
- const inMemDeleteIds = [];
2328
- for (let i = 0;i < serverData.length; i++) {
2329
- const serverItem = serverData[i];
2330
- const localItem = localItems[i];
2331
- const dirtyChange = dirtyChangesMap.get(String(serverItem._id));
2332
- if (serverItem._ts) {
2333
- if (!maxTs || this.compareTimestamps(serverItem._ts, maxTs) > 0) {
2334
- maxTs = serverItem._ts;
2431
+ const allUpdatedIds = [];
2432
+ const BATCH = SyncEngine.SYNC_BATCH_SIZE;
2433
+ for (let offset = 0;offset < serverData.length; offset += BATCH) {
2434
+ const chunk = serverData.slice(offset, offset + BATCH);
2435
+ const chunkIds = chunk.map((item) => item._id);
2436
+ const localItems = await this.dexieDb.getByIds(collectionName, chunkIds);
2437
+ const dirtyChangesMap = await this.dexieDb.getDirtyChangesBatch(collectionName, chunkIds);
2438
+ const dexieBatch = [];
2439
+ const inMemSaveBatch = [];
2440
+ const inMemDeleteIds = [];
2441
+ for (let i = 0;i < chunk.length; i++) {
2442
+ const serverItem = chunk[i];
2443
+ const localItem = localItems[i];
2444
+ const dirtyChange = dirtyChangesMap.get(String(serverItem._id));
2445
+ if (serverItem._ts) {
2446
+ if (!maxTs || this.compareTimestamps(serverItem._ts, maxTs) > 0) {
2447
+ maxTs = serverItem._ts;
2448
+ }
2335
2449
  }
2336
- }
2337
- if (localItem) {
2338
- if (dirtyChange) {
2339
- conflictsResolved++;
2340
- const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
2341
- dexieBatch.push(resolved);
2342
- if (!resolved._deleted) {
2343
- inMemSaveBatch.push(resolved);
2450
+ if (localItem) {
2451
+ if (dirtyChange) {
2452
+ conflictsResolved++;
2453
+ const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
2454
+ dexieBatch.push(resolved);
2455
+ if (!resolved._deleted && !resolved._archived) {
2456
+ inMemSaveBatch.push(resolved);
2457
+ } else {
2458
+ inMemDeleteIds.push(serverItem._id);
2459
+ }
2344
2460
  } else {
2345
- inMemDeleteIds.push(serverItem._id);
2461
+ dexieBatch.push(serverItem);
2462
+ if (!serverItem._deleted && !serverItem._archived) {
2463
+ inMemSaveBatch.push(serverItem);
2464
+ } else {
2465
+ inMemDeleteIds.push(serverItem._id);
2466
+ }
2346
2467
  }
2347
2468
  } else {
2348
2469
  dexieBatch.push(serverItem);
2349
- if (!serverItem._deleted) {
2470
+ if (!serverItem._deleted && !serverItem._archived) {
2350
2471
  inMemSaveBatch.push(serverItem);
2351
- } else {
2352
- inMemDeleteIds.push(serverItem._id);
2353
2472
  }
2354
2473
  }
2355
- } else {
2356
- dexieBatch.push(serverItem);
2357
- if (!serverItem._deleted) {
2358
- inMemSaveBatch.push(serverItem);
2359
- }
2360
2474
  }
2361
- }
2362
- if (dexieBatch.length > 0) {
2363
- await this.dexieDb.saveMany(collectionName, dexieBatch);
2364
- }
2365
- if (inMemSaveBatch.length > 0) {
2366
- this.deps.writeToInMemBatch(collectionName, inMemSaveBatch, "upsert");
2367
- }
2368
- if (inMemDeleteIds.length > 0) {
2369
- this.deps.writeToInMemBatch(collectionName, inMemDeleteIds.map((id) => ({ _id: id })), "delete");
2475
+ if (dexieBatch.length > 0) {
2476
+ await this.dexieDb.saveMany(collectionName, dexieBatch);
2477
+ }
2478
+ if (inMemSaveBatch.length > 0) {
2479
+ this.deps.writeToInMemBatch(collectionName, inMemSaveBatch, "upsert");
2480
+ }
2481
+ if (inMemDeleteIds.length > 0) {
2482
+ this.deps.writeToInMemBatch(collectionName, inMemDeleteIds.map((id) => ({ _id: id })), "delete");
2483
+ }
2484
+ for (const id of chunkIds) {
2485
+ allUpdatedIds.push(String(id));
2486
+ }
2370
2487
  }
2371
2488
  if (maxTs) {
2372
2489
  await this.dexieDb.setSyncMeta(collectionName, maxTs);
@@ -2376,8 +2493,7 @@ class SyncEngine {
2376
2493
  lastSyncTs: maxTs
2377
2494
  });
2378
2495
  }
2379
- const updatedIds = serverIds.map((id) => String(id));
2380
- return { conflictsResolved, maxTs, updatedIds };
2496
+ return { conflictsResolved, maxTs, updatedIds: allUpdatedIds };
2381
2497
  }
2382
2498
  compareTimestamps(a, b) {
2383
2499
  const aT = typeof a === "object" && "t" in a ? a.t : 0;
@@ -2597,12 +2713,12 @@ class ServerUpdateHandler {
2597
2713
  if (dirtyChange && !metaChanged) {
2598
2714
  await this.dexieDb.clearDirtyChange(collection, serverItem._id);
2599
2715
  }
2600
- if (!serverItem._deleted) {
2716
+ if (!serverItem._deleted && !serverItem._archived) {
2601
2717
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(serverItem)], "upsert");
2602
2718
  }
2603
2719
  } else {
2604
2720
  await this.dexieDb.insert(collection, serverItem);
2605
- if (!serverItem._deleted) {
2721
+ if (!serverItem._deleted && !serverItem._archived) {
2606
2722
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(serverItem)], "upsert");
2607
2723
  }
2608
2724
  }
@@ -2628,7 +2744,7 @@ class ServerUpdateHandler {
2628
2744
  }
2629
2745
  const currentInMemState = { ...localItem, ...pendingChange.data };
2630
2746
  const merged = this.mergeLocalWithDelta(currentInMemState, serverDelta);
2631
- if (!merged._deleted) {
2747
+ if (!merged._deleted && !merged._archived) {
2632
2748
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
2633
2749
  }
2634
2750
  return;
@@ -2640,7 +2756,7 @@ class ServerUpdateHandler {
2640
2756
  if (metaChanged) {
2641
2757
  await this.dexieDb.save(collection, serverDelta._id, merged);
2642
2758
  }
2643
- if (!merged._deleted) {
2759
+ if (!merged._deleted && !merged._archived) {
2644
2760
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
2645
2761
  }
2646
2762
  } else {
@@ -2648,7 +2764,7 @@ class ServerUpdateHandler {
2648
2764
  return;
2649
2765
  const merged = this.mergeLocalWithDelta(localItem, serverDelta);
2650
2766
  await this.dexieDb.save(collection, serverDelta._id, merged);
2651
- if (!merged._deleted) {
2767
+ if (!merged._deleted && !merged._archived) {
2652
2768
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert");
2653
2769
  } else {
2654
2770
  this.deps.writeToInMemBatch(collection, [{ _id: serverDelta._id }], "delete");
@@ -2918,6 +3034,8 @@ class SyncedDb {
2918
3034
  unsubscribeServerUpdates;
2919
3035
  cleanupNotifierCallbacks;
2920
3036
  beforeUnloadHandler;
3037
+ defaultReturnDeleted;
3038
+ defaultReturnArchived;
2921
3039
  onSync;
2922
3040
  onConflictResolved;
2923
3041
  onWsNotification;
@@ -2932,6 +3050,8 @@ class SyncedDb {
2932
3050
  this.updaterId = Math.random().toString(36).substring(2, 15);
2933
3051
  this.syncedDbInstanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2934
3052
  const windowId = config._testWindowId ?? this.getOrCreateWindowId();
3053
+ this.defaultReturnDeleted = config.returnDeleted ?? false;
3054
+ this.defaultReturnArchived = config.returnArchived ?? false;
2935
3055
  this.onSync = config.onSync;
2936
3056
  this.onConflictResolved = config.onConflictResolved;
2937
3057
  this.onWsNotification = config.onWsNotification;
@@ -3140,8 +3260,15 @@ class SyncedDb {
3140
3260
  await this.pendingChanges.recoverPendingWrites();
3141
3261
  for (const [name] of this.collections) {
3142
3262
  const data = await this.dexieDb.getAll(name);
3143
- const activeData = data.filter((item) => !item._deleted);
3144
- this.inMemManager.initCollection(name, activeData);
3263
+ let writeIdx = 0;
3264
+ for (let i = 0;i < data.length; i++) {
3265
+ const item = data[i];
3266
+ if (!item._deleted && !item._archived) {
3267
+ data[writeIdx++] = item;
3268
+ }
3269
+ }
3270
+ data.length = writeIdx;
3271
+ this.inMemManager.initCollection(name, data);
3145
3272
  const meta = await this.dexieDb.getSyncMeta(name);
3146
3273
  if (meta) {
3147
3274
  this.syncMetaCache.set(name, meta);
@@ -3231,41 +3358,119 @@ class SyncedDb {
3231
3358
  async setOnline(online) {
3232
3359
  await this.connectionManager.setOnline(online);
3233
3360
  }
3234
- async findById(collection, id) {
3361
+ async findById(collection, id, opts) {
3235
3362
  this.assertCollection(collection);
3363
+ opts = this.resolveOpts(opts);
3364
+ if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
3365
+ try {
3366
+ const serverItem = await this.connectionManager.withRestTimeout(this.restInterface.findById(collection, id), "findById");
3367
+ if (serverItem) {
3368
+ await this.dexieDb.saveMany(collection, [serverItem]);
3369
+ if (!serverItem._deleted && !serverItem._archived) {
3370
+ this.inMemManager.writeBatch(collection, [serverItem], "upsert");
3371
+ }
3372
+ }
3373
+ } catch {}
3374
+ }
3236
3375
  const item = await this.dexieDb.getById(collection, id);
3237
- if (!item || item._deleted)
3376
+ if (!item)
3238
3377
  return null;
3239
- return item;
3378
+ if (!opts?.returnDeleted && item._deleted)
3379
+ return null;
3380
+ if (!opts?.returnArchived && item._archived)
3381
+ return null;
3382
+ let result = item;
3383
+ if (opts?.project) {
3384
+ const [projected] = applyQueryOpts([result], { project: opts.project });
3385
+ result = projected;
3386
+ }
3387
+ if (opts?.referToServer && this.isOnline()) {
3388
+ this.referToServerSync(collection);
3389
+ }
3390
+ return result;
3240
3391
  }
3241
- async findByIds(collection, ids) {
3392
+ async findByIds(collection, ids, opts) {
3242
3393
  this.assertCollection(collection);
3394
+ opts = this.resolveOpts(opts);
3243
3395
  if (ids.length === 0)
3244
3396
  return [];
3397
+ if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
3398
+ try {
3399
+ const serverItems = await this.connectionManager.withRestTimeout(this.restInterface.findByIds(collection, ids), "findByIds");
3400
+ if (serverItems && serverItems.length > 0) {
3401
+ await this.dexieDb.saveMany(collection, serverItems);
3402
+ const toInMem = serverItems.filter((s) => !s._deleted && !s._archived);
3403
+ if (toInMem.length > 0) {
3404
+ this.inMemManager.writeBatch(collection, toInMem, "upsert");
3405
+ }
3406
+ }
3407
+ } catch {}
3408
+ }
3245
3409
  const items = await this.dexieDb.getByIds(collection, ids);
3246
3410
  const results = [];
3247
3411
  for (const item of items) {
3248
- if (item && !item._deleted) {
3249
- results.push(item);
3250
- }
3412
+ if (!item)
3413
+ continue;
3414
+ if (!opts?.returnDeleted && item._deleted)
3415
+ continue;
3416
+ if (!opts?.returnArchived && item._archived)
3417
+ continue;
3418
+ results.push(item);
3251
3419
  }
3252
- return results;
3420
+ const final = applyQueryOpts(results, opts);
3421
+ if (opts?.referToServer && this.isOnline()) {
3422
+ this.referToServerSync(collection);
3423
+ }
3424
+ return final;
3253
3425
  }
3254
- async findOne(collection, query) {
3426
+ async findOne(collection, query, opts) {
3255
3427
  this.assertCollection(collection);
3428
+ opts = this.resolveOpts(opts);
3429
+ if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
3430
+ await this.syncCollectionForFind(collection, query, opts);
3431
+ }
3256
3432
  const all = await this.dexieDb.getAll(collection);
3257
- const active = all.filter((item) => !item._deleted);
3433
+ const active = all.filter((item) => {
3434
+ if (!opts?.returnDeleted && item._deleted)
3435
+ return false;
3436
+ if (!opts?.returnArchived && item._archived)
3437
+ return false;
3438
+ return true;
3439
+ });
3258
3440
  const filtered = filterByQuery(active, query);
3259
- if (filtered.length === 0)
3441
+ if (filtered.length === 0) {
3442
+ if (opts?.referToServer && this.isOnline()) {
3443
+ this.referToServerSync(collection, query);
3444
+ }
3260
3445
  return null;
3261
- return filtered[0];
3446
+ }
3447
+ const sorted = applyQueryOpts(filtered, { sort: opts?.sort, project: opts?.project });
3448
+ const result = sorted[0];
3449
+ if (opts?.referToServer && this.isOnline()) {
3450
+ this.referToServerSync(collection, query);
3451
+ }
3452
+ return result;
3262
3453
  }
3263
- async find(collection, query) {
3454
+ async find(collection, query, opts) {
3264
3455
  this.assertCollection(collection);
3456
+ opts = this.resolveOpts(opts);
3457
+ if ((opts?.returnDeleted || opts?.returnArchived) && this.isOnline()) {
3458
+ await this.syncCollectionForFind(collection, query, opts);
3459
+ }
3265
3460
  const all = await this.dexieDb.getAll(collection);
3266
- const active = all.filter((item) => !item._deleted);
3461
+ const active = all.filter((item) => {
3462
+ if (!opts?.returnDeleted && item._deleted)
3463
+ return false;
3464
+ if (!opts?.returnArchived && item._archived)
3465
+ return false;
3466
+ return true;
3467
+ });
3267
3468
  const filtered = query ? filterByQuery(active, query) : active;
3268
- return filtered;
3469
+ const result = applyQueryOpts(filtered, opts);
3470
+ if (opts?.referToServer && this.isOnline()) {
3471
+ this.referToServerSync(collection, query);
3472
+ }
3473
+ return result;
3269
3474
  }
3270
3475
  async aggregate(collection, pipeline, opts) {
3271
3476
  this.assertCollection(collection);
@@ -3274,6 +3479,67 @@ class SyncedDb {
3274
3479
  }
3275
3480
  return this.connectionManager.withRestTimeout(this.restInterface.aggregate(collection, pipeline, opts), "aggregate");
3276
3481
  }
3482
+ async syncCollectionForFind(collection, query, opts) {
3483
+ const meta = this.syncMetaCache.get(collection);
3484
+ const timestamp = meta?.lastSyncTs || 0;
3485
+ try {
3486
+ const serverData = await this.connectionManager.withRestTimeout(this.restInterface.findNewer(collection, timestamp, query, {
3487
+ returnDeleted: opts?.returnDeleted || false,
3488
+ returnArchived: opts?.returnArchived || false
3489
+ }), "syncCollectionForFind");
3490
+ if (serverData.length > 0) {
3491
+ await this.syncEngine.processCollectionServerData(collection, serverData);
3492
+ }
3493
+ } catch {}
3494
+ }
3495
+ referToServerSync(collection, query) {
3496
+ const meta = this.syncMetaCache.get(collection);
3497
+ const timestamp = meta?.lastSyncTs || 0;
3498
+ this.connectionManager.withRestTimeout(this.restInterface.findNewer(collection, timestamp, query, { returnDeleted: true }), "referToServer").then(async (serverData) => {
3499
+ if (serverData.length > 0) {
3500
+ await this.syncEngine.processCollectionServerData(collection, serverData);
3501
+ }
3502
+ }).catch((err) => {
3503
+ console.error(`referToServer sync failed for ${collection}:`, err);
3504
+ });
3505
+ }
3506
+ async ensureItemsAreLoaded(collection, ids, withDeleted) {
3507
+ this.assertCollection(collection);
3508
+ if (ids.length === 0)
3509
+ return;
3510
+ const localItems = await this.dexieDb.getByIds(collection, ids);
3511
+ const missingIds = [];
3512
+ for (let i = 0;i < ids.length; i++) {
3513
+ if (!localItems[i]) {
3514
+ missingIds.push(ids[i]);
3515
+ }
3516
+ }
3517
+ if (missingIds.length === 0)
3518
+ return;
3519
+ if (!this.isOnline())
3520
+ return;
3521
+ const serverItems = await this.connectionManager.withRestTimeout(this.restInterface.findByIds(collection, missingIds), "ensureItemsAreLoaded");
3522
+ if (!serverItems || serverItems.length === 0)
3523
+ return;
3524
+ const toSaveDexie = [];
3525
+ const toSaveInMem = [];
3526
+ for (const item of serverItems) {
3527
+ if (!item)
3528
+ continue;
3529
+ if (!withDeleted && item._deleted)
3530
+ continue;
3531
+ toSaveDexie.push(item);
3532
+ if (!item._deleted && !item._archived) {
3533
+ toSaveInMem.push(item);
3534
+ }
3535
+ }
3536
+ if (toSaveDexie.length > 0) {
3537
+ await this.dexieDb.saveMany(collection, toSaveDexie);
3538
+ }
3539
+ if (toSaveInMem.length > 0) {
3540
+ this.inMemManager.writeBatch(collection, toSaveInMem, "upsert");
3541
+ }
3542
+ }
3277
3543
  async save(collection, id, update) {
3278
3544
  this.assertCollection(collection);
3279
3545
  const existing = await this.dexieDb.getById(collection, id);
@@ -3288,7 +3554,7 @@ class SyncedDb {
3288
3554
  this.pendingChanges.schedule(collection, id, newData, 0, "save");
3289
3555
  const currentMem = this.inMemDb.getById(collection, id);
3290
3556
  const merged = { ...currentMem || existing || { _id: id }, ...update };
3291
- if (!existing?._deleted) {
3557
+ if (!existing?._deleted && !existing?._archived) {
3292
3558
  this.inMemManager.writeBatch(collection, [merged], "upsert");
3293
3559
  }
3294
3560
  return merged;
@@ -3308,7 +3574,7 @@ class SyncedDb {
3308
3574
  this.assertCollection(collection);
3309
3575
  const id = data._id || new ObjectId2;
3310
3576
  const existing = await this.dexieDb.getById(collection, id);
3311
- if (existing && !existing._deleted) {
3577
+ if (existing && !existing._deleted && !existing._archived) {
3312
3578
  console.warn(`SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`);
3313
3579
  }
3314
3580
  const insertChanges = { ...data, _lastUpdaterId: this.updaterId };
@@ -3588,6 +3854,16 @@ class SyncedDb {
3588
3854
  getOrCreateWindowId() {
3589
3855
  return `shared-${this.tenant}`;
3590
3856
  }
3857
+ resolveOpts(opts) {
3858
+ if (!this.defaultReturnDeleted && !this.defaultReturnArchived) {
3859
+ return opts;
3860
+ }
3861
+ return {
3862
+ ...opts,
3863
+ returnDeleted: opts?.returnDeleted ?? this.defaultReturnDeleted,
3864
+ returnArchived: opts?.returnArchived ?? this.defaultReturnArchived
3865
+ };
3866
+ }
3591
3867
  assertCollection(name) {
3592
3868
  if (!this.collections.has(name)) {
3593
3869
  throw new Error(`Collection "${name}" not configured`);
@@ -5949,7 +6225,7 @@ var unpackr = new Unpackr({ structuredClone: true });
5949
6225
  var pack2 = (x) => packr.pack(preprocessForPack(x));
5950
6226
  var unpack2 = (x) => unpackr.unpack(x);
5951
6227
  var DEFAULT_TIMEOUT = 5000;
5952
- var DEFAULT_PROGRESS_CHUNK_SIZE = 16384;
6228
+ var DEFAULT_PROGRESS_CHUNK_SIZE = 16 * 1024;
5953
6229
 
5954
6230
  class RestProxy {
5955
6231
  endpoint;
@@ -6493,9 +6769,13 @@ class Ebus2ProxyServerUpdateNotifier {
6493
6769
  }
6494
6770
  }
6495
6771
  export {
6772
+ sortItems,
6496
6773
  resolveConflict,
6774
+ projectItem,
6497
6775
  matchesQuery,
6498
6776
  filterByQuery,
6777
+ applySkipLimit,
6778
+ applyQueryOpts,
6499
6779
  SyncedDb,
6500
6780
  RestProxy,
6501
6781
  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>): Promise<T | null>;
58
- find<T extends DbEntity>(collection: string, query?: QuerySpec<T>): Promise<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,15 @@ 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
+ }>;
41
+ /** Max items to process per batch in processIncomingServerData */
42
+ private static readonly SYNC_BATCH_SIZE;
33
43
  private processIncomingServerData;
34
44
  private compareTimestamps;
35
45
  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;
@@ -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";
@@ -14,6 +14,7 @@ export interface DbEntity {
14
14
  _ts?: Timestamp;
15
15
  _csq?: number;
16
16
  _deleted?: Date;
17
+ _archived?: Date;
17
18
  _blocked?: boolean;
18
19
  /** ID zadnjega updaterja - za detekcijo loopback */
19
20
  _lastUpdaterId?: string;
@@ -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";
@@ -206,7 +206,7 @@ export interface CollectionSyncStats {
206
206
  receivedCount: number;
207
207
  /** Number of dirty items sent to server for this collection */
208
208
  sentCount: number;
209
- /** The actual items received from server (for debugging/logging) */
209
+ /** @deprecated Use receivedCount instead. Will be empty array in future streaming mode. */
210
210
  receivedItems: LocalDbEntity[];
211
211
  }
212
212
  /**
@@ -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>): Promise<T | null>;
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>): Promise<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[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.69",
3
+ "version": "0.1.72",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",