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 +74 -0
- package/dist/index.js +75 -16
- package/dist/src/db/sync/SyncEngine.d.ts +1 -0
- package/dist/src/db/types/managers.d.ts +5 -0
- package/dist/src/types/I_SyncedDb.d.ts +37 -0
- package/package.json +1 -1
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 =
|
|
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:
|
|
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
|