cry-synced-db-client 0.1.161 → 0.1.163

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
@@ -1,5 +1,192 @@
1
1
  # Versions
2
2
 
3
+ ## Unreleased
4
+
5
+ ### `onServerSyncWrite` callback
6
+
7
+ Single-shot callback that fires once per `restInterface.updateCollections`
8
+ round-trip with both the request payload and either the server response
9
+ or a thrown error in one payload. Convenience over correlating
10
+ `onServerWriteRequest` + `onServerWriteResult`:
11
+
12
+ ```typescript
13
+ const syncedDb = new SyncedDb({
14
+ // ...
15
+ onServerSyncWrite: (info) => {
16
+ if (info.error) {
17
+ console.error('upload failed', info.error.message, info.error.stack);
18
+ } else {
19
+ logToSyslog({ request: info.request, response: info.response });
20
+ }
21
+ },
22
+ });
23
+ ```
24
+
25
+ `ServerSyncWriteInfo`: `{ request, response?, error?, durationMs, calledFrom?, timestamp }`.
26
+ `response` is `undefined` on transport / timeout / network failure;
27
+ `error` is `{message, name?, stack?}` (serialized — safe for logging).
28
+
29
+ ### Local Dexie write uses diff-applied merged row (parity fix)
30
+
31
+ `SyncedDb.save()` now schedules the **mongo-symmetric `merged` row** to
32
+ Dexie via `pendingChanges`, not the raw partial `update`. Without this,
33
+ `Dexie.table.update(key, partialUpdate)` would replace top-level array
34
+ fields wholesale (postavke, koraki, etc.) and drop element sub-fields
35
+ the caller didn't mention — same data-loss class the in-mem path
36
+ already protected against. New regression test
37
+ `test/node-only/insertUpdateRacunPostavke.test.ts` (real cry-db) verifies
38
+ parity across **mongo + Dexie + in-mem** simultaneously after a partial
39
+ `save({ postavke: [{_id: "P1", kolicina: 2}] })` over an existing
40
+ `postavke[0] = {_id: "P1", opis: "postavka 1", kolicina: 1}`.
41
+
42
+ ## 0.1.162
43
+
44
+ ### Bracket-by-_id paths flow through server unchanged
45
+
46
+ `SyncEngine.uploadDirtyItems` no longer calls `translateBracketPathsToIndex`.
47
+ Bracket paths (`arr[<_id>].field`) leave the client as-is; cry-db ≥ 2.4.33
48
+ translates them server-side to mongo `arr.$[<filterId>].field` +
49
+ `arrayFilters` atomically against the **live document**.
50
+
51
+ This eliminates the race window where another writer's
52
+ reorder/insert/delete shifted the targeted index between the client's
53
+ read and the server's apply. Translation against a stale Dexie snapshot
54
+ (client-side) is replaced by atomic resolution against mongo's current
55
+ state (server-side).
56
+
57
+ ### `undefined` value = delete the field (cry-db `$unset` convention)
58
+
59
+ App code now signals "delete this field" by setting the value to
60
+ `undefined` in `update`. End-to-end behavior:
61
+
62
+ - `computeDiff` preserves `undefined` in the emitted diff (no longer
63
+ normalizes to `null`)
64
+ - Dirty change in Dexie carries `undefined` (Dexie's structured clone
65
+ preserves it)
66
+ - REST upload via `msgpackr` (`structuredClone: true`) preserves
67
+ `undefined` in the binary frame
68
+ - Server cry-db `_processUpdateObject` routes `key: undefined` to
69
+ `$unset[key] = true`; mongo physically removes the field
70
+ - Local in-mem and Dexie: `applyDiffLocally` detects `undefined`
71
+ values in the diff and uses `deleteByPath` (mongo-symmetric)
72
+
73
+ Caller convention: read with strict `racun.field === undefined` is
74
+ now safe (the field is genuinely absent, not stored as `null`).
75
+
76
+ ```typescript
77
+ // remove `navodilo` from the racun
78
+ await syncedDb.save("racuni", id, { navodilo: undefined });
79
+ ```
80
+
81
+ ### Diff-aware local apply (`applyDiffLocally`)
82
+
83
+ `SyncedDb.save()` no longer uses shallow `{ ...currentMem, ...update }`
84
+ spread that would replace top-level array/object fields wholesale and
85
+ drop nested fields the caller's `update` didn't mention.
86
+
87
+ Replaced with `applyDiffLocally(base, diff, id)`:
88
+ 1. Deep-clone `base` (currentMem ?? existing) via `safeDeepClone`
89
+ (handles Date and `ObjectId`-like values; avoids `structuredClone`
90
+ throwing on bson class instances)
91
+ 2. For each `(path, value)` in `diff`: `setByPath` if value is
92
+ defined, `deleteByPath` if value is `undefined`
93
+ 3. Result matches what server-side `$set` + `$unset` would produce
94
+
95
+ `deleteByPath` is now a sibling export of `setByPath` in `computeDiff.ts`.
96
+
97
+ ## 0.1.161
98
+
99
+ ### Don't auto-stamp `_id` on bracket-array elements
100
+
101
+ Reverted automatic `_id` stamping for objects appearing as array elements.
102
+ If an array of objects lacks `_id`, the caller's element shape is now
103
+ preserved. This allows callers to mix:
104
+ - Whole-element bracket replace: `update.postavke = [{...}]`
105
+ - Bracket-by-_id sub-field path: `update["postavke[<id>].field"] = value`
106
+ in the same payload without the client mutating element identity.
107
+
108
+ ## 0.1.160
109
+
110
+ ### Composition changes emit precise paths (not full-array replace)
111
+
112
+ `computeArrayDiff` strategy matrix is expanded:
113
+
114
+ | Composition | Strategy |
115
+ |---|---|
116
+ | Empty → empty | no diff |
117
+ | Same `_id` set, same order | element-wise: `arr[<id>].field` |
118
+ | Same `_id` set, different order | full replace `arr` |
119
+ | Different `_id` set | mixed: `$pull` + `$push` + sub-field |
120
+
121
+ For composition changes, `computeDiff` now emits:
122
+ - **Removed `_id`**: `arr[<id>] = undefined` (server: `$pull`)
123
+ - **Added `_id`**: `arr[<id>] = [element]` (server: `$concatArrays + $filter`)
124
+ - **Retained `_id`**: element-wise sub-field via `arr[<id>].field`
125
+
126
+ Pre-fix, composition change emitted full-array replace at `basePath`,
127
+ which `mergeDirtyPath` Case 2 then dropped pending sub-field paths on
128
+ the same parent — race-y data-loss strip pattern visible in production.
129
+
130
+ ## 0.1.159
131
+
132
+ ### Self-echo WS suppression for `_rev <= local._rev`
133
+
134
+ `ServerUpdateHandler` now ignores its own WS notifications when
135
+ `_lastUpdaterId === self.updaterId AND _rev <= local._rev`. Three sub-cases:
136
+
137
+ | `_rev` relation | Action |
138
+ |---|---|
139
+ | `=== local._rev + 1` | Bump meta to match server, clear dirty (existing loopback case) |
140
+ | `=== local._rev` | WS arrived AFTER post-upload writeback. Local is already at server's rev — drop the WS payload entirely. |
141
+ | `< local._rev` | Stale duplicate / out-of-order delivery. Never downgrade local. |
142
+
143
+ In B and C, `mergeLocalWithDelta` and `writeToInMemBatch` are skipped
144
+ entirely. Production data-loss case (2026-05-09): page re-mount loaded
145
+ older snapshot because a self-echo WS arrived after writeback and
146
+ overwrote in-mem with the server's `$set`-iterated copy of postavke
147
+ (missing freshly-set `pop` and `navodilo` fields). Now in-mem is preserved.
148
+
149
+ ## 0.1.158
150
+
151
+ Internal version bump consolidating 0.1.157 fixes for production publish.
152
+
153
+ ## 0.1.157
154
+
155
+ ### Recursive server-managed metadata strip at upload boundary
156
+
157
+ `stripServerManagedFromChanges(changes)` walks dirty payload paths AND
158
+ values, removing every `_ts` / `_rev` / `_csq` at every depth (including
159
+ inside nested arrays and objects). Replaces SyncEngine's prefix-only
160
+ filter (`startsWith('_ts.')`) which missed deep paths like
161
+ `_redundanca.terapije[<id>]._rev` and silent-rejected uploads on the
162
+ server.
163
+
164
+ ### `fixDotnetArrays` recognizes bracket-by-_id paths and nested top-level array keys
165
+
166
+ When a top-level full array key (e.g. `zaracunaj` or
167
+ `_redundanca.terapije`) coexists with element paths (`arr[<id>].field`
168
+ or `arr.0.field`), the element paths are dropped to prevent mongo
169
+ "path conflict" rejection (`Updating the path 'X' would create a
170
+ conflict at 'Y'`). Now also catches dot-key array names like
171
+ `_redundanca.terapije` and bracket-by-_id paths.
172
+
173
+ ### Auto-stamp `_id` on every plain object inside arrays (`SyncedDb.ensureNestedIds`)
174
+
175
+ Write operations (save / upsert / insert) recursively walk the data
176
+ tree and stamp `_id = new ObjectId().toHexString()` on every plain
177
+ object that appears as an array element but lacks one. This keeps
178
+ every save on the element-wise bracket-by-_id path
179
+ (`computeDiff`'s `allElementsHaveId` check).
180
+
181
+ (Note: 0.1.161 reverted this — see entry above.)
182
+
183
+ ### Production stuck-dirty regression test
184
+
185
+ `test/stuckDirtyDirectInject.test.ts` injects an exact production
186
+ stuck-dirty payload (mixing top-level full arrays with bracket paths)
187
+ into Dexie's `_dirty_changes` and asserts upload succeeds without
188
+ mongo path-conflict errors via a `MongoFaithfulRestInterface` mock.
189
+
3
190
  ## 0.1.156
4
191
 
5
192
  Three related fixes targeting **dirty-payload metadata leak** and
package/dist/index.js CHANGED
@@ -2888,11 +2888,6 @@ function fixDotnetArrays(changes, serverRev, baseRev) {
2888
2888
  return cleaned;
2889
2889
  }
2890
2890
 
2891
- // src/utils/translateBracketPaths.ts
2892
- function translateBracketPathsToIndex(changes, _entity) {
2893
- return changes;
2894
- }
2895
-
2896
2891
  // src/utils/stripServerManaged.ts
2897
2892
  var SERVER_MANAGED_KEYS2 = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
2898
2893
  function isObjectIdLike3(v) {
@@ -3142,7 +3137,7 @@ var _SyncEngine = class _SyncEngine {
3142
3137
  const delta = dirtyChangesMap.get(String(fullItem._id));
3143
3138
  if (delta) {
3144
3139
  const currentServerRev = typeof fullItem._rev === "number" ? fullItem._rev : void 0;
3145
- updates.push({ _id: fullItem._id, delta, currentServerRev, fullItem });
3140
+ updates.push({ _id: fullItem._id, delta, currentServerRev });
3146
3141
  } else {
3147
3142
  skipped.push({ _id: String(fullItem._id), reason: "no-delta-for-fullitem" });
3148
3143
  }
@@ -3209,10 +3204,9 @@ var _SyncEngine = class _SyncEngine {
3209
3204
  item.currentServerRev,
3210
3205
  dirtyBaseRev
3211
3206
  );
3212
- const cleanedChanges = translateBracketPathsToIndex(fixed, item.fullItem);
3213
3207
  return {
3214
3208
  _id: item._id,
3215
- update: cleanedChanges
3209
+ update: fixed
3216
3210
  };
3217
3211
  }),
3218
3212
  deletes: []
@@ -3224,6 +3218,7 @@ var _SyncEngine = class _SyncEngine {
3224
3218
  }
3225
3219
  this.callOnServerWriteRequest(collectionBatches, calledFrom);
3226
3220
  const writeStartTime = Date.now();
3221
+ const writeStartedAt = /* @__PURE__ */ new Date();
3227
3222
  let results;
3228
3223
  try {
3229
3224
  results = await this.deps.withSyncTimeout(
@@ -3231,8 +3226,24 @@ var _SyncEngine = class _SyncEngine {
3231
3226
  "updateCollections"
3232
3227
  );
3233
3228
  this.callOnServerWriteResult(results, writeStartTime, true, calledFrom);
3229
+ this.callOnServerSyncWrite(
3230
+ collectionBatches,
3231
+ results,
3232
+ void 0,
3233
+ writeStartTime,
3234
+ writeStartedAt,
3235
+ calledFrom
3236
+ );
3234
3237
  } catch (err) {
3235
3238
  this.callOnServerWriteResult([], writeStartTime, false, calledFrom, err);
3239
+ this.callOnServerSyncWrite(
3240
+ collectionBatches,
3241
+ void 0,
3242
+ err,
3243
+ writeStartTime,
3244
+ writeStartedAt,
3245
+ calledFrom
3246
+ );
3236
3247
  throw err;
3237
3248
  }
3238
3249
  let sentCount = 0;
@@ -3634,6 +3645,26 @@ var _SyncEngine = class _SyncEngine {
3634
3645
  }
3635
3646
  }
3636
3647
  }
