cry-synced-db-client 2.0.14 → 2.0.15
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 +18 -0
- package/dist/index.js +118 -114
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 2.0.15 (2026-08-18)
|
|
4
|
+
|
|
5
|
+
### Fix: preserve explicit `save()` intent after direct live-row mutation
|
|
6
|
+
|
|
7
|
+
`SyncedDb.save()` now computes its dirty delta against both the live in-memory
|
|
8
|
+
row and the pending server state (the Dexie row with an existing dirty entry
|
|
9
|
+
applied). A caller that mutates a live row before calling `save()` can no
|
|
10
|
+
longer silently lose the update, or retain an older dirty value when reverting
|
|
11
|
+
inside the Dexie debounce window.
|
|
12
|
+
|
|
13
|
+
The combined delta is normalized with `mergeDirtyChanges`, preventing MongoDB
|
|
14
|
+
parent/child `$set` conflicts such as `profile` together with
|
|
15
|
+
`profile.colour`.
|
|
16
|
+
|
|
17
|
+
**Regression coverage:** `test/saveAfterDirectInMemMutation.test.ts` covers
|
|
18
|
+
live-row drag-style mutation, mixed changes, pending-intent reverts, scalar to
|
|
19
|
+
object shape changes, and the unchanged meta-only dirty guard.
|
|
20
|
+
|
|
3
21
|
## 2.0.2 (2026-07-13)
|
|
4
22
|
|
|
5
23
|
### Fix: `PendingChangesManager.schedule` — deleted fields resurrected from prior pending save
|
package/dist/index.js
CHANGED
|
@@ -885,6 +885,119 @@ function childPathForArrayElement(prefix, element, index) {
|
|
|
885
885
|
return prefix ? `${prefix}.${index}` : String(index);
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
// src/utils/computeDiff.ts
|
|
889
|
+
function isDescendantOrEqual(path, candidate) {
|
|
890
|
+
if (path === candidate) return true;
|
|
891
|
+
if (!path.startsWith(candidate)) return false;
|
|
892
|
+
const next = path[candidate.length];
|
|
893
|
+
return next === "." || next === "[";
|
|
894
|
+
}
|
|
895
|
+
function pathsOverlap(a, b) {
|
|
896
|
+
return isDescendantOrEqual(a, b) || isDescendantOrEqual(b, a);
|
|
897
|
+
}
|
|
898
|
+
function isTerminalBracketKey(path) {
|
|
899
|
+
const tokens = tokenizePath(path);
|
|
900
|
+
const last = tokens[tokens.length - 1];
|
|
901
|
+
return !!(last && last.length >= 2 && last.charCodeAt(0) === 91 && last.charCodeAt(last.length - 1) === 93);
|
|
902
|
+
}
|
|
903
|
+
function canExpandArrayToBrackets(value) {
|
|
904
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
905
|
+
for (const el of value) {
|
|
906
|
+
if (el === null || typeof el !== "object") return false;
|
|
907
|
+
if (el._id == null) return false;
|
|
908
|
+
}
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
function unwrapWholeElement(value) {
|
|
912
|
+
if (Array.isArray(value)) {
|
|
913
|
+
return value.length === 1 && value[0] && typeof value[0] === "object" ? value[0] : null;
|
|
914
|
+
}
|
|
915
|
+
if (value && typeof value === "object") return value;
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
function pickLayerTarget(newPath, newValue) {
|
|
919
|
+
if (!isTerminalBracketKey(newPath)) return null;
|
|
920
|
+
if (newValue === void 0) return null;
|
|
921
|
+
if (Array.isArray(newValue) && newValue.length === 1 && newValue[0] && typeof newValue[0] === "object") {
|
|
922
|
+
return newValue[0];
|
|
923
|
+
}
|
|
924
|
+
if (newValue && typeof newValue === "object" && !Array.isArray(newValue)) {
|
|
925
|
+
return newValue;
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
function mergeDirtyPath(accumulated, newPath, newValue) {
|
|
930
|
+
for (const existingKey of Object.keys(accumulated)) {
|
|
931
|
+
if (existingKey === newPath) continue;
|
|
932
|
+
if (isDescendantOrEqual(newPath, existingKey)) {
|
|
933
|
+
const existingValue = accumulated[existingKey];
|
|
934
|
+
const existingIsTerminal = isTerminalBracketKey(existingKey);
|
|
935
|
+
if (existingIsTerminal && existingValue === void 0) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
let mutationTarget = existingValue;
|
|
939
|
+
if (existingIsTerminal && Array.isArray(existingValue) && existingValue.length === 1) {
|
|
940
|
+
mutationTarget = existingValue[0];
|
|
941
|
+
}
|
|
942
|
+
if (!existingIsTerminal && (existingValue === null || existingValue === void 0)) {
|
|
943
|
+
accumulated[existingKey] = {};
|
|
944
|
+
mutationTarget = accumulated[existingKey];
|
|
945
|
+
}
|
|
946
|
+
if (!existingIsTerminal && newPath[existingKey.length] === "[" && canExpandArrayToBrackets(existingValue)) {
|
|
947
|
+
delete accumulated[existingKey];
|
|
948
|
+
for (const el of existingValue) {
|
|
949
|
+
accumulated[`${existingKey}[${String(el._id)}]`] = [el];
|
|
950
|
+
}
|
|
951
|
+
mergeDirtyPath(accumulated, newPath, newValue);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const sepChar = newPath[existingKey.length];
|
|
955
|
+
const relativePath = sepChar === "[" ? newPath.substring(existingKey.length) : newPath.substring(existingKey.length + 1);
|
|
956
|
+
const ok = setByPath(mutationTarget, relativePath, newValue);
|
|
957
|
+
if (ok) return;
|
|
958
|
+
delete accumulated[existingKey];
|
|
959
|
+
accumulated[newPath] = newValue;
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const descendants = [];
|
|
964
|
+
for (const existingKey of Object.keys(accumulated)) {
|
|
965
|
+
if (existingKey === newPath) continue;
|
|
966
|
+
if (isDescendantOrEqual(existingKey, newPath)) {
|
|
967
|
+
descendants.push(existingKey);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const layerInto = pickLayerTarget(newPath, newValue);
|
|
971
|
+
if (layerInto && descendants.length > 0) {
|
|
972
|
+
for (const desc of descendants) {
|
|
973
|
+
const sepChar = desc[newPath.length];
|
|
974
|
+
const relativePath = sepChar === "[" ? desc.substring(newPath.length) : desc.substring(newPath.length + 1);
|
|
975
|
+
const descValue = accumulated[desc];
|
|
976
|
+
if (descValue === void 0) {
|
|
977
|
+
deleteByPath(layerInto, relativePath);
|
|
978
|
+
} else {
|
|
979
|
+
setByPath(layerInto, relativePath, descValue);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
for (const k of descendants) delete accumulated[k];
|
|
984
|
+
if (newValue !== void 0 && isTerminalBracketKey(newPath) && Object.prototype.hasOwnProperty.call(accumulated, newPath)) {
|
|
985
|
+
const existingValue = accumulated[newPath];
|
|
986
|
+
const existingElement = unwrapWholeElement(existingValue);
|
|
987
|
+
const newElement = unwrapWholeElement(newValue);
|
|
988
|
+
if (existingElement && newElement && (Array.isArray(existingValue) || Array.isArray(newValue))) {
|
|
989
|
+
accumulated[newPath] = [newElement];
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
accumulated[newPath] = newValue;
|
|
994
|
+
}
|
|
995
|
+
function mergeDirtyChanges(accumulated, newChanges) {
|
|
996
|
+
for (const path of Object.keys(newChanges)) {
|
|
997
|
+
mergeDirtyPath(accumulated, path, newChanges[path]);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
888
1001
|
// src/utils/networkError.ts
|
|
889
1002
|
function networkError(message, ...rest) {
|
|
890
1003
|
const isOnline = typeof navigator === "undefined" || navigator.onLine !== false;
|
|
@@ -6827,7 +6940,11 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6827
6940
|
const fullChanges = __spreadProps(__spreadValues({}, update), { _lastUpdaterId: this.updaterId });
|
|
6828
6941
|
const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
|
|
6829
6942
|
const base = currentMem != null ? currentMem : existing;
|
|
6830
|
-
const
|
|
6943
|
+
const pendingDirty = await this.dexieDb.getDirtyChange(collection, id);
|
|
6944
|
+
const pendingServerState = pendingDirty ? applyObjDiff(existing != null ? existing : null, pendingDirty.changes, id, collection) : existing != null ? existing : null;
|
|
6945
|
+
const diff = {};
|
|
6946
|
+
mergeDirtyChanges(diff, computeObjDiff(base, fullChanges));
|
|
6947
|
+
mergeDirtyChanges(diff, computeObjDiff(pendingServerState, fullChanges));
|
|
6831
6948
|
await this.dexieDb.addDirtyChange(collection, id, diff, {
|
|
6832
6949
|
_ts: existing == null ? void 0 : existing._ts,
|
|
6833
6950
|
_rev: existing == null ? void 0 : existing._rev
|
|
@@ -8429,119 +8546,6 @@ import Dexie from "dexie";
|
|
|
8429
8546
|
// src/types/I_DexieDb.ts
|
|
8430
8547
|
var DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS = 2;
|
|
8431
8548
|
|
|
8432
|
-
// src/utils/computeDiff.ts
|
|
8433
|
-
function isDescendantOrEqual(path, candidate) {
|
|
8434
|
-
if (path === candidate) return true;
|
|
8435
|
-
if (!path.startsWith(candidate)) return false;
|
|
8436
|
-
const next = path[candidate.length];
|
|
8437
|
-
return next === "." || next === "[";
|
|
8438
|
-
}
|
|
8439
|
-
function pathsOverlap(a, b) {
|
|
8440
|
-
return isDescendantOrEqual(a, b) || isDescendantOrEqual(b, a);
|
|
8441
|
-
}
|
|
8442
|
-
function isTerminalBracketKey(path) {
|
|
8443
|
-
const tokens = tokenizePath(path);
|
|
8444
|
-
const last = tokens[tokens.length - 1];
|
|
8445
|
-
return !!(last && last.length >= 2 && last.charCodeAt(0) === 91 && last.charCodeAt(last.length - 1) === 93);
|
|
8446
|
-
}
|
|
8447
|
-
function canExpandArrayToBrackets(value) {
|
|
8448
|
-
if (!Array.isArray(value) || value.length === 0) return false;
|
|
8449
|
-
for (const el of value) {
|
|
8450
|
-
if (el === null || typeof el !== "object") return false;
|
|
8451
|
-
if (el._id == null) return false;
|
|
8452
|
-
}
|
|
8453
|
-
return true;
|
|
8454
|
-
}
|
|
8455
|
-
function unwrapWholeElement(value) {
|
|
8456
|
-
if (Array.isArray(value)) {
|
|
8457
|
-
return value.length === 1 && value[0] && typeof value[0] === "object" ? value[0] : null;
|
|
8458
|
-
}
|
|
8459
|
-
if (value && typeof value === "object") return value;
|
|
8460
|
-
return null;
|
|
8461
|
-
}
|
|
8462
|
-
function pickLayerTarget(newPath, newValue) {
|
|
8463
|
-
if (!isTerminalBracketKey(newPath)) return null;
|
|
8464
|
-
if (newValue === void 0) return null;
|
|
8465
|
-
if (Array.isArray(newValue) && newValue.length === 1 && newValue[0] && typeof newValue[0] === "object") {
|
|
8466
|
-
return newValue[0];
|
|
8467
|
-
}
|
|
8468
|
-
if (newValue && typeof newValue === "object" && !Array.isArray(newValue)) {
|
|
8469
|
-
return newValue;
|
|
8470
|
-
}
|
|
8471
|
-
return null;
|
|
8472
|
-
}
|
|
8473
|
-
function mergeDirtyPath(accumulated, newPath, newValue) {
|
|
8474
|
-
for (const existingKey of Object.keys(accumulated)) {
|
|
8475
|
-
if (existingKey === newPath) continue;
|
|
8476
|
-
if (isDescendantOrEqual(newPath, existingKey)) {
|
|
8477
|
-
const existingValue = accumulated[existingKey];
|
|
8478
|
-
const existingIsTerminal = isTerminalBracketKey(existingKey);
|
|
8479
|
-
if (existingIsTerminal && existingValue === void 0) {
|
|
8480
|
-
return;
|
|
8481
|
-
}
|
|
8482
|
-
let mutationTarget = existingValue;
|
|
8483
|
-
if (existingIsTerminal && Array.isArray(existingValue) && existingValue.length === 1) {
|
|
8484
|
-
mutationTarget = existingValue[0];
|
|
8485
|
-
}
|
|
8486
|
-
if (!existingIsTerminal && (existingValue === null || existingValue === void 0)) {
|
|
8487
|
-
accumulated[existingKey] = {};
|
|
8488
|
-
mutationTarget = accumulated[existingKey];
|
|
8489
|
-
}
|
|
8490
|
-
if (!existingIsTerminal && newPath[existingKey.length] === "[" && canExpandArrayToBrackets(existingValue)) {
|
|
8491
|
-
delete accumulated[existingKey];
|
|
8492
|
-
for (const el of existingValue) {
|
|
8493
|
-
accumulated[`${existingKey}[${String(el._id)}]`] = [el];
|
|
8494
|
-
}
|
|
8495
|
-
mergeDirtyPath(accumulated, newPath, newValue);
|
|
8496
|
-
return;
|
|
8497
|
-
}
|
|
8498
|
-
const sepChar = newPath[existingKey.length];
|
|
8499
|
-
const relativePath = sepChar === "[" ? newPath.substring(existingKey.length) : newPath.substring(existingKey.length + 1);
|
|
8500
|
-
const ok = setByPath(mutationTarget, relativePath, newValue);
|
|
8501
|
-
if (ok) return;
|
|
8502
|
-
delete accumulated[existingKey];
|
|
8503
|
-
accumulated[newPath] = newValue;
|
|
8504
|
-
return;
|
|
8505
|
-
}
|
|
8506
|
-
}
|
|
8507
|
-
const descendants = [];
|
|
8508
|
-
for (const existingKey of Object.keys(accumulated)) {
|
|
8509
|
-
if (existingKey === newPath) continue;
|
|
8510
|
-
if (isDescendantOrEqual(existingKey, newPath)) {
|
|
8511
|
-
descendants.push(existingKey);
|
|
8512
|
-
}
|
|
8513
|
-
}
|
|
8514
|
-
const layerInto = pickLayerTarget(newPath, newValue);
|
|
8515
|
-
if (layerInto && descendants.length > 0) {
|
|
8516
|
-
for (const desc of descendants) {
|
|
8517
|
-
const sepChar = desc[newPath.length];
|
|
8518
|
-
const relativePath = sepChar === "[" ? desc.substring(newPath.length) : desc.substring(newPath.length + 1);
|
|
8519
|
-
const descValue = accumulated[desc];
|
|
8520
|
-
if (descValue === void 0) {
|
|
8521
|
-
deleteByPath(layerInto, relativePath);
|
|
8522
|
-
} else {
|
|
8523
|
-
setByPath(layerInto, relativePath, descValue);
|
|
8524
|
-
}
|
|
8525
|
-
}
|
|
8526
|
-
}
|
|
8527
|
-
for (const k of descendants) delete accumulated[k];
|
|
8528
|
-
if (newValue !== void 0 && isTerminalBracketKey(newPath) && Object.prototype.hasOwnProperty.call(accumulated, newPath)) {
|
|
8529
|
-
const existingValue = accumulated[newPath];
|
|
8530
|
-
const existingElement = unwrapWholeElement(existingValue);
|
|
8531
|
-
const newElement = unwrapWholeElement(newValue);
|
|
8532
|
-
if (existingElement && newElement && (Array.isArray(existingValue) || Array.isArray(newValue))) {
|
|
8533
|
-
accumulated[newPath] = [newElement];
|
|
8534
|
-
return;
|
|
8535
|
-
}
|
|
8536
|
-
}
|
|
8537
|
-
accumulated[newPath] = newValue;
|
|
8538
|
-
}
|
|
8539
|
-
function mergeDirtyChanges(accumulated, newChanges) {
|
|
8540
|
-
for (const path of Object.keys(newChanges)) {
|
|
8541
|
-
mergeDirtyPath(accumulated, path, newChanges[path]);
|
|
8542
|
-
}
|
|
8543
|
-
}
|
|
8544
|
-
|
|
8545
8549
|
// src/db/DexieDb.ts
|
|
8546
8550
|
var SYNC_META_TABLE = "_sync_meta";
|
|
8547
8551
|
var DIRTY_CHANGES_TABLE = "_dirty_changes";
|