cry-synced-db-client 2.0.13 → 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.js +118 -114
  3. 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 diff = computeObjDiff(base, fullChanges);
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",