3648
+ callOnServerSyncWrite(request, response, error, startTime, timestamp, calledFrom) {
3649
+ if (!this.callbacks.onServerSyncWrite) return;
3650
+ try {
3651
+ const errInfo = error ? {
3652
+ message: error instanceof Error ? error.message : String(error),
3653
+ name: error instanceof Error ? error.name : void 0,
3654
+ stack: error instanceof Error ? error.stack : void 0
3655
+ } : void 0;
3656
+ this.callbacks.onServerSyncWrite({
3657
+ request,
3658
+ response,
3659
+ error: errInfo,
3660
+ durationMs: Date.now() - startTime,
3661
+ calledFrom,
3662
+ timestamp
3663
+ });
3664
+ } catch (err) {
3665
+ console.error("onServerSyncWrite callback failed:", err);
3666
+ }
3667
+ }
3637
3668
  };
3638
3669
  // ============================================================
3639
3670
  // Private: Process Incoming Data
@@ -4254,6 +4285,7 @@ var _SyncedDb = class _SyncedDb {
4254
4285
  onConflictResolved: config.onConflictResolved,
4255
4286
  onServerWriteRequest: config.onServerWriteRequest,
4256
4287
  onServerWriteResult: config.onServerWriteResult,
4288
+ onServerSyncWrite: config.onServerSyncWrite,
4257
4289
  onFindNewerManyCall: config.onFindNewerManyCall,
4258
4290
  onFindNewerManyResult: config.onFindNewerManyResult,
4259
4291
  onUploadSkip: config.onUploadSkip
@@ -4889,7 +4921,6 @@ var _SyncedDb = class _SyncedDb {
4889
4921
  });
