cry-synced-db-client 0.1.157 → 0.1.159

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/dist/index.js CHANGED
@@ -2723,19 +2723,32 @@ function isPlainObject4(value) {
2723
2723
  function hasArrayIndexPath(key) {
2724
2724
  return /\.\d+(\.|$)/.test(key);
2725
2725
  }
2726
+ function pathTargetsArrayElement(key, arrayPathTokens) {
2727
+ const keyTokens = tokenizePath(key);
2728
+ if (keyTokens.length <= arrayPathTokens.length) return false;
2729
+ for (let i = 0; i < arrayPathTokens.length; i++) {
2730
+ if (keyTokens[i] !== arrayPathTokens[i]) return false;
2731
+ }
2732
+ const next = keyTokens[arrayPathTokens.length];
2733
+ return next.startsWith("[") || /^\d+$/.test(next);
2734
+ }
2726
2735
  function fixDotnetArrays(changes, serverRev, baseRev) {
2727
- const topLevelArrays = /* @__PURE__ */ new Set();
2736
+ const arrayPathTokens = [];
2728
2737
  for (const [k, v] of Object.entries(changes)) {
2729
- if (Array.isArray(v)) topLevelArrays.add(k);
2738
+ if (Array.isArray(v)) arrayPathTokens.push(tokenizePath(k));
2730
2739
  }
2731
2740
  const isStale = typeof serverRev === "number" && typeof baseRev === "number" && serverRev > baseRev;
2732
2741
  const cleaned = {};
2733
2742
  for (const [key, value] of Object.entries(changes)) {
2734
- if (hasArrayIndexPath(key)) {
2735
- const arrayField = key.split(".")[0];
2736
- if (arrayField && topLevelArrays.has(arrayField)) continue;
2737
- if (isStale) continue;
2743
+ let conflictsWithFullArray = false;
2744
+ for (const tokens of arrayPathTokens) {
2745
+ if (pathTargetsArrayElement(key, tokens)) {
2746
+ conflictsWithFullArray = true;
2747
+ break;
2748
+ }
2738
2749
  }
2750
+ if (conflictsWithFullArray) continue;
2751
+ if (isStale && hasArrayIndexPath(key)) continue;
2739
2752
  cleaned[key] = value;
2740
2753
  }
2741
2754
  return cleaned;
@@ -2789,6 +2802,42 @@ function translateKey(key, entity) {
2789
2802
  return out.join(".");
2790
2803
  }
2791
2804
 
2805
+ // src/utils/stripServerManaged.ts
2806
+ var SERVER_MANAGED_KEYS = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
2807
+ function isObjectIdLike2(v) {
2808
+ return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || v._bsontype === "ObjectID"));
2809
+ }
2810
+ function isServerManagedPath(key) {
2811
+ for (const part of tokenizePath(key)) {
2812
+ if (part.startsWith("[")) continue;
2813
+ if (SERVER_MANAGED_KEYS.has(part)) return true;
2814
+ }
2815
+ return false;
2816
+ }
2817
+ function scrubServerManagedDeep(value) {
2818
+ if (value === null || value === void 0) return value;
2819
+ if (Array.isArray(value)) {
2820
+ return value.map((item) => scrubServerManagedDeep(item));
2821
+ }
2822
+ if (typeof value !== "object") return value;
2823
+ if (value instanceof Date) return value;
2824
+ if (isObjectIdLike2(value)) return value;
2825
+ const out = {};
2826
+ for (const k of Object.keys(value)) {
2827
+ if (SERVER_MANAGED_KEYS.has(k)) continue;
2828
+ out[k] = scrubServerManagedDeep(value[k]);
2829
+ }
2830
+ return out;
2831
+ }
2832
+ function stripServerManagedFromChanges(changes) {
2833
+ const out = {};
2834
+ for (const [key, value] of Object.entries(changes)) {
2835
+ if (isServerManagedPath(key)) continue;
2836
+ out[key] = scrubServerManagedDeep(value);
2837
+ }
2838
+ return out;
2839
+ }
2840
+
2792
2841
  // src/db/sync/SyncEngine.ts
2793
2842
  var _SyncEngine = class _SyncEngine {
2794
2843
  constructor(config) {
@@ -3060,16 +3109,14 @@ var _SyncEngine = class _SyncEngine {
3060
3109
  collection: collectionName,
3061
3110
  batch: {
3062
3111
  updates: updates.map((item) => {
3063
- const _a2 = item.delta, { _ts, _rev: dirtyBaseRev } = _a2, changes = __objRest(_a2, ["_ts", "_rev"]);
3064
- const stripped = {};
3065
- for (const [k, v] of Object.entries(changes)) {
3066
- if (k.startsWith("_ts.") || k.startsWith("_rev.") || k.startsWith("_csq.")) continue;
3067
- stripped[k] = v;
3068
- }
3112
+ const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
3113
+ const stripped = stripServerManagedFromChanges(
3114
+ item.delta
3115
+ );
3069
3116
  const fixed = fixDotnetArrays(
3070
3117
  stripped,
3071
3118
  item.currentServerRev,
3072
- typeof dirtyBaseRev === "number" ? dirtyBaseRev : void 0
3119
+ dirtyBaseRev
3073
3120
  );
3074
3121
  const cleanedChanges = translateBracketPathsToIndex(fixed, item.fullItem);
3075
3122
  return {
@@ -3624,6 +3671,11 @@ var ServerUpdateHandler = class {
3624
3671
  async handleServerItemInsert(collection, serverItem) {
3625
3672
  const localItem = await this.dexieDb.getById(collection, serverItem._id);
3626
3673
  if (localItem) {
3674
+ const isStaleSelfEcho = serverItem._lastUpdaterId === this.updaterId && typeof serverItem._rev === "number" && typeof localItem._rev === "number" && serverItem._rev <= localItem._rev;
3675
+ if (isStaleSelfEcho) {
3676
+ await this.dexieDb.clearDirtyChange(collection, serverItem._id);
3677
+ return;
3678
+ }
3627
3679
  const dirtyChange = await this.dexieDb.getDirtyChange(collection, serverItem._id);
3628
3680
  const metaChanged = localItem._rev !== serverItem._rev || !this.timestampsEqual(localItem._ts, serverItem._ts);
3629
3681
  if (metaChanged) {
@@ -3649,12 +3701,13 @@ var ServerUpdateHandler = class {
3649
3701
  * Handle server item update (delta).
3650
3702
  */
3651
3703
  async handleServerItemUpdate(collection, localItem, serverDelta) {
3652
- const isLoopback = serverDelta._lastUpdaterId === this.updaterId && serverDelta._rev !== void 0 && localItem._rev !== void 0 && serverDelta._rev === localItem._rev + 1;
3653
- if (isLoopback) {
3654
- const metaChanged2 = localItem._rev !== serverDelta._rev || !this.timestampsEqual(localItem._ts, serverDelta._ts);
3655
- if (metaChanged2) {
3704
+ const serverRev = serverDelta._rev;
3705
+ const localRev = localItem._rev;
3706
+ const isSelfEcho = serverDelta._lastUpdaterId === this.updaterId && typeof serverRev === "number" && typeof localRev === "number" && serverRev <= localRev + 1;
3707
+ if (isSelfEcho) {
3708
+ if (serverRev > localRev) {
3656
3709
  await this.dexieDb.save(collection, serverDelta._id, {
3657
- _rev: serverDelta._rev,
3710
+ _rev: serverRev,
3658
3711
  _ts: serverDelta._ts
3659
3712
  });
3660
3713
  }
@@ -4745,6 +4798,7 @@ var _SyncedDb = class _SyncedDb {
4745
4798
  });
4746
4799
  delete update._id;
4747
4800
  }
4801
+ _SyncedDb.ensureNestedIds(update);
4748
4802
  update = _SyncedDb.stringifyObjectIds(update);
4749
4803
  const existing = await this.dexieDb.getById(collection, id);
4750
4804
  if (!existing && !((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
@@ -4775,6 +4829,7 @@ var _SyncedDb = class _SyncedDb {
4775
4829
  async upsert(collection, query, update) {
4776
4830
  this.assertCollection(collection);
4777
4831
  this.ensureId(update, "upsert", collection);
4832
+ _SyncedDb.ensureNestedIds(update);
4778
4833
  query = _SyncedDb.stringifyObjectIds(query);
4779
4834
  update = _SyncedDb.stringifyObjectIds(update);
4780
4835
  const existing = await this.findOne(collection, query);
@@ -4788,6 +4843,7 @@ var _SyncedDb = class _SyncedDb {
4788
4843
  var _a;
4789
4844
  this.assertCollection(collection);
4790
4845
  this.ensureId(data, "insert", collection);
4846
+ _SyncedDb.ensureNestedIds(data);
4791
4847
  data = _SyncedDb.stringifyObjectIds(data);
4792
4848
  const id = String(data._id);
4793
4849
  const existing = await this.dexieDb.getById(collection, id);
@@ -5889,6 +5945,48 @@ var _SyncedDb = class _SyncedDb {
5889
5945
  static isObjectIdLike(v) {
5890
5946
  return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || typeof v.toHexString === "function"));
5891
5947
  }
5948
+ /**
5949
+ * Recursively walk `value` and ensure every plain object that appears
5950
+ * as an element of an array carries an `_id`. Missing `_id`s are
5951
+ * generated as `new ObjectId().toHexString()` (string, not BSON instance).
5952
+ *
5953
+ * Mutates the input tree in place — caller sees the freshly-assigned ids
5954
+ * (matches the pattern of `ensureId`).
5955
+ *
5956
+ * Why: `computeDiff` falls back to a full-array replace whenever any
5957
+ * element of an array of objects lacks `_id` (see `allElementsHaveId`).
5958
+ * That defeats element-wise `arr[<_id>].field` paths and re-introduces
5959
+ * the stale-array-overwrite bug. Stamping ids upfront keeps every save
5960
+ * on the per-element bracket path.
5961
+ *
5962
+ * Skipped:
5963
+ * - primitives (numbers, strings, booleans, null, undefined)
5964
+ * - `Date`, `ObjectId`-like values
5965
+ * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
5966
+ * itself is handled by `ensureId`)
5967
+ */
5968
+ static ensureNestedIds(value) {
5969
+ if (value === null || value === void 0) return;
5970
+ if (typeof value !== "object") return;
5971
+ if (value instanceof Date) return;
5972
+ if (_SyncedDb.isObjectIdLike(value)) return;
5973
+ if (Array.isArray(value)) {
5974
+ for (const element of value) {
5975
+ if (element !== null && typeof element === "object" && !Array.isArray(element) && !(element instanceof Date) && !_SyncedDb.isObjectIdLike(element)) {
5976
+ if (element._id == null || element._id === "") {
5977
+ element._id = new ObjectId2().toHexString();
5978
+ } else if (_SyncedDb.isObjectIdLike(element._id)) {
5979
+ element._id = String(element._id);
5980
+ }
5981
+ }
5982
+ _SyncedDb.ensureNestedIds(element);
5983
+ }
5984
+ return;
5985
+ }
5986
+ for (const key of Object.keys(value)) {
5987
+ _SyncedDb.ensureNestedIds(value[key]);
5988
+ }
5989
+ }
5892
5990
  /**
5893
5991
  * Asserts write-only collection has online connectivity for reads.
5894
5992
  * @throws Error if offline
@@ -403,6 +403,27 @@ export declare class SyncedDb implements I_SyncedDb {
403
403
  */
404
404
  private static stringifyObjectIds;
405
405
  private static isObjectIdLike;
406
+ /**
407
+ * Recursively walk `value` and ensure every plain object that appears
408
+ * as an element of an array carries an `_id`. Missing `_id`s are
409
+ * generated as `new ObjectId().toHexString()` (string, not BSON instance).
410
+ *
411
+ * Mutates the input tree in place — caller sees the freshly-assigned ids
412
+ * (matches the pattern of `ensureId`).
413
+ *
414
+ * Why: `computeDiff` falls back to a full-array replace whenever any
415
+ * element of an array of objects lacks `_id` (see `allElementsHaveId`).
416
+ * That defeats element-wise `arr[<_id>].field` paths and re-introduces
417
+ * the stale-array-overwrite bug. Stamping ids upfront keeps every save
418
+ * on the per-element bracket path.
419
+ *
420
+ * Skipped:
421
+ * - primitives (numbers, strings, booleans, null, undefined)
422
+ * - `Date`, `ObjectId`-like values
423
+ * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
424
+ * itself is handled by `ensureId`)
425
+ */
426
+ private static ensureNestedIds;
406
427
  /**
407
428
  * Asserts write-only collection has online connectivity for reads.
408
429
  * @throws Error if offline
@@ -13,6 +13,28 @@
13
13
  * "arr.10" → true (multi-digit index)
14
14
  */
15
15
  export declare function hasArrayIndexPath(key: string): boolean;
16
+ /**
17
+ * `true` if `key` targets an element of the array at `arrayPath`.
18
+ *
19
+ * Both inputs are tokenized via `tokenizePath` (so dot AND bracket-by-_id
20
+ * segments are normalized). The key targets an element iff:
21
+ * 1. its first N tokens equal `arrayPath`'s tokens (prefix match), AND
22
+ * 2. the very next token is a numeric index ("0", "1", …) OR a bracket
23
+ * `[<_id>]` segment.
24
+ *
25
+ * Examples (with arrayPath = "zaracunaj"):
26
+ * "zaracunaj" → false (same path, not a child)
27
+ * "zaracunaj.0.kolicina" → true (child via numeric index)
28
+ * "zaracunaj[<_id>].kolicina" → true (child via bracket _id)
29
+ * "zaracunajX" → false (different field)
30
+ * "zaracunaj.kolicina" → false (object field, not element)
31
+ *
32
+ * Examples (with arrayPath = "_redundanca.terapije"):
33
+ * "_redundanca.terapije[A].postavke[B].x" → true
34
+ * "_redundanca.terapije.0.x" → true
35
+ * "_redundanca.terapija" → false (different field)
36
+ */
37
+ export declare function pathTargetsArrayElement(key: string, arrayPathTokens: readonly string[]): boolean;
16
38
  /**
17
39
  * TEMPORARY mitigation: when server has advanced past client's base _rev
18
40
  * (i.e., the dirty was emitted against a stale snapshot of the record),
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `true` if `key` (a dot/bracket-notation path) targets a server-managed
3
+ * metadata field anywhere along its tokenized path.
4
+ *
5
+ * Examples:
6
+ * "_ts" → true
7
+ * "_ts.t" → true (token `_ts`)
8
+ * "_rev" → true
9
+ * "field._ts" → true
10
+ * "arr[<_id>]._rev" → true
11
+ * "_redundanca.terapije[X]._ts.i" → true
12
+ * "stranka.gsm" → false
13
+ * "zaracunaj[<_id>].kolicina" → false
14
+ */
15
+ export declare function isServerManagedPath(key: string): boolean;
16
+ /**
17
+ * Recursively scrub `_ts` / `_rev` / `_csq` from a value tree without
18
+ * mutating the input. Returns a new tree with the offending keys removed
19
+ * at every depth, including inside arrays.
20
+ *
21
+ * Primitives, Date, and ObjectId-like instances pass through by reference.
22
+ */
23
+ export declare function scrubServerManagedDeep<T>(value: T): T;
24
+ /**
25
+ * Strip server-managed metadata from a dirty-changes payload at upload time.
26
+ *
27
+ * 1. Drop any path key whose tokenized path includes `_ts`, `_rev`, or `_csq`.
28
+ * 2. For surviving entries, recursively scrub the value (in case a value is
29
+ * a full object/array replace that contains nested `_ts` / `_rev`).
30
+ *
31
+ * Returns a new object — input is not mutated.
32
+ */
33
+ export declare function stripServerManagedFromChanges(changes: Record<string, unknown>): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.157",
3
+ "version": "0.1.159",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",