cry-synced-db-client 2.0.9 → 2.0.11
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 +129 -12
- package/dist/src/db/sync/SyncEngine.d.ts +28 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3231,6 +3231,17 @@ function stripServerManagedFromChanges(changes) {
|
|
|
3231
3231
|
|
|
3232
3232
|
// src/db/sync/SyncEngine.ts
|
|
3233
3233
|
var SUPRESS_DB_WARNINGS = true;
|
|
3234
|
+
function sameDirtyValue(a, b) {
|
|
3235
|
+
if (a === b) return true;
|
|
3236
|
+
if (a !== null && b !== null && typeof a === "object" && typeof b === "object") {
|
|
3237
|
+
try {
|
|
3238
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
3239
|
+
} catch (e) {
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
return false;
|
|
3244
|
+
}
|
|
3234
3245
|
var SyncEngine = class {
|
|
3235
3246
|
constructor(config) {
|
|
3236
3247
|
var _a;
|
|
@@ -3592,6 +3603,72 @@ var SyncEngine = class {
|
|
|
3592
3603
|
}
|
|
3593
3604
|
return out;
|
|
3594
3605
|
}
|
|
3606
|
+
/**
|
|
3607
|
+
* Clear dirty entries for server-ACKed ids WITHOUT losing edits authored
|
|
3608
|
+
* during the upload round-trip. SyncedDb.save() merges new diffs into the
|
|
3609
|
+
* SAME dirty entry the in-flight upload read from (addDirtyChange runs at
|
|
3610
|
+
* save()-time, not at flush-time), so a wholesale per-id delete destroys
|
|
3611
|
+
* the only upload tracking of any edit that landed between getDirty and
|
|
3612
|
+
* this clear — its content stays in the row (flushAllPendingChanges before
|
|
3613
|
+
* the re-fetch sees it) at the ACKed _rev, never re-uploads, and no
|
|
3614
|
+
* equal-rev echo ever repairs the server (mirror of the 2026-07-17
|
|
3615
|
+
* incident; regression test: test/ackClearsRoundTripDirty.test.ts).
|
|
3616
|
+
*
|
|
3617
|
+
* Re-add rule: keep exactly the paths that are NEW or CHANGED relative to
|
|
3618
|
+
* the upload-start snapshot (`dirtyAtUpload`). Everything unchanged is
|
|
3619
|
+
* dropped — it was either uploaded+ACKed, or deliberately NOT sent
|
|
3620
|
+
* (fixDotnetArrays stale-drop, preprocessDirtyItem transform); re-adding
|
|
3621
|
+
* the latter would bypass that protection AND rebase it onto the fresh
|
|
3622
|
+
* _rev so the staleness detection never fires again (regression witness:
|
|
3623
|
+
* test/concurrentArrayMerge.test.ts LEGACY case). Comparison is against
|
|
3624
|
+
* the upload-start snapshot, NOT against the sent payload: the payload
|
|
3625
|
+
* holds STRIPPED values (nested _ts/_rev/_csq removed) and possibly
|
|
3626
|
+
* translated keys, so it would false-positive every object/array path
|
|
3627
|
+
* into a re-add loop (regression witness: test/stuckDirtyDirectInject).
|
|
3628
|
+
*
|
|
3629
|
+
* Skipped ids: server-deleted (local row is gone — re-adding would
|
|
3630
|
+
* resurrect an upload) and mustRefresh (adoptMustRefresh re-adds its own
|
|
3631
|
+
* remainder from dirtyBeforeRefresh).
|
|
3632
|
+
*/
|
|
3633
|
+
async clearAckedDirtyPreservingRoundTripEdits(collection, allSuccessIds, ackedEntities, deletedEntities, mustRefreshIds, dirtyAtUpload) {
|
|
3634
|
+
var _a;
|
|
3635
|
+
if (allSuccessIds.length === 0) return;
|
|
3636
|
+
const dirtyBeforeClear = await this.dexieDb.getDirtyChangesBatch(
|
|
3637
|
+
collection,
|
|
3638
|
+
allSuccessIds
|
|
3639
|
+
);
|
|
3640
|
+
await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
|
|
3641
|
+
const deletedIds = /* @__PURE__ */ new Set();
|
|
3642
|
+
for (const e of deletedEntities) deletedIds.add(String(e._id));
|
|
3643
|
+
const ackedById = /* @__PURE__ */ new Map();
|
|
3644
|
+
for (const e of ackedEntities) ackedById.set(String(e._id), e);
|
|
3645
|
+
const reAdd = [];
|
|
3646
|
+
for (const [sid, entry] of dirtyBeforeClear) {
|
|
3647
|
+
if (deletedIds.has(sid) || mustRefreshIds.has(sid)) continue;
|
|
3648
|
+
const atUpload = dirtyAtUpload.get(sid);
|
|
3649
|
+
const remaining = {};
|
|
3650
|
+
for (const path of Object.keys(entry.changes)) {
|
|
3651
|
+
const value = entry.changes[path];
|
|
3652
|
+
if (atUpload && path in atUpload && sameDirtyValue(value, atUpload[path])) {
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
remaining[path] = value;
|
|
3656
|
+
}
|
|
3657
|
+
if (Object.keys(remaining).length === 0) continue;
|
|
3658
|
+
const ack = ackedById.get(sid);
|
|
3659
|
+
reAdd.push({
|
|
3660
|
+
id: sid,
|
|
3661
|
+
changes: remaining,
|
|
3662
|
+
baseMeta: {
|
|
3663
|
+
_ts: (_a = ack == null ? void 0 : ack._ts) != null ? _a : entry.baseTs,
|
|
3664
|
+
_rev: typeof (ack == null ? void 0 : ack._rev) === "number" ? ack._rev : entry.baseRev
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
if (reAdd.length > 0) {
|
|
3669
|
+
await this.dexieDb.addDirtyChangesBatch(collection, reAdd);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3595
3672
|
/**
|
|
3596
3673
|
* Diagnostic guard for the ACK writeback path: after pending writes have
|
|
3597
3674
|
* been flushed and the row re-read, every plain scalar top-level field of
|
|
@@ -3632,8 +3709,9 @@ var SyncEngine = class {
|
|
|
3632
3709
|
* Upload dirty items for all collections.
|
|
3633
3710
|
*/
|
|
3634
3711
|
async uploadDirtyItems(calledFrom) {
|
|
3635
|
-
var
|
|
3712
|
+
var _b, _c, _d;
|
|
3636
3713
|
const collectionBatches = [];
|
|
3714
|
+
const dirtyAtUploadByCollection = /* @__PURE__ */ new Map();
|
|
3637
3715
|
for (const [collectionName] of this.collections)
|
|
3638
3716
|
try {
|
|
3639
3717
|
const dirtyChanges = await this.dexieDb.getDirty(collectionName);
|
|
@@ -3642,6 +3720,12 @@ var SyncEngine = class {
|
|
|
3642
3720
|
for (const dirtyItem of dirtyChanges) {
|
|
3643
3721
|
dirtyChangesMap.set(String(dirtyItem._id), dirtyItem);
|
|
3644
3722
|
}
|
|
3723
|
+
const atUpload = /* @__PURE__ */ new Map();
|
|
3724
|
+
for (const [sid, dc] of dirtyChangesMap) {
|
|
3725
|
+
const _a = dc, { _id, _ts, _rev } = _a, changes = __objRest(_a, ["_id", "_ts", "_rev"]);
|
|
3726
|
+
atUpload.set(sid, changes);
|
|
3727
|
+
}
|
|
3728
|
+
dirtyAtUploadByCollection.set(collectionName, atUpload);
|
|
3645
3729
|
const updates = [];
|
|
3646
3730
|
const skipped = [];
|
|
3647
3731
|
const ids = dirtyChanges.map((dc) => dc._id);
|
|
@@ -3829,7 +3913,7 @@ var SyncEngine = class {
|
|
|
3829
3913
|
for (const b of batch) {
|
|
3830
3914
|
const allIds = [];
|
|
3831
3915
|
for (const u of b.batch.updates) allIds.push(u._id);
|
|
3832
|
-
for (const d of b.batch.deletes) allIds.push((
|
|
3916
|
+
for (const d of b.batch.deletes) allIds.push((_b = d._id) != null ? _b : d);
|
|
3833
3917
|
const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(
|
|
3834
3918
|
b.collection,
|
|
3835
3919
|
allIds
|
|
@@ -3891,7 +3975,14 @@ var SyncEngine = class {
|
|
|
3891
3975
|
]) : /* @__PURE__ */ new Map();
|
|
3892
3976
|
const uploadedByIdRefresh = mustRefreshIds.size > 0 ? this.uploadedSnapshotFor(collectionBatches, collection) : /* @__PURE__ */ new Map();
|
|
3893
3977
|
if (allSuccessIds.length > 0) {
|
|
3894
|
-
await this.
|
|
3978
|
+
await this.clearAckedDirtyPreservingRoundTripEdits(
|
|
3979
|
+
collection,
|
|
3980
|
+
allSuccessIds,
|
|
3981
|
+
inserted.concat(updated),
|
|
3982
|
+
deleted,
|
|
3983
|
+
mustRefreshIds,
|
|
3984
|
+
(_c = dirtyAtUploadByCollection.get(collection)) != null ? _c : /* @__PURE__ */ new Map()
|
|
3985
|
+
);
|
|
3895
3986
|
}
|
|
3896
3987
|
const sentIdsForCollection = /* @__PURE__ */ new Set();
|
|
3897
3988
|
for (const batch of collectionBatches) {
|
|
@@ -3917,7 +4008,7 @@ var SyncEngine = class {
|
|
|
3917
4008
|
await this.callOnDirtyItemStuck(collection, newlyStuck, calledFrom);
|
|
3918
4009
|
}
|
|
3919
4010
|
let collectionSentCount = 0;
|
|
3920
|
-
const isWriteOnly = (
|
|
4011
|
+
const isWriteOnly = (_d = this.collections.get(collection)) == null ? void 0 : _d.writeOnly;
|
|
3921
4012
|
const insertedAndUpdated = inserted.concat(updated);
|
|
3922
4013
|
if (insertedAndUpdated.length > 0) {
|
|
3923
4014
|
const idsToCheck = [];
|
|
@@ -4098,6 +4189,11 @@ var SyncEngine = class {
|
|
|
4098
4189
|
for (const dirtyItem of dirtyItems) {
|
|
4099
4190
|
dirtyChangesMap.set(String(dirtyItem._id), dirtyItem);
|
|
4100
4191
|
}
|
|
4192
|
+
const dirtyAtUpload = /* @__PURE__ */ new Map();
|
|
4193
|
+
for (const [sid, dc] of dirtyChangesMap) {
|
|
4194
|
+
const _a = dc, { _id, _ts, _rev } = _a, changes = __objRest(_a, ["_id", "_ts", "_rev"]);
|
|
4195
|
+
dirtyAtUpload.set(sid, changes);
|
|
4196
|
+
}
|
|
4101
4197
|
const updates = [];
|
|
4102
4198
|
for (const fullItem of fullItems) {
|
|
4103
4199
|
if (!fullItem) continue;
|
|
@@ -4115,7 +4211,7 @@ var SyncEngine = class {
|
|
|
4115
4211
|
collection,
|
|
4116
4212
|
batch: {
|
|
4117
4213
|
updates: updates.map((item) => {
|
|
4118
|
-
const
|
|
4214
|
+
const _a2 = item.delta, { _ts, _rev } = _a2, changes = __objRest(_a2, ["_ts", "_rev"]);
|
|
4119
4215
|
return {
|
|
4120
4216
|
_id: item._id,
|
|
4121
4217
|
_rev: typeof _rev === "number" ? _rev : 0,
|
|
@@ -4187,7 +4283,14 @@ var SyncEngine = class {
|
|
|
4187
4283
|
const dirtyBeforeRefresh = refreshIds.length > 0 ? await this.dexieDb.getDirtyChangesBatch(collection, refreshIds) : /* @__PURE__ */ new Map();
|
|
4188
4284
|
const uploadedByIdRefresh = refreshIds.length > 0 ? this.uploadedSnapshotFor(collectionBatches, collection) : /* @__PURE__ */ new Map();
|
|
4189
4285
|
if (allSuccessIds.length > 0) {
|
|
4190
|
-
await this.
|
|
4286
|
+
await this.clearAckedDirtyPreservingRoundTripEdits(
|
|
4287
|
+
collection,
|
|
4288
|
+
allSuccessIds,
|
|
4289
|
+
inserted.concat(updated),
|
|
4290
|
+
deleted,
|
|
4291
|
+
new Set(refreshIds),
|
|
4292
|
+
dirtyAtUpload
|
|
4293
|
+
);
|
|
4191
4294
|
}
|
|
4192
4295
|
const sentIdsForCollection = /* @__PURE__ */ new Set();
|
|
4193
4296
|
for (const batch of collectionBatches) {
|
|
@@ -6022,12 +6125,26 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6022
6125
|
* @param calledFrom Diagnostic tag threaded through to upload callbacks
|
|
6023
6126
|
*/
|
|
6024
6127
|
async flushToServer(calledFrom) {
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6128
|
+
try {
|
|
6129
|
+
if (!this.initialized) return;
|
|
6130
|
+
if (!this.connectionManager.canSync()) return;
|
|
6131
|
+
this.pendingChanges.cancelRestUploadTimer();
|
|
6132
|
+
await this.pendingChanges.flushAll();
|
|
6133
|
+
await this.pendingChanges.awaitRestUpload();
|
|
6134
|
+
await this.syncEngine.uploadDirtyItems(calledFrom);
|
|
6135
|
+
} catch (err) {
|
|
6136
|
+
console.error(`[SyncedDb] flushToServer ${(err == null ? void 0 : err.rejectMessage) || (err == null ? void 0 : err.message) || ""}`, {
|
|
6137
|
+
err,
|
|
6138
|
+
calledFrom,
|
|
6139
|
+
callstack: (() => {
|
|
6140
|
+
try {
|
|
6141
|
+
return new Error().stack;
|
|
6142
|
+
} catch (e) {
|
|
6143
|
+
return void 0;
|
|
6144
|
+
}
|
|
6145
|
+
})()
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6031
6148
|
}
|
|
6032
6149
|
/**
|
|
6033
6150
|
* Returns when all collections were last successfully synced
|
|
@@ -54,6 +54,34 @@ export declare class SyncEngine implements I_SyncEngine {
|
|
|
54
54
|
* server-reconciled paths from local edits made during the round-trip.
|
|
55
55
|
*/
|
|
56
56
|
private uploadedSnapshotFor;
|
|
57
|
+
/**
|
|
58
|
+
* Clear dirty entries for server-ACKed ids WITHOUT losing edits authored
|
|
59
|
+
* during the upload round-trip. SyncedDb.save() merges new diffs into the
|
|
60
|
+
* SAME dirty entry the in-flight upload read from (addDirtyChange runs at
|
|
61
|
+
* save()-time, not at flush-time), so a wholesale per-id delete destroys
|
|
62
|
+
* the only upload tracking of any edit that landed between getDirty and
|
|
63
|
+
* this clear — its content stays in the row (flushAllPendingChanges before
|
|
64
|
+
* the re-fetch sees it) at the ACKed _rev, never re-uploads, and no
|
|
65
|
+
* equal-rev echo ever repairs the server (mirror of the 2026-07-17
|
|
66
|
+
* incident; regression test: test/ackClearsRoundTripDirty.test.ts).
|
|
67
|
+
*
|
|
68
|
+
* Re-add rule: keep exactly the paths that are NEW or CHANGED relative to
|
|
69
|
+
* the upload-start snapshot (`dirtyAtUpload`). Everything unchanged is
|
|
70
|
+
* dropped — it was either uploaded+ACKed, or deliberately NOT sent
|
|
71
|
+
* (fixDotnetArrays stale-drop, preprocessDirtyItem transform); re-adding
|
|
72
|
+
* the latter would bypass that protection AND rebase it onto the fresh
|
|
73
|
+
* _rev so the staleness detection never fires again (regression witness:
|
|
74
|
+
* test/concurrentArrayMerge.test.ts LEGACY case). Comparison is against
|
|
75
|
+
* the upload-start snapshot, NOT against the sent payload: the payload
|
|
76
|
+
* holds STRIPPED values (nested _ts/_rev/_csq removed) and possibly
|
|
77
|
+
* translated keys, so it would false-positive every object/array path
|
|
78
|
+
* into a re-add loop (regression witness: test/stuckDirtyDirectInject).
|
|
79
|
+
*
|
|
80
|
+
* Skipped ids: server-deleted (local row is gone — re-adding would
|
|
81
|
+
* resurrect an upload) and mustRefresh (adoptMustRefresh re-adds its own
|
|
82
|
+
* remainder from dirtyBeforeRefresh).
|
|
83
|
+
*/
|
|
84
|
+
private clearAckedDirtyPreservingRoundTripEdits;
|
|
57
85
|
/**
|
|
58
86
|
* Diagnostic guard for the ACK writeback path: after pending writes have
|
|
59
87
|
* been flushed and the row re-read, every plain scalar top-level field of
|