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 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 || item._deleted)
3360
+ if (!item)
3238
3361
  return null;
3239
- return item;
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 (item && !item._deleted) {
3249
- results.push(item);
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 results;
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) => !item._deleted);
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
- return filtered[0];
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) => !item._deleted);
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
- return filtered;
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 = 16384;
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>): 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,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;
@@ -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";
@@ -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.71",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",