cry-synced-db-client 0.1.171 → 0.1.173

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/CHANGELOG.md CHANGED
@@ -2,6 +2,80 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### `preprocessDirtyItem` callback — per-item filter / transform before upload
6
+
7
+ New optional config callback fired for **every** dirty item just before it
8
+ becomes part of an `updateCollections` batch. Runs after `_ts`/`_rev` strip
9
+ and legacy-path fixup, so `item.update` already reflects the wire-shape
10
+ that would normally be uploaded.
11
+
12
+ Return values:
13
+
14
+ | Return | Result |
15
+ |---|---|
16
+ | same or modified `{_id, update}` | uploaded for this cycle |
17
+ | `undefined` | SKIP upload this cycle — dirty record **left untouched** |
18
+ | throw | SKIP upload + `console.error` — dirty record **left untouched** |
19
+
20
+ Both skip paths leave the underlying dirty change as-is so the next sync
21
+ cycle re-runs preprocessing. Use the regular `save`/`insert` API to
22
+ overwrite locally or `hardDeleteOne` to permanently remove.
23
+
24
+ ```typescript
25
+ new SyncedDb({
26
+ // ...
27
+ preprocessDirtyItem: (item, collection) => {
28
+ // e.g. skip uploads during a specific business window
29
+ if (collection === "logs" && now() < businessStart) return undefined;
30
+
31
+ // or transform: strip a field that another tab will recompute
32
+ const { ephemeral, ...rest } = item.update as any;
33
+ return { _id: item._id, update: rest };
34
+ },
35
+ });
36
+ ```
37
+
38
+ Useful for: per-tenant data sanitization, conditional upload gating,
39
+ audit-trail injection.
40
+
41
+ ### Nested-bracket terminal layering in `mergeDirtyPath` Case 2
42
+
43
+ When a new terminal-bracket whole-element write arrives AFTER pending
44
+ sub-field edits on the same element (e.g. existing `postavke[p1].kolicina
45
+ = 99`, new `postavke[p1] = [{_id:p1, opis:"fresh"}]`), the pending
46
+ sub-field values are now LAYERED into the new element value before the
47
+ descendant entries are dropped — producing one canonical
48
+ `postavke[p1] = [{_id:p1, opis:"fresh", kolicina:99}]` entry rather than
49
+ silently losing the prior sub-field write.
50
+
51
+ Applies only to terminal-bracket new paths whose value is element-shaped
52
+ (`[<el>]` insert wire form or plain object). Plain non-bracket ancestors
53
+ (`koraki = [...]` whole-array replace) and REMOVE markers (`undefined`)
54
+ keep the original "drop descendants" behavior.
55
+
56
+ Pairs with cry-db Unreleased's nested-bracket pipeline support — works
57
+ both with and without the lift (`containsIdArrayDescendant`) in
58
+ `computeArrayDiff`.
59
+
60
+ ### Module-prefixed console logs
61
+
62
+ All `console.error` / `warn` / `log` / `info` / `debug` calls in
63
+ `src/db/**`, `src/utils/**`, and `src/types/**` are now tagged with a
64
+ module-scoped prefix (`[SyncedDb]`, `[SyncEngine]`, `[LeaderElection]`,
65
+ `[PendingChanges]`, `[Connection]`, `[CrossTabSync]`, `[InMem]`,
66
+ `[NetworkStatus]`, `[WakeSync]`, `[Ebus2ProxyNotifier]`, `[DexieDb]`,
67
+ `[CrashRecovery]`). Consumer log aggregators can now filter or
68
+ namespace-route library output by prefix.
69
+
70
+ ### `DB-WARNING` tag for cry-db per-item warnings + `SUPRESS_DB_WARNINGS` kill-switch
71
+
72
+ Per-item `warnings` returned by `updateCollections` are now surfaced on
73
+ `console.error` with a `DB-WARNING [<collection>] _id=…:` prefix
74
+ (previously `console.warn` with a generic message), so they show up
75
+ alongside actual upload errors in observability pipelines.
76
+ `SUPRESS_DB_WARNINGS` constant in `SyncEngine.ts` silences them when
77
+ needed (e.g. during noisy migrations).
78
+
5
79
  ### Runtime collection registration (`addCollectionToSync`, `replaceSyncCollection`)
6
80
 
7
81
  Two methods to install / replace collection configs at runtime; both load the
package/dist/index.js CHANGED
@@ -600,6 +600,17 @@ function canExpandArrayToBrackets(value) {
600
600
  }
601
601
  return true;
602
602
  }
603
+ function pickLayerTarget(newPath, newValue) {
604
+ if (!isTerminalBracketKey(newPath)) return null;
605
+ if (newValue === void 0) return null;
606
+ if (Array.isArray(newValue) && newValue.length === 1 && newValue[0] && typeof newValue[0] === "object") {
607
+ return newValue[0];
608
+ }
609
+ if (newValue && typeof newValue === "object" && !Array.isArray(newValue)) {
610
+ return newValue;
611
+ }
612
+ return null;
613
+ }
603
614
  function mergeDirtyPath(accumulated, newPath, newValue) {
604
615
  for (const existingKey of Object.keys(accumulated)) {
605
616
  if (existingKey === newPath) continue;
@@ -628,14 +639,27 @@ function mergeDirtyPath(accumulated, newPath, newValue) {
628
639
  break;
629
640
  }
630
641
  }
631
- const toDelete = [];
642
+ const descendants = [];
632
643
  for (const existingKey of Object.keys(accumulated)) {
633
644
  if (existingKey === newPath) continue;
634
645
  if (isDescendantOrEqual(existingKey, newPath)) {
635
- toDelete.push(existingKey);
646
+ descendants.push(existingKey);
647
+ }
648
+ }
649
+ const layerInto = pickLayerTarget(newPath, newValue);
650
+ if (layerInto && descendants.length > 0) {
651
+ for (const desc of descendants) {
652
+ const sepChar = desc[newPath.length];
653
+ const relativePath = sepChar === "[" ? desc.substring(newPath.length) : desc.substring(newPath.length + 1);
654
+ const descValue = accumulated[desc];
655
+ if (descValue === void 0) {
656
+ deleteByPath(layerInto, relativePath);
657
+ } else {
658
+ setByPath(layerInto, relativePath, descValue);
659
+ }
636
660
  }
637
661
  }
638
- for (const k of toDelete) delete accumulated[k];
662
+ for (const k of descendants) delete accumulated[k];
639
663
  accumulated[newPath] = newValue;
640
664
  }
641
665
  function mergeDirtyChanges(accumulated, newChanges) {
@@ -722,14 +746,14 @@ var InMemManager = class {
722
746
  try {
723
747
  metadatas = config.onObjectsUpdated(items);
724
748
  } catch (err) {
725
- console.error("onObjectsUpdated callback failed:", err);
749
+ console.error("[InMem] onObjectsUpdated callback failed:", err);
726
750
  return;
727
751
  }
728
752
  } else if (config.onObjectUpdated) {
729
753
  try {
730
754
  metadatas = items.map((item) => config.onObjectUpdated(item));
731
755
  } catch (err) {
732
- console.error("onObjectUpdated callback failed:", err);
756
+ console.error("[InMem] onObjectUpdated callback failed:", err);
733
757
  return;
734
758
  }
735
759
  } else {
@@ -762,14 +786,14 @@ var InMemManager = class {
762
786
  try {
763
787
  metadatas = config.onObjectsUpdated(items);
764
788
  } catch (err) {
765
- console.error("onObjectsUpdated callback failed:", err);
789
+ console.error("[InMem] onObjectsUpdated callback failed:", err);
766
790
  return;
767
791
  }
768
792
  } else if (config.onObjectUpdated) {
769
793
  try {
770
794
  metadatas = items.map((item) => config.onObjectUpdated(item));
771
795
  } catch (err) {
772
- console.error("onObjectUpdated callback failed:", err);
796
+ console.error("[InMem] onObjectUpdated callback failed:", err);
773
797
  return;
774
798
  }
775
799
  } else {
@@ -1035,7 +1059,7 @@ var LeaderElectionManager = class {
1035
1059
  try {
1036
1060
  this.leaderReelectionChannel.postMessage({ type: "reelect-leader" });
1037
1061
  } catch (err) {
1038
- console.error("Failed to broadcast leader reelection:", err);
1062
+ console.error("[LeaderElection] Failed to broadcast leader reelection:", err);
1039
1063
  }
1040
1064
  }
1041
1065
  }
@@ -1046,7 +1070,7 @@ var LeaderElectionManager = class {
1046
1070
  try {
1047
1071
  this.callbacks.onBecameLeader();
1048
1072
  } catch (err) {
1049
- console.error("onBecameLeader callback failed:", err);
1073
+ console.error("[LeaderElection] onBecameLeader callback failed:", err);
1050
1074
  }
1051
1075
  }
1052
1076
  }
@@ -1057,7 +1081,7 @@ var LeaderElectionManager = class {
1057
1081
  try {
1058
1082
  this.callbacks.onLostLeadership();
1059
1083
  } catch (err) {
1060
- console.error("onLostLeadership callback failed:", err);
1084
+ console.error("[LeaderElection] onLostLeadership callback failed:", err);
1061
1085
  }
1062
1086
  }
1063
1087
  }