4890
4922
  delete update._id;
4891
4923
  }
4892
- _SyncedDb.ensureNestedIds(update);
4893
4924
  update = _SyncedDb.stringifyObjectIds(update);
4894
4925
  const existing = await this.dexieDb.getById(collection, id);
4895
4926
  if (!existing && !((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
@@ -4905,10 +4936,6 @@ var _SyncedDb = class _SyncedDb {
4905
4936
  diff,
4906
4937
  { _ts: existing == null ? void 0 : existing._ts, _rev: existing == null ? void 0 : existing._rev }
4907
4938
  );
4908
- const newData = __spreadProps(__spreadValues({}, update), {
4909
- _lastUpdaterId: this.updaterId
4910
- });
4911
- this.pendingChanges.schedule(collection, id, newData, 0, "save");
4912
4939
  const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
4913
4940
  const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
4914
4941
  const merged = _SyncedDb.applyDiffLocally(
@@ -4916,6 +4943,7 @@ var _SyncedDb = class _SyncedDb {
4916
4943
  diff,
4917
4944
  id
4918
4945
  );
4946
+ this.pendingChanges.schedule(collection, id, merged, 0, "save");
4919
4947
  if (!isWriteOnly && !(existing == null ? void 0 : existing._deleted) && !(existing == null ? void 0 : existing._archived)) {
4920
4948
  this.inMemManager.writeBatch(collection, [merged], "upsert", { source: "incremental" });
4921
4949
  }
@@ -4924,7 +4952,6 @@ var _SyncedDb = class _SyncedDb {
4924
4952
  async upsert(collection, query, update) {
4925
4953
  this.assertCollection(collection);
4926
4954
  this.ensureId(update, "upsert", collection);
4927
- _SyncedDb.ensureNestedIds(update);
4928
4955
  query = _SyncedDb.stringifyObjectIds(query);
4929
4956
  update = _SyncedDb.stringifyObjectIds(update);
4930
4957
  const existing = await this.findOne(collection, query);
@@ -4938,7 +4965,6 @@ var _SyncedDb = class _SyncedDb {
4938
4965
  var _a;
4939
4966
  this.assertCollection(collection);
4940
4967
  this.ensureId(data, "insert", collection);
4941
- _SyncedDb.ensureNestedIds(data);
4942
4968
  data = _SyncedDb.stringifyObjectIds(data);
4943
4969
  const unsetPaths = collectUnsetPaths(data);
4944
4970
  const id = String(data._id);
@@ -6100,48 +6126,6 @@ var _SyncedDb = class _SyncedDb {
6100
6126
  }
6101
6127
  return out;
6102
6128
  }
6103
- /**
6104
- * Recursively walk `value` and ensure every plain object that appears
6105
- * as an element of an array carries an `_id`. Missing `_id`s are
6106
- * generated as `new ObjectId().toHexString()` (string, not BSON instance).
6107
- *
6108
- * Mutates the input tree in place — caller sees the freshly-assigned ids
6109
- * (matches the pattern of `ensureId`).
6110
- *
6111
- * Why: `computeDiff` falls back to a full-array replace whenever any
6112
- * element of an array of objects lacks `_id` (see `allElementsHaveId`).
6113
- * That defeats element-wise `arr[<_id>].field` paths and re-introduces
6114
- * the stale-array-overwrite bug. Stamping ids upfront keeps every save
6115
- * on the per-element bracket path.
6116
- *
6117
- * Skipped:
6118
- * - primitives (numbers, strings, booleans, null, undefined)
6119
- * - `Date`, `ObjectId`-like values
6120
- * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
6121
- * itself is handled by `ensureId`)
6122
- */
6123
- static ensureNestedIds(value) {
6124
- if (value === null || value === void 0) return;
6125
- if (typeof value !== "object") return;
6126
- if (value instanceof Date) return;
6127
- if (_SyncedDb.isObjectIdLike(value)) return;
6128
- if (Array.isArray(value)) {
6129
- for (const element of value) {
6130
- if (element !== null && typeof element === "object" && !Array.isArray(element) && !(element instanceof Date) && !_SyncedDb.isObjectIdLike(element)) {
6131
- if (element._id == null || element._id === "") {
6132
- element._id = new ObjectId2().toHexString();
6133
- } else if (_SyncedDb.isObjectIdLike(element._id)) {
6134
- element._id = String(element._id);
6135
- }
6136
- }
6137
- _SyncedDb.ensureNestedIds(element);
6138
- }
6139
- return;
6140
- }
6141
- for (const key of Object.keys(value)) {
6142
- _SyncedDb.ensureNestedIds(value[key]);
6143
- }
6144
- }
6145
6129
  /**
6146
6130
  * Asserts write-only collection has online connectivity for reads.
6147
6131
  * @throws Error if offline
@@ -427,27 +427,6 @@ export declare class SyncedDb implements I_SyncedDb {
427
427
  * instances like bson `ObjectId`.
428
428
  */
429
429
  private static safeDeepClone;
430
- /**
431
- * Recursively walk `value` and ensure every plain object that appears
432
- * as an element of an array carries an `_id`. Missing `_id`s are
433
- * generated as `new ObjectId().toHexString()` (string, not BSON instance).
434
- *
435
- * Mutates the input tree in place — caller sees the freshly-assigned ids
436
- * (matches the pattern of `ensureId`).
437
- *
438
- * Why: `computeDiff` falls back to a full-array replace whenever any
439
- * element of an array of objects lacks `_id` (see `allElementsHaveId`).
440
- * That defeats element-wise `arr[<_id>].field` paths and re-introduces
441
- * the stale-array-overwrite bug. Stamping ids upfront keeps every save
442
- * on the per-element bracket path.
443
- *
444
- * Skipped:
445
- * - primitives (numbers, strings, booleans, null, undefined)
446
- * - `Date`, `ObjectId`-like values
447
- * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
448
- * itself is handled by `ensureId`)
449
- */
450
- private static ensureNestedIds;
451
430
  /**
452
431
  * Asserts write-only collection has online connectivity for reads.
453
432
  * @throws Error if offline
@@ -63,4 +63,5 @@ export declare class SyncEngine implements I_SyncEngine {
63
63
  private callOnFindNewerManyResult;
64
64
  private callOnServerWriteRequest;
65
65
  private callOnServerWriteResult;
66
+ private callOnServerSyncWrite;
66
67
  }
@@ -7,7 +7,7 @@ import type { I_DexieDb, SyncMeta, MetaUpdateBroadcast } from "../../types/I_Dex
7
7
  import type { I_InMemDb, SyncSource } from "../../types/I_InMemDb";
8
8
  import type { I_RestInterface } from "../../types/I_RestInterface";
9
9
  import type { PublishDataPayload } from "../../types/PublishRevsPayload";
10
- import type { CollectionConfig, SyncInfo, ConflictResolutionReport, ServerWriteRequestInfo, ServerWriteResultInfo, FindNewerManyCallInfo, FindNewerManyResultInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, CrossTabSyncInfo } from "../../types/I_SyncedDb";
10
+ import type { CollectionConfig, SyncInfo, ConflictResolutionReport, ServerWriteRequestInfo, ServerWriteResultInfo, ServerSyncWriteInfo, FindNewerManyCallInfo, FindNewerManyResultInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, CrossTabSyncInfo } from "../../types/I_SyncedDb";
11
11
  import type { PendingChange, UploadResult } from "./internal";
12
12
  export interface LeaderElectionCallbacks {
13
13
  onBecameLeader?: () => void;
@@ -259,6 +259,7 @@ export interface SyncEngineCallbacks {
259
259
  onConflictResolved?: (report: ConflictResolutionReport) => void;
260
260
  onServerWriteRequest?: (info: ServerWriteRequestInfo) => void;
261
261
  onServerWriteResult?: (info: ServerWriteResultInfo) => void;
262
+ onServerSyncWrite?: (info: ServerSyncWriteInfo) => void;
262
263
  onFindNewerManyCall?: (info: FindNewerManyCallInfo) => void;
263
264
  onFindNewerManyResult?: (info: FindNewerManyResultInfo) => void;
264
265
  onUploadSkip?: (info: import("../../types/I_SyncedDb").UploadSkipInfo) => void;
@@ -117,6 +117,41 @@ export interface ServerWriteResultInfo {
117
117
  /** Where sync was called from (for debugging) */
118
118
  calledFrom?: string;
119
119
  }
120
+ /**
121
+ * Callback payload for a single `updateCollections` round-trip — fires
122
+ * once per call with both the request and either the response OR the
123
+ * error description in a single payload.
124
+ *
125
+ * Convenience over `onServerWriteRequest` + `onServerWriteResult`: lets
126
+ * consumers route the entire request/response pair (or a thrown error)
127
+ * to syslog / observability with a single hook, without correlating
128
+ * across the two events.
129
+ */
130
+ export interface ServerSyncWriteInfo {
131
+ /** Batches sent to the server (the request payload). */
132
+ request: CollectionUpdateRequest<any>[];
133
+ /**
134
+ * Per-collection results from the server — present when the call
135
+ * succeeded (no transport / timeout / network error). Individual
136
+ * items inside the result may still report per-record `errors`.
137
+ */
138
+ response?: CollectionUpdateResult[];
139
+ /**
140
+ * Populated when `restInterface.updateCollections` threw or timed
141
+ * out. `response` is `undefined` in that case.
142
+ */
143
+ error?: {
144
+ message: string;
145
+ name?: string;
146
+ stack?: string;
147
+ };
148
+ /** Round-trip duration in ms. */
149
+ durationMs: number;
150
+ /** Where sync was called from (for debugging). */
151
+ calledFrom?: string;
152
+ /** Wall-clock time when the round-trip started. */
153
+ timestamp: Date;
154
+ }
120
155
  /**
121
156
  * Callback payload for findNewerMany calls
122
157
  */
@@ -459,6 +494,14 @@ export interface SyncedDbConfig {
459
494
  onServerWriteRequest?: (info: ServerWriteRequestInfo) => void;
460
495
  /** Callback after receiving result from server (updateCollections) */
461
496
  onServerWriteResult?: (info: ServerWriteResultInfo) => void;
497
+ /**
498
+ * Single-shot callback for each `updateCollections` round-trip — fires
499
+ * once with both the request payload AND either the server response or
500
+ * the thrown error (`error.message`/`name`/`stack`). Convenient when a
501
+ * consumer wants to log the full request/response pair (e.g. to syslog)
502
+ * without correlating across `onServerWriteRequest` + `onServerWriteResult`.
503
+ */
504
+ onServerSyncWrite?: (info: ServerSyncWriteInfo) => void;
462
505
  /** Callback when findNewerMany is called */
463
506
  onFindNewerManyCall?: (info: FindNewerManyCallInfo) => void;
464
507
  /** Callback when findNewerMany completes */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.161",
3
+ "version": "0.1.163",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",