cry-synced-db-client 0.1.172 → 0.1.174

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
@@ -3012,7 +3012,7 @@ function stripServerManagedFromChanges(changes) {
3012
3012
  }
3013
3013
 
3014
3014
  // src/db/sync/SyncEngine.ts
3015
- var SUPRESS_DB_WARNINGS = false;
3015
+ var SUPRESS_DB_WARNINGS = true;
3016
3016
  var _SyncEngine = class _SyncEngine {
3017
3017
  constructor(config) {
3018
3018
  this.tenant = config.tenant;
@@ -3022,6 +3022,7 @@ var _SyncEngine = class _SyncEngine {
3022
3022
  this.restInterface = config.restInterface;
3023
3023
  this.callbacks = config.callbacks;
3024
3024
  this.deps = config.deps;
3025
+ this.preprocessDirtyItem = config.preprocessDirtyItem;
3025
3026
  }
3026
3027
  /**
3027
3028
  * Execute full sync cycle.
@@ -3279,24 +3280,43 @@ var _SyncEngine = class _SyncEngine {
3279
3280
  console.error("[SyncEngine] onUploadSkip callback failed:", err);
3280
3281
  }
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
+ }
3312
+ }
3313
+ mappedUpdates.push(candidate);
3314
+ }
3315
+ if (mappedUpdates.length === 0) continue;
3282
3316
  collectionBatches.push([{
3283
3317
  collection: collectionName,
3284
3318
  batch: {
3285
- updates: updates.map((item) => {
3286
- const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
3287
- const stripped = stripServerManagedFromChanges(
3288
- item.delta
3289
- );
3290
- const fixed = fixDotnetArrays(
3291
- stripped,
3292
- item.currentServerRev,
3293
- dirtyBaseRev
3294
- );
3295
- return {
3296
- _id: item._id,
3297
- update: fixed
3298
- };
3299
- }),
3319
+ updates: mappedUpdates,
3300
3320
  deletes: []
3301
3321
  }
3302
3322
  }]);
@@ -4450,6 +4470,7 @@ var _SyncedDb = class _SyncedDb {
4450
4470
  collections: this.collections,
4451
4471
  dexieDb: this.dexieDb,
4452
4472
  restInterface: this.restInterface,
4473
+ preprocessDirtyItem: config.preprocessDirtyItem,
4453
4474
  callbacks: {
4454
4475
  onSyncStart: config.onSyncStart ? (info) => config.onSyncStart(__spreadProps(__spreadValues({}, info), { initialSync: !this._lastFullSyncDate })) : void 0,
4455
4476
  onSyncEnd: config.onSyncEnd,
@@ -6241,6 +6262,44 @@ var _SyncedDb = class _SyncedDb {
6241
6262
  }
6242
6263
  }
6243
6264
  });
6265
+ const dirty = await this.dexieDb.getDirty(name);
6266
+ if (dirty.length > 0) {
6267
+ const itemById = /* @__PURE__ */ new Map();
6268
+ for (let i = 0; i < allItems.length; i++) {
6269
+ itemById.set(String(allItems[i]._id), allItems[i]);
6270
+ }
6271
+ let orphanCount = 0;
6272
+ for (const dirtyItem of dirty) {
6273
+ const id = dirtyItem._id;
6274
+ if (id == null) continue;
6275
+ const idStr = String(id);
6276
+ const existing = itemById.get(idStr);
6277
+ const diff = {};
6278
+ for (const key of Object.keys(dirtyItem)) {
6279
+ if (key === "_id" || key === "_ts" || key === "_rev") continue;
6280
+ diff[key] = dirtyItem[key];
6281
+ }
6282
+ const merged = _SyncedDb.applyDiffLocally(
6283
+ existing != null ? existing : null,
6284
+ diff,
6285
+ id,
6286
+ name
6287
+ );
6288
+ if (merged._deleted || merged._archived) {
6289
+ itemById.delete(idStr);
6290
+ continue;
6291
+ }
6292
+ if (!existing) orphanCount++;
6293
+ itemById.set(idStr, merged);
6294
+ }
6295
+ if (orphanCount > 0) {
6296
+ console.warn(
6297
+ `[SyncedDb] init(${name}): ${orphanCount} dirty record(s) had no matching Dexie main row \u2014 included in in-mem pending next sync.`
6298
+ );
6299
+ }
6300
+ allItems.length = 0;
6301
+ for (const item of itemById.values()) allItems.push(item);
6302
+ }
6244
6303
  this.inMemManager.initCollection(name, allItems);
6245
6304
  const meta = await this.dexieDb.getSyncMeta(name);
6246
6305
  if (meta) {
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.172",
3
+ "version": "0.1.174",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",