cry-synced-db-client 0.1.156 → 0.1.158

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,16 +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
- if (typeof serverRev !== "number" || typeof baseRev !== "number") {
2728
- return changes;
2729
- }
2730
- if (serverRev <= baseRev) {
2731
- return changes;
2736
+ const arrayPathTokens = [];
2737
+ for (const [k, v] of Object.entries(changes)) {
2738
+ if (Array.isArray(v)) arrayPathTokens.push(tokenizePath(k));
2732
2739
  }
2740
+ const isStale = typeof serverRev === "number" && typeof baseRev === "number" && serverRev > baseRev;
2733
2741
  const cleaned = {};
2734
2742
  for (const [key, value] of Object.entries(changes)) {
2735
- if (hasArrayIndexPath(key)) continue;
2743
+ let conflictsWithFullArray = false;
2744
+ for (const tokens of arrayPathTokens) {
2745
+ if (pathTargetsArrayElement(key, tokens)) {
2746
+ conflictsWithFullArray = true;
2747
+ break;
2748
+ }
2749
+ }
2750
+ if (conflictsWithFullArray) continue;
2751
+ if (isStale && hasArrayIndexPath(key)) continue;
2736
2752
  cleaned[key] = value;
2737
2753
  }
2738
2754
  return cleaned;
@@ -2786,6 +2802,42 @@ function translateKey(key, entity) {
2786
2802
  return out.join(".");
2787
2803
  }
2788
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
+
2789
2841
  // src/db/sync/SyncEngine.ts
2790
2842
  var _SyncEngine = class _SyncEngine {
2791
2843
  constructor(config) {
@@ -2989,6 +3041,7 @@ var _SyncEngine = class _SyncEngine {
2989
3041
  dirtyChangesMap.set(String(dirtyItem._id), dirtyItem);
2990
3042
  }
2991
3043
  const updates = [];
3044
+ const skipped = [];
2992
3045
  const ids = dirtyChanges.map((dc) => dc._id);
2993
3046
  const fullItems = await this.dexieDb.getByIds(collectionName, ids);
2994
3047
  for (let i = 0; i < fullItems.length; i++) {
@@ -2999,6 +3052,8 @@ var _SyncEngine = class _SyncEngine {
2999
3052
  if (delta) {
3000
3053
  const currentServerRev = typeof fullItem._rev === "number" ? fullItem._rev : void 0;
3001
3054
  updates.push({ _id: fullItem._id, delta, currentServerRev, fullItem });
3055
+ } else {
3056
+ skipped.push({ _id: String(fullItem._id), reason: "no-delta-for-fullitem" });
3002
3057
  }
3003
3058
  } else if (id != null) {
3004
3059
  const delta = dirtyChangesMap.get(String(id));
@@ -3006,29 +3061,62 @@ var _SyncEngine = class _SyncEngine {
3006
3061
  const reconstructed = __spreadProps(__spreadValues({}, delta), { _id: id });
3007
3062
  await this.dexieDb.save(collectionName, id, reconstructed);
3008
3063
  updates.push({ _id: id, delta });
3064
+ } else {
3065
+ skipped.push({ _id: String(id), reason: "no-delta-for-orphan" });
3009
3066
  }
3067
+ } else {
3068
+ skipped.push({ _id: "<null>", reason: "no-fullitem-no-id" });
3010
3069
  }
3011
3070
  }
3012
3071
  if (updates.length === 0) {
3013
3072
  console.warn(
3014
- `uploadDirtyItems: ${collectionName} has ${dirtyChanges.length} dirty entries but 0 resolvable items`
3073
+ `uploadDirtyItems: ${collectionName} has ${dirtyChanges.length} dirty entries but 0 resolvable items`,
3074
+ skipped
3015
3075
  );
3076
+ if (this.callbacks.onUploadSkip) {
3077
+ try {
3078
+ this.callbacks.onUploadSkip({
3079
+ collection: collectionName,
3080
+ reason: "no-resolvable-items",
3081
+ dirtyCount: dirtyChanges.length,
3082
+ skippedIds: skipped.slice(0, 20).map((s) => s._id),
3083
+ skipReasons: skipped.slice(0, 20),
3084
+ calledFrom,
3085
+ timestamp: /* @__PURE__ */ new Date()
3086
+ });
3087
+ } catch (err) {
3088
+ console.error("onUploadSkip callback failed:", err);
3089
+ }
3090
+ }
3016
3091
  continue;
3017
3092
  }
3093
+ if (skipped.length > 0 && this.callbacks.onUploadSkip) {
3094
+ try {
3095
+ this.callbacks.onUploadSkip({
3096
+ collection: collectionName,
3097
+ reason: "no-resolvable-items",
3098
+ dirtyCount: dirtyChanges.length,
3099
+ skippedIds: skipped.slice(0, 20).map((s) => s._id),
3100
+ skipReasons: skipped.slice(0, 20),
3101
+ calledFrom,
3102
+ timestamp: /* @__PURE__ */ new Date()
3103
+ });
3104
+ } catch (err) {
3105
+ console.error("onUploadSkip callback failed:", err);
3106
+ }
3107
+ }
3018
3108
  collectionBatches.push([{
3019
3109
  collection: collectionName,
3020
3110
  batch: {
3021
3111
  updates: updates.map((item) => {
3022
- const _a2 = item.delta, { _ts, _rev: dirtyBaseRev } = _a2, changes = __objRest(_a2, ["_ts", "_rev"]);
3023
- const stripped = {};
3024
- for (const [k, v] of Object.entries(changes)) {
3025
- if (k.startsWith("_ts.") || k.startsWith("_rev.") || k.startsWith("_csq.")) continue;
3026
- stripped[k] = v;
3027
- }
3112
+ const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
3113
+ const stripped = stripServerManagedFromChanges(
3114
+ item.delta
3115
+ );
3028
3116
  const fixed = fixDotnetArrays(
3029
3117
  stripped,
3030
3118
  item.currentServerRev,
3031
- typeof dirtyBaseRev === "number" ? dirtyBaseRev : void 0
3119
+ dirtyBaseRev
3032
3120
  );
3033
3121
  const cleanedChanges = translateBracketPathsToIndex(fixed, item.fullItem);
3034
3122
  return {
@@ -3932,6 +4020,8 @@ var _SyncedDb = class _SyncedDb {
3932
4020
  this.onWakeSync = config.onWakeSync;
3933
4021
  this.onEvictionStart = config.onEvictionStart;
3934
4022
  this.onEviction = config.onEviction;
4023
+ this.onSaveIdMismatch = config.onSaveIdMismatch;
4024
+ this.onUploadSkip = config.onUploadSkip;
3935
4025
  this.evictStaleRecordsEveryHrs = (_e = config.evictStaleRecordsEveryHrs) != null ? _e : 0;
3936
4026
  this.scopeExitLookbehindMs = (_f = config.scopeExitLookbehindMs) != null ? _f : 0;
3937
4027
  this.evictOnWake = (_g = config.evictOnWake) != null ? _g : false;
@@ -4068,7 +4158,8 @@ var _SyncedDb = class _SyncedDb {
4068
4158
  onServerWriteRequest: config.onServerWriteRequest,
4069
4159
  onServerWriteResult: config.onServerWriteResult,
4070
4160
  onFindNewerManyCall: config.onFindNewerManyCall,
4071
- onFindNewerManyResult: config.onFindNewerManyResult
4161
+ onFindNewerManyResult: config.onFindNewerManyResult,
4162
+ onUploadSkip: config.onUploadSkip
4072
4163
  },
4073
4164
  deps: {
4074
4165
  getSyncMetaCache: () => this.syncMetaCache,
@@ -4679,6 +4770,29 @@ var _SyncedDb = class _SyncedDb {
4679
4770
  );
4680
4771
  delete update._id;
4681
4772
  }
4773
+ if ("_id" in update && update._id && String(update._id) !== String(id)) {
4774
+ const updateKeys = Object.keys(update);
4775
+ const stack = (() => {
4776
+ try {
4777
+ return new Error().stack;
4778
+ } catch (e) {
4779
+ return void 0;
4780
+ }
4781
+ })();
4782
+ console.error(
4783
+ `SyncedDb.save("${collection}", "${String(id)}"): update._id (${JSON.stringify(update._id)}) does NOT match id (${JSON.stringify(String(id))}). Stripped from update to prevent stuck-dirty bug. The caller likely passed a stale this._id while building update from a freshly-generated record. Data keys: [${updateKeys.join(", ")}]`
4784
+ );
4785
+ this.safeCallback(this.onSaveIdMismatch, {
4786
+ collection,
4787
+ id: String(id),
4788
+ updateId: String(update._id),
4789
+ updateKeys,
4790
+ stack,
4791
+ timestamp: /* @__PURE__ */ new Date()
4792
+ });
4793
+ delete update._id;
4794
+ }
4795
+ _SyncedDb.ensureNestedIds(update);
4682
4796
  update = _SyncedDb.stringifyObjectIds(update);
4683
4797
  const existing = await this.dexieDb.getById(collection, id);
4684
4798
  if (!existing && !((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
@@ -4709,6 +4823,7 @@ var _SyncedDb = class _SyncedDb {
4709
4823
  async upsert(collection, query, update) {
4710
4824
  this.assertCollection(collection);
4711
4825
  this.ensureId(update, "upsert", collection);
4826
+ _SyncedDb.ensureNestedIds(update);
4712
4827
  query = _SyncedDb.stringifyObjectIds(query);
4713
4828
  update = _SyncedDb.stringifyObjectIds(update);
4714
4829
  const existing = await this.findOne(collection, query);
@@ -4722,6 +4837,7 @@ var _SyncedDb = class _SyncedDb {
4722
4837
  var _a;
4723
4838
  this.assertCollection(collection);
4724
4839
  this.ensureId(data, "insert", collection);
4840
+ _SyncedDb.ensureNestedIds(data);
4725
4841
  data = _SyncedDb.stringifyObjectIds(data);
4726
4842
  const id = String(data._id);
4727
4843
  const existing = await this.dexieDb.getById(collection, id);
@@ -5823,6 +5939,48 @@ var _SyncedDb = class _SyncedDb {
5823
5939
  static isObjectIdLike(v) {
5824
5940
  return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || typeof v.toHexString === "function"));
5825
5941
  }
5942
+ /**
5943
+ * Recursively walk `value` and ensure every plain object that appears
5944
+ * as an element of an array carries an `_id`. Missing `_id`s are
5945
+ * generated as `new ObjectId().toHexString()` (string, not BSON instance).
5946
+ *
5947
+ * Mutates the input tree in place — caller sees the freshly-assigned ids
5948
+ * (matches the pattern of `ensureId`).
5949
+ *
5950
+ * Why: `computeDiff` falls back to a full-array replace whenever any
5951
+ * element of an array of objects lacks `_id` (see `allElementsHaveId`).
5952
+ * That defeats element-wise `arr[<_id>].field` paths and re-introduces
5953
+ * the stale-array-overwrite bug. Stamping ids upfront keeps every save
5954
+ * on the per-element bracket path.
5955
+ *
5956
+ * Skipped:
5957
+ * - primitives (numbers, strings, booleans, null, undefined)
5958
+ * - `Date`, `ObjectId`-like values
5959
+ * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
5960
+ * itself is handled by `ensureId`)
5961
+ */
5962
+ static ensureNestedIds(value) {
5963
+ if (value === null || value === void 0) return;
5964
+ if (typeof value !== "object") return;
5965
+ if (value instanceof Date) return;
5966
+ if (_SyncedDb.isObjectIdLike(value)) return;
5967
+ if (Array.isArray(value)) {
5968
+ for (const element of value) {
5969
+ if (element !== null && typeof element === "object" && !Array.isArray(element) && !(element instanceof Date) && !_SyncedDb.isObjectIdLike(element)) {
5970
+ if (element._id == null || element._id === "") {
5971
+ element._id = new ObjectId2().toHexString();
5972
+ } else if (_SyncedDb.isObjectIdLike(element._id)) {
5973
+ element._id = String(element._id);
5974
+ }
5975
+ }
5976
+ _SyncedDb.ensureNestedIds(element);
5977
+ }
5978
+ return;
5979
+ }
5980
+ for (const key of Object.keys(value)) {
5981
+ _SyncedDb.ensureNestedIds(value[key]);
5982
+ }
5983
+ }
5826
5984
  /**
5827
5985
  * Asserts write-only collection has online connectivity for reads.
5828
5986
  * @throws Error if offline
@@ -52,6 +52,8 @@ export declare class SyncedDb implements I_SyncedDb {
52
52
  private readonly onWakeSync?;
53
53
  private readonly onEvictionStart?;
54
54
  private readonly onEviction?;
55
+ private readonly onSaveIdMismatch?;
56
+ private readonly onUploadSkip?;
55
57
  private readonly evictStaleRecordsEveryHrs;
56
58
  private readonly scopeExitLookbehindMs;
57
59
  private readonly evictOnWake;
@@ -401,6 +403,27 @@ export declare class SyncedDb implements I_SyncedDb {
401
403
  */
402
404
  private static stringifyObjectIds;
403
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;
404
427
  /**
405
428
  * Asserts write-only collection has online connectivity for reads.
406
429
  * @throws Error if offline
@@ -261,6 +261,7 @@ export interface SyncEngineCallbacks {
261
261
  onServerWriteResult?: (info: ServerWriteResultInfo) => void;
262
262
  onFindNewerManyCall?: (info: FindNewerManyCallInfo) => void;
263
263
  onFindNewerManyResult?: (info: FindNewerManyResultInfo) => void;
264
+ onUploadSkip?: (info: import("../../types/I_SyncedDb").UploadSkipInfo) => void;
264
265
  }
265
266
  export interface SyncEngineDeps {
266
267
  getSyncMetaCache: () => Map<string, SyncMeta>;
@@ -35,6 +35,62 @@ export interface InfrastructureErrorInfo {
35
35
  /** Timestamp when error occurred */
36
36
  timestamp: Date;
37
37
  }
38
+ /**
39
+ * Payload za upload-skip callback. Sproži se, ko `uploadDirtyItems` najde
40
+ * dirty zapise v Dexie `_dirty_changes`, ampak iz njih ne more zgraditi
41
+ * resolve-able update batch (npr. fullItem manjka in `id` je null/undefined,
42
+ * ali `dirtyChangesMap.get` vrne undefined zaradi key mismatch-a).
43
+ *
44
+ * Production incident 2026-05-08 (etv/prvak/studenci/tackakp racuni stuck):
45
+ * 14 dirty zapisov nikoli ne pride na server, brez napake na rdb2/errors,
46
+ * `lastDirtySyncAt: null` (sentCount=0). Ta callback razkrije za katere
47
+ * konkretne `_id`-je se to dogaja.
48
+ */
49
+ export interface UploadSkipInfo {
50
+ /** Collection kjer je skip nastal */
51
+ collection: string;
52
+ /** Razlog za skip */
53
+ reason: "no-resolvable-items" | "no-batches";
54
+ /** Število dirty zapisov v Dexie pred skip-om */
55
+ dirtyCount: number;
56
+ /** Konkretni _id-ji, ki so šli skozi skip pot (do 20) */
57
+ skippedIds: string[];
58
+ /** Razlog za vsak skipped _id (do 20) */
59
+ skipReasons: {
60
+ _id: string;
61
+ reason: "no-fullitem-no-id" | "no-delta-for-fullitem" | "no-delta-for-orphan";
62
+ }[];
63
+ /** calledFrom iz sync flow-a */
64
+ calledFrom?: string;
65
+ /** Timestamp */
66
+ timestamp: Date;
67
+ }
68
+ /**
69
+ * Payload za save() id-mismatch callback. Sproži se, ko caller pokliče
70
+ * `save(collection, id, update)` z `update._id` ki se NE ujema z `id`.
71
+ *
72
+ * Brez popravka bi addDirtyChange shranil entry.id = id, entry.changes._id =
73
+ * update._id; pri uploadu bi server dobil update._id, vrnil ack pod update._id,
74
+ * client bi poskušal clear pod id → ne ujema → dirty stuck forever.
75
+ *
76
+ * Library v save() avtomatsko strip-a `_id` iz update preden pokliče
77
+ * addDirtyChange. Callback je samo za telemetry/syslog/alerting — caller bug
78
+ * mora biti popravljen v aplikaciji.
79
+ */
80
+ export interface SaveIdMismatchInfo {
81
+ /** Collection name */
82
+ collection: string;
83
+ /** id parameter passed to save() */
84
+ id: string;
85
+ /** _id field present in update object (mismatched) */
86
+ updateId: string;
87
+ /** Top-level keys of the update object (no values, no PII) */
88
+ updateKeys: string[];
89
+ /** Stack trace at the call site */
90
+ stack?: string;
91
+ /** Timestamp when mismatch detected */
92
+ timestamp: Date;
93
+ }
38
94
  /**
39
95
  * Callback payload for server write requests (before sending)
40
96
  */
@@ -462,6 +518,29 @@ export interface SyncedDbConfig {
462
518
  * Client code can use this to show warnings, log telemetry, or adjust behavior.
463
519
  */
464
520
  onInfrastructureError?: (info: InfrastructureErrorInfo) => void;
521
+ /**
522
+ * Callback when `uploadDirtyItems` skips dirty entries (silent skip without
523
+ * sending to server). Used to diagnose stuck-dirty pattern where dirty
524
+ * exists but `sentCount === 0` (lastDirtySyncAt stays null).
525
+ *
526
+ * Production incident 2026-05-08 — 14 stuck racuni/protokoli on prod.
527
+ * Server has no record, no rdb2 error, dirty stays forever. This callback
528
+ * reveals which dirty entries hit which skip path.
529
+ */
530
+ onUploadSkip?: (info: UploadSkipInfo) => void;
531
+ /**
532
+ * Callback when `save(collection, id, update)` is called with `update._id`
533
+ * that does NOT match `id`. Library auto-strips `_id` from update to prevent
534
+ * the stuck-dirty bug, but the caller bug should be fixed in the app code.
535
+ *
536
+ * Use this callback for telemetry/syslog/alerting — the mismatch is silent
537
+ * data corruption (dirty stays forever) without observability.
538
+ *
539
+ * Production incident 2026-05-08 (zvitorepka, 3 stuck protokoli) was caused
540
+ * by `pages/protokol.vue` calling `save("protokoli", this._id, this.protokol)`
541
+ * where `this._id` and `this.protokol._id` had drifted apart.
542
+ */
543
+ onSaveIdMismatch?: (info: SaveIdMismatchInfo) => void;
465
544
  /**
466
545
  * Enable in-memory object metadata feature.
467
546
  * When true, collections with hasMetadata=true will have their metadata callbacks invoked
@@ -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.156",
3
+ "version": "0.1.158",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",