@@ -1066,7 +1090,7 @@ var LeaderElectionManager = class {
1066
1090
  try {
1067
1091
  this.callbacks.onInfrastructureError(type, message, error);
1068
1092
  } catch (err) {
1069
- console.error("onInfrastructureError callback failed:", err);
1093
+ console.error("[LeaderElection] onInfrastructureError callback failed:", err);
1070
1094
  }
1071
1095
  }
1072
1096
  }
@@ -1191,7 +1215,7 @@ var CrossTabSyncManager = class {
1191
1215
  try {
1192
1216
  this.metaUpdateChannel.postMessage(payload);
1193
1217
  } catch (err) {
1194
- console.error("Failed to broadcast meta update:", err);
1218
+ console.error("[CrossTabSync] Failed to broadcast meta update:", err);
1195
1219
  }
1196
1220
  }
1197
1221
  this.pendingBroadcasts.clear();
@@ -1237,7 +1261,7 @@ var CrossTabSyncManager = class {
1237
1261
  try {
1238
1262
  await this.deps.reloadCollectionFromDexie(collection);
1239
1263
  } catch (err) {
1240
- console.error(`Error reloading collection ${collection} from Dexie:`, err);
1264
+ console.error(`[CrossTabSync] Error reloading collection ${collection} from Dexie:`, err);
1241
1265
  }
1242
1266
  }
1243
1267
  }
@@ -1285,11 +1309,11 @@ var CrossTabSyncManager = class {
1285
1309
  };
1286
1310
  this.callbacks.onCrossTabSync(info);
1287
1311
  } catch (err) {
1288
- console.error("onCrossTabSync callback failed:", err);
1312
+ console.error("[CrossTabSync] onCrossTabSync callback failed:", err);
1289
1313
  }
1290
1314
  }
1291
1315
  } catch (err) {
1292
- console.error(`Error handling cross-tab delta update for ${collection}:`, err);
1316
+ console.error(`[CrossTabSync] Error handling cross-tab delta update for ${collection}:`, err);
1293
1317
  }
1294
1318
  }
1295
1319
  }
@@ -1305,7 +1329,7 @@ var CrossTabSyncManager = class {
1305
1329
  try {
1306
1330
  this.metaUpdateChannel.postMessage(payload);
1307
1331
  } catch (err) {
1308
- console.error("Failed to broadcast reload:", err);
1332
+ console.error("[CrossTabSync] Failed to broadcast reload:", err);
1309
1333
  }
1310
1334
  }
1311
1335
  callOnInfrastructureError(type, message, error) {
@@ -1313,7 +1337,7 @@ var CrossTabSyncManager = class {
1313
1337
  try {
1314
1338
  this.callbacks.onInfrastructureError(type, message, error);
1315
1339
  } catch (err) {
1316
- console.error("onInfrastructureError callback failed:", err);
1340
+ console.error("[CrossTabSync] onInfrastructureError callback failed:", err);
1317
1341
  }
1318
1342
  }
1319
1343
  }
@@ -1379,7 +1403,7 @@ var ConnectionManager = class {
1379
1403
  } else {
1380
1404
  this.deps.tryBecomeLeader();
1381
1405
  this.tryGoOnline().catch((err) => {
1382
- console.error("Failed to go online after forceOffline release:", err);
1406
+ console.error("[Connection] Failed to go online after forceOffline release:", err);
1383
1407
  });
1384
1408
  }
1385
1409
  }
@@ -1403,13 +1427,13 @@ var ConnectionManager = class {
1403
1427
  "ping"
1404
1428
  );
1405
1429
  } catch (err) {
1406
- console.warn("tryGoOnline: ping failed:", err);
1430
+ console.warn("[Connection] tryGoOnline: ping failed:", err);
1407
1431
  this.online = false;
1408
1432
  return;
1409
1433
  }
1410
1434
  if (!pingResult) {
1411
1435
  const url = (_a = this.restInterface.endpoint) != null ? _a : "unknown";
1412
- console.warn(`Ping to ${url} failed - staying offline`);
1436
+ console.warn(`[Connection] Ping to ${url} failed - staying offline`);
1413
1437
  return;
1414
1438
  }
1415
1439
  this.online = true;
@@ -1421,7 +1445,7 @@ var ConnectionManager = class {
1421
1445
  try {
1422
1446
  await this.deps.sync("INITIAL SYNC");
1423
1447
  } catch (err) {
1424
- console.warn("INITIAL SYNC after tryGoOnline failed (stays online):", err);
1448
+ console.warn("[Connection] INITIAL SYNC after tryGoOnline failed (stays online):", err);
1425
1449
  }
1426
1450
  } finally {
1427
1451
  this.tryGoOnlineInFlight = false;
@@ -1437,7 +1461,7 @@ var ConnectionManager = class {
1437
1461
  this.autoSyncTimer = setInterval(() => {
1438
1462
  if (this.forcedOffline || !this.online) return;
1439
1463
  this.deps.sync(`interval ${intervalMs}ms`).catch((err) => {
1440
- console.error("Auto-sync failed:", err);
1464
+ console.error("[Connection] Auto-sync failed:", err);
1441
1465
  });
1442
1466
  }, intervalMs);
1443
1467
  }
@@ -1446,7 +1470,7 @@ var ConnectionManager = class {
1446
1470
  this.reconnectTimer = setInterval(() => {
1447
1471
  if (this.forcedOffline || this.online || this.tryGoOnlineInFlight) return;
1448
1472
  this.tryGoOnline().catch((err) => {
1449
- console.error("Reconnect tryGoOnline failed:", err);
1473
+ console.error("[Connection] Reconnect tryGoOnline failed:", err);
1450
1474
  });
1451
1475
  }, retryMs);
1452
1476
  }
@@ -1530,7 +1554,7 @@ var ConnectionManager = class {
1530
1554
  try {
1531
1555
  this.callbacks.onInfrastructureError(type, message, error);
1532
1556
  } catch (err) {
1533
- console.error("onInfrastructureError callback failed:", err);
1557
+ console.error("[Connection] onInfrastructureError callback failed:", err);
1534
1558
  }
1535
1559
  }
1536
1560
  }
@@ -1543,7 +1567,7 @@ var ConnectionManager = class {
1543
1567
  try {
1544
1568
  this.callbacks.onSyncFailed(reason);
1545
1569
  } catch (err) {
1546
- console.error("onSyncFailed callback failed:", err);
1570
+ console.error("[Connection] onSyncFailed callback failed:", err);
1547
1571
  }
1548
1572
  }
1549
1573
  }
@@ -1555,7 +1579,7 @@ var ConnectionManager = class {
1555
1579
  try {
1556
1580
  this.callbacks.onWsConnect();
1557
1581
  } catch (err) {
1558
- console.error("onWsConnect callback failed:", err);
1582
+ console.error("[Connection] onWsConnect callback failed:", err);
1559
1583
  }
1560
1584
  }
1561
1585
  }
@@ -1567,7 +1591,7 @@ var ConnectionManager = class {
1567
1591
  try {
1568
1592
  this.callbacks.onWsDisconnect(reason);
1569
1593
  } catch (err) {
1570
- console.error("onWsDisconnect callback failed:", err);
1594
+ console.error("[Connection] onWsDisconnect callback failed:", err);
1571
1595
  }
1572
1596
  }
