cry-synced-db-client 0.1.151 → 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 +243 -31
- 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/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
|
@@ -273,6 +273,183 @@ function applyQueryOpts(items, opts) {
|
|
|
273
273
|
return result;
|
|
274
274
|
}
|
|
275
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
|
+
|
|
276
453
|
// src/db/managers/InMemManager.ts
|
|
277
454
|
var InMemManager = class {
|
|
278
455
|
constructor(config) {
|
|
@@ -1303,7 +1480,7 @@ var CustomTransformerRegistry = class {
|
|
|
1303
1480
|
var getType = (payload) => Object.prototype.toString.call(payload).slice(8, -1);
|
|
1304
1481
|
var isUndefined = (payload) => typeof payload === "undefined";
|
|
1305
1482
|
var isNull = (payload) => payload === null;
|
|
1306
|
-
var
|
|
1483
|
+
var isPlainObject2 = (payload) => {
|
|
1307
1484
|
if (typeof payload !== "object" || payload === null)
|
|
1308
1485
|
return false;
|
|
1309
1486
|
if (payload === Object.prototype)
|
|
@@ -1312,7 +1489,7 @@ var isPlainObject = (payload) => {
|
|
|
1312
1489
|
return true;
|
|
1313
1490
|
return Object.getPrototypeOf(payload) === Object.prototype;
|
|
1314
1491
|
};
|
|
1315
|
-
var isEmptyObject = (payload) =>
|
|
1492
|
+
var isEmptyObject = (payload) => isPlainObject2(payload) && Object.keys(payload).length === 0;
|
|
1316
1493
|
var isArray = (payload) => Array.isArray(payload);
|
|
1317
1494
|
var isString = (payload) => typeof payload === "string";
|
|
1318
1495
|
var isNumber = (payload) => typeof payload === "number" && !isNaN(payload);
|
|
@@ -1625,7 +1802,7 @@ var setDeep = (object, path, mapper) => {
|
|
|
1625
1802
|
if (isArray(parent)) {
|
|
1626
1803
|
const index = +key;
|
|
1627
1804
|
parent = parent[index];
|
|
1628
|
-
} else if (
|
|
1805
|
+
} else if (isPlainObject2(parent)) {
|
|
1629
1806
|
parent = parent[key];
|
|
1630
1807
|
} else if (isSet(parent)) {
|
|
1631
1808
|
const row = +key;
|
|
@@ -1651,7 +1828,7 @@ var setDeep = (object, path, mapper) => {
|
|
|
1651
1828
|
const lastKey = path[path.length - 1];
|
|
1652
1829
|
if (isArray(parent)) {
|
|
1653
1830
|
parent[+lastKey] = mapper(parent[+lastKey]);
|
|
1654
|
-
} else if (
|
|
1831
|
+
} else if (isPlainObject2(parent)) {
|
|
1655
1832
|
parent[lastKey] = mapper(parent[lastKey]);
|
|
1656
1833
|
}
|
|
1657
1834
|
if (isSet(parent)) {
|
|
@@ -1736,7 +1913,7 @@ function applyReferentialEqualityAnnotations(plain, annotations, version) {
|
|
|
1736
1913
|
}
|
|
1737
1914
|
return plain;
|
|
1738
1915
|
}
|
|
1739
|
-
var isDeep = (object, superJson) =>
|
|
1916
|
+
var isDeep = (object, superJson) => isPlainObject2(object) || isArray(object) || isMap(object) || isSet(object) || isError(object) || isInstanceOfRegisteredClass(object, superJson);
|
|
1740
1917
|
function addIdentity(object, path, identities) {
|
|
1741
1918
|
const existingSet = identities.get(object);
|
|
1742
1919
|
if (existingSet) {
|
|
@@ -1814,7 +1991,7 @@ var walker = (object, identities, superJson, dedupe, path = [], objectsInThisPat
|
|
|
1814
1991
|
transformedValue[index] = recursiveResult.transformedValue;
|
|
1815
1992
|
if (isArray(recursiveResult.annotations)) {
|
|
1816
1993
|
innerAnnotations[escapeKey(index)] = recursiveResult.annotations;
|
|
1817
|
-
} else if (
|
|
1994
|
+
} else if (isPlainObject2(recursiveResult.annotations)) {
|
|
1818
1995
|
forEach(recursiveResult.annotations, (tree, key) => {
|
|
1819
1996
|
innerAnnotations[escapeKey(index) + "." + key] = tree;
|
|
1820
1997
|
});
|
|
@@ -1844,7 +2021,7 @@ function isArray2(payload) {
|
|
|
1844
2021
|
}
|
|
1845
2022
|
|
|
1846
2023
|
// node_modules/is-what/dist/isPlainObject.js
|
|
1847
|
-
function
|
|
2024
|
+
function isPlainObject3(payload) {
|
|
1848
2025
|
if (getType2(payload) !== "Object")
|
|
1849
2026
|
return false;
|
|
1850
2027
|
const prototype = Object.getPrototypeOf(payload);
|
|
@@ -1869,7 +2046,7 @@ function copy(target2, options = {}) {
|
|
|
1869
2046
|
if (isArray2(target2)) {
|
|
1870
2047
|
return target2.map((item) => copy(item, options));
|
|
1871
2048
|
}
|
|
1872
|
-
if (!
|
|
2049
|
+
if (!isPlainObject3(target2)) {
|
|
1873
2050
|
return target2;
|
|
1874
2051
|
}
|
|
1875
2052
|
const props = Object.getOwnPropertyNames(target2);
|
|
@@ -2384,7 +2561,7 @@ function mergeObjects(local, external) {
|
|
|
2384
2561
|
}
|
|
2385
2562
|
if (Array.isArray(localValue) && Array.isArray(externalValue)) {
|
|
2386
2563
|
result[key] = mergeArrays(localValue, externalValue);
|
|
2387
|
-
} else if (
|
|
2564
|
+
} else if (isPlainObject4(localValue) && isPlainObject4(externalValue)) {
|
|
2388
2565
|
result[key] = mergeObjects(localValue, externalValue);
|
|
2389
2566
|
} else if (serverWinsOnConflict) {
|
|
2390
2567
|
result[key] = externalValue;
|
|
@@ -2403,7 +2580,7 @@ function mergeArrays(local, external) {
|
|
|
2403
2580
|
for (const item of external) set2.add(item);
|
|
2404
2581
|
return Array.from(set2);
|
|
2405
2582
|
}
|
|
2406
|
-
if (
|
|
2583
|
+
if (isPlainObject4(firstLocal) || isPlainObject4(firstExternal)) {
|
|
2407
2584
|
return mergeObjectArrays(local, external);
|
|
2408
2585
|
}
|
|
2409
2586
|
const set = new Set(local);
|
|
@@ -2437,7 +2614,7 @@ function mergeObjectArrays(local, external) {
|
|
|
2437
2614
|
}
|
|
2438
2615
|
return result;
|
|
2439
2616
|
}
|
|
2440
|
-
function
|
|
2617
|
+
function isPlainObject4(value) {
|
|
2441
2618
|
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
2442
2619
|
}
|
|
2443
2620
|
|
|
@@ -3549,7 +3726,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3549
3726
|
this.syncOnlyCollections = null;
|
|
3550
3727
|
// Sync metadata cache
|
|
3551
3728
|
this.syncMetaCache = /* @__PURE__ */ new Map();
|
|
3552
|
-
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;
|
|
3553
3730
|
this.tenant = config.tenant;
|
|
3554
3731
|
this.dexieDb = config.dexieDb;
|
|
3555
3732
|
this.inMemDb = config.inMemDb;
|
|
@@ -3576,13 +3753,15 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3576
3753
|
this.onEvictionStart = config.onEvictionStart;
|
|
3577
3754
|
this.onEviction = config.onEviction;
|
|
3578
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;
|
|
3579
3758
|
for (const col of config.collections) {
|
|
3580
3759
|
this.collections.set(col.name, col);
|
|
3581
3760
|
}
|
|
3582
3761
|
this.inMemManager = new InMemManager({
|
|
3583
3762
|
inMemDb: this.inMemDb,
|
|
3584
3763
|
collections: this.collections,
|
|
3585
|
-
useObjectMetadata: (
|
|
3764
|
+
useObjectMetadata: (_h = config.useObjectMetadata) != null ? _h : false
|
|
3586
3765
|
});
|
|
3587
3766
|
this.leaderElection = new LeaderElectionManager({
|
|
3588
3767
|
tenant: this.tenant,
|
|
@@ -3617,7 +3796,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3617
3796
|
tenant: this.tenant,
|
|
3618
3797
|
instanceId: this.syncedDbInstanceId,
|
|
3619
3798
|
windowId,
|
|
3620
|
-
debounceMs: (
|
|
3799
|
+
debounceMs: (_i = config.crossTabSyncDebounceMs) != null ? _i : 100,
|
|
3621
3800
|
callbacks: {
|
|
3622
3801
|
onCrossTabSync: config.onCrossTabSync,
|
|
3623
3802
|
onInfrastructureError: config.onInfrastructureError ? (type, message, error) => {
|
|
@@ -3643,8 +3822,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3643
3822
|
});
|
|
3644
3823
|
this.connectionManager = new ConnectionManager({
|
|
3645
3824
|
restInterface: this.restInterface,
|
|
3646
|
-
restTimeoutMs: (
|
|
3647
|
-
syncTimeoutMs: (
|
|
3825
|
+
restTimeoutMs: (_j = config.restTimeoutMs) != null ? _j : 9e4,
|
|
3826
|
+
syncTimeoutMs: (_k = config.syncTimeoutMs) != null ? _k : 12e4,
|
|
3648
3827
|
autoSyncIntervalMs: config.autoSyncIntervalMs,
|
|
3649
3828
|
onlineRetryIntervalMs: config.onlineRetryIntervalMs,
|
|
3650
3829
|
callbacks: {
|
|
@@ -3671,8 +3850,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3671
3850
|
});
|
|
3672
3851
|
this.pendingChanges = new PendingChangesManager({
|
|
3673
3852
|
tenant: this.tenant,
|
|
3674
|
-
debounceDexieWritesMs: (
|
|
3675
|
-
debounceRestWritesMs: (
|
|
3853
|
+
debounceDexieWritesMs: (_l = config.debounceDexieWritesMs) != null ? _l : 500,
|
|
3854
|
+
debounceRestWritesMs: (_m = config.debounceRestWritesMs) != null ? _m : 100,
|
|
3676
3855
|
callbacks: {
|
|
3677
3856
|
onDexieWriteRequest: config.onDexieWriteRequest,
|
|
3678
3857
|
onDexieWriteResult: config.onDexieWriteResult,
|
|
@@ -3749,8 +3928,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3749
3928
|
});
|
|
3750
3929
|
if (config.wakeSyncEnabled) {
|
|
3751
3930
|
this.wakeSync = new WakeSyncManager({
|
|
3752
|
-
gapThresholdMs: (
|
|
3753
|
-
debounceMs: (
|
|
3931
|
+
gapThresholdMs: (_n = config.wakeSyncGapThresholdMs) != null ? _n : 1e4,
|
|
3932
|
+
debounceMs: (_o = config.wakeSyncDebounceMs) != null ? _o : 2e3,
|
|
3754
3933
|
callbacks: {
|
|
3755
3934
|
onWakeSync: config.onWakeSync
|
|
3756
3935
|
},
|
|
@@ -3765,7 +3944,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
3765
3944
|
}
|
|
3766
3945
|
if (config.networkStatusEnabled) {
|
|
3767
3946
|
this.networkStatus = new NetworkStatusManager({
|
|
3768
|
-
debounceMs: (
|
|
3947
|
+
debounceMs: (_p = config.networkStatusDebounceMs) != null ? _p : 100,
|
|
3769
3948
|
callbacks: {
|
|
3770
3949
|
onBrowserNetworkChange: config.onBrowserNetworkChange,
|
|
3771
3950
|
onBrowserOnline: config.onBrowserOnline,
|
|
@@ -4327,10 +4506,12 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4327
4506
|
`SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`
|
|
4328
4507
|
);
|
|
4329
4508
|
}
|
|
4509
|
+
const fullChanges = __spreadProps(__spreadValues({}, update), { _lastUpdaterId: this.updaterId });
|
|
4510
|
+
const diff = computeDiff(existing, fullChanges);
|
|
4330
4511
|
await this.dexieDb.addDirtyChange(
|
|
4331
4512
|
collection,
|
|
4332
4513
|
id,
|
|
4333
|
-
|
|
4514
|
+
diff,
|
|
4334
4515
|
{ _ts: existing == null ? void 0 : existing._ts, _rev: existing == null ? void 0 : existing._rev }
|
|
4335
4516
|
);
|
|
4336
4517
|
const newData = __spreadProps(__spreadValues({}, update), {
|
|
@@ -4534,7 +4715,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4534
4715
|
this.crossTabSync.startServerSync();
|
|
4535
4716
|
let evictionPlan;
|
|
4536
4717
|
let evictionServerFailed = false;
|
|
4537
|
-
if (await this._isAutoEvictionDue()) {
|
|
4718
|
+
if (await this._isAutoEvictionDue(calledFrom)) {
|
|
4538
4719
|
try {
|
|
4539
4720
|
evictionPlan = await this._collectScopeExitPlan("auto");
|
|
4540
4721
|
} catch (err) {
|
|
@@ -4850,11 +5031,12 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4850
5031
|
let serverEvictedCount = 0;
|
|
4851
5032
|
if (serverAssisted && serverCandidateIds.length > 0) {
|
|
4852
5033
|
let scopeExitTimestamp;
|
|
4853
|
-
const
|
|
4854
|
-
|
|
5034
|
+
const callerLookbehindMs = opts == null ? void 0 : opts.outOfWindowLookbehindMs;
|
|
5035
|
+
const effectiveLookbehindMs = callerLookbehindMs != null && callerLookbehindMs > 0 ? callerLookbehindMs : this.scopeExitLookbehindMs;
|
|
5036
|
+
if (effectiveLookbehindMs > 0) {
|
|
4855
5037
|
scopeExitTimestamp = Math.max(
|
|
4856
5038
|
0,
|
|
4857
|
-
Math.floor((Date.now() -
|
|
5039
|
+
Math.floor((Date.now() - effectiveLookbehindMs) / 1e3)
|
|
4858
5040
|
);
|
|
4859
5041
|
} else {
|
|
4860
5042
|
scopeExitTimestamp = (_d = (_c = this.syncMetaCache.get(collection)) == null ? void 0 : _c.lastSyncTs) != null ? _d : 0;
|
|
@@ -5010,7 +5192,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5010
5192
|
* callback is a reliable lifecycle marker.
|
|
5011
5193
|
*/
|
|
5012
5194
|
async _collectScopeExitPlan(trigger) {
|
|
5013
|
-
var _a
|
|
5195
|
+
var _a;
|
|
5014
5196
|
const startTime = Date.now();
|
|
5015
5197
|
const eligible = [];
|
|
5016
5198
|
for (const [name, config] of this.collections) {
|
|
@@ -5060,7 +5242,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5060
5242
|
collection: name,
|
|
5061
5243
|
config,
|
|
5062
5244
|
query,
|
|
5063
|
-
timestamp:
|
|
5245
|
+
timestamp: this._scopeExitTimestamp(name),
|
|
5064
5246
|
evictIds,
|
|
5065
5247
|
localEvictedCount: evictIds.length,
|
|
5066
5248
|
serverEvictedCount: 0,
|
|
@@ -5168,14 +5350,44 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5168
5350
|
this.safeCallback(this.onEviction, info);
|
|
5169
5351
|
return info;
|
|
5170
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
|
+
}
|
|
5171
5374
|
/**
|
|
5172
5375
|
* Whether auto-eviction is due to run on the next sync. Mirrors the
|
|
5173
5376
|
* gating logic of the old `maybeAutoEvict` (interval check + persisted
|
|
5174
5377
|
* `__lastEviction` cursor) but split out so `sync()` can pre-compute
|
|
5175
5378
|
* the eviction plan BEFORE issuing the streamed `findNewerMany` —
|
|
5176
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.
|
|
5177
5386
|
*/
|
|
5178
|
-
async _isAutoEvictionDue() {
|
|
5387
|
+
async _isAutoEvictionDue(calledFrom) {
|
|
5388
|
+
if (this.evictOnWake && (calledFrom == null ? void 0 : calledFrom.startsWith("wake-sync:"))) {
|
|
5389
|
+
return true;
|
|
5390
|
+
}
|
|
5179
5391
|
if (this.evictStaleRecordsEveryHrs <= 0) return false;
|
|
5180
5392
|
const intervalMs = this.evictStaleRecordsEveryHrs * 36e5;
|
|
5181
5393
|
if (!this._lastEvictionDate) {
|
|
@@ -5656,7 +5868,7 @@ var DexieDb = class extends Dexie {
|
|
|
5656
5868
|
const existing = await this.dirtyChanges.get([collection, stringId]);
|
|
5657
5869
|
const now = Date.now();
|
|
5658
5870
|
if (existing) {
|
|
5659
|
-
|
|
5871
|
+
mergeDirtyChanges(existing.changes, changes);
|
|
5660
5872
|
existing.updatedAt = now;
|
|
5661
5873
|
await this.dirtyChanges.put(existing);
|
|
5662
5874
|
} else {
|
|
@@ -5684,7 +5896,7 @@ var DexieDb = class extends Dexie {
|
|
|
5684
5896
|
const stringId = this.idToString(changeItem.id);
|
|
5685
5897
|
const existing = existingEntries[i];
|
|
5686
5898
|
if (existing) {
|
|
5687
|
-
|
|
5899
|
+
mergeDirtyChanges(existing.changes, changeItem.changes);
|
|
5688
5900
|
existing.updatedAt = now;
|
|
5689
5901
|
toWrite.push(existing);
|
|
5690
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;
|