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 +187 -0
- package/dist/index.js +41 -57
- package/dist/src/db/SyncedDb.d.ts +0 -21
- package/dist/src/db/sync/SyncEngine.d.ts +1 -0
- package/dist/src/db/types/managers.d.ts +2 -1
- package/dist/src/types/I_SyncedDb.d.ts +43 -0
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
|
@@ -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 */
|