1573
1597
  this.reportInfrastructureError(
@@ -1583,7 +1607,7 @@ var ConnectionManager = class {
1583
1607
  try {
1584
1608
  this.callbacks.onWsReconnect(attempt);
1585
1609
  } catch (err) {
1586
- console.error("onWsReconnect callback failed:", err);
1610
+ console.error("[Connection] onWsReconnect callback failed:", err);
1587
1611
  }
1588
1612
  }
1589
1613
  }
@@ -2450,7 +2474,7 @@ function savePendingWrite(tenant, collection, id, delta) {
2450
2474
  }
2451
2475
  localStorage.setItem(key, dist_default.stringify(pending));
2452
2476
  } catch (e) {
2453
- console.warn("Failed to save pending write to localStorage");
2477
+ console.warn("[CrashRecovery] Failed to save pending write to localStorage");
2454
2478
  }
2455
2479
  }
2456
2480
  function clearPendingWrite(tenant, collection, id) {
@@ -2632,7 +2656,7 @@ var PendingChangesManager = class {
2632
2656
  clearPendingWrite(this.tenant, write.collection, write.id);
2633
2657
  }
2634
2658
  } catch (err) {
2635
- console.error(`Failed to recover pending writes for ${collection}:`, err);
2659
+ console.error(`[PendingChanges] Failed to recover pending writes for ${collection}:`, err);
2636
2660
  }
2637
2661
  }
2638
2662
  }
@@ -2682,7 +2706,7 @@ var PendingChangesManager = class {
2682
2706
  calledFrom: pending.calledFrom
2683
2707
  });
2684
2708
  } catch (err) {
2685
- console.error("onDexieWriteRequest callback failed:", err);
2709
+ console.error("[PendingChanges] onDexieWriteRequest callback failed:", err);
2686
2710
  }
2687
2711
  }
2688
2712
  if (existing) {
@@ -2697,7 +2721,7 @@ var PendingChangesManager = class {
2697
2721
  // ensure _id is after spread
2698
2722
  });
2699
2723
  if (typeof insertData._id === "object") {
2700
- console.error(`Dexie: _id is object type in ${pending.collection}:`, typeof insertData._id, insertData._id);
2724
+ console.error(`[PendingChanges] Dexie: _id is object type in ${pending.collection}:`, typeof insertData._id, insertData._id);
2701
2725
  }
2702
2726
  await this.deps.dexieDb.insert(pending.collection, insertData);
2703
2727
  }
@@ -2711,7 +2735,7 @@ var PendingChangesManager = class {
2711
2735
  calledFrom: pending.calledFrom
2712
2736
  });
2713
2737
  } catch (err) {
2714
- console.error("onDexieWriteResult callback failed:", err);
2738
+ console.error("[PendingChanges] onDexieWriteResult callback failed:", err);
2715
2739
  }
2716
2740
  }
2717
2741
  clearPendingWrite(this.tenant, pending.collection, pending.id);
@@ -2725,12 +2749,12 @@ var PendingChangesManager = class {
2725
2749
  calledFrom: pending.calledFrom
2726
2750
  });
2727
2751
  } catch (err) {
2728
- console.error("onLocalstorageWriteResult callback failed:", err);
2752
+ console.error("[PendingChanges] onLocalstorageWriteResult callback failed:", err);
2729
2753
  }
2730
2754
  }
2731
2755
  this.scheduleRestUpload();
2732
2756
  } catch (err) {
2733
- console.error("Failed to write to Dexie:", err);
2757
+ console.error("[PendingChanges] Failed to write to Dexie:", err);
2734
2758
  if (this.callbacks.onDexieWriteResult) {
2735
2759
  try {
2736
2760
  this.callbacks.onDexieWriteResult({
@@ -2742,13 +2766,13 @@ var PendingChangesManager = class {
2742
2766
  calledFrom: pending.calledFrom
2743
2767
  });
2744
2768
  } catch (callbackErr) {
2745
- console.error("onDexieWriteResult callback failed:", callbackErr);
2769
+ console.error("[PendingChanges] onDexieWriteResult callback failed:", callbackErr);
2746
2770
  }
2747
2771
  }
2748
2772
  const newRetryCount = pending.retryCount + 1;
2749
2773
  if (newRetryCount >= MAX_RETRY_COUNT) {
2750
2774
  console.error(
2751
- `Max retry count (${MAX_RETRY_COUNT}) reached for pending change ${key}. Data remains in localStorage for crash recovery.`
2775
+ `[PendingChanges] Max retry count (${MAX_RETRY_COUNT}) reached for pending change ${key}. Data remains in localStorage for crash recovery.`
2752
2776
  );
2753
2777
  return;
2754
2778
  }
@@ -2784,7 +2808,7 @@ var PendingChangesManager = class {
2784
2808
  try {
2785
2809
  await this.deps.uploadDirtyItems();
2786
2810
  } catch (err) {
2787
- console.error("REST upload failed:", err);
2811
+ console.error("[PendingChanges] REST upload failed:", err);
2788
2812
  } finally {
2789
2813
  this.isUploadingToRest = false;
2790
2814
  resolveUpload();
@@ -2861,10 +2885,6 @@ function mergeObjectArrays(local, external, parentServerWins = false) {
2861
2885
  }
2862
2886
  if (objectItemsMissingId.length > 0) {
2863
2887
  for (const item of objectItemsMissingId) {
2864
- console.error(
2865
- "[mergeObjectArrays] array element without _id \u2014 falling back to whole-array replace by higher _rev:",
2866
- item
2867
- );
2868
2888
  }
2869
2889
  return parentServerWins ? external.slice() : local.slice();
2870
2890
  }
@@ -2992,6 +3012,7 @@ function stripServerManagedFromChanges(changes) {
2992
3012
  }
2993
3013
 
2994
3014
  // src/db/sync/SyncEngine.ts
3015
+ var SUPRESS_DB_WARNINGS = true;
2995
3016
  var _SyncEngine = class _SyncEngine {
2996
3017
  constructor(config) {
2997
3018
  this.tenant = config.tenant;
@@ -3001,6 +3022,7 @@ var _SyncEngine = class _SyncEngine {
3001
3022
  this.restInterface = config.restInterface;
3002
3023
  this.callbacks = config.callbacks;
3003
3024
  this.deps = config.deps;
3025
+ this.preprocessDirtyItem = config.preprocessDirtyItem;
3004
3026
  }
3005
3027
  /**
3006
3028
  * Execute full sync cycle.
@@ -3139,7 +3161,7 @@ var _SyncEngine = class _SyncEngine {
3139
3161
  }
3140
3162
  } catch (err) {
3141
3163
  console.error(
3142
- "uploadDirtyItems failed (download succeeded, staying online):",
3164
+ "[SyncEngine] uploadDirtyItems failed (download succeeded, staying online):",
3143
3165
  err
3144
3166
  );
3145
3167
  }
@@ -3165,7 +3187,7 @@ var _SyncEngine = class _SyncEngine {
3165
3187
  });
3166
3188
  } catch (err) {
3167
3189
  const reason = err instanceof Error ? err.message : String(err);
3168
- console.error("Sync failed:", err);
3190
+ console.error("[SyncEngine] Sync failed:", err);
3169
3191
  this.deps.onSyncFailed(`Sync failed: ${reason}`);
3170
3192
  this.callOnSyncEnd({
3171
3193
  durationMs: Date.now() - startTime,
@@ -3223,7 +3245,7 @@ var _SyncEngine = class _SyncEngine {
3223
3245
  }
3224
3246
  if (updates.length === 0) {
3225
3247
  console.warn(
3226
- `uploadDirtyItems: ${collectionName} has ${dirtyChanges.length} dirty entries but 0 resolvable items`,
3248
+ `[SyncEngine] uploadDirtyItems: ${collectionName} has ${dirtyChanges.length} dirty entries but 0 resolvable items`,
3227
3249
  skipped
3228
3250
  );
3229
3251
  if (this.callbacks.onUploadSkip) {
@@ -3238,7 +3260,7 @@ var _SyncEngine = class _SyncEngine {
3238
3260
  timestamp: /* @__PURE__ */ new Date()
3239
3261
  });
3240
3262
  } catch (err) {
3241
- console.error("onUploadSkip callback failed:", err);
3263
+ console.error("[SyncEngine] onUploadSkip callback failed:", err);
3242
3264
  }
3243
3265
  }
3244
3266
  continue;
@@ -3255,27 +3277,46 @@ var _SyncEngine = class _SyncEngine {
3255
3277
  timestamp: /* @__PURE__ */ new Date()
3256
3278
  });
3257
3279
  } catch (err) {
3258
- console.error("onUploadSkip callback failed:", err);
3280
+ console.error("[SyncEngine] onUploadSkip callback failed:", err);
3281
+ }
3282
+ }
3283
+ const mappedUpdates = [];
3284
+ for (const item of updates) {
3285
+ const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
3286
+ const stripped = stripServerManagedFromChanges(
3287
+ item.delta
3288
+ );
3289
+ const fixed = fixDotnetArrays(
3290
+ stripped,
3291
+ item.currentServerRev,
3292
+ dirtyBaseRev
3293
+ );
3294
+ let candidate = {
3295
+ _id: item._id,
3296
+ update: fixed
3297
+ };
3298
+ if (this.preprocessDirtyItem) {
3299
+ try {
3300
+ const processed = this.preprocessDirtyItem(candidate, collectionName);
3301
+ if (processed === void 0) {
3302
+ continue;
3303
+ }
3304
+ candidate = processed;
3305
+ } catch (err) {
3306
+ console.error(
3307
+ `[SyncEngine] preprocessDirtyItem(${collectionName}) failed for _id=${String(item._id)}; keeping dirty for retry:`,
3308
+ err
3309
+ );
3310
+ continue;
3311
+ }
3259
3312
  }
3313
+ mappedUpdates.push(candidate);
3260
3314
  }
