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
|
|
2736
|
+
const arrayPathTokens = [];
|
|
2728
2737
|
for (const [k, v] of Object.entries(changes)) {
|
|
2729
|
-
if (Array.isArray(v))
|
|
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
|
-
|
|
2735
|
-
|
|
2736
|
-
if (
|
|
2737
|
-
|
|
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
|
|
3064
|
-
const stripped =
|
|
3065
|
-
|
|
3066
|
-
|
|
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
|
-
|
|
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
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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:
|
|
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>;
|