cry-synced-db-client 2.0.5 → 2.0.7
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
|
@@ -919,21 +919,23 @@ var InMemManager = class {
|
|
|
919
919
|
const config = this.collections.get(collection);
|
|
920
920
|
const source = (_a = opts == null ? void 0 : opts.source) != null ? _a : "incremental";
|
|
921
921
|
if (operation === "upsert") {
|
|
922
|
-
this.
|
|
922
|
+
const itemsToWrite = source === "incremental" ? this.dropRevRegressions(collection, items) : items;
|
|
923
|
+
if (itemsToWrite.length === 0) return;
|
|
924
|
+
this.inMemDb.saveMany(collection, itemsToWrite, { source });
|
|
923
925
|
if (this.useObjectMetadata && (config == null ? void 0 : config.hasMetadata)) {
|
|
924
926
|
const ids = [];
|
|
925
|
-
for (const item of
|
|
927
|
+
for (const item of itemsToWrite) ids.push(item._id);
|
|
926
928
|
let metadatas;
|
|
927
929
|
if (config.onObjectsUpdated) {
|
|
928
930
|
try {
|
|
929
|
-
metadatas = config.onObjectsUpdated(
|
|
931
|
+
metadatas = config.onObjectsUpdated(itemsToWrite);
|
|
930
932
|
} catch (err) {
|
|
931
933
|
console.error(`[InMem] onObjectsUpdated callback failed: ${err}`, err);
|
|
932
934
|
return;
|
|
933
935
|
}
|
|
934
936
|
} else if (config.onObjectUpdated) {
|
|
935
937
|
try {
|
|
936
|
-
metadatas =
|
|
938
|
+
metadatas = itemsToWrite.map((item) => config.onObjectUpdated(item));
|
|
937
939
|
} catch (err) {
|
|
938
940
|
console.error(`[InMem] onObjectUpdated callback failed: ${err}`, err);
|
|
939
941
|
return;
|
|
@@ -952,6 +954,30 @@ var InMemManager = class {
|
|
|
952
954
|
}
|
|
953
955
|
}
|
|
954
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* Filter out items whose `_rev` is lower than what in-mem already holds
|
|
959
|
+
* for that id — an incremental write with a stale rev is, by definition,
|
|
960
|
+
* confirming/echoing a write that a later one has already superseded.
|
|
961
|
+
* Items without a numeric `_rev` on either side are let through unchanged
|
|
962
|
+
* (legacy collections without rev tracking).
|
|
963
|
+
*/
|
|
964
|
+
dropRevRegressions(collection, items) {
|
|
965
|
+
const result = [];
|
|
966
|
+
for (const item of items) {
|
|
967
|
+
const incomingRev = item._rev;
|
|
968
|
+
if (typeof incomingRev !== "number") {
|
|
969
|
+
result.push(item);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const current = this.inMemDb.getById(collection, item._id);
|
|
973
|
+
const currentRev = current == null ? void 0 : current._rev;
|
|
974
|
+
if (typeof currentRev === "number" && incomingRev < currentRev) {
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
result.push(item);
|
|
978
|
+
}
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
955
981
|
/**
|
|
956
982
|
* Initialize collection from Dexie data.
|
|
957
983
|
* Called during SyncedDb.init().
|
|
@@ -3866,8 +3892,8 @@ var SyncEngine = class {
|
|
|
3866
3892
|
collection,
|
|
3867
3893
|
idsToCheck
|
|
3868
3894
|
);
|
|
3869
|
-
const
|
|
3870
|
-
const
|
|
3895
|
+
const saveIds = [];
|
|
3896
|
+
const revById = /* @__PURE__ */ new Map();
|
|
3871
3897
|
const inMemDeleteIds = [];
|
|
3872
3898
|
const dexieDeleteIds = [];
|
|
3873
3899
|
for (let i = 0; i < insertedAndUpdated.length; i++) {
|
|
@@ -3875,17 +3901,29 @@ var SyncEngine = class {
|
|
|
3875
3901
|
const dexieItem = dexieItems[i];
|
|
3876
3902
|
if (mustRefreshIds.has(String(entity._id))) continue;
|
|
3877
3903
|
if (dexieItem) {
|
|
3878
|
-
dexieItem._rev = entity._rev;
|
|
3879
|
-
dexieItem._ts = entity._ts;
|
|
3880
3904
|
if (dexieItem._deleted) {
|
|
3881
3905
|
inMemDeleteIds.push(entity._id);
|
|
3882
3906
|
dexieDeleteIds.push(entity._id);
|
|
3883
3907
|
} else {
|
|
3884
|
-
|
|
3885
|
-
|
|
3908
|
+
saveIds.push(entity._id);
|
|
3909
|
+
revById.set(String(entity._id), { rev: entity._rev, ts: entity._ts });
|
|
3886
3910
|
}
|
|
3887
3911
|
}
|
|
3888
3912
|
}
|
|
3913
|
+
const dexieSaveBatch = [];
|
|
3914
|
+
const inMemUpdateBatch = [];
|
|
3915
|
+
if (saveIds.length > 0) {
|
|
3916
|
+
const freshItems = await this.dexieDb.getByIds(collection, saveIds);
|
|
3917
|
+
for (const freshItem of freshItems) {
|
|
3918
|
+
if (!freshItem) continue;
|
|
3919
|
+
const revTs = revById.get(String(freshItem._id));
|
|
3920
|
+
if (!revTs) continue;
|
|
3921
|
+
freshItem._rev = revTs.rev;
|
|
3922
|
+
freshItem._ts = revTs.ts;
|
|
3923
|
+
dexieSaveBatch.push(freshItem);
|
|
3924
|
+
inMemUpdateBatch.push(freshItem);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3889
3927
|
if (dexieSaveBatch.length > 0) {
|
|
3890
3928
|
await this.dexieDb.saveMany(collection, dexieSaveBatch);
|
|
3891
3929
|
}
|
|
@@ -4525,7 +4563,7 @@ var ServerUpdateHandler = class {
|
|
|
4525
4563
|
return updatedIds;
|
|
4526
4564
|
}
|
|
4527
4565
|
async processBracketedItem(collection, item) {
|
|
4528
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
4566
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
4529
4567
|
const localItem = await this.dexieDb.getById(collection, item._id);
|
|
4530
4568
|
if (!localItem) {
|
|
4531
4569
|
if (item.operation === "insert" && item.bracketedData) {
|
|
@@ -4580,13 +4618,18 @@ var ServerUpdateHandler = class {
|
|
|
4580
4618
|
case "apply":
|
|
4581
4619
|
break;
|
|
4582
4620
|
}
|
|
4621
|
+
const incomingUpdaterId = (_f = item._lastUpdaterId) != null ? _f : (_e = item.bracketedData) == null ? void 0 : _e._lastUpdaterId;
|
|
4583
4622
|
const pendingChange = this.deps.getPendingChange(collection, item._id);
|
|
4584
4623
|
if (pendingChange && item.operation === "update") {
|
|
4585
|
-
const diff = this.
|
|
4624
|
+
const diff = this.dropFieldsOwnedByPending(
|
|
4625
|
+
this.stripMetaFromDiff(item.bracketedData),
|
|
4626
|
+
pendingChange.data,
|
|
4627
|
+
incomingUpdaterId
|
|
4628
|
+
);
|
|
4586
4629
|
const currentInMemState = Object.assign({}, localItem, pendingChange.data);
|
|
4587
4630
|
const merged = applyObjDiff(currentInMemState, diff, item._id, "bracketed");
|
|
4588
|
-
merged._rev = (
|
|
4589
|
-
merged._ts = (
|
|
4631
|
+
merged._rev = (_g = item._rev) != null ? _g : localItem._rev;
|
|
4632
|
+
merged._ts = (_h = item._ts) != null ? _h : localItem._ts;
|
|
4590
4633
|
if (!merged._deleted && !merged._archived) {
|
|
4591
4634
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4592
4635
|
source: "incremental"
|
|
@@ -4596,10 +4639,14 @@ var ServerUpdateHandler = class {
|
|
|
4596
4639
|
}
|
|
4597
4640
|
const dirtyChange = await this.dexieDb.getDirtyChange(collection, item._id);
|
|
4598
4641
|
if (dirtyChange && item.operation === "update") {
|
|
4599
|
-
const diff = this.
|
|
4642
|
+
const diff = this.dropFieldsOwnedByPending(
|
|
4643
|
+
this.stripMetaFromDiff(item.bracketedData),
|
|
4644
|
+
dirtyChange,
|
|
4645
|
+
incomingUpdaterId
|
|
4646
|
+
);
|
|
4600
4647
|
const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
|
|
4601
|
-
merged._rev = (
|
|
4602
|
-
merged._ts = (
|
|
4648
|
+
merged._rev = (_i = item._rev) != null ? _i : localItem._rev;
|
|
4649
|
+
merged._ts = (_j = item._ts) != null ? _j : localItem._ts;
|
|
4603
4650
|
if (!merged._deleted && !merged._archived) {
|
|
4604
4651
|
await this.dexieDb.insert(collection, merged);
|
|
4605
4652
|
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
@@ -4883,7 +4930,12 @@ var ServerUpdateHandler = class {
|
|
|
4883
4930
|
Object.assign(pendingChange.data, newFields);
|
|
4884
4931
|
}
|
|
4885
4932
|
const currentInMemState = Object.assign({}, localItem, pendingChange.data);
|
|
4886
|
-
const
|
|
4933
|
+
const filteredDelta = this.dropFieldsOwnedByPending(
|
|
4934
|
+
serverDelta,
|
|
4935
|
+
pendingChange.data,
|
|
4936
|
+
serverDelta._lastUpdaterId
|
|
4937
|
+
);
|
|
4938
|
+
const merged = this.mergeLocalWithDelta(currentInMemState, filteredDelta);
|
|
4887
4939
|
if (!merged._deleted && !merged._archived) {
|
|
4888
4940
|
this.deps.writeToInMemBatch(
|
|
4889
4941
|
collection,
|
|
@@ -4897,7 +4949,12 @@ var ServerUpdateHandler = class {
|
|
|
4897
4949
|
const dirtyChange = await this.dexieDb.getDirtyChange(collection, serverDelta._id);
|
|
4898
4950
|
const metaChanged = localItem._rev !== serverDelta._rev || !this.timestampsEqual(localItem._ts, serverDelta._ts);
|
|
4899
4951
|
if (dirtyChange) {
|
|
4900
|
-
const
|
|
4952
|
+
const filteredDelta = this.dropFieldsOwnedByPending(
|
|
4953
|
+
serverDelta,
|
|
4954
|
+
dirtyChange,
|
|
4955
|
+
serverDelta._lastUpdaterId
|
|
4956
|
+
);
|
|
4957
|
+
const merged = this.mergeLocalWithDelta(localItem, filteredDelta);
|
|
4901
4958
|
if (metaChanged) {
|
|
4902
4959
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
4903
4960
|
}
|
|
@@ -4975,6 +5032,50 @@ var ServerUpdateHandler = class {
|
|
|
4975
5032
|
}
|
|
4976
5033
|
return local;
|
|
4977
5034
|
}
|
|
5035
|
+
/**
|
|
5036
|
+
* Drop fields from an incoming server delta/diff that our OWN in-flight
|
|
5037
|
+
* change (pending or already-dirty) already explicitly touches — but ONLY
|
|
5038
|
+
* when the incoming delta was itself authored by this same client
|
|
5039
|
+
* (`incomingUpdaterId === this.updaterId`). That is the one case where we
|
|
5040
|
+
* can safely assume our own newer local pending/dirty write supersedes the
|
|
5041
|
+
* incoming payload for that field: a slower/older echo of our OWN prior
|
|
5042
|
+
* write (e.g. a lock-write's confirmation arriving after we've already
|
|
5043
|
+
* locally reverted it, via a rev/timing gap the entry-level self-echo
|
|
5044
|
+
* check in `handleServerItemUpdate`/`checkRev` didn't catch) can otherwise
|
|
5045
|
+
* resurrect a field our own newer pending change had deliberately set or
|
|
5046
|
+
* cleared — both `mergeLocalWithDelta` and `applyObjDiff` apply whatever
|
|
5047
|
+
* the incoming payload says with no awareness of a competing, newer local
|
|
5048
|
+
* intent.
|
|
5049
|
+
*
|
|
5050
|
+
* When the incoming delta comes from a genuinely different actor (a true
|
|
5051
|
+
* second writer, or `incomingUpdaterId` is absent/unknown), this is a
|
|
5052
|
+
* skip-through no-op — that delta must always apply so real-time UX stays
|
|
5053
|
+
* live (see `test/secondActor.test.ts`): the field-name overlap alone does
|
|
5054
|
+
* NOT imply staleness for another writer's edit, only for our own.
|
|
5055
|
+
*
|
|
5056
|
+
* `ownedKeys` is checked via `hasOwnProperty` (not truthiness) because a
|
|
5057
|
+
* deliberately-cleared field is `undefined`, not absent, on the owning
|
|
5058
|
+
* diff/pendingChange.data.
|
|
5059
|
+
*
|
|
5060
|
+
* Path-based (bracketed) diff keys like `koraki[id].field` are matched by
|
|
5061
|
+
* their top-level segment — if `koraki` is owned, every path under it is
|
|
5062
|
+
* dropped, which is conservative but safe (reproduced with izdajRacun.vue's
|
|
5063
|
+
* sedajObdeluje lock/unlock race via the dbbracketed/ channel, 2026-07-14).
|
|
5064
|
+
*/
|
|
5065
|
+
dropFieldsOwnedByPending(incoming, ownedKeys, incomingUpdaterId) {
|
|
5066
|
+
var _a;
|
|
5067
|
+
if (incomingUpdaterId === void 0 || incomingUpdaterId !== this.updaterId) {
|
|
5068
|
+
return incoming;
|
|
5069
|
+
}
|
|
5070
|
+
const result = {};
|
|
5071
|
+
for (const key in incoming) {
|
|
5072
|
+
if (!Object.prototype.hasOwnProperty.call(incoming, key)) continue;
|
|
5073
|
+
const topField = (_a = key.split(/[.[]/)[0]) != null ? _a : key;
|
|
5074
|
+
if (Object.prototype.hasOwnProperty.call(ownedKeys, topField)) continue;
|
|
5075
|
+
result[key] = incoming[key];
|
|
5076
|
+
}
|
|
5077
|
+
return result;
|
|
5078
|
+
}
|
|
4978
5079
|
/**
|
|
4979
5080
|
* Fields from an incoming server delta that our OWN in-flight pending
|
|
4980
5081
|
* change doesn't already touch — safe to backfill into it so a later
|
|
@@ -28,6 +28,14 @@ export declare class InMemManager implements I_InMemManager {
|
|
|
28
28
|
writeBatch<T extends DbEntity>(collection: string, items: T[], operation: "upsert" | "delete", opts?: {
|
|
29
29
|
source?: SyncSource;
|
|
30
30
|
}): void;
|
|
31
|
+
/**
|
|
32
|
+
* Filter out items whose `_rev` is lower than what in-mem already holds
|
|
33
|
+
* for that id — an incremental write with a stale rev is, by definition,
|
|
34
|
+
* confirming/echoing a write that a later one has already superseded.
|
|
35
|
+
* Items without a numeric `_rev` on either side are let through unchanged
|
|
36
|
+
* (legacy collections without rev tracking).
|
|
37
|
+
*/
|
|
38
|
+
private dropRevRegressions;
|
|
31
39
|
/**
|
|
32
40
|
* Initialize collection from Dexie data.
|
|
33
41
|
* Called during SyncedDb.init().
|
|
@@ -81,6 +81,37 @@ export declare class ServerUpdateHandler implements I_ServerUpdateHandler {
|
|
|
81
81
|
private timestampsEqual;
|
|
82
82
|
/** @mutates local — mutira vhodni objekt namesto kopiranja */
|
|
83
83
|
private mergeLocalWithDelta;
|
|
84
|
+
/**
|
|
85
|
+
* Drop fields from an incoming server delta/diff that our OWN in-flight
|
|
86
|
+
* change (pending or already-dirty) already explicitly touches — but ONLY
|
|
87
|
+
* when the incoming delta was itself authored by this same client
|
|
88
|
+
* (`incomingUpdaterId === this.updaterId`). That is the one case where we
|
|
89
|
+
* can safely assume our own newer local pending/dirty write supersedes the
|
|
90
|
+
* incoming payload for that field: a slower/older echo of our OWN prior
|
|
91
|
+
* write (e.g. a lock-write's confirmation arriving after we've already
|
|
92
|
+
* locally reverted it, via a rev/timing gap the entry-level self-echo
|
|
93
|
+
* check in `handleServerItemUpdate`/`checkRev` didn't catch) can otherwise
|
|
94
|
+
* resurrect a field our own newer pending change had deliberately set or
|
|
95
|
+
* cleared — both `mergeLocalWithDelta` and `applyObjDiff` apply whatever
|
|
96
|
+
* the incoming payload says with no awareness of a competing, newer local
|
|
97
|
+
* intent.
|
|
98
|
+
*
|
|
99
|
+
* When the incoming delta comes from a genuinely different actor (a true
|
|
100
|
+
* second writer, or `incomingUpdaterId` is absent/unknown), this is a
|
|
101
|
+
* skip-through no-op — that delta must always apply so real-time UX stays
|
|
102
|
+
* live (see `test/secondActor.test.ts`): the field-name overlap alone does
|
|
103
|
+
* NOT imply staleness for another writer's edit, only for our own.
|
|
104
|
+
*
|
|
105
|
+
* `ownedKeys` is checked via `hasOwnProperty` (not truthiness) because a
|
|
106
|
+
* deliberately-cleared field is `undefined`, not absent, on the owning
|
|
107
|
+
* diff/pendingChange.data.
|
|
108
|
+
*
|
|
109
|
+
* Path-based (bracketed) diff keys like `koraki[id].field` are matched by
|
|
110
|
+
* their top-level segment — if `koraki` is owned, every path under it is
|
|
111
|
+
* dropped, which is conservative but safe (reproduced with izdajRacun.vue's
|
|
112
|
+
* sedajObdeluje lock/unlock race via the dbbracketed/ channel, 2026-07-14).
|
|
113
|
+
*/
|
|
114
|
+
private dropFieldsOwnedByPending;
|
|
84
115
|
/**
|
|
85
116
|
* Fields from an incoming server delta that our OWN in-flight pending
|
|
86
117
|
* change doesn't already touch — safe to backfill into it so a later
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cry-synced-db-client",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
"build": "bun run clean && bun run build:js && bun run build:types",
|
|
21
21
|
"build:js": "esbuild ./src/index.ts --bundle --outdir=./dist --target=es2017 --format=esm --platform=browser --external:dexie --external:bson --external:cry-helpers",
|
|
22
22
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
23
|
-
"test": "
|
|
24
|
-
"test:
|
|
23
|
+
"test": "bash test/run-all.sh",
|
|
24
|
+
"test:ci": "SKIP_REAL_MONGO=1 bash test/run-all.sh",
|
|
25
|
+
"test:bun": "bun test --preload ./test/preload-bun.cjs test/*.test.ts test/restProxy/*.test.ts test/node-only-bun/*.test.ts",
|
|
25
26
|
"test:node-only": "vitest run",
|
|
26
27
|
"test:real-mongo": "USE_REAL_MONGO=1 npx vitest run test/node-only/",
|
|
27
28
|
"prepublishOnly": "bun run build"
|