3315
+ if (mappedUpdates.length === 0) continue;
3261
3316
  collectionBatches.push([{
3262
3317
  collection: collectionName,
3263
3318
  batch: {
3264
- updates: updates.map((item) => {
3265
- const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
3266
- const stripped = stripServerManagedFromChanges(
3267
- item.delta
3268
- );
3269
- const fixed = fixDotnetArrays(
3270
- stripped,
3271
- item.currentServerRev,
3272
- dirtyBaseRev
3273
- );
3274
- return {
3275
- _id: item._id,
3276
- update: fixed
3277
- };
3278
- }),
3319
+ updates: mappedUpdates,
3279
3320
  deletes: []
3280
3321
  }
3281
3322
  }]);
@@ -3352,7 +3393,7 @@ var _SyncEngine = class _SyncEngine {
3352
3393
  }
3353
3394
  if (ambiguous.length > 0) {
3354
3395
  console.error(
3355
- `Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
3396
+ `[SyncEngine] Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
3356
3397
  );
3357
3398
  }
3358
3399
  if (allSuccessIds.length > 0) {
@@ -3450,14 +3491,14 @@ var _SyncEngine = class _SyncEngine {
3450
3491
  if (errors2 && errors2.length > 0) {
3451
3492
  for (const e of errors2) {
3452
3493
  console.error(
3453
- `Sync upload error [${collection}] _id=${e._id}: ${e.error} \u2014 dirty entry will persist until retry`
3494
+ `[SyncEngine] Sync upload error [${collection}] _id=${e._id}: ${e.error} \u2014 dirty entry will persist until retry`
3454
3495
  );
3455
3496
  }
3456
3497
  }
3457
- if (warnings && warnings.length > 0) {
3498
+ if (warnings && warnings.length > 0 && !SUPRESS_DB_WARNINGS) {
3458
3499
  for (const w of warnings) {
3459
- console.warn(
3460
- `Sync upload warning [${collection}] _id=${w._id}: ${w.error}`
3500
+ console.error(
3501
+ `[SyncEngine] DB-WARNING [${collection}] _id=${w._id}: ${w.error}`
3461
3502
  );
3462
3503
  }
3463
3504
  }
@@ -3471,7 +3512,7 @@ var _SyncEngine = class _SyncEngine {
3471
3512
  const unacked = [...sentIds].filter((id) => !ackIds.has(id));
3472
3513
  if (unacked.length > 0) {
3473
3514
  console.warn(
3474
- `uploadDirtyItems: ${collection}: ${unacked.length} items sent but not acknowledged:`,
3515
+ `[SyncEngine] uploadDirtyItems: ${collection}: ${unacked.length} items sent but not acknowledged:`,
3475
3516
  unacked
3476
3517
  );
3477
3518
  }
@@ -3527,14 +3568,14 @@ var _SyncEngine = class _SyncEngine {
3527
3568
  for (const e of errors2) {
3528
3569
  erroredIds.add(String(e._id));
3529
3570
  console.error(
3530
- `Sync upload error [${collection}] _id=${e._id}: ${e.error} \u2014 dirty entry will persist until retry`
3571
+ `[SyncEngine] Sync upload error [${collection}] _id=${e._id}: ${e.error} \u2014 dirty entry will persist until retry`
3531
3572
  );
3532
3573
  }
3533
3574
  }
3534
- if (warnings && warnings.length > 0) {
3575
+ if (warnings && warnings.length > 0 && !SUPRESS_DB_WARNINGS) {
3535
3576
  for (const w of warnings) {
3536
- console.warn(
3537
- `Sync upload warning [${collection}] _id=${w._id}: ${w.error}`
3577
+ console.error(
3578
+ `[SyncEngine] DB-WARNING [${collection}] _id=${w._id}: ${w.error}`
3538
3579
  );
3539
3580
  }
3540
3581
  }
@@ -3566,7 +3607,7 @@ var _SyncEngine = class _SyncEngine {
3566
3607
  }
3567
3608
  if (ambiguous.length > 0) {
3568
3609
  console.error(
3569
- `Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
3610
+ `[SyncEngine] Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
3570
3611
  );
3571
3612
  }
3572
3613
  if (allSuccessIds.length > 0) {
@@ -3705,7 +3746,7 @@ var _SyncEngine = class _SyncEngine {
3705
3746
  timestamp: /* @__PURE__ */ new Date()
3706
3747
  });
3707
3748
  } catch (err) {
3708
- console.error("onConflictResolved callback failed:", err);
3749
+ console.error("[SyncEngine] onConflictResolved callback failed:", err);
3709
3750
  }
3710
3751
  }
3711
3752
  return resolved;
@@ -3719,7 +3760,7 @@ var _SyncEngine = class _SyncEngine {
3719
3760
  try {
3720
3761
  fn(info);
3721
3762
  } catch (err) {
3722
- console.error("Callback failed:", err);
3763
+ console.error("[SyncEngine] Callback failed:", err);
3723
3764
  }
3724
3765
  }
3725
3766
  }
@@ -3728,7 +3769,7 @@ var _SyncEngine = class _SyncEngine {
3728
3769
  try {
3729
3770
  this.callbacks.onSyncStart(info);
3730
3771
  } catch (err) {
3731
- console.error("onSyncStart callback failed:", err);
3772
+ console.error("[SyncEngine] onSyncStart callback failed:", err);
3732
3773
  }
3733
3774
  }
3734
3775
  }
@@ -3737,7 +3778,7 @@ var _SyncEngine = class _SyncEngine {
3737
3778
  try {
3738
3779
  this.callbacks.onSyncEnd(info);
3739
3780
  } catch (err) {
3740
- console.error("onSyncEnd callback failed:", err);
3781
+ console.error("[SyncEngine] onSyncEnd callback failed:", err);
3741
3782
  }
3742
3783
  }
3743
3784
  }
@@ -3750,7 +3791,7 @@ var _SyncEngine = class _SyncEngine {
3750
3791
  calledFrom
3751
3792
  });
3752
3793
  } catch (err) {
3753
- console.error("onFindNewerManyCall callback failed:", err);
3794
+ console.error("[SyncEngine] onFindNewerManyCall callback failed:", err);
3754
3795
  }
3755
3796
  }
3756
3797
  }
@@ -3766,7 +3807,7 @@ var _SyncEngine = class _SyncEngine {
3766
3807
  calledFrom
3767
3808
  });
