cry-synced-db-client 0.1.67 → 0.1.71

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