cry-synced-db-client 0.1.150 → 0.1.155
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 +45 -0
- package/dist/index.js +272 -47
- package/dist/src/db/SyncedDb.d.ts +20 -0
- package/dist/src/types/I_SyncedDb.d.ts +33 -0
- package/dist/src/utils/computeDiff.d.ts +76 -0
- package/dist/src/utils/localQuery.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 0.1.154
|
|
4
|
+
|
|
5
|
+
### `evictOnWake` config
|
|
6
|
+
|
|
7
|
+
New optional field on `SyncedDbConfig`. When set to `true`, scope-exit
|
|
8
|
+
eviction runs on every wake event (`sync()` whose `calledFrom` starts
|
|
9
|
+
with `'wake-sync:'`), bypassing the `evictStaleRecordsEveryHrs` interval
|
|
10
|
+
gate. The eviction is bundled into the wake sync's `findNewerManyStream`
|
|
11
|
+
call — single RTT, same shape as existing 8h auto-eviction.
|
|
12
|
+
|
|
13
|
+
Use with `scopeExitLookbehindMs` to ensure scope-exit detection covers
|
|
14
|
+
records mutated within the lookbehind window. Together they guarantee
|
|
15
|
+
cross-device close-outs become visible on the very next wake instead of
|
|
16
|
+
waiting up to `evictStaleRecordsEveryHrs`.
|
|
17
|
+
|
|
18
|
+
`evictStaleRecordsEveryHrs` continues to gate non-wake syncs (interval
|
|
19
|
+
auto-sync ticks, manual `sync()` calls without `'wake-sync:*'` prefix).
|
|
20
|
+
|
|
21
|
+
Default: `false`.
|
|
22
|
+
|
|
23
|
+
## 0.1.153
|
|
24
|
+
|
|
25
|
+
### `scopeExitLookbehindMs` config
|
|
26
|
+
|
|
27
|
+
New optional field on `SyncedDbConfig`. When set (> 0), server-assisted
|
|
28
|
+
scope-exit detection (auto-eviction bundled with `sync()`, plus the batch
|
|
29
|
+
`evictOutOfScopeRecordsAll` path) uses `_ts > now - scopeExitLookbehindMs`
|
|
30
|
+
instead of `_ts > lastSyncTs` for the `findNewerMany` spec timestamp.
|
|
31
|
+
|
|
32
|
+
Why: with the legacy `lastSyncTs` cursor, the scope-exit query
|
|
33
|
+
`{$nor:[positiveQuery], _id:{$in:chunk}, _ts > lastSyncTs}` excluded any
|
|
34
|
+
record whose server-side `_ts` predates the device's `lastSyncTs`. Typical
|
|
35
|
+
cross-device close-out scenario (device A closes a record while device B
|
|
36
|
+
is offline; B comes back online and syncs many newer records, advancing
|
|
37
|
+
`lastSyncTs` past the close-out's `_ts`) → server returned 0 scope-exit
|
|
38
|
+
candidates forever. Per-call override via
|
|
39
|
+
`evictOutOfScopeRecords({outOfWindowLookbehindMs})` is unchanged and now
|
|
40
|
+
also falls back to `scopeExitLookbehindMs` when no per-call value is given.
|
|
41
|
+
|
|
42
|
+
Default: `0` (legacy behavior). Recommended for multi-device deployments:
|
|
43
|
+
`3 * 24 * 3600 * 1000` (3 days). Combine with periodic
|
|
44
|
+
`refreshDatabaseFromServer()` (e.g. when `lastInitialSync()` exceeds the
|
|
45
|
+
same threshold) to also cover scope-exit events older than the lookbehind
|
|
46
|
+
window.
|
|
47
|
+
|
|
3
48
|
## Unreleased
|
|
4
49
|
|
|
5
50
|
### `SyncSource` flag in `I_InMemDb.saveMany` / `deleteManyByIds`
|
package/dist/index.js
CHANGED
|
@@ -69,16 +69,13 @@ function matchesQuery(item, query) {
|
|
|
69
69
|
function matchesCondition(item, key, condition) {
|
|
70
70
|
const value = getNestedValue(item, key);
|
|
71
71
|
if (condition === null || typeof condition !== "object") {
|
|
72
|
-
return value
|
|
72
|
+
return scalarMatches(value, condition);
|
|
73
73
|
}
|
|
74
74
|
if (condition instanceof Date) {
|
|
75
|
-
|
|
76
|
-
return value.getTime() === condition.getTime();
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
75
|
+
return scalarMatches(value, condition);
|
|
79
76
|
}
|
|
80
77
|
if (Array.isArray(condition)) {
|
|
81
|
-
return condition.
|
|
78
|
+
return condition.some((c) => scalarMatches(value, c));
|
|
82
79
|
}
|
|
83
80
|
for (const [op, opValue] of Object.entries(condition)) {
|
|
84
81
|
if (!matchesOperator(value, op, opValue)) {
|
|
@@ -90,29 +87,32 @@ function matchesCondition(item, key, condition) {
|
|
|
90
87
|
function matchesOperator(value, operator, operand) {
|
|
91
88
|
switch (operator) {
|
|
92
89
|
case "$eq":
|
|
93
|
-
return
|
|
90
|
+
return scalarMatches(value, operand);
|
|
94
91
|
case "$ne":
|
|
95
|
-
return !
|
|
92
|
+
return !scalarMatches(value, operand);
|
|
96
93
|
case "$gt":
|
|
97
|
-
return value > operand;
|
|
94
|
+
return compareScalarOrArray(value, (v) => v > operand);
|
|
98
95
|
case "$gte":
|
|
99
|
-
return value >= operand;
|
|
96
|
+
return compareScalarOrArray(value, (v) => v >= operand);
|
|
100
97
|
case "$lt":
|
|
101
|
-
return value < operand;
|
|
98
|
+
return compareScalarOrArray(value, (v) => v < operand);
|
|
102
99
|
case "$lte":
|
|
103
|
-
return value <= operand;
|
|
100
|
+
return compareScalarOrArray(value, (v) => v <= operand);
|
|
104
101
|
case "$in":
|
|
105
102
|
if (!Array.isArray(operand)) return false;
|
|
106
|
-
return operand.some((item) =>
|
|
103
|
+
return operand.some((item) => scalarMatches(value, item));
|
|
107
104
|
case "$nin":
|
|
108
105
|
if (!Array.isArray(operand)) return true;
|
|
109
|
-
return !operand.some((item) =>
|
|
106
|
+
return !operand.some((item) => scalarMatches(value, item));
|
|
110
107
|
case "$exists":
|
|
111
108
|
return operand ? value !== void 0 : value === void 0;
|
|
112
109
|
case "$regex": {
|
|
113
|
-
if (typeof value !== "string") return false;
|
|
114
110
|
const regex = operand instanceof RegExp ? operand : new RegExp(operand);
|
|
115
|
-
return regex.test(value);
|
|
111
|
+
if (typeof value === "string") return regex.test(value);
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
return value.some((v) => typeof v === "string" && regex.test(v));
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
116
|
}
|
|
117
117
|
case "$elemMatch":
|
|
118
118
|
if (!Array.isArray(value)) return false;
|
|
@@ -152,6 +152,19 @@ function getNestedValue(obj, path) {
|
|
|
152
152
|
}
|
|
153
153
|
return current;
|
|
154
154
|
}
|
|
155
|
+
function scalarMatches(value, scalar) {
|
|
156
|
+
if (equals(value, scalar)) return true;
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
return value.some((elem) => equals(elem, scalar));
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function compareScalarOrArray(value, predicate) {
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
return value.some((v) => predicate(v));
|
|
165
|
+
}
|
|
166
|
+
return predicate(value);
|
|
167
|
+
}
|
|
155
168
|
function equals(a, b) {
|
|
156
169
|
var _a, _b;
|
|
157
170
|
if (a === b) return true;
|
|
@@ -260,6 +273,183 @@ function applyQueryOpts(items, opts) {
|
|
|
260
273
|
return result;
|
|
261
274
|
}
|
|
262
275
|
|
|
276
|
+
// src/utils/computeDiff.ts
|
|
277
|
+
function isObjectIdLike(v) {
|
|
278
|
+
return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || v._bsontype === "ObjectID") && typeof v.toString === "function");
|
|
279
|
+
}
|
|
280
|
+
function isPlainObject(v) {
|
|
281
|
+
if (v === null || typeof v !== "object") return false;
|
|
282
|
+
if (Array.isArray(v)) return false;
|
|
283
|
+
if (v instanceof Date) return false;
|
|
284
|
+
if (isObjectIdLike(v)) return false;
|
|
285
|
+
const proto = Object.getPrototypeOf(v);
|
|
286
|
+
return proto === Object.prototype || proto === null;
|
|
287
|
+
}
|
|
288
|
+
function deepEquals(a, b) {
|
|
289
|
+
if (a === b) return true;
|
|
290
|
+
if (a === null || b === null) return false;
|
|
291
|
+
if (a === void 0 || b === void 0) return false;
|
|
292
|
+
if (a instanceof Date && b instanceof Date) {
|
|
293
|
+
return a.getTime() === b.getTime();
|
|
294
|
+
}
|
|
295
|
+
if (isObjectIdLike(a) && isObjectIdLike(b)) {
|
|
296
|
+
return String(a) === String(b);
|
|
297
|
+
}
|
|
298
|
+
if (typeof a !== typeof b) return false;
|
|
299
|
+
if (typeof a !== "object") return false;
|
|
300
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
301
|
+
if (Array.isArray(a)) {
|
|
302
|
+
if (a.length !== b.length) return false;
|
|
303
|
+
for (let i = 0; i < a.length; i++) {
|
|
304
|
+
if (!deepEquals(a[i], b[i])) return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
const ak = Object.keys(a);
|
|
309
|
+
const bk = Object.keys(b);
|
|
310
|
+
if (ak.length !== bk.length) return false;
|
|
311
|
+
for (const k of ak) {
|
|
312
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
313
|
+
if (!deepEquals(a[k], b[k])) return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
function allElementsHaveId(arr) {
|
|
318
|
+
if (arr.length === 0) return false;
|
|
319
|
+
for (const e of arr) {
|
|
320
|
+
if (!e || typeof e !== "object") return false;
|
|
321
|
+
if (e._id == null) return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
function sameIdSequence(a, b) {
|
|
326
|
+
if (a.length !== b.length) return false;
|
|
327
|
+
for (let i = 0; i < a.length; i++) {
|
|
328
|
+
if (String(a[i]._id) !== String(b[i]._id)) return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
function computeArrayDiff(existingArr, updateArr, basePath, diff) {
|
|
333
|
+
if (existingArr.length === 0) {
|
|
334
|
+
if (updateArr.length > 0) diff[basePath] = updateArr;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (!allElementsHaveId(existingArr) || !allElementsHaveId(updateArr)) {
|
|
338
|
+
if (!deepEquals(existingArr, updateArr)) {
|
|
339
|
+
diff[basePath] = updateArr;
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!sameIdSequence(existingArr, updateArr)) {
|
|
344
|
+
diff[basePath] = updateArr;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
for (let i = 0; i < updateArr.length; i++) {
|
|
348
|
+
computeDiffInto(
|
|
349
|
+
existingArr[i],
|
|
350
|
+
updateArr[i],
|
|
351
|
+
`${basePath}.${i}`,
|
|
352
|
+
diff
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function computeDiffInto(existing, update, basePath, diff) {
|
|
357
|
+
if (deepEquals(existing, update)) return;
|
|
358
|
+
if (update === null || update === void 0 || typeof update !== "object" || update instanceof Date || isObjectIdLike(update)) {
|
|
359
|
+
diff[basePath] = update;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (existing === null || existing === void 0 || typeof existing !== "object" || existing instanceof Date || isObjectIdLike(existing)) {
|
|
363
|
+
diff[basePath] = update;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(update) !== Array.isArray(existing)) {
|
|
367
|
+
diff[basePath] = update;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(update)) {
|
|
371
|
+
computeArrayDiff(existing, update, basePath, diff);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!isPlainObject(update) || !isPlainObject(existing)) {
|
|
375
|
+
diff[basePath] = update;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const key of Object.keys(update)) {
|
|
379
|
+
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
380
|
+
computeDiffInto(existing[key], update[key], childPath, diff);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function computeDiff(existing, update) {
|
|
384
|
+
const diff = {};
|
|
385
|
+
if (!update || typeof update !== "object") return diff;
|
|
386
|
+
if (existing === null || existing === void 0) {
|
|
387
|
+
return __spreadValues({}, update);
|
|
388
|
+
}
|
|
389
|
+
for (const key of Object.keys(update)) {
|
|
390
|
+
computeDiffInto(existing[key], update[key], key, diff);
|
|
391
|
+
}
|
|
392
|
+
return diff;
|
|
393
|
+
}
|
|
394
|
+
function isDescendantOrEqual(path, candidate) {
|
|
395
|
+
if (path === candidate) return true;
|
|
396
|
+
return path.startsWith(candidate + ".");
|
|
397
|
+
}
|
|
398
|
+
function setByPath(target2, path, value) {
|
|
399
|
+
if (target2 === null || target2 === void 0) return false;
|
|
400
|
+
const parts = path.split(".");
|
|
401
|
+
let current = target2;
|
|
402
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
403
|
+
const part = parts[i];
|
|
404
|
+
const idx = /^\d+$/.test(part) ? Number(part) : null;
|
|
405
|
+
let next;
|
|
406
|
+
if (Array.isArray(current) && idx !== null) {
|
|
407
|
+
next = current[idx];
|
|
408
|
+
} else if (typeof current === "object") {
|
|
409
|
+
next = current[part];
|
|
410
|
+
} else {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
if (next === null || next === void 0) return false;
|
|
414
|
+
current = next;
|
|
415
|
+
}
|
|
416
|
+
const last = parts[parts.length - 1];
|
|
417
|
+
const lastIdx = /^\d+$/.test(last) ? Number(last) : null;
|
|
418
|
+
if (Array.isArray(current) && lastIdx !== null) {
|
|
419
|
+
current[lastIdx] = value;
|
|
420
|
+
} else if (typeof current === "object") {
|
|
421
|
+
current[last] = value;
|
|
422
|
+
} else {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
function mergeDirtyPath(accumulated, newPath, newValue) {
|
|
428
|
+
for (const existingKey of Object.keys(accumulated)) {
|
|
429
|
+
if (existingKey === newPath) continue;
|
|
430
|
+
if (isDescendantOrEqual(newPath, existingKey)) {
|
|
431
|
+
const relativePath = newPath.substring(existingKey.length + 1);
|
|
432
|
+
const ok = setByPath(accumulated[existingKey], relativePath, newValue);
|
|
433
|
+
if (ok) return;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const toDelete = [];
|
|
438
|
+
for (const existingKey of Object.keys(accumulated)) {
|
|
439
|
+
if (existingKey === newPath) continue;
|
|
440
|
+
if (isDescendantOrEqual(existingKey, newPath)) {
|
|
441
|
+
toDelete.push(existingKey);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const k of toDelete) delete accumulated[k];
|
|
445
|
+
accumulated[newPath] = newValue;
|
|
446
|
+
}
|
|
447
|
+
function mergeDirtyChanges(accumulated, newChanges) {
|
|
448
|
+
for (const path of Object.keys(newChanges)) {
|
|
449
|
+
mergeDirtyPath(accumulated, path, newChanges[path]);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
263
453
|
// src/db/managers/InMemManager.ts
|
|
264
454
|
var InMemManager = class {
|
|
265
455
|
constructor(config) {
|
|
@@ -1290,7 +1480,7 @@ var CustomTransformerRegistry = class {
|
|
|
1290
1480
|
var getType = (payload) => Object.prototype.toString.call(payload).slice(8, -1);
|
|
1291
1481
|
var isUndefined = (payload) => typeof payload === "undefined";
|
|
1292
1482
|
var isNull = (payload) => payload === null;
|
|
1293
|
-
var
|
|
1483
|
+
var isPlainObject2 = (payload) => {
|
|
1294
1484
|
if (typeof payload !== "object" || payload === null)
|
|
1295
1485
|
return false;
|
|
1296
1486
|
if (payload === Object.prototype)
|
|
@@ -1299,7 +1489,7 @@ var isPlainObject = (payload) => {
|
|
|
1299
1489
|
return true;
|
|
1300
1490
|
return Object.getPrototypeOf(payload) === Object.prototype;
|
|
1301
1491
|
};
|
|
1302
|
-
var isEmptyObject = (payload) =>
|
|
1492
|
+
var isEmptyObject = (payload) => isPlainObject2(payload) && Object.keys(payload).length === 0;
|
|
1303
1493
|
var isArray = (payload) => Array.isArray(payload);
|
|
1304
1494
|
var isString = (payload) => typeof payload === "string";
|
|
1305
1495
|
var isNumber = (payload) => typeof payload === "number" && !isNaN(payload);
|
|
@@ -1612,7 +1802,7 @@ var setDeep = (object, path, mapper) => {
|
|
|
1612
1802
|
if (isArray(parent)) {
|
|
1613
1803
|
const index = +key;
|
|
1614
1804
|
parent = parent[index];
|
|
1615
|
-
} else if (
|
|
1805
|
+
} else if (isPlainObject2(parent)) {
|
|
1616
1806
|
parent = parent[key];
|
|
1617
1807
|
} else if (isSet(parent)) {
|
|
1618
1808
|
const row = +key;
|
|
@@ -1638,7 +1828,7 @@ var setDeep = (object, path, mapper) => {
|
|
|
1638
1828
|
const lastKey = path[path.length - 1];
|
|
1639
1829
|
if (isArray(parent)) {
|
|
1640
1830
|
parent[+lastKey] = mapper(parent[+lastKey]);
|
|
1641
|
-
} else if (
|
|
1831
|
+
} else if (isPlainObject2(parent)) {
|
|
1642
1832
|
parent[lastKey] = mapper(parent[lastKey]);
|
|
1643
1833
|
}
|
|
1644
1834
|
if (isSet(parent)) {
|
|
@@ -1723,7 +1913,7 @@ function applyReferentialEqualityAnnotations(plain, annotations, version) {
|
|
|
1723
1913
|
}
|
|
1724
1914
|
return plain;
|
|
1725
1915
|
}
|
|
1726
|
-
var isDeep = (object, superJson) =>
|
|
1916
|
+
var isDeep = (object, superJson) => isPlainObject2(object) || isArray(object) || isMap(object) || isSet(object) || isError(object) || isInstanceOfRegisteredClass(object, superJson);
|
|
1727
1917
|
function addIdentity(object, path, identities) {
|
|
1728
1918
|
const existingSet = identities.get(object);
|
|
1729
1919
|
if (existingSet) {
|
|
@@ -1801,7 +1991,7 @@ var walker = (object, identities, superJson, dedupe, path = [], objectsInThisPat
|
|
|
1801
1991
|
transformedValue[index] = recursiveResult.transformedValue;
|
|
1802
1992
|
if (isArray(recursiveResult.annotations)) {
|
|
1803
1993
|
innerAnnotations[escapeKey(index)] = recursiveResult.annotations;
|
|
1804
|
-
} else if (
|
|
1994
|
+
} else if (isPlainObject2(recursiveResult.annotations)) {
|
|
1805
1995
|
forEach(recursiveResult.annotations, (tree, key) => {
|
|
1806
1996
|
innerAnnotations[escapeKey(index) + "." + key] = tree;
|
|
1807
1997
|
});
|
|
@@ -1831,7 +2021,7 @@ function isArray2(payload) {
|
|
|
1831
2021
|
}
|
|
1832
2022
|
|
|
1833
2023
|
// node_modules/is-what/dist/isPlainObject.js
|
|
1834
|
-
function
|
|
2024
|
+
function isPlainObject3(payload) {
|
|
1835
2025
|
if (getType2(payload) !== "Object")
|
|
1836
2026
|
return false;
|
|
1837
2027
|
const prototype = Object.getPrototypeOf(payload);
|
|
@@ -1856,7 +2046,7 @@ function copy(target2, options = {}) {
|
|
|
1856
2046
|
if (isArray2(target2)) {
|
|
1857
2047
|
return target2.map((item) => copy(item, options));
|
|
1858
2048
|
}
|
|
1859
|
-
if (!
|
|
2049
|
+
if (!isPlainObject3(target2)) {
|
|
1860
2050
|
return target2;
|
|
1861
2051
|
}
|
|
1862
2052
|
const props = Object.getOwnPropertyNames(target2);
|
|
@@ -2371,7 +2561,7 @@ function mergeObjects(local, external) {
|
|
|
2371
2561
|
}
|
|
2372
2562
|
if (Array.isArray(localValue) && Array.isArray(externalValue)) {
|
|
2373
2563
|
result[key] = mergeArrays(localValue, externalValue);
|
|
2374
|
-
} else if (
|
|
2564
|
+
} else if (isPlainObject4(localValue) && isPlainObject4(externalValue)) {
|
|
2375
2565
|
result[key] = mergeObjects(localValue, externalValue);
|
|
2376
2566
|
} else if (serverWinsOnConflict) {
|
|
2377
2567
|
result[key] = externalValue;
|
|
@@ -2390,7 +2580,7 @@ function mergeArrays(local, external) {
|
|
|
2390
2580
|
for (const item of external) set2.add(item);
|
|
2391
2581
|
return Array.from(set2);
|
|
2392
2582
|
}
|
|
2393
|
-
if (
|
|
2583
|
+
if (isPlainObject4(firstLocal) || isPlainObject4(firstExternal)) {
|
|
2394
2584
|
return mergeObjectArrays(local, external);
|
|
2395
2585
|
}
|
|
2396
2586
|
const set = new Set(local);
|
|
@@ -2424,7 +2614,7 @@ function mergeObjectArrays(local, external) {
|
|
|
2424
2614
|
}
|
|
2425
2615
|
return result;
|
|
2426
2616
|
}
|
|
2427
|
-
function
|
|
2617
|
+
function isPlainObject4(value) {
|
|
2428
2618
|
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
2429
2619
|
}
|
|
2430
2620
|
|
|
@@ -3536,7 +3726,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3536
3726
|
this.syncOnlyCollections = null;
|
|
3537
3727
|
// Sync metadata cache
|
|
3538
3728
|
this.syncMetaCache = /* @__PURE__ */ new Map();
|
|
3539
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
|
|
3729
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
|
|
3540
3730
|
this.tenant = config.tenant;
|
|
3541
3731
|
this.dexieDb = config.dexieDb;
|
|
3542
3732
|
this.inMemDb = config.inMemDb;
|
|
@@ -3563,13 +3753,15 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3563
3753
|
this.onEvictionStart = config.onEvictionStart;
|
|
3564
3754
|
this.onEviction = config.onEviction;
|
|
3565
3755
|
this.evictStaleRecordsEveryHrs = (_e = config.evictStaleRecordsEveryHrs) != null ? _e : 0;
|
|
3756
|
+
this.scopeExitLookbehindMs = (_f = config.scopeExitLookbehindMs) != null ? _f : 0;
|
|
3757
|
+
this.evictOnWake = (_g = config.evictOnWake) != null ? _g : false;
|
|
3566
3758
|
for (const col of config.collections) {
|
|
3567
3759
|
this.collections.set(col.name, col);
|
|
3568
3760
|
}
|
|
3569
3761
|
this.inMemManager = new InMemManager({
|
|
3570
3762
|
inMemDb: this.inMemDb,
|
|
3571
3763
|
collections: this.collections,
|
|
3572
|
-
useObjectMetadata: (
|
|
3764
|
+
useObjectMetadata: (_h = config.useObjectMetadata) != null ? _h : false
|
|
3573
3765
|
});
|
|
3574
3766
|
this.leaderElection = new LeaderElectionManager({
|
|
3575
3767
|
tenant: this.tenant,
|
|
@@ -3604,7 +3796,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3604
3796
|
tenant: this.tenant,
|
|
3605
3797
|
instanceId: this.syncedDbInstanceId,
|
|
3606
3798
|
windowId,
|
|
3607
|
-
debounceMs: (
|
|
3799
|
+
debounceMs: (_i = config.crossTabSyncDebounceMs) != null ? _i : 100,
|
|
3608
3800
|
callbacks: {
|
|
3609
3801
|
onCrossTabSync: config.onCrossTabSync,
|
|
3610
3802
|
onInfrastructureError: config.onInfrastructureError ? (type, message, error) => {
|
|
@@ -3630,8 +3822,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3630
3822
|
});
|
|
3631
3823
|
this.connectionManager = new ConnectionManager({
|
|
3632
3824
|
restInterface: this.restInterface,
|
|
3633
|
-
restTimeoutMs: (
|
|
3634
|
-
syncTimeoutMs: (
|
|
3825
|
+
restTimeoutMs: (_j = config.restTimeoutMs) != null ? _j : 9e4,
|
|
3826
|
+
syncTimeoutMs: (_k = config.syncTimeoutMs) != null ? _k : 12e4,
|
|
3635
3827
|
autoSyncIntervalMs: config.autoSyncIntervalMs,
|
|
3636
3828
|
onlineRetryIntervalMs: config.onlineRetryIntervalMs,
|
|
3637
3829
|
callbacks: {
|
|
@@ -3658,8 +3850,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3658
3850
|
});
|
|
3659
3851
|
this.pendingChanges = new PendingChangesManager({
|
|
3660
3852
|
tenant: this.tenant,
|
|
3661
|
-
debounceDexieWritesMs: (
|
|
3662
|
-
debounceRestWritesMs: (
|
|
3853
|
+
debounceDexieWritesMs: (_l = config.debounceDexieWritesMs) != null ? _l : 500,
|
|
3854
|
+
debounceRestWritesMs: (_m = config.debounceRestWritesMs) != null ? _m : 100,
|
|
3663
3855
|
callbacks: {
|
|
3664
3856
|
onDexieWriteRequest: config.onDexieWriteRequest,
|
|
3665
3857
|
onDexieWriteResult: config.onDexieWriteResult,
|
|
@@ -3736,8 +3928,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3736
3928
|
});
|
|
3737
3929
|
if (config.wakeSyncEnabled) {
|
|
3738
3930
|
this.wakeSync = new WakeSyncManager({
|
|
3739
|
-
gapThresholdMs: (
|
|
3740
|
-
debounceMs: (
|
|
3931
|
+
gapThresholdMs: (_n = config.wakeSyncGapThresholdMs) != null ? _n : 1e4,
|
|
3932
|
+
debounceMs: (_o = config.wakeSyncDebounceMs) != null ? _o : 2e3,
|
|
3741
3933
|
callbacks: {
|
|
3742
3934
|
onWakeSync: config.onWakeSync
|
|
3743
3935
|
},
|
|
@@ -3752,7 +3944,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3752
3944
|
}
|
|
3753
3945
|
if (config.networkStatusEnabled) {
|
|
3754
3946
|
this.networkStatus = new NetworkStatusManager({
|
|
3755
|
-
debounceMs: (
|
|
3947
|
+
debounceMs: (_p = config.networkStatusDebounceMs) != null ? _p : 100,
|
|
3756
3948
|
callbacks: {
|
|
3757
3949
|
onBrowserNetworkChange: config.onBrowserNetworkChange,
|
|
3758
3950
|
onBrowserOnline: config.onBrowserOnline,
|
|
@@ -4314,10 +4506,12 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4314
4506
|
`SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`
|
|
4315
4507
|
);
|
|
4316
4508
|
}
|
|
4509
|
+
const fullChanges = __spreadProps(__spreadValues({}, update), { _lastUpdaterId: this.updaterId });
|
|
4510
|
+
const diff = computeDiff(existing, fullChanges);
|
|
4317
4511
|
await this.dexieDb.addDirtyChange(
|
|
4318
4512
|
collection,
|
|
4319
4513
|
id,
|
|
4320
|
-
|
|
4514
|
+
diff,
|
|
4321
4515
|
{ _ts: existing == null ? void 0 : existing._ts, _rev: existing == null ? void 0 : existing._rev }
|
|
4322
4516
|
);
|
|
4323
4517
|
const newData = __spreadProps(__spreadValues({}, update), {
|
|
@@ -4521,7 +4715,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4521
4715
|
this.crossTabSync.startServerSync();
|
|
4522
4716
|
let evictionPlan;
|
|
4523
4717
|
let evictionServerFailed = false;
|
|
4524
|
-
if (await this._isAutoEvictionDue()) {
|
|
4718
|
+
if (await this._isAutoEvictionDue(calledFrom)) {
|
|
4525
4719
|
try {
|
|
4526
4720
|
evictionPlan = await this._collectScopeExitPlan("auto");
|
|
4527
4721
|
} catch (err) {
|
|
@@ -4837,11 +5031,12 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4837
5031
|
let serverEvictedCount = 0;
|
|
4838
5032
|
if (serverAssisted && serverCandidateIds.length > 0) {
|
|
4839
5033
|
let scopeExitTimestamp;
|
|
4840
|
-
const
|
|
4841
|
-
|
|
5034
|
+
const callerLookbehindMs = opts == null ? void 0 : opts.outOfWindowLookbehindMs;
|
|
5035
|
+
const effectiveLookbehindMs = callerLookbehindMs != null && callerLookbehindMs > 0 ? callerLookbehindMs : this.scopeExitLookbehindMs;
|
|
5036
|
+
if (effectiveLookbehindMs > 0) {
|
|
4842
5037
|
scopeExitTimestamp = Math.max(
|
|
4843
5038
|
0,
|
|
4844
|
-
Math.floor((Date.now() -
|
|
5039
|
+
Math.floor((Date.now() - effectiveLookbehindMs) / 1e3)
|
|
4845
5040
|
);
|
|
4846
5041
|
} else {
|
|
4847
5042
|
scopeExitTimestamp = (_d = (_c = this.syncMetaCache.get(collection)) == null ? void 0 : _c.lastSyncTs) != null ? _d : 0;
|
|
@@ -4997,7 +5192,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4997
5192
|
* callback is a reliable lifecycle marker.
|
|
4998
5193
|
*/
|
|
4999
5194
|
async _collectScopeExitPlan(trigger) {
|
|
5000
|
-
var _a
|
|
5195
|
+
var _a;
|
|
5001
5196
|
const startTime = Date.now();
|
|
5002
5197
|
const eligible = [];
|
|
5003
5198
|
for (const [name, config] of this.collections) {
|
|
@@ -5047,7 +5242,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5047
5242
|
collection: name,
|
|
5048
5243
|
config,
|
|
5049
5244
|
query,
|
|
5050
|
-
timestamp:
|
|
5245
|
+
timestamp: this._scopeExitTimestamp(name),
|
|
5051
5246
|
evictIds,
|
|
5052
5247
|
localEvictedCount: evictIds.length,
|
|
5053
5248
|
serverEvictedCount: 0,
|
|
@@ -5155,14 +5350,44 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5155
5350
|
this.safeCallback(this.onEviction, info);
|
|
5156
5351
|
return info;
|
|
5157
5352
|
}
|
|
5353
|
+
/**
|
|
5354
|
+
* Compute the `timestamp` value used in server-assisted scope-exit
|
|
5355
|
+
* `findNewerMany` specs for a given collection. Honors the global
|
|
5356
|
+
* `scopeExitLookbehindMs` config: when > 0, the scope-exit query
|
|
5357
|
+
* uses `_ts > now - lookbehindMs`; otherwise falls back to the
|
|
5358
|
+
* collection's lastSyncTs cursor (legacy behavior).
|
|
5359
|
+
*
|
|
5360
|
+
* Per-call override (`evictOutOfScopeRecords({outOfWindowLookbehindMs})`)
|
|
5361
|
+
* is computed at the call site — this helper covers the auto-eviction
|
|
5362
|
+
* path and `evictOutOfScopeRecordsAll`.
|
|
5363
|
+
*/
|
|
5364
|
+
_scopeExitTimestamp(collection) {
|
|
5365
|
+
var _a, _b;
|
|
5366
|
+
if (this.scopeExitLookbehindMs > 0) {
|
|
5367
|
+
return Math.max(
|
|
5368
|
+
0,
|
|
5369
|
+
Math.floor((Date.now() - this.scopeExitLookbehindMs) / 1e3)
|
|
5370
|
+
);
|
|
5371
|
+
}
|
|
5372
|
+
return (_b = (_a = this.syncMetaCache.get(collection)) == null ? void 0 : _a.lastSyncTs) != null ? _b : 0;
|
|
5373
|
+
}
|
|
5158
5374
|
/**
|
|
5159
5375
|
* Whether auto-eviction is due to run on the next sync. Mirrors the
|
|
5160
5376
|
* gating logic of the old `maybeAutoEvict` (interval check + persisted
|
|
5161
5377
|
* `__lastEviction` cursor) but split out so `sync()` can pre-compute
|
|
5162
5378
|
* the eviction plan BEFORE issuing the streamed `findNewerMany` —
|
|
5163
5379
|
* letting scope-exit specs ride along on the same call.
|
|
5380
|
+
*
|
|
5381
|
+
* `calledFrom` is honored when `evictOnWake` is true: any sync whose
|
|
5382
|
+
* `calledFrom` starts with `'wake-sync:'` forces eviction regardless
|
|
5383
|
+
* of the `evictStaleRecordsEveryHrs` interval gate. This ensures
|
|
5384
|
+
* cross-device scope-exit records become visible on the very next
|
|
5385
|
+
* wake instead of waiting up to the configured interval.
|
|
5164
5386
|
*/
|
|
5165
|
-
async _isAutoEvictionDue() {
|
|
5387
|
+
async _isAutoEvictionDue(calledFrom) {
|
|
5388
|
+
if (this.evictOnWake && (calledFrom == null ? void 0 : calledFrom.startsWith("wake-sync:"))) {
|
|
5389
|
+
return true;
|
|
5390
|
+
}
|
|
5166
5391
|
if (this.evictStaleRecordsEveryHrs <= 0) return false;
|
|
5167
5392
|
const intervalMs = this.evictStaleRecordsEveryHrs * 36e5;
|
|
5168
5393
|
if (!this._lastEvictionDate) {
|
|
@@ -5643,7 +5868,7 @@ var DexieDb = class extends Dexie {
|
|
|
5643
5868
|
const existing = await this.dirtyChanges.get([collection, stringId]);
|
|
5644
5869
|
const now = Date.now();
|
|
5645
5870
|
if (existing) {
|
|
5646
|
-
|
|
5871
|
+
mergeDirtyChanges(existing.changes, changes);
|
|
5647
5872
|
existing.updatedAt = now;
|
|
5648
5873
|
await this.dirtyChanges.put(existing);
|
|
5649
5874
|
} else {
|
|
@@ -5671,7 +5896,7 @@ var DexieDb = class extends Dexie {
|
|
|
5671
5896
|
const stringId = this.idToString(changeItem.id);
|
|
5672
5897
|
const existing = existingEntries[i];
|
|
5673
5898
|
if (existing) {
|
|
5674
|
-
|
|
5899
|
+
mergeDirtyChanges(existing.changes, changeItem.changes);
|
|
5675
5900
|
existing.updatedAt = now;
|
|
5676
5901
|
toWrite.push(existing);
|
|
5677
5902
|
} else {
|
|
@@ -53,6 +53,8 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
53
53
|
private readonly onEvictionStart?;
|
|
54
54
|
private readonly onEviction?;
|
|
55
55
|
private readonly evictStaleRecordsEveryHrs;
|
|
56
|
+
private readonly scopeExitLookbehindMs;
|
|
57
|
+
private readonly evictOnWake;
|
|
56
58
|
private _lastEvictionDate?;
|
|
57
59
|
constructor(config: SyncedDbConfig);
|
|
58
60
|
getInstanceId(): string;
|
|
@@ -295,12 +297,30 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
295
297
|
* info and still fires the callback so consumers see a lifecycle event.
|
|
296
298
|
*/
|
|
297
299
|
private _applyScopeExitPlan;
|
|
300
|
+
/**
|
|
301
|
+
* Compute the `timestamp` value used in server-assisted scope-exit
|
|
302
|
+
* `findNewerMany` specs for a given collection. Honors the global
|
|
303
|
+
* `scopeExitLookbehindMs` config: when > 0, the scope-exit query
|
|
304
|
+
* uses `_ts > now - lookbehindMs`; otherwise falls back to the
|
|
305
|
+
* collection's lastSyncTs cursor (legacy behavior).
|
|
306
|
+
*
|
|
307
|
+
* Per-call override (`evictOutOfScopeRecords({outOfWindowLookbehindMs})`)
|
|
308
|
+
* is computed at the call site — this helper covers the auto-eviction
|
|
309
|
+
* path and `evictOutOfScopeRecordsAll`.
|
|
310
|
+
*/
|
|
311
|
+
private _scopeExitTimestamp;
|
|
298
312
|
/**
|
|
299
313
|
* Whether auto-eviction is due to run on the next sync. Mirrors the
|
|
300
314
|
* gating logic of the old `maybeAutoEvict` (interval check + persisted
|
|
301
315
|
* `__lastEviction` cursor) but split out so `sync()` can pre-compute
|
|
302
316
|
* the eviction plan BEFORE issuing the streamed `findNewerMany` —
|
|
303
317
|
* letting scope-exit specs ride along on the same call.
|
|
318
|
+
*
|
|
319
|
+
* `calledFrom` is honored when `evictOnWake` is true: any sync whose
|
|
320
|
+
* `calledFrom` starts with `'wake-sync:'` forces eviction regardless
|
|
321
|
+
* of the `evictStaleRecordsEveryHrs` interval gate. This ensures
|
|
322
|
+
* cross-device scope-exit records become visible on the very next
|
|
323
|
+
* wake instead of waiting up to the configured interval.
|
|
304
324
|
*/
|
|
305
325
|
private _isAutoEvictionDue;
|
|
306
326
|
/**
|
|
@@ -550,6 +550,39 @@ export interface SyncedDbConfig {
|
|
|
550
550
|
* Default: 0 (disabled).
|
|
551
551
|
*/
|
|
552
552
|
evictStaleRecordsEveryHrs?: number;
|
|
553
|
+
/**
|
|
554
|
+
* Default lookbehind window (ms) for server-assisted scope-exit detection
|
|
555
|
+
* in auto-eviction (`sync()` bundled) and `evictOutOfScopeRecordsAll`.
|
|
556
|
+
* When set (> 0), the scope-exit query uses `_ts > now - lookbehindMs`
|
|
557
|
+
* instead of `_ts > lastSyncTs`. This catches records whose server-side
|
|
558
|
+
* `_ts` predates `lastSyncTs` but still fail `$nor [positiveQuery]`
|
|
559
|
+
* server-side (typical cross-device close-out scenario where the local
|
|
560
|
+
* client was offline at the moment of mutation and findNewer's positive
|
|
561
|
+
* query never re-shipped the post-mutation row).
|
|
562
|
+
*
|
|
563
|
+
* Per-call override remains via `evictOutOfScopeRecords({outOfWindowLookbehindMs})`.
|
|
564
|
+
*
|
|
565
|
+
* Default: 0 (= use lastSyncTs cursor — current behavior).
|
|
566
|
+
* Recommended for multi-device deployments: 3 * 24 * 3600 * 1000 (3 days).
|
|
567
|
+
*/
|
|
568
|
+
scopeExitLookbehindMs?: number;
|
|
569
|
+
/**
|
|
570
|
+
* Force scope-exit eviction on every wake event (`calledFrom` starts
|
|
571
|
+
* with `'wake-sync:'`), bypassing the `evictStaleRecordsEveryHrs`
|
|
572
|
+
* interval gate. Eviction is bundled into the wake sync's
|
|
573
|
+
* `findNewerManyStream` call (single RTT, same as positive sync).
|
|
574
|
+
*
|
|
575
|
+
* Combine with `scopeExitLookbehindMs` so the scope-exit query covers
|
|
576
|
+
* records mutated within the lookbehind window — guarantees that
|
|
577
|
+
* cross-device close-outs become visible to the local cache on the
|
|
578
|
+
* very next wake instead of waiting up to `evictStaleRecordsEveryHrs`.
|
|
579
|
+
*
|
|
580
|
+
* `evictStaleRecordsEveryHrs` still gates non-wake syncs (interval
|
|
581
|
+
* sync, manual `sync()` triggers without `'wake-sync:*'` prefix).
|
|
582
|
+
*
|
|
583
|
+
* Default: false.
|
|
584
|
+
*/
|
|
585
|
+
evictOnWake?: boolean;
|
|
553
586
|
/**
|
|
554
587
|
* Callback fired before each eviction run begins, after the eligible
|
|
555
588
|
* collection list has been resolved. Useful as a debug breadcrumb to
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* computeDiff — produces minimal MongoDB-style dot-notation paths
|
|
3
|
+
* representing the actual differences between `existing` and `update`.
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE: Solves the "stale array upload" bug where a save() call
|
|
6
|
+
* with a full nested array captures unchanged fields into the dirty
|
|
7
|
+
* change. When the device later uploads, those unchanged fields
|
|
8
|
+
* overwrite concurrent edits made by other devices.
|
|
9
|
+
*
|
|
10
|
+
* INVARIANT: applyDiff(existing, computeDiff(existing, update)) deep-equals
|
|
11
|
+
* the merged result of `existing` ⊔ `update` (where update wins on conflicts,
|
|
12
|
+
* and missing keys in update preserve existing values).
|
|
13
|
+
*
|
|
14
|
+
* STRATEGY:
|
|
15
|
+
* - Primitives, Date, ObjectId: emit single path if value differs
|
|
16
|
+
* - Plain objects: recurse, accumulating paths
|
|
17
|
+
* - Arrays of objects with _id (same composition + order): element-wise diff
|
|
18
|
+
* - Arrays with composition or order changes: full replace at array path
|
|
19
|
+
* - Arrays of primitives or mixed types: full replace
|
|
20
|
+
*
|
|
21
|
+
* MongoDB native supports dot-notation in $set, so output paths can be
|
|
22
|
+
* passed directly to the server without protocol changes.
|
|
23
|
+
*/
|
|
24
|
+
/** Deep equality for values that may include Date, ObjectId, plain objects, arrays. */
|
|
25
|
+
export declare function deepEquals(a: any, b: any): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Compute the minimal set of MongoDB dot-notation $set paths
|
|
28
|
+
* that transform `existing` into `update` (only for keys present in `update`).
|
|
29
|
+
*
|
|
30
|
+
* @returns Record where keys are dot-notation paths and values are new values.
|
|
31
|
+
* Empty object means no changes.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* existing = { koraki: [{ _id: "k1", diag: "old", ter: "x" }] }
|
|
35
|
+
* update = { koraki: [{ _id: "k1", diag: "new", ter: "x" }] }
|
|
36
|
+
* → { "koraki.0.diag": "new" }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* existing = { koraki: [{ _id: "k1" }] }
|
|
40
|
+
* update = { koraki: [{ _id: "k1" }, { _id: "k2", diag: "x" }] }
|
|
41
|
+
* → { "koraki": [{ _id: "k1" }, { _id: "k2", diag: "x" }] } // composition change
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeDiff(existing: Record<string, any> | null | undefined, update: Record<string, any>): Record<string, any>;
|
|
44
|
+
/**
|
|
45
|
+
* Detect whether a `changes` object uses dot-notation paths (new format)
|
|
46
|
+
* or top-level keys with full nested values (legacy format).
|
|
47
|
+
*
|
|
48
|
+
* Used to decide whether smart path-based merging applies, or fall back
|
|
49
|
+
* to legacy Object.assign accumulation.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hasDotNotationPaths(changes: Record<string, any>): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Smart-merge a single (path, value) entry into an accumulated dirty changes
|
|
54
|
+
* object. Resolves three relationships between the new path and existing keys:
|
|
55
|
+
*
|
|
56
|
+
* 1. New path is a DESCENDANT of an existing key (e.g. existing has "koraki",
|
|
57
|
+
* new is "koraki.0.diag"): mutate the value inside the existing parent
|
|
58
|
+
* rather than adding a conflicting child path.
|
|
59
|
+
*
|
|
60
|
+
* 2. New path is an ANCESTOR of existing keys (e.g. existing has "koraki.0.diag",
|
|
61
|
+
* new is "koraki" with full array): remove the now-redundant descendants and
|
|
62
|
+
* set the parent path. The new full value supersedes any field-level deltas.
|
|
63
|
+
*
|
|
64
|
+
* 3. New path is ORTHOGONAL to existing keys: simple set (Object.assign-equivalent).
|
|
65
|
+
*
|
|
66
|
+
* Used by DexieDb.addDirtyChange to accumulate changes across multiple save()
|
|
67
|
+
* calls without producing MongoDB-conflicting payloads (parent + child paths
|
|
68
|
+
* in same $set yields nondeterministic results).
|
|
69
|
+
*/
|
|
70
|
+
export declare function mergeDirtyPath(accumulated: Record<string, any>, newPath: string, newValue: any): void;
|
|
71
|
+
/**
|
|
72
|
+
* Bulk-merge an entire changes object into the accumulator using path semantics.
|
|
73
|
+
* Order matters: paths are processed sequentially, so later entries can supersede
|
|
74
|
+
* earlier ones. This is acceptable because mergeDirtyPath is idempotent.
|
|
75
|
+
*/
|
|
76
|
+
export declare function mergeDirtyChanges(accumulated: Record<string, any>, newChanges: Record<string, any>): void;
|
|
@@ -5,6 +5,10 @@ import type { DbEntity } from "../types/DbEntity";
|
|
|
5
5
|
* Podpira polje-operatorje: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin,
|
|
6
6
|
* $exists, $regex, $elemMatch, $size, $all — in logične operatorje
|
|
7
7
|
* $and, $or, $nor na top-levelu.
|
|
8
|
+
*
|
|
9
|
+
* Skalarni operandi se ujemajo tudi z array-polji (MongoDB semantika):
|
|
10
|
+
* `{ a: "ok" }` matcha tako `{ a: "ok" }` kot `{ a: ["ok", "more"] }`.
|
|
11
|
+
* Velja za neposredno enakost, $eq, $ne, $in, $nin, $gt/$gte/$lt/$lte in $regex.
|
|
8
12
|
*/
|
|
9
13
|
export declare function matchesQuery<T extends DbEntity>(item: T, query: QuerySpec<T>): boolean;
|
|
10
14
|
/**
|