3768
3809
  } catch (err) {
3769
- console.error("onFindNewerManyResult callback failed:", err);
3810
+ console.error("[SyncEngine] onFindNewerManyResult callback failed:", err);
3770
3811
  }
3771
3812
  }
3772
3813
  }
@@ -3779,7 +3820,7 @@ var _SyncEngine = class _SyncEngine {
3779
3820
  calledFrom
3780
3821
  });
3781
3822
  } catch (err) {
3782
- console.error("onServerWriteRequest callback failed:", err);
3823
+ console.error("[SyncEngine] onServerWriteRequest callback failed:", err);
3783
3824
  }
3784
3825
  }
3785
3826
  }
@@ -3794,7 +3835,7 @@ var _SyncEngine = class _SyncEngine {
3794
3835
  calledFrom
3795
3836
  });
3796
3837
  } catch (err) {
3797
- console.error("onServerWriteResult callback failed:", err);
3838
+ console.error("[SyncEngine] onServerWriteResult callback failed:", err);
3798
3839
  }
3799
3840
  }
3800
3841
  }
@@ -3815,7 +3856,7 @@ var _SyncEngine = class _SyncEngine {
3815
3856
  timestamp
3816
3857
  });
3817
3858
  } catch (err) {
3818
- console.error("onServerSyncWrite callback failed:", err);
3859
+ console.error("[SyncEngine] onServerSyncWrite callback failed:", err);
3819
3860
  }
3820
3861
  }
3821
3862
  };
@@ -4080,7 +4121,7 @@ var ServerUpdateHandler = class {
4080
4121
  timestamp: /* @__PURE__ */ new Date()
4081
4122
  });
4082
4123
  } catch (err) {
4083
- console.error("onWsNotification callback failed:", err);
4124
+ console.error("[ServerUpdateHandler] onWsNotification callback failed:", err);
4084
4125
  }
4085
4126
  }
4086
4127
  }
@@ -4162,11 +4203,11 @@ var WakeSyncManager = class {
4162
4203
  timestamp: /* @__PURE__ */ new Date()
4163
4204
  });
4164
4205
  } catch (err) {
4165
- console.error("onWakeSync callback failed:", err);
4206
+ console.error("[WakeSync] onWakeSync callback failed:", err);
4166
4207
  }
4167
4208
  }
4168
4209
  this.deps.sync(`wake-sync:${trigger}`).catch((err) => {
4169
- console.error(`Wake sync (${trigger}) failed:`, err);
4210
+ console.error(`[WakeSync] Wake sync (${trigger}) failed:`, err);
4170
4211
  });
4171
4212
  }, this.debounceMs);
4172
4213
  }
@@ -4235,7 +4276,7 @@ var NetworkStatusManager = class {
4235
4276
  try {
4236
4277
  this.callbacks.onBrowserNetworkChange(info);
4237
4278
  } catch (err) {
4238
- console.error("onBrowserNetworkChange callback failed:", err);
4279
+ console.error("[NetworkStatus] onBrowserNetworkChange callback failed:", err);
4239
4280
  }
4240
4281
  }
4241
4282
  if (finalOnlineState) {
@@ -4243,7 +4284,7 @@ var NetworkStatusManager = class {
4243
4284
  try {
4244
4285
  this.callbacks.onBrowserOnline();
4245
4286
  } catch (err) {
4246
- console.error("onBrowserOnline callback failed:", err);
4287
+ console.error("[NetworkStatus] onBrowserOnline callback failed:", err);
4247
4288
  }
4248
4289
  }
4249
4290
  } else {
@@ -4251,12 +4292,12 @@ var NetworkStatusManager = class {
4251
4292
  try {
4252
4293
  this.callbacks.onBrowserOffline();
4253
4294
  } catch (err) {
4254
- console.error("onBrowserOffline callback failed:", err);
4295
+ console.error("[NetworkStatus] onBrowserOffline callback failed:", err);
4255
4296
  }
4256
4297
  }
4257
4298
  }
4258
4299
  this.deps.setOnline(finalOnlineState).catch((err) => {
4259
- console.error("Failed to set online status:", err);
4300
+ console.error("[NetworkStatus] Failed to set online status:", err);
4260
4301
  });
4261
4302
  }, this.debounceMs);
4262
4303
  }
@@ -4321,14 +4362,14 @@ var _SyncedDb = class _SyncedDb {
4321
4362
  onBecameLeader: () => {
4322
4363
  if (this.initialized && !this.connectionManager.isOnline() && !this.connectionManager.isForcedOffline()) {
4323
4364
  this.connectionManager.tryGoOnline().catch((err) => {
4324
- console.error("tryGoOnline on becameLeader failed:", err);
4365
+ console.error("[SyncedDb] tryGoOnline on becameLeader failed:", err);
4325
4366
  });
4326
4367
  }
4327
4368
  if (config.onBecameLeader) {
4328
4369
  try {
4329
4370
  config.onBecameLeader();
4330
4371
  } catch (err) {
4331
- console.error("onBecameLeader callback failed:", err);
4372
+ console.error("[SyncedDb] onBecameLeader callback failed:", err);
4332
4373
  }
4333
4374
  }
4334
4375
  },
@@ -4429,6 +4470,7 @@ var _SyncedDb = class _SyncedDb {
4429
4470
  collections: this.collections,
4430
4471
  dexieDb: this.dexieDb,
4431
4472
  restInterface: this.restInterface,
4473
+ preprocessDirtyItem: config.preprocessDirtyItem,
4432
4474
  callbacks: {
4433
4475
  onSyncStart: config.onSyncStart ? (info) => config.onSyncStart(__spreadProps(__spreadValues({}, info), { initialSync: !this._lastFullSyncDate })) : void 0,
4434
4476
  onSyncEnd: config.onSyncEnd,
@@ -4631,7 +4673,7 @@ var _SyncedDb = class _SyncedDb {
4631
4673
  try {
4632
4674
  this.onDatabaseCreated();
4633
4675
  } catch (err) {
4634
- console.error("onDatabaseCreated callback failed:", err);
4676
+ console.error("[SyncedDb] onDatabaseCreated callback failed:", err);
4635
4677
  }
4636
4678
  }
4637
4679
  await this.pendingChanges.recoverPendingWrites();
@@ -4664,10 +4706,10 @@ var _SyncedDb = class _SyncedDb {
4664
4706
  try {
4665
4707
  await this.serverUpdateNotifier.connect();
4666
4708
  const ep = (_c = this.serverUpdateNotifier.endpoint) != null ? _c : "unknown";
4667
- console.log(`SyncedDb: ebus-proxy connected to ${ep}`);
4709
+ console.log(`[SyncedDb] SyncedDb: ebus-proxy connected to ${ep}`);
4668
4710
  } catch (err) {
4669
4711
  const ep = (_d = this.serverUpdateNotifier.endpoint) != null ? _d : "unknown";
4670
- console.warn(`SyncedDb: ebus-proxy connection to ${ep} failed`);
4712
+ console.warn(`[SyncedDb] SyncedDb: ebus-proxy connection to ${ep} failed`);
4671
4713
  this.connectionManager.reportInfrastructureError(
4672
4714
  "WEBSOCKET_CONNECTION_FAILED",
4673
4715
  `WebSocket connection to ${ep} failed during initialization`,
@@ -4679,7 +4721,7 @@ var _SyncedDb = class _SyncedDb {
4679
4721
  this.beforeUnloadHandler = () => {
4680
4722
  if (this.initialized && this.pendingChanges.hasPendingChanges()) {
4681
4723
  console.warn(
4682
- `SyncedDb: pending changes not flushed. Call close() before page unload.`
4724
+ `[SyncedDb] SyncedDb: pending changes not flushed. Call close() before page unload.`
4683
4725
  );
4684
4726
  }
4685
4727
  };
@@ -4689,7 +4731,7 @@ var _SyncedDb = class _SyncedDb {
4689
4731
  this.visibilityFlushHandler = () => {
4690
4732
  if (document.visibilityState !== "hidden") return;
4691
4733
  this.flushToServer("visibility-hidden").catch((err) => {
4692
- console.warn("flushToServer on visibility-hidden failed:", err == null ? void 0 : err.message);
4734
+ console.warn("[SyncedDb] flushToServer on visibility-hidden failed:", err == null ? void 0 : err.message);
4693
4735
  });
4694
4736
  };
4695
4737
  document.addEventListener("visibilitychange", this.visibilityFlushHandler);
@@ -4830,6 +4872,11 @@ var _SyncedDb = class _SyncedDb {
4830
4872
  async findById(collection, id, opts) {
4831
4873
  var _a;
4832
4874
  this.assertCollection(collection);
4875
+ if (!id) {
4876
+ const err = new Error(`[SyncedDb] findById ${collection} no id ${id}`);
4877
+ console.error(err);
4878
+ return null;
4879
+ }
4833
4880
  id = this.normalizeId(id, "findById", collection);
4834
4881
  opts = this.resolveOpts(opts);
4835
4882
  if ((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly) {
@@ -5062,7 +5109,7 @@ var _SyncedDb = class _SyncedDb {
5062
5109
  await this.syncEngine.processCollectionServerData(collection, serverData, { source: "refresh" });
5063
5110
  }
5064
5111
  }).catch((err) => {
5065
- console.error(`referToServer failed for ${collection}:`, err);
5112
+ console.error(`[SyncedDb] referToServer failed for ${collection}:`, err);
5066
5113
  });
5067
5114
  }
5068
5115
  /**
@@ -5083,7 +5130,7 @@ var _SyncedDb = class _SyncedDb {
5083
5130
  if (!serverItems || serverItems.length === 0) return;
5084
5131
  await this.syncEngine.processCollectionServerData(collection, serverItems, { source: "incremental" });
5085
5132
  }).catch((err) => {
5086
- console.error(`refreshInBackground failed for ${collection}:`, err);
5133
+ console.error(`[SyncedDb] refreshInBackground failed for ${collection}:`, err);
5087
5134
  });
5088
5135
  }
5089
5136
  async ensureItemsAreLoaded(collection, ids, withDeleted) {
@@ -5129,7 +5176,7 @@ var _SyncedDb = class _SyncedDb {
5129
5176
  id = this.normalizeId(id, "save", collection);
5130
5177
  if ("_id" in update && !update._id) {
5131
5178
  console.error(
5132
- `SyncedDb.save("${collection}", "${String(id)}"): update._id is present but falsy (${JSON.stringify(update._id)}). Stripped from update to prevent overwriting valid id. This is a bug \u2014 the caller should not pass falsy _id in update. Data keys: [${Object.keys(update).join(", ")}]`
5179
+ `[SyncedDb] SyncedDb.save("${collection}", "${String(id)}"): update._id is present but falsy (${JSON.stringify(update._id)}). Stripped from update to prevent overwriting valid id. This is a bug \u2014 the caller should not pass falsy _id in update. Data keys: [${Object.keys(update).join(", ")}]`
5133
5180
  );
5134
5181
  delete update._id;
5135
5182
  }
@@ -5143,7 +5190,7 @@ var _SyncedDb = class _SyncedDb {
5143
5190
  }
5144
5191
  })();
5145
5192
  console.error(
5146
- `SyncedDb.save("${collection}", "${String(id)}"): update._id (${JSON.stringify(update._id)}) does NOT match id (${JSON.stringify(String(id))}). Stripped from update to prevent stuck-dirty bug. The caller likely passed a stale this._id while building update from a freshly-generated record. Data keys: [${updateKeys.join(", ")}]`
5193
+ `[SyncedDb] SyncedDb.save("${collection}", "${String(id)}"): update._id (${JSON.stringify(update._id)}) does NOT match id (${JSON.stringify(String(id))}). Stripped from update to prevent stuck-dirty bug. The caller likely passed a stale this._id while building update from a freshly-generated record. Data keys: [${updateKeys.join(", ")}]`
5147
5194
  );
5148
5195
  this.safeCallback(this.onSaveIdMismatch, {
5149
5196
  collection,
@@ -5159,7 +5206,7 @@ var _SyncedDb = class _SyncedDb {
5159
5206
  const existing = await this.dexieDb.getById(collection, id);
5160
5207
  if (!existing && !((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
5161
5208
  console.warn(
5162
- `SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`
5209
+ `[SyncedDb] SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`
5163
5210
  );
5164
5211
  }
5165
5212
  const fullChanges = __spreadProps(__spreadValues({}, update), { _lastUpdaterId: this.updaterId });
@@ -5209,7 +5256,7 @@ var _SyncedDb = class _SyncedDb {
5209
5256
  const existing = await this.dexieDb.getById(collection, id);
5210
5257
  if (existing && !existing._deleted && !existing._archived) {
5211
5258
  console.warn(
5212
- `SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`
5259
+ `[SyncedDb] SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`
5213
5260
  );
5214
5261
  }
5215
5262
  const insertChanges = __spreadProps(__spreadValues({}, data), { _lastUpdaterId: this.updaterId });
@@ -5348,7 +5395,7 @@ var _SyncedDb = class _SyncedDb {
5348
5395
  this.inMemManager.writeBatch(collection, [{ _id: item.id }], "delete", { source: "incremental" });
5349
5396
  results.push(true);
5350
5397
  } catch (err) {
5351
- console.error(`Failed to hard delete ${String(item.id)}:`, err);
5398
+ console.error(`[SyncedDb] Failed to hard delete ${String(item.id)}:`, err);
5352
5399
  results.push(false);
5353
5400
  }
5354
5401
  }
@@ -5383,7 +5430,7 @@ var _SyncedDb = class _SyncedDb {
5383
5430
  evictionPlan = await this._collectScopeExitPlan("auto");
5384
5431
  } catch (err) {
5385
5432
  console.error(
5386
- "[evict] phase 1 failed (skipping bundled eviction):",
5433
+ "[SyncedDb] [evict] phase 1 failed (skipping bundled eviction):",
5387
5434
  err
5388
5435
  );
5389
5436
  }
@@ -5400,11 +5447,11 @@ var _SyncedDb = class _SyncedDb {
5400
5447
  const now = /* @__PURE__ */ new Date();
5401
5448
  if (!this._lastFullSyncDate) {
5402
5449
  this._setLastInitialSync(now).catch((err) => {
5403
- console.error("Failed to persist lastInitialSync:", err);
5450
+ console.error("[SyncedDb] Failed to persist lastInitialSync:", err);
5404
5451
  });
5405
5452
  }
5406
5453
  this._setLastFullSync(now).catch((err) => {
5407
- console.error("Failed to persist lastFullSync:", err);
5454
+ console.error("[SyncedDb] Failed to persist lastFullSync:", err);
5408
5455
  });
5409
5456
  }
5410
5457
  } catch (err) {
@@ -5423,7 +5470,7 @@ var _SyncedDb = class _SyncedDb {
5423
5470
  );
5424
5471
  await this._persistEvictionTimestamp();
5425
5472
  } catch (err) {
5426
- console.error("[evict] phase 3 failed:", err);
5473
+ console.error("[SyncedDb] [evict] phase 3 failed:", err);
5427
5474
  }
5428
5475
  }
5429
5476
  }
@@ -5730,7 +5777,7 @@ var _SyncedDb = class _SyncedDb {
5730
5777
  serverEvictedCount = serverExits.length;
5731
5778
  } catch (err) {
5732
5779
  console.error(
5733
- `[evict] server-assisted pass failed for ${collection} (proceeding with local-only):`,
5780
+ `[SyncedDb] [evict] server-assisted pass failed for ${collection} (proceeding with local-only):`,
5734
5781
  err
5735
5782
  );
5736
5783
  }
@@ -5847,7 +5894,7 @@ var _SyncedDb = class _SyncedDb {
5847
5894
  } catch (err) {
5848
5895
  serverFailed = true;
5849
5896
  console.error(
5850
- "[evict] server-assisted batch failed (proceeding with local-only):",
5897
+ "[SyncedDb] [evict] server-assisted batch failed (proceeding with local-only):",
5851
5898
  err
5852
5899
  );
5853
5900
  }
@@ -6159,7 +6206,7 @@ var _SyncedDb = class _SyncedDb {
6159
6206
  try {
6160
6207
  fn(info);
6161
6208
  } catch (err) {
6162
- console.error("Callback failed:", err);
6209
+ console.error("[SyncedDb] Callback failed:", err);
6163
6210
  }
6164
6211
  }
6165
6212
  }
@@ -6250,12 +6297,12 @@ var _SyncedDb = class _SyncedDb {
6250
6297
  normalizeId(id, method, collection) {
6251
6298
  if (!id && id !== void 0) {
6252
6299
  console.error(
6253
- `SyncedDb.${method != null ? method : "?"}("${collection != null ? collection : "?"}"): id parameter is falsy (${JSON.stringify(id)}). This is a bug \u2014 the caller must provide a valid _id.`
6300
+ `[SyncedDb] SyncedDb.${method != null ? method : "?"}("${collection != null ? collection : "?"}"): id parameter is falsy (${JSON.stringify(id)}). This is a bug \u2014 the caller must provide a valid _id.`
6254
6301
  );
6255
6302
  }
6256
6303
  if (typeof id === "string" && _SyncedDb.STRINGIFIED_FALSY.has(id)) {
6257
6304
  console.error(
6258
- `SyncedDb.${method != null ? method : "?"}("${collection != null ? collection : "?"}"): id is a stringified falsy value ("${id}"). This is a bug \u2014 a falsy value was coerced to string before being passed as _id.`
6305
+ `[SyncedDb] SyncedDb.${method != null ? method : "?"}("${collection != null ? collection : "?"}"): id is a stringified falsy value ("${id}"). This is a bug \u2014 a falsy value was coerced to string before being passed as _id.`
6259
6306
  );
6260
6307
  }
6261
6308
  return typeof id === "object" && id !== null ? String(id) : id;
@@ -6267,12 +6314,12 @@ var _SyncedDb = class _SyncedDb {
6267
6314
  warnFalsyQueryId(data, method, collection) {
6268
6315
  if ("_id" in data && !data._id) {
6269
6316
  console.error(
6270
- `SyncedDb.${method}("${collection}"): _id is present in query/data but falsy (${JSON.stringify(data._id)}). This is a bug \u2014 _id must be valid when specified. Data keys: [${Object.keys(data).join(", ")}]`
6317
+ `[SyncedDb] SyncedDb.${method}("${collection}"): _id is present in query/data but falsy (${JSON.stringify(data._id)}). This is a bug \u2014 _id must be valid when specified. Data keys: [${Object.keys(data).join(", ")}]`
6271
6318
  );
6272
6319
  }
6273
6320
  if ("_id" in data && typeof data._id === "string" && _SyncedDb.STRINGIFIED_FALSY.has(data._id)) {
6274
6321
  console.error(
6275
- `SyncedDb.${method}("${collection}"): _id is a stringified falsy value ("${data._id}"). This is a bug \u2014 a falsy value was coerced to string before being passed as _id. Data keys: [${Object.keys(data).join(", ")}]`
6322
+ `[SyncedDb] SyncedDb.${method}("${collection}"): _id is a stringified falsy value ("${data._id}"). This is a bug \u2014 a falsy value was coerced to string before being passed as _id. Data keys: [${Object.keys(data).join(", ")}]`
6276
6323
  );
6277
6324
  }
6278
6325
  }
@@ -6285,7 +6332,7 @@ var _SyncedDb = class _SyncedDb {
6285
6332
  ensureId(data, method, collection) {
6286
6333
  if (typeof data._id === "string" && _SyncedDb.STRINGIFIED_FALSY.has(data._id)) {
6287
6334
  console.error(
6288
- `SyncedDb.${method}("${collection}"): _id is a stringified falsy value ("${data._id}"). This is a bug \u2014 a falsy value was coerced to string before being passed as _id. Data keys: [${Object.keys(data).join(", ")}]`
6335
+ `[SyncedDb] SyncedDb.${method}("${collection}"): _id is a stringified falsy value ("${data._id}"). This is a bug \u2014 a falsy value was coerced to string before being passed as _id. Data keys: [${Object.keys(data).join(", ")}]`
6289
6336
  );
6290
6337
  data._id = null;
6291
6338
  }
@@ -6293,7 +6340,7 @@ var _SyncedDb = class _SyncedDb {
6293
6340
  const newId = new ObjectId2().toHexString();
6294
6341
  if ("_id" in data) {
6295
6342
  console.error(
6296
- `SyncedDb.${method}("${collection}"): _id is present but falsy (${JSON.stringify(data._id)}). Replaced with "${newId}". This is a bug \u2014 the caller should provide a valid _id. Data keys: [${Object.keys(data).join(", ")}]`
6343
+ `[SyncedDb] SyncedDb.${method}("${collection}"): _id is present but falsy (${JSON.stringify(data._id)}). Replaced with "${newId}". This is a bug \u2014 the caller should provide a valid _id. Data keys: [${Object.keys(data).join(", ")}]`
6297
6344
  );
6298
6345
  }
6299
6346
  data._id = newId;
@@ -6383,7 +6430,7 @@ var _SyncedDb = class _SyncedDb {
6383
6430
  const drop = (reason) => {
6384
6431
  if (dropSilently) return;
6385
6432
  console.error(
6386
- `SyncedDb.applyDiffLocally: dropping bracket-path diff entry (${reason})`,
6433
+ `[SyncedDb] applyDiffLocally: dropping bracket-path diff entry (${reason})`,
6387
6434
  { collection, _id: String(id), path, value }
6388
6435
  );
6389
6436
  };
@@ -6599,7 +6646,7 @@ var DexieDb = class extends Dexie {
6599
6646
  this.ensureStringId(item);
6600
6647
  if (typeof item._id !== "string" || item._id.length === 0) {
6601
6648
  console.error(
6602
- `DexieDb.saveMany: skipping item with invalid _id in "${collection}"`,
6649
+ `[DexieDb] DexieDb.saveMany: skipping item with invalid _id in "${collection}"`,
6603
6650
  { _id: item._id, _idType: typeof item._id, item }
6604
6651
  );
6605
6652
  continue;
@@ -6612,7 +6659,7 @@ var DexieDb = class extends Dexie {
6612
6659
  } catch (err) {
6613
6660
  const ids = valid.map((it) => String(it._id));
6614
6661
  console.error(
6615
- `DexieDb.saveMany: bulkPut failed for "${collection}" (${valid.length} items):`,
6662
+ `[DexieDb] DexieDb.saveMany: bulkPut failed for "${collection}" (${valid.length} items):`,
6616
6663
  err,
6617
6664
  "_ids:",
6618
6665
  ids
@@ -9567,7 +9614,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
9567
9614
  this.reconnectAttempt = 0;
9568
9615
  this.currentReconnectDelay = this.reconnectDelayMs;
9569
9616
  console.log(
9570
- `Ebus2Proxy connected to ${this.wsUrl} (db: ${this.dbName})`
9617
+ `[Ebus2ProxyNotifier] Ebus2Proxy connected to ${this.wsUrl} (db: ${this.dbName})`
9571
9618
  );
9572
9619
  this.sendSubscribe(`db/${this.dbName}`);
9573
9620
  if (this.subscribeServices) {
@@ -9578,7 +9625,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
9578
9625
  try {
9579
9626
  callback();
9580
9627
  } catch (err) {
9581
- console.error("onWsConnect callback failed:", err);
9628
+ console.error("[Ebus2ProxyNotifier] onWsConnect callback failed:", err);
9582
9629
  }
9583
9630
  }
9584
9631
  }
@@ -9593,7 +9640,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
9593
9640
  try {
9594
9641
  callback(reason);
9595
9642
  } catch (err) {
9596
- console.error("onWsDisconnect callback failed:", err);
9643
+ console.error("[Ebus2ProxyNotifier] onWsDisconnect callback failed:", err);
9597
9644
  }
9598
9645
  }
9599
9646
  }
@@ -9602,7 +9649,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
9602
9649
  }
9603
9650
  }
9604
9651
  handleError(event) {
9605
- console.error("WebSocket error:", event);
9652
+ console.error("[Ebus2ProxyNotifier] WebSocket error:", event);
9606
9653
  }
9607
9654
  handleMessage(event) {
9608
9655
  try {
@@ -9624,11 +9671,11 @@ var Ebus2ProxyServerUpdateNotifier = class {
9624
9671
  this.handlePong();
9625
9672
  break;
9626
9673
  case "error":
9627
- console.error("WebSocket server error:", message.error);
9674
+ console.error("[Ebus2ProxyNotifier] WebSocket server error:", message.error);
9628
9675
  break;
9629
9676
  }
9630
9677
  } catch (err) {
9631
- console.error("Failed to parse WebSocket message:", err);
9678
+ console.error("[Ebus2ProxyNotifier] Failed to parse WebSocket message:", err);
9632
9679
  }
9633
9680
  }
9634
9681
  handleChannelMessage(message) {
@@ -9636,7 +9683,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
9636
9683
  try {
9637
9684
  this.onServicesChange(message.data);
9638
9685
  } catch (err) {
9639
- console.error("onServicesChange callback failed:", err);
9686
+ console.error("[Ebus2ProxyNotifier] onServicesChange callback failed:", err);
9640
9687
  }
9641
9688
  return;
9642
9689
  }
@@ -9653,14 +9700,14 @@ var Ebus2ProxyServerUpdateNotifier = class {
9653
9700
  try {
9654
9701
  this.onWsNotification(payload);
9655
9702
  } catch (err) {
9656
- console.error("onWsNotification callback failed:", err);
9703
+ console.error("[Ebus2ProxyNotifier] onWsNotification callback failed:", err);
9657
9704
  }
9658
9705
  }
9659
9706
  for (const callback of this.callbacks) {
9660
9707
  try {
9661
9708
  callback(payload);
9662
9709
  } catch (err) {
9663
- console.error("ServerUpdateCallback failed:", err);
9710
+ console.error("[Ebus2ProxyNotifier] ServerUpdateCallback failed:", err);
9664
9711
  }
9665
9712
  }
9666
9713
  }
@@ -9679,14 +9726,14 @@ var Ebus2ProxyServerUpdateNotifier = class {
9679
9726
  try {
9680
9727
  callback(this.reconnectAttempt);
9681
9728
  } catch (err) {
9682
- console.error("onWsReconnect callback failed:", err);
9729
+ console.error("[Ebus2ProxyNotifier] onWsReconnect callback failed:", err);
9683
9730
  }
9684
9731
  }
9685
9732
  this.reconnectTimer = setTimeout(() => {
9686
9733
  this.reconnectTimer = void 0;
9687
9734
  if (this.shouldReconnect && !this.forcedOffline) {
9688
9735
  this.createWebSocket().catch((err) => {
9689
- console.error("Reconnection failed:", err);
9736
+ console.error("[Ebus2ProxyNotifier] Reconnection failed:", err);
9690
9737
  this.currentReconnectDelay = Math.min(
9691
9738
  this.currentReconnectDelay * 2,
9692
9739
  this.maxReconnectDelayMs
@@ -9709,7 +9756,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
9709
9756
  const pingMsg = { type: "ping", id: `ping-${Date.now()}` };
9710
9757
  this.ws.send(packr2.pack(preprocessForPack2(pingMsg)));
9711
9758
  this.pongTimer = setTimeout(() => {
9712
- console.warn("Pong timeout - closing WebSocket");
9759
+ console.warn("[Ebus2ProxyNotifier] Pong timeout - closing WebSocket");
9713
9760
  if (this.ws) {
9714
9761
  this.ws.close(4e3, "Pong timeout");
9715
9762
  }
@@ -18,6 +18,7 @@ export declare class SyncEngine implements I_SyncEngine {
18
18
  private readonly restInterface;
19
19
  private readonly callbacks;
20
20
  private readonly deps;
21
+ private readonly preprocessDirtyItem?;
21
22
  constructor(config: SyncEngineConfig);
22
23
  /**
23
24
  * Execute full sync cycle.
@@ -292,6 +292,11 @@ export interface SyncEngineConfig {
292
292
  collections: Map<string, CollectionConfig>;
293
293
  dexieDb: I_DexieDb;
294
294
  restInterface: I_RestInterface;
295
+ /**
296
+ * Optional per-item filter / transform applied before upload. See
297
+ * `SyncedDbConfig.preprocessDirtyItem` for full semantics.
298
+ */
299
+ preprocessDirtyItem?: (item: import("../../types/I_SyncedDb").PreprocessDirtyItem, collection: string) => import("../../types/I_SyncedDb").PreprocessDirtyItem | undefined;
295
300
  callbacks: SyncEngineCallbacks;
296
301
  deps: SyncEngineDeps;
297
302
  }
@@ -117,6 +117,16 @@ export interface ServerWriteResultInfo {
117
117
  /** Where sync was called from (for debugging) */
118
118
  calledFrom?: string;
119
119
  }
120
+ /**
121
+ * Item handed to `preprocessDirtyItem` immediately before it would be sent
122
+ * to the server. Carries the per-record `_id` and the wire-form `update`
123
+ * payload (paths → values, with `_ts` / `_rev` already stripped and
124
+ * legacy `arr.0.field` paths fixed).
125
+ */
126
+ export interface PreprocessDirtyItem {
127
+ _id: Id;
128
+ update: Partial<LocalDbEntity>;
129
+ }
120
130
  /**
121
131
  * Callback payload for a single `updateCollections` round-trip — fires
122
132
  * once per call with both the request and either the response OR the
@@ -440,6 +450,33 @@ export interface SyncedDbConfig {
440
450
  debounceDexieWritesMs?: number;
441
451
  /** Debounce čas za pošiljanje na REST v ms (default: 1000) - po uspešnem zapisu v Dexie */
442
452
  debounceRestWritesMs?: number;
453
+ /**
454
+ * Per-item filter / transform applied to each dirty payload just before it
455
+ * is sent to the server. Runs after `_ts`/`_rev` strip and legacy-path
456
+ * fixup, so `item.update` already reflects the wire-shape that would
457
+ * normally be uploaded.
458
+ *
459
+ * Return values:
460
+ * - the same or a modified `{ _id, update }` → use it for upload
461
+ * - `undefined` → SKIP upload of this item for the current cycle; the
462
+ * dirty change is **left untouched** so the next sync cycle
463
+ * re-runs preprocessing
464
+ * - throw → log `console.error` and SKIP upload for this item; the
465
+ * dirty change is **left untouched** as well (same as `undefined`,
466
+ * just additionally surfaced as an error)
467
+ *
468
+ * Neither return path clears or modifies the underlying dirty change —
469
+ * use the regular `save` / `insert` / `upsert` API to overwrite or
470
+ * `hardDeleteOne` to remove records.
471
+ *
472
+ * Items that survive are batched into a single `updateCollections` call
473
+ * per collection.
474
+ *
475
+ * @param item The candidate payload (id + wire-form update)
476
+ * @param collection The collection name the item belongs to
477
+ * @returns Modified item, the same item, or `undefined` to skip this cycle
478
+ */
479
+ preprocessDirtyItem?: (item: PreprocessDirtyItem, collection: string) => PreprocessDirtyItem | undefined;
443
480
  /**
444
481
  * Callback fired on each sync failure. Unlike the removed `onForcedOffline`,
445
482
  * this does NOT mutate online state — the library keeps trying on the next
@@ -84,9 +84,15 @@ export declare function deleteByPath(target: any, path: string): boolean;
84
84
  * wrapped element[0], not into the array itself. The pending insert
85
85
  * absorbs the sub-field edit.
86
86
  *
87
- * 2. New path is an ANCESTOR of existing keys (e.g. existing has "koraki.0.diag",
88
- * new is "koraki" with full array): remove the now-redundant descendants and
89
- * set the parent path. The new full value supersedes any field-level deltas.
87
+ * 2. New path is an ANCESTOR of existing keys.
88
+ * - Plain ancestor (e.g. existing "koraki.0.diag", new "koraki" with full
89
+ * array): drop the descendants; the new full value supersedes any
90
+ * field-level deltas.
91
+ * - TERMINAL-bracket ancestor whose `newValue` is element-shaped
92
+ * (`arr[a].sub[b]` with `[<el>]` or plain object): LAYER pending
93
+ * descendant sub-field edits INTO `newValue` before dropping them,
94
+ * so the element-level write absorbs the prior sub-field-level
95
+ * writes instead of silently overwriting them.
90
96
  *
91
97
  * 3. New path is ORTHOGONAL to existing keys: simple set (Object.assign-equivalent).
92
98
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.171",
3
+ "version": "0.1.173",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",