cry-synced-db-client 0.1.197 → 0.1.200
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/CHANGELOG.md +48 -0
- package/dist/index.js +328 -55
- package/dist/src/db/DexieDb.d.ts +1 -0
- package/dist/src/db/SyncedDb.d.ts +25 -0
- package/dist/src/db/sync/SyncEngine.d.ts +2 -1
- package/dist/src/db/types/managers.d.ts +2 -1
- package/dist/src/types/I_DexieDb.d.ts +15 -0
- package/dist/src/types/I_SyncedDb.d.ts +42 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 0.1.200 (2026-06-14)
|
|
4
|
+
|
|
5
|
+
### `preprocessDirtyItem` skip/throw zdaj incrementa `numUploadAttempts`
|
|
6
|
+
|
|
7
|
+
`SyncEngine.uploadDirtyItems`: ko `preprocessDirtyItem` vrne `undefined` ali vrže
|
|
8
|
+
exception, se item ne samo preskoči — zdaj dobi tudi `incrementDirtyUploadAttempts`,
|
|
9
|
+
tako da po 2+ skipih postane stuck in viden prek `getStuckItems()` / `onDirtyItemStuck`.
|
|
10
|
+
|
|
11
|
+
Prej so skipani itemi tiho ostali v `_dirty_changes` brez metapodatkov in nikoli ne
|
|
12
|
+
bi postali stuck — zdaj se po 3 skipih (vsak sync da 2 incrementa = main + followUp)
|
|
13
|
+
dobijo `stuckSince`.
|
|
14
|
+
|
|
15
|
+
**Internal:**
|
|
16
|
+
- V `uploadDirtyItems` zbira `preprocessSkippedIds` v obeh poteh, batch-write v
|
|
17
|
+
`incrementDirtyUploadAttempts` takoj za `for` zanko (en DB write za vse skipane
|
|
18
|
+
iteme namesto N write-ov)
|
|
19
|
+
|
|
20
|
+
**Tests:** 5 novih testov v `test/preprocessDirtyItem.test.ts`:
|
|
21
|
+
- `undefined/throw: numUploadAttempts increments across sync cycles, eventually stuck`
|
|
22
|
+
- `undefined/throw: onDirtyItemStuck fires when item becomes stuck`
|
|
23
|
+
- `mixed: stuck item via getStuckItems while non-stuck dirty still active`
|
|
24
|
+
|
|
25
|
+
876 pass, 0 fail.
|
|
26
|
+
|
|
27
|
+
## 0.1.198 (2026-06-13)
|
|
28
|
+
|
|
29
|
+
### Stuck-item tracking (`getStuckItems` / `discardStuckItems` / `onDirtyItemStuck`)
|
|
30
|
+
|
|
31
|
+
Nov mehanizem za detekcijo dirty itemov, ki jih server vztrajno zavrača.
|
|
32
|
+
|
|
33
|
+
**Fields na `DirtyChange` / `DirtyMeta`:**
|
|
34
|
+
- `firstUploadAttempt?`, `lastUploadAttempt?`, `numUploadAttempts?`, `stuckSince?`
|
|
35
|
+
- `stuckSince` se nastavi ko `numUploadAttempts > DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` (2)
|
|
36
|
+
|
|
37
|
+
**Nove metode na `SyncedDb`:**
|
|
38
|
+
- `getStuckItems()` — vrne samo stuck dirty iteme po kolekcijah (`Record<collection, DirtyMeta[]>`)
|
|
39
|
+
- `discardStuckItems(calledFrom?)` — zbriše vse stuck dirty iteme, sproži `onBeforeDirtyClearAll` z `reason: "discard-stuck"`
|
|
40
|
+
|
|
41
|
+
**Nov callback:**
|
|
42
|
+
- `onDirtyItemStuck(info: DirtyItemStuckInfo)` — sproži se ko item prvič postane stuck (po 3. neuspelem uploadu). Vsebuje `collection`, `items: DirtyMeta[]`, `calledFrom`, `timestamp`.
|
|
43
|
+
|
|
44
|
+
**Internal:**
|
|
45
|
+
- `I_DexieDb.incrementDirtyUploadAttempts(collection, ids)` — nova metoda, vrača `DirtyMeta[]` na novo stuck itemov
|
|
46
|
+
- Vgrajena v `SyncEngine.uploadDirtyItems` in `uploadDirtyItemsForCollection` — per-collection result (errored ids) in catch block (network/timeout)
|
|
47
|
+
- `DexieDb.getDirtyMeta` sedaj vključuje nova polja v izhod
|
|
48
|
+
|
|
49
|
+
**Tests:** 17 testov v `test/stuckItems.test.ts` — unit testi za increment, integracijski za get/discard, e2e za callback skozi upload pipeline.
|
|
50
|
+
|
|
3
51
|
## 0.1.196 (2026-06-10)
|
|
4
52
|
|
|
5
53
|
### `refreshImmediately` — blokirni server-fetch z verzijsko primerjavo
|
package/dist/index.js
CHANGED
|
@@ -3245,7 +3245,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3245
3245
|
configMap.set(collectionName, config);
|
|
3246
3246
|
}
|
|
3247
3247
|
this.callOnFindNewerManyCall(syncSpecs, calledFrom);
|
|
3248
|
-
this.callbackSafe(this.callbacks.onServerSyncStart, {
|
|
3248
|
+
this.callbackSafe(this.callbacks.onServerSyncStart, {
|
|
3249
|
+
calledFrom,
|
|
3250
|
+
collectionCount: syncSpecs.length
|
|
3251
|
+
});
|
|
3249
3252
|
const findNewerManyStartTime = Date.now();
|
|
3250
3253
|
const collectionState = /* @__PURE__ */ new Map();
|
|
3251
3254
|
for (const [name] of configMap) {
|
|
@@ -3276,7 +3279,12 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3276
3279
|
if (!config) return;
|
|
3277
3280
|
const state = collectionState.get(collection);
|
|
3278
3281
|
state.receivedCount += items.length;
|
|
3279
|
-
const stats = await this.processIncomingServerData(
|
|
3282
|
+
const stats = await this.processIncomingServerData(
|
|
3283
|
+
collection,
|
|
3284
|
+
config,
|
|
3285
|
+
items,
|
|
3286
|
+
state.source
|
|
3287
|
+
);
|
|
3280
3288
|
state.conflicts += stats.conflictsResolved;
|
|
3281
3289
|
if (stats.maxTs) {
|
|
3282
3290
|
if (!state.maxTs || this.compareTimestamps(stats.maxTs, state.maxTs) > 0) {
|
|
@@ -3358,12 +3366,16 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3358
3366
|
try {
|
|
3359
3367
|
uploadStats = await this.uploadDirtyItems(calledFrom);
|
|
3360
3368
|
await this.deps.flushAllPendingChanges();
|
|
3361
|
-
const followUp = await this.uploadDirtyItems(
|
|
3369
|
+
const followUp = await this.uploadDirtyItems(
|
|
3370
|
+
`${calledFrom != null ? calledFrom : "sync"}:followUp`
|
|
3371
|
+
);
|
|
3362
3372
|
if (followUp.sentCount > 0) {
|
|
3363
3373
|
uploadStats.sentCount += followUp.sentCount;
|
|
3364
3374
|
if (followUp.collectionSentCounts) {
|
|
3365
3375
|
uploadStats.collectionSentCounts = uploadStats.collectionSentCounts || {};
|
|
3366
|
-
for (const [c, n] of Object.entries(
|
|
3376
|
+
for (const [c, n] of Object.entries(
|
|
3377
|
+
followUp.collectionSentCounts
|
|
3378
|
+
)) {
|
|
3367
3379
|
uploadStats.collectionSentCounts[c] = (uploadStats.collectionSentCounts[c] || 0) + n;
|
|
3368
3380
|
}
|
|
3369
3381
|
}
|
|
@@ -3375,7 +3387,9 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3375
3387
|
);
|
|
3376
3388
|
}
|
|
3377
3389
|
sentCount = uploadStats.sentCount;
|
|
3378
|
-
for (const [collectionName, stats] of Object.entries(
|
|
3390
|
+
for (const [collectionName, stats] of Object.entries(
|
|
3391
|
+
uploadStats.collectionSentCounts || {}
|
|
3392
|
+
)) {
|
|
3379
3393
|
if (collectionStats[collectionName]) {
|
|
3380
3394
|
collectionStats[collectionName].sentCount = stats;
|
|
3381
3395
|
} else {
|
|
@@ -3473,15 +3487,20 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3473
3487
|
});
|
|
3474
3488
|
}
|
|
3475
3489
|
}
|
|
3476
|
-
if (dexieSave.length > 0)
|
|
3490
|
+
if (dexieSave.length > 0)
|
|
3491
|
+
await this.dexieDb.saveMany(collection, dexieSave);
|
|
3477
3492
|
if (dexieDeleteIds.length > 0) {
|
|
3478
3493
|
await this.dexieDb.deleteMany(collection, dexieDeleteIds);
|
|
3479
3494
|
}
|
|
3480
3495
|
if (memUpsert.length > 0) {
|
|
3481
|
-
this.deps.writeToInMemBatch(collection, memUpsert, "upsert", {
|
|
3496
|
+
this.deps.writeToInMemBatch(collection, memUpsert, "upsert", {
|
|
3497
|
+
source: "incremental"
|
|
3498
|
+
});
|
|
3482
3499
|
}
|
|
3483
3500
|
if (memDelete.length > 0) {
|
|
3484
|
-
this.deps.writeToInMemBatch(collection, memDelete, "delete", {
|
|
3501
|
+
this.deps.writeToInMemBatch(collection, memDelete, "delete", {
|
|
3502
|
+
source: "incremental"
|
|
3503
|
+
});
|
|
3485
3504
|
}
|
|
3486
3505
|
if (reAddDirty.length > 0) {
|
|
3487
3506
|
await this.dexieDb.addDirtyChangesBatch(collection, reAddDirty);
|
|
@@ -3508,7 +3527,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3508
3527
|
* Upload dirty items for all collections.
|
|
3509
3528
|
*/
|
|
3510
3529
|
async uploadDirtyItems(calledFrom) {
|
|
3511
|
-
var _a;
|
|
3530
|
+
var _a, _b;
|
|
3512
3531
|
const collectionBatches = [];
|
|
3513
3532
|
for (const [collectionName] of this.collections) {
|
|
3514
3533
|
const dirtyChanges = await this.dexieDb.getDirty(collectionName);
|
|
@@ -3520,7 +3539,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3520
3539
|
const updates = [];
|
|
3521
3540
|
const skipped = [];
|
|
3522
3541
|
const ids = dirtyChanges.map((dc) => dc._id);
|
|
3523
|
-
const fullItems = await this.dexieDb.getByIds(
|
|
3542
|
+
const fullItems = await this.dexieDb.getByIds(
|
|
3543
|
+
collectionName,
|
|
3544
|
+
ids
|
|
3545
|
+
);
|
|
3524
3546
|
const orphanReconstructed = [];
|
|
3525
3547
|
for (let i = 0; i < fullItems.length; i++) {
|
|
3526
3548
|
const fullItem = fullItems[i];
|
|
@@ -3531,7 +3553,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3531
3553
|
const currentServerRev = typeof fullItem._rev === "number" ? fullItem._rev : void 0;
|
|
3532
3554
|
updates.push({ _id: fullItem._id, delta, currentServerRev });
|
|
3533
3555
|
} else {
|
|
3534
|
-
skipped.push({
|
|
3556
|
+
skipped.push({
|
|
3557
|
+
_id: String(fullItem._id),
|
|
3558
|
+
reason: "no-delta-for-fullitem"
|
|
3559
|
+
});
|
|
3535
3560
|
}
|
|
3536
3561
|
} else if (id != null) {
|
|
3537
3562
|
const delta = dirtyChangesMap.get(String(id));
|
|
@@ -3568,7 +3593,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3568
3593
|
timestamp: /* @__PURE__ */ new Date()
|
|
3569
3594
|
});
|
|
3570
3595
|
} catch (err) {
|
|
3571
|
-
console.error(
|
|
3596
|
+
console.error(
|
|
3597
|
+
`[SyncEngine] onUploadSkip callback failed: ${err}`,
|
|
3598
|
+
err
|
|
3599
|
+
);
|
|
3572
3600
|
}
|
|
3573
3601
|
}
|
|
3574
3602
|
continue;
|
|
@@ -3585,10 +3613,14 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3585
3613
|
timestamp: /* @__PURE__ */ new Date()
|
|
3586
3614
|
});
|
|
3587
3615
|
} catch (err) {
|
|
3588
|
-
console.error(
|
|
3616
|
+
console.error(
|
|
3617
|
+
`[SyncEngine] onUploadSkip callback failed: ${err}`,
|
|
3618
|
+
err
|
|
3619
|
+
);
|
|
3589
3620
|
}
|
|
3590
3621
|
}
|
|
3591
3622
|
const mappedUpdates = [];
|
|
3623
|
+
const preprocessSkippedIds = [];
|
|
3592
3624
|
for (const item of updates) {
|
|
3593
3625
|
const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
|
|
3594
3626
|
const stripped = stripServerManagedFromChanges(
|
|
@@ -3605,8 +3637,12 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3605
3637
|
};
|
|
3606
3638
|
if (this.preprocessDirtyItem) {
|
|
3607
3639
|
try {
|
|
3608
|
-
const processed = this.preprocessDirtyItem(
|
|
3640
|
+
const processed = this.preprocessDirtyItem(
|
|
3641
|
+
candidate,
|
|
3642
|
+
collectionName
|
|
3643
|
+
);
|
|
3609
3644
|
if (processed === void 0) {
|
|
3645
|
+
preprocessSkippedIds.push(item._id);
|
|
3610
3646
|
continue;
|
|
3611
3647
|
}
|
|
3612
3648
|
candidate = processed;
|
|
@@ -3615,6 +3651,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3615
3651
|
`[SyncEngine] preprocessDirtyItem(${collectionName}) failed for _id=${String(item._id)}; keeping dirty for retry:`,
|
|
3616
3652
|
err
|
|
3617
3653
|
);
|
|
3654
|
+
preprocessSkippedIds.push(item._id);
|
|
3618
3655
|
continue;
|
|
3619
3656
|
}
|
|
3620
3657
|
}
|
|
@@ -3624,14 +3661,23 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3624
3661
|
update: candidate.update
|
|
3625
3662
|
});
|
|
3626
3663
|
}
|
|
3664
|
+
if (preprocessSkippedIds.length > 0) {
|
|
3665
|
+
const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(
|
|
3666
|
+
collectionName,
|
|
3667
|
+
preprocessSkippedIds
|
|
3668
|
+
);
|
|
3669
|
+
this.callOnDirtyItemStuck(collectionName, newlyStuck, calledFrom);
|
|
3670
|
+
}
|
|
3627
3671
|
if (mappedUpdates.length === 0) continue;
|
|
3628
|
-
collectionBatches.push([
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3672
|
+
collectionBatches.push([
|
|
3673
|
+
{
|
|
3674
|
+
collection: collectionName,
|
|
3675
|
+
batch: {
|
|
3676
|
+
updates: mappedUpdates,
|
|
3677
|
+
deletes: []
|
|
3678
|
+
}
|
|
3633
3679
|
}
|
|
3634
|
-
|
|
3680
|
+
]);
|
|
3635
3681
|
}
|
|
3636
3682
|
if (collectionBatches.length === 0) {
|
|
3637
3683
|
return { sentCount: 0 };
|
|
@@ -3664,6 +3710,18 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3664
3710
|
writeStartedAt,
|
|
3665
3711
|
calledFrom
|
|
3666
3712
|
);
|
|
3713
|
+
for (const batch of collectionBatches) {
|
|
3714
|
+
for (const b of batch) {
|
|
3715
|
+
const allIds = [];
|
|
3716
|
+
for (const u of b.batch.updates) allIds.push(u._id);
|
|
3717
|
+
for (const d of b.batch.deletes) allIds.push((_a = d._id) != null ? _a : d);
|
|
3718
|
+
const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(
|
|
3719
|
+
b.collection,
|
|
3720
|
+
allIds
|
|
3721
|
+
);
|
|
3722
|
+
this.callOnDirtyItemStuck(b.collection, newlyStuck, calledFrom);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3667
3725
|
throw err;
|
|
3668
3726
|
}
|
|
3669
3727
|
let sentCount = 0;
|
|
@@ -3712,13 +3770,38 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3712
3770
|
`[SyncEngine] Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
|
|
3713
3771
|
);
|
|
3714
3772
|
}
|
|
3715
|
-
const dirtyBeforeRefresh = mustRefreshIds.size > 0 ? await this.dexieDb.getDirtyChangesBatch(collection, [
|
|
3773
|
+
const dirtyBeforeRefresh = mustRefreshIds.size > 0 ? await this.dexieDb.getDirtyChangesBatch(collection, [
|
|
3774
|
+
...mustRefreshIds
|
|
3775
|
+
]) : /* @__PURE__ */ new Map();
|
|
3716
3776
|
const uploadedByIdRefresh = mustRefreshIds.size > 0 ? this.uploadedSnapshotFor(collectionBatches, collection) : /* @__PURE__ */ new Map();
|
|
3717
3777
|
if (allSuccessIds.length > 0) {
|
|
3718
3778
|
await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
|
|
3719
3779
|
}
|
|
3780
|
+
const sentIdsForCollection = /* @__PURE__ */ new Set();
|
|
3781
|
+
for (const batch of collectionBatches) {
|
|
3782
|
+
for (const b of batch) {
|
|
3783
|
+
if (b.collection !== collection) continue;
|
|
3784
|
+
for (const u of b.batch.updates)
|
|
3785
|
+
sentIdsForCollection.add(String(u._id));
|
|
3786
|
+
for (const d of b.batch.deletes)
|
|
3787
|
+
sentIdsForCollection.add(String(d._id));
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
const retainedIds = [];
|
|
3791
|
+
for (const sid of sentIdsForCollection) {
|
|
3792
|
+
if (!allSuccessIds.find((aid) => String(aid) === sid)) {
|
|
3793
|
+
retainedIds.push(sid);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
if (retainedIds.length > 0) {
|
|
3797
|
+
const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(
|
|
3798
|
+
collection,
|
|
3799
|
+
retainedIds
|
|
3800
|
+
);
|
|
3801
|
+
this.callOnDirtyItemStuck(collection, newlyStuck, calledFrom);
|
|
3802
|
+
}
|
|
3720
3803
|
let collectionSentCount = 0;
|
|
3721
|
-
const isWriteOnly = (
|
|
3804
|
+
const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
|
|
3722
3805
|
const insertedAndUpdated = inserted.concat(updated);
|
|
3723
3806
|
if (insertedAndUpdated.length > 0) {
|
|
3724
3807
|
const idsToCheck = [];
|
|
@@ -3726,7 +3809,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3726
3809
|
if (isWriteOnly) {
|
|
3727
3810
|
await this.dexieDb.deleteMany(collection, idsToCheck);
|
|
3728
3811
|
} else {
|
|
3729
|
-
const dexieItems = await this.dexieDb.getByIds(
|
|
3812
|
+
const dexieItems = await this.dexieDb.getByIds(
|
|
3813
|
+
collection,
|
|
3814
|
+
idsToCheck
|
|
3815
|
+
);
|
|
3730
3816
|
const dexieSaveBatch = [];
|
|
3731
3817
|
const inMemUpdateBatch = [];
|
|
3732
3818
|
const inMemDeleteIds = [];
|
|
@@ -3754,7 +3840,12 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3754
3840
|
await this.dexieDb.deleteMany(collection, dexieDeleteIds);
|
|
3755
3841
|
}
|
|
3756
3842
|
if (inMemUpdateBatch.length > 0) {
|
|
3757
|
-
this.deps.writeToInMemBatch(
|
|
3843
|
+
this.deps.writeToInMemBatch(
|
|
3844
|
+
collection,
|
|
3845
|
+
inMemUpdateBatch,
|
|
3846
|
+
"upsert",
|
|
3847
|
+
{ source: "incremental" }
|
|
3848
|
+
);
|
|
3758
3849
|
}
|
|
3759
3850
|
if (inMemDeleteIds.length > 0) {
|
|
3760
3851
|
this.deps.writeToInMemBatch(
|
|
@@ -3777,7 +3868,9 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3777
3868
|
}
|
|
3778
3869
|
await this.dexieDb.deleteMany(collection, deleteIds);
|
|
3779
3870
|
if (!isWriteOnly) {
|
|
3780
|
-
this.deps.writeToInMemBatch(collection, deleteDbEntities, "delete", {
|
|
3871
|
+
this.deps.writeToInMemBatch(collection, deleteDbEntities, "delete", {
|
|
3872
|
+
source: "incremental"
|
|
3873
|
+
});
|
|
3781
3874
|
}
|
|
3782
3875
|
sentCount += deleted.length;
|
|
3783
3876
|
collectionSentCount += deleted.length;
|
|
@@ -3793,7 +3886,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3793
3886
|
if (collectionSentCount > 0) {
|
|
3794
3887
|
collectionSentCounts[collection] = collectionSentCount;
|
|
3795
3888
|
}
|
|
3796
|
-
let maxTs
|
|
3889
|
+
let maxTs;
|
|
3797
3890
|
for (const arr of [inserted, updated, deleted]) {
|
|
3798
3891
|
for (const item of arr) {
|
|
3799
3892
|
if (item._ts) {
|
|
@@ -3857,13 +3950,16 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3857
3950
|
/**
|
|
3858
3951
|
* Upload dirty items for a specific collection.
|
|
3859
3952
|
*/
|
|
3860
|
-
async uploadDirtyItemsForCollection(collection) {
|
|
3953
|
+
async uploadDirtyItemsForCollection(collection, calledFrom) {
|
|
3861
3954
|
const dirtyItems = await this.dexieDb.getDirty(collection);
|
|
3862
3955
|
if (dirtyItems.length === 0) {
|
|
3863
3956
|
return { sentCount: 0 };
|
|
3864
3957
|
}
|
|
3865
3958
|
const ids = dirtyItems.map((item) => item._id);
|
|
3866
|
-
const fullItems = await this.dexieDb.getByIds(
|
|
3959
|
+
const fullItems = await this.dexieDb.getByIds(
|
|
3960
|
+
collection,
|
|
3961
|
+
ids
|
|
3962
|
+
);
|
|
3867
3963
|
const dirtyChangesMap = /* @__PURE__ */ new Map();
|
|
3868
3964
|
for (const dirtyItem of dirtyItems) {
|
|
3869
3965
|
dirtyChangesMap.set(String(dirtyItem._id), dirtyItem);
|
|
@@ -3879,20 +3975,24 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3879
3975
|
if (updates.length === 0) {
|
|
3880
3976
|
return { sentCount: 0 };
|
|
3881
3977
|
}
|
|
3882
|
-
const collectionBatches = [
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3978
|
+
const collectionBatches = [
|
|
3979
|
+
[
|
|
3980
|
+
{
|
|
3981
|
+
collection,
|
|
3982
|
+
batch: {
|
|
3983
|
+
updates: updates.map((item) => {
|
|
3984
|
+
const _a = item.delta, { _ts, _rev } = _a, changes = __objRest(_a, ["_ts", "_rev"]);
|
|
3985
|
+
return {
|
|
3986
|
+
_id: item._id,
|
|
3987
|
+
_rev: typeof _rev === "number" ? _rev : 0,
|
|
3988
|
+
update: changes
|
|
3989
|
+
};
|
|
3990
|
+
}),
|
|
3991
|
+
deletes: []
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
]
|
|
3995
|
+
];
|
|
3896
3996
|
const results = await this.deps.withSyncTimeout(
|
|
3897
3997
|
this.restInterface.updateCollections(collectionBatches),
|
|
3898
3998
|
"updateCollections"
|
|
@@ -3955,6 +4055,29 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3955
4055
|
if (allSuccessIds.length > 0) {
|
|
3956
4056
|
await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
|
|
3957
4057
|
}
|
|
4058
|
+
const sentIdsForCollection = /* @__PURE__ */ new Set();
|
|
4059
|
+
for (const batch of collectionBatches) {
|
|
4060
|
+
for (const b of batch) {
|
|
4061
|
+
if (b.collection !== collection) continue;
|
|
4062
|
+
for (const u of b.batch.updates)
|
|
4063
|
+
sentIdsForCollection.add(String(u._id));
|
|
4064
|
+
for (const d of b.batch.deletes)
|
|
4065
|
+
sentIdsForCollection.add(String(d._id));
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
const retainedIds = [];
|
|
4069
|
+
for (const sid of sentIdsForCollection) {
|
|
4070
|
+
if (!allSuccessIds.find((aid) => String(aid) === sid)) {
|
|
4071
|
+
retainedIds.push(sid);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
if (retainedIds.length > 0) {
|
|
4075
|
+
const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(
|
|
4076
|
+
collection,
|
|
4077
|
+
retainedIds
|
|
4078
|
+
);
|
|
4079
|
+
this.callOnDirtyItemStuck(collection, newlyStuck, calledFrom);
|
|
4080
|
+
}
|
|
3958
4081
|
if (mustRefresh && mustRefresh.length > 0) {
|
|
3959
4082
|
await this.adoptMustRefresh(
|
|
3960
4083
|
collection,
|
|
@@ -3980,7 +4103,12 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3980
4103
|
const config = this.collections.get(collectionName);
|
|
3981
4104
|
if (!config) return { updatedIds: [] };
|
|
3982
4105
|
const source = (_a = opts == null ? void 0 : opts.source) != null ? _a : "incremental";
|
|
3983
|
-
const result = await this.processIncomingServerData(
|
|
4106
|
+
const result = await this.processIncomingServerData(
|
|
4107
|
+
collectionName,
|
|
4108
|
+
config,
|
|
4109
|
+
serverData,
|
|
4110
|
+
source
|
|
4111
|
+
);
|
|
3984
4112
|
if (result.updatedIds.length > 0) {
|
|
3985
4113
|
this.deps.broadcastUpdates({ [collectionName]: result.updatedIds });
|
|
3986
4114
|
}
|
|
@@ -3998,8 +4126,14 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3998
4126
|
const chunk = serverData.slice(offset, offset + BATCH);
|
|
3999
4127
|
const chunkIds = [];
|
|
4000
4128
|
for (const item of chunk) chunkIds.push(item._id);
|
|
4001
|
-
const localItems = await this.dexieDb.getByIds(
|
|
4002
|
-
|
|
4129
|
+
const localItems = await this.dexieDb.getByIds(
|
|
4130
|
+
collectionName,
|
|
4131
|
+
chunkIds
|
|
4132
|
+
);
|
|
4133
|
+
const dirtyChangesMap = await this.dexieDb.getDirtyChangesBatch(
|
|
4134
|
+
collectionName,
|
|
4135
|
+
chunkIds
|
|
4136
|
+
);
|
|
4003
4137
|
const dexieBatch = [];
|
|
4004
4138
|
const inMemSaveBatch = [];
|
|
4005
4139
|
const inMemDeleteIds = [];
|
|
@@ -4048,7 +4182,9 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4048
4182
|
await this.dexieDb.saveMany(collectionName, dexieBatch);
|
|
4049
4183
|
}
|
|
4050
4184
|
if (inMemSaveBatch.length > 0) {
|
|
4051
|
-
this.deps.writeToInMemBatch(collectionName, inMemSaveBatch, "upsert", {
|
|
4185
|
+
this.deps.writeToInMemBatch(collectionName, inMemSaveBatch, "upsert", {
|
|
4186
|
+
source
|
|
4187
|
+
});
|
|
4052
4188
|
}
|
|
4053
4189
|
if (inMemDeleteIds.length > 0) {
|
|
4054
4190
|
this.deps.writeToInMemBatch(
|
|
@@ -4096,7 +4232,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4096
4232
|
timestamp: /* @__PURE__ */ new Date()
|
|
4097
4233
|
});
|
|
4098
4234
|
} catch (err) {
|
|
4099
|
-
console.error(
|
|
4235
|
+
console.error(
|
|
4236
|
+
`[SyncEngine] onConflictResolved callback failed: ${err}`,
|
|
4237
|
+
err
|
|
4238
|
+
);
|
|
4100
4239
|
}
|
|
4101
4240
|
}
|
|
4102
4241
|
return resolved;
|
|
@@ -4141,7 +4280,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4141
4280
|
calledFrom
|
|
4142
4281
|
});
|
|
4143
4282
|
} catch (err) {
|
|
4144
|
-
console.error(
|
|
4283
|
+
console.error(
|
|
4284
|
+
`[SyncEngine] onFindNewerManyCall callback failed: ${err}`,
|
|
4285
|
+
err
|
|
4286
|
+
);
|
|
4145
4287
|
}
|
|
4146
4288
|
}
|
|
4147
4289
|
}
|
|
@@ -4160,7 +4302,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4160
4302
|
ttfbMs: metrics == null ? void 0 : metrics.ttfbMs
|
|
4161
4303
|
});
|
|
4162
4304
|
} catch (err) {
|
|
4163
|
-
console.error(
|
|
4305
|
+
console.error(
|
|
4306
|
+
`[SyncEngine] onFindNewerManyResult callback failed: ${err}`,
|
|
4307
|
+
err
|
|
4308
|
+
);
|
|
4164
4309
|
}
|
|
4165
4310
|
}
|
|
4166
4311
|
}
|
|
@@ -4173,7 +4318,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4173
4318
|
calledFrom
|
|
4174
4319
|
});
|
|
4175
4320
|
} catch (err) {
|
|
4176
|
-
console.error(
|
|
4321
|
+
console.error(
|
|
4322
|
+
`[SyncEngine] onServerWriteRequest callback failed: ${err}`,
|
|
4323
|
+
err
|
|
4324
|
+
);
|
|
4177
4325
|
}
|
|
4178
4326
|
}
|
|
4179
4327
|
}
|
|
@@ -4188,10 +4336,29 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4188
4336
|
calledFrom
|
|
4189
4337
|
});
|
|
4190
4338
|
} catch (err) {
|
|
4191
|
-
console.error(
|
|
4339
|
+
console.error(
|
|
4340
|
+
`[SyncEngine] onServerWriteResult callback failed: ${err}`,
|
|
4341
|
+
err
|
|
4342
|
+
);
|
|
4192
4343
|
}
|
|
4193
4344
|
}
|
|
4194
4345
|
}
|
|
4346
|
+
callOnDirtyItemStuck(collection, stuckMetas, calledFrom) {
|
|
4347
|
+
if (!this.callbacks.onDirtyItemStuck || stuckMetas.length === 0) return;
|
|
4348
|
+
try {
|
|
4349
|
+
this.callbacks.onDirtyItemStuck({
|
|
4350
|
+
collection,
|
|
4351
|
+
items: stuckMetas,
|
|
4352
|
+
calledFrom,
|
|
4353
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
4354
|
+
});
|
|
4355
|
+
} catch (err) {
|
|
4356
|
+
console.error(
|
|
4357
|
+
`[SyncEngine] onDirtyItemStuck callback failed: ${err}`,
|
|
4358
|
+
err
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4195
4362
|
callOnServerSyncWrite(request, response, error, startTime, timestamp, calledFrom) {
|
|
4196
4363
|
if (!this.callbacks.onServerSyncWrite) return;
|
|
4197
4364
|
try {
|
|
@@ -4209,7 +4376,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
4209
4376
|
timestamp
|
|
4210
4377
|
});
|
|
4211
4378
|
} catch (err) {
|
|
4212
|
-
console.error(
|
|
4379
|
+
console.error(
|
|
4380
|
+
`[SyncEngine] onServerSyncWrite callback failed: ${err}`,
|
|
4381
|
+
err
|
|
4382
|
+
);
|
|
4213
4383
|
}
|
|
4214
4384
|
}
|
|
4215
4385
|
};
|
|
@@ -4867,7 +5037,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4867
5037
|
onServerSyncWrite: config.onServerSyncWrite,
|
|
4868
5038
|
onFindNewerManyCall: config.onFindNewerManyCall,
|
|
4869
5039
|
onFindNewerManyResult: config.onFindNewerManyResult,
|
|
4870
|
-
onUploadSkip: config.onUploadSkip
|
|
5040
|
+
onUploadSkip: config.onUploadSkip,
|
|
5041
|
+
onDirtyItemStuck: config.onDirtyItemStuck
|
|
4871
5042
|
},
|
|
4872
5043
|
deps: {
|
|
4873
5044
|
getSyncMetaCache: () => this.syncMetaCache,
|
|
@@ -6330,6 +6501,59 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6330
6501
|
await this.dexieDb.clearDirtyChanges(collection);
|
|
6331
6502
|
return metas;
|
|
6332
6503
|
}
|
|
6504
|
+
/**
|
|
6505
|
+
* Return dirty entries whose `stuckSince` is set across all collections.
|
|
6506
|
+
* A stuck entry has exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload
|
|
6507
|
+
* attempts without success — typically a server-side error that prevents
|
|
6508
|
+
* the record from being accepted.
|
|
6509
|
+
*
|
|
6510
|
+
* Same shape as `getDirtyMeta()` but filtered to stuck items only.
|
|
6511
|
+
* Collections with no stuck items are omitted from the result.
|
|
6512
|
+
*/
|
|
6513
|
+
async getStuckItems() {
|
|
6514
|
+
const result = {};
|
|
6515
|
+
for (const [collectionName] of this.collections) {
|
|
6516
|
+
const metas = await this.dexieDb.getDirtyMeta(collectionName);
|
|
6517
|
+
const stuck = metas.filter((m) => m.stuckSince !== void 0);
|
|
6518
|
+
if (stuck.length > 0) {
|
|
6519
|
+
result[collectionName] = stuck;
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
return result;
|
|
6523
|
+
}
|
|
6524
|
+
/**
|
|
6525
|
+
* Discard all stuck dirty entries across ALL collections without touching
|
|
6526
|
+
* the main row data. A stuck entry is one where `stuckSince` is set
|
|
6527
|
+
* (exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload attempts).
|
|
6528
|
+
*
|
|
6529
|
+
* Fires `onBeforeDirtyClearAll` once per collection that has stuck items,
|
|
6530
|
+
* with `reason: "discard-stuck"`. Returns the `DirtyMeta[]` of every
|
|
6531
|
+
* entry that was removed.
|
|
6532
|
+
*
|
|
6533
|
+
* Does NOT await in-flight uploads — call `flushToServer()` first if a
|
|
6534
|
+
* last-chance upload is desired.
|
|
6535
|
+
*
|
|
6536
|
+
* @param calledFrom Diagnostic tag threaded through to the callback.
|
|
6537
|
+
*/
|
|
6538
|
+
async discardStuckItems(calledFrom) {
|
|
6539
|
+
const cleared = [];
|
|
6540
|
+
for (const [collectionName] of this.collections) {
|
|
6541
|
+
const metas = await this.dexieDb.getDirtyMeta(collectionName);
|
|
6542
|
+
const stuck = metas.filter((m) => m.stuckSince !== void 0);
|
|
6543
|
+
if (stuck.length === 0) continue;
|
|
6544
|
+
this.safeCallback(this.onBeforeDirtyClearAll, {
|
|
6545
|
+
reason: "discard-stuck",
|
|
6546
|
+
collection: collectionName,
|
|
6547
|
+
items: stuck,
|
|
6548
|
+
calledFrom,
|
|
6549
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
6550
|
+
});
|
|
6551
|
+
const ids = stuck.map((m) => m.id);
|
|
6552
|
+
await this.dexieDb.clearDirtyChangesBatch(collectionName, ids);
|
|
6553
|
+
for (const m of stuck) cleared.push(m);
|
|
6554
|
+
}
|
|
6555
|
+
return cleared;
|
|
6556
|
+
}
|
|
6333
6557
|
// ==================== Data Deletion ====================
|
|
6334
6558
|
async dropCollection(collection, force = false) {
|
|
6335
6559
|
this.assertCollection(collection);
|
|
@@ -7359,6 +7583,9 @@ var SyncedDb = _SyncedDb;
|
|
|
7359
7583
|
// src/db/DexieDb.ts
|
|
7360
7584
|
import Dexie from "dexie";
|
|
7361
7585
|
|
|
7586
|
+
// src/types/I_DexieDb.ts
|
|
7587
|
+
var DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS = 2;
|
|
7588
|
+
|
|
7362
7589
|
// src/utils/computeDiff.ts
|
|
7363
7590
|
function isDescendantOrEqual(path, candidate) {
|
|
7364
7591
|
if (path === candidate) return true;
|
|
@@ -7645,7 +7872,11 @@ var DexieDb = class extends Dexie {
|
|
|
7645
7872
|
baseTs: entry.baseTs,
|
|
7646
7873
|
baseRev: entry.baseRev,
|
|
7647
7874
|
createdAt: entry.createdAt,
|
|
7648
|
-
updatedAt: entry.updatedAt
|
|
7875
|
+
updatedAt: entry.updatedAt,
|
|
7876
|
+
firstUploadAttempt: entry.firstUploadAttempt,
|
|
7877
|
+
lastUploadAttempt: entry.lastUploadAttempt,
|
|
7878
|
+
numUploadAttempts: entry.numUploadAttempts,
|
|
7879
|
+
stuckSince: entry.stuckSince
|
|
7649
7880
|
});
|
|
7650
7881
|
}
|
|
7651
7882
|
return result;
|
|
@@ -7723,6 +7954,45 @@ var DexieDb = class extends Dexie {
|
|
|
7723
7954
|
}
|
|
7724
7955
|
return result;
|
|
7725
7956
|
}
|
|
7957
|
+
async incrementDirtyUploadAttempts(collection, ids) {
|
|
7958
|
+
var _a;
|
|
7959
|
+
if (ids.length === 0) return [];
|
|
7960
|
+
const keys = [];
|
|
7961
|
+
for (const id of ids) keys.push([collection, this.idToString(id)]);
|
|
7962
|
+
const entries = await this.dirtyChanges.bulkGet(keys);
|
|
7963
|
+
const now = Date.now();
|
|
7964
|
+
const toUpdate = [];
|
|
7965
|
+
const newlyStuck = [];
|
|
7966
|
+
for (let i = 0; i < entries.length; i++) {
|
|
7967
|
+
const entry = entries[i];
|
|
7968
|
+
if (!entry) continue;
|
|
7969
|
+
entry.numUploadAttempts = ((_a = entry.numUploadAttempts) != null ? _a : 0) + 1;
|
|
7970
|
+
if (entry.firstUploadAttempt === void 0) {
|
|
7971
|
+
entry.firstUploadAttempt = now;
|
|
7972
|
+
}
|
|
7973
|
+
entry.lastUploadAttempt = now;
|
|
7974
|
+
if (entry.numUploadAttempts > DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS && entry.stuckSince === void 0) {
|
|
7975
|
+
entry.stuckSince = now;
|
|
7976
|
+
newlyStuck.push({
|
|
7977
|
+
collection: entry.collection,
|
|
7978
|
+
id: entry.id,
|
|
7979
|
+
baseTs: entry.baseTs,
|
|
7980
|
+
baseRev: entry.baseRev,
|
|
7981
|
+
createdAt: entry.createdAt,
|
|
7982
|
+
updatedAt: entry.updatedAt,
|
|
7983
|
+
firstUploadAttempt: entry.firstUploadAttempt,
|
|
7984
|
+
lastUploadAttempt: entry.lastUploadAttempt,
|
|
7985
|
+
numUploadAttempts: entry.numUploadAttempts,
|
|
7986
|
+
stuckSince: entry.stuckSince
|
|
7987
|
+
});
|
|
7988
|
+
}
|
|
7989
|
+
toUpdate.push(entry);
|
|
7990
|
+
}
|
|
7991
|
+
if (toUpdate.length > 0) {
|
|
7992
|
+
await this.dirtyChanges.bulkPut(toUpdate);
|
|
7993
|
+
}
|
|
7994
|
+
return newlyStuck;
|
|
7995
|
+
}
|
|
7726
7996
|
async clearDirtyChange(collection, id) {
|
|
7727
7997
|
const stringId = this.idToString(id);
|
|
7728
7998
|
await this.dirtyChanges.delete([collection, stringId]);
|
|
@@ -8231,6 +8501,7 @@ function createStructureReader(structure, firstId) {
|
|
|
8231
8501
|
inlineObjectReadThreshold = Infinity;
|
|
8232
8502
|
return readObject();
|
|
8233
8503
|
}
|
|
8504
|
+
structure.read0 = optimizedReadObject;
|
|
8234
8505
|
if (structure.highByte === 0)
|
|
8235
8506
|
structure.read = createSecondByteReader(firstId, structure.read);
|
|
8236
8507
|
return optimizedReadObject();
|
|
@@ -8247,6 +8518,7 @@ function createStructureReader(structure, firstId) {
|
|
|
8247
8518
|
return object;
|
|
8248
8519
|
}
|
|
8249
8520
|
readObject.count = 0;
|
|
8521
|
+
structure.read0 = readObject;
|
|
8250
8522
|
if (structure.highByte === 0) {
|
|
8251
8523
|
return createSecondByteReader(firstId, readObject);
|
|
8252
8524
|
}
|
|
@@ -8646,7 +8918,7 @@ var recordDefinition = (id, highByte) => {
|
|
|
8646
8918
|
}
|
|
8647
8919
|
currentStructures[id] = structure;
|
|
8648
8920
|
structure.read = createStructureReader(structure, firstByte);
|
|
8649
|
-
return structure.read();
|
|
8921
|
+
return (structure.read0 || structure.read)();
|
|
8650
8922
|
};
|
|
8651
8923
|
currentExtensions[0] = () => {
|
|
8652
8924
|
};
|
|
@@ -9024,6 +9296,7 @@ var Packr = class extends Unpackr {
|
|
|
9024
9296
|
let newSharedData = (packr3._prepareStructures || prepareStructures)(structures, packr3);
|
|
9025
9297
|
if (!encodingError) {
|
|
9026
9298
|
if (packr3.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
|
|
9299
|
+
structures.uninitialized = true;
|
|
9027
9300
|
return packr3.pack(value, encodeOptions);
|
|
9028
9301
|
}
|
|
9029
9302
|
packr3.lastNamedStructuresLength = sharedLength;
|
package/dist/src/db/DexieDb.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
|
|
|
46
46
|
}>): Promise<void>;
|
|
47
47
|
getDirtyChange(collection: string, id: Id): Promise<DirtyChange | undefined>;
|
|
48
48
|
getDirtyChangesBatch(collection: string, ids: Id[]): Promise<Map<string, DirtyChange>>;
|
|
49
|
+
incrementDirtyUploadAttempts(collection: string, ids: Id[]): Promise<DirtyMeta[]>;
|
|
49
50
|
clearDirtyChange(collection: string, id: Id): Promise<void>;
|
|
50
51
|
clearDirtyChangesBatch(collection: string, ids: Id[]): Promise<void>;
|
|
51
52
|
clearDirtyChanges(collection: string): Promise<void>;
|
|
@@ -333,6 +333,31 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
333
333
|
* upload is desired.
|
|
334
334
|
*/
|
|
335
335
|
clearDirty(collection?: string, ids?: Id[], calledFrom?: string): Promise<DirtyMeta[]>;
|
|
336
|
+
/**
|
|
337
|
+
* Return dirty entries whose `stuckSince` is set across all collections.
|
|
338
|
+
* A stuck entry has exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload
|
|
339
|
+
* attempts without success — typically a server-side error that prevents
|
|
340
|
+
* the record from being accepted.
|
|
341
|
+
*
|
|
342
|
+
* Same shape as `getDirtyMeta()` but filtered to stuck items only.
|
|
343
|
+
* Collections with no stuck items are omitted from the result.
|
|
344
|
+
*/
|
|
345
|
+
getStuckItems(): Promise<Readonly<Record<string, readonly DirtyMeta[]>>>;
|
|
346
|
+
/**
|
|
347
|
+
* Discard all stuck dirty entries across ALL collections without touching
|
|
348
|
+
* the main row data. A stuck entry is one where `stuckSince` is set
|
|
349
|
+
* (exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload attempts).
|
|
350
|
+
*
|
|
351
|
+
* Fires `onBeforeDirtyClearAll` once per collection that has stuck items,
|
|
352
|
+
* with `reason: "discard-stuck"`. Returns the `DirtyMeta[]` of every
|
|
353
|
+
* entry that was removed.
|
|
354
|
+
*
|
|
355
|
+
* Does NOT await in-flight uploads — call `flushToServer()` first if a
|
|
356
|
+
* last-chance upload is desired.
|
|
357
|
+
*
|
|
358
|
+
* @param calledFrom Diagnostic tag threaded through to the callback.
|
|
359
|
+
*/
|
|
360
|
+
discardStuckItems(calledFrom?: string): Promise<DirtyMeta[]>;
|
|
336
361
|
dropCollection(collection: string, force?: boolean): Promise<void>;
|
|
337
362
|
dropDatabase(force?: boolean): Promise<void>;
|
|
338
363
|
/**
|
|
@@ -62,7 +62,7 @@ export declare class SyncEngine implements I_SyncEngine {
|
|
|
62
62
|
/**
|
|
63
63
|
* Upload dirty items for a specific collection.
|
|
64
64
|
*/
|
|
65
|
-
uploadDirtyItemsForCollection(collection: string): Promise<UploadResult>;
|
|
65
|
+
uploadDirtyItemsForCollection(collection: string, calledFrom?: string): Promise<UploadResult>;
|
|
66
66
|
/**
|
|
67
67
|
* Process incoming server data for a single collection.
|
|
68
68
|
* Used by referToServer to process findNewer results.
|
|
@@ -89,5 +89,6 @@ export declare class SyncEngine implements I_SyncEngine {
|
|
|
89
89
|
private callOnFindNewerManyResult;
|
|
90
90
|
private callOnServerWriteRequest;
|
|
91
91
|
private callOnServerWriteResult;
|
|
92
|
+
private callOnDirtyItemStuck;
|
|
92
93
|
private callOnServerSyncWrite;
|
|
93
94
|
}
|
|
@@ -267,6 +267,7 @@ export interface SyncEngineCallbacks {
|
|
|
267
267
|
onFindNewerManyCall?: (info: FindNewerManyCallInfo) => void;
|
|
268
268
|
onFindNewerManyResult?: (info: FindNewerManyResultInfo) => void;
|
|
269
269
|
onUploadSkip?: (info: import("../../types/I_SyncedDb").UploadSkipInfo) => void;
|
|
270
|
+
onDirtyItemStuck?: (info: import("../../types/I_SyncedDb").DirtyItemStuckInfo) => void;
|
|
270
271
|
}
|
|
271
272
|
export interface SyncEngineDeps {
|
|
272
273
|
getSyncMetaCache: () => Map<string, SyncMeta>;
|
|
@@ -326,7 +327,7 @@ export interface I_SyncEngine {
|
|
|
326
327
|
/** Upload dirty items for all collections. */
|
|
327
328
|
uploadDirtyItems(calledFrom?: string): Promise<UploadResult>;
|
|
328
329
|
/** Upload dirty items for a specific collection. */
|
|
329
|
-
uploadDirtyItemsForCollection(collection: string): Promise<UploadResult>;
|
|
330
|
+
uploadDirtyItemsForCollection(collection: string, calledFrom?: string): Promise<UploadResult>;
|
|
330
331
|
/** Process incoming server data for a single collection (used by referToServer). */
|
|
331
332
|
processCollectionServerData(collectionName: string, serverData: import("../../types/DbEntity").LocalDbEntity[], opts?: {
|
|
332
333
|
source?: SyncSource;
|
|
@@ -7,6 +7,11 @@ export interface SyncMeta {
|
|
|
7
7
|
collection: string;
|
|
8
8
|
lastSyncTs?: any;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* After this many upload attempts without success, the item is considered
|
|
12
|
+
* stuck and `stuckSince` is set on the DirtyChange entry.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS = 2;
|
|
10
15
|
/**
|
|
11
16
|
* Dirty change entry - tracks accumulated changes for a record
|
|
12
17
|
* Stored in _dirty_changes table with composite key [collection, id]
|
|
@@ -26,6 +31,14 @@ export interface DirtyChange {
|
|
|
26
31
|
createdAt: number;
|
|
27
32
|
/** When last change was accumulated */
|
|
28
33
|
updatedAt: number;
|
|
34
|
+
/** When this dirty entry was first attempted for upload (ms timestamp) */
|
|
35
|
+
firstUploadAttempt?: number;
|
|
36
|
+
/** When this dirty entry was last attempted for upload (ms timestamp) */
|
|
37
|
+
lastUploadAttempt?: number;
|
|
38
|
+
/** How many upload attempts have been made so far */
|
|
39
|
+
numUploadAttempts?: number;
|
|
40
|
+
/** Set when numUploadAttempts > DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS (ms timestamp) */
|
|
41
|
+
stuckSince?: number;
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
44
|
* Meta fields of a DirtyChange entry, without the `changes` payload.
|
|
@@ -110,6 +123,8 @@ export interface I_DexieDb {
|
|
|
110
123
|
getDirtyChange(collection: string, id: Id): Promise<DirtyChange | undefined>;
|
|
111
124
|
/** Get dirty change entries for multiple records (batch) */
|
|
112
125
|
getDirtyChangesBatch(collection: string, ids: Id[]): Promise<Map<string, DirtyChange>>;
|
|
126
|
+
/** Increment upload attempt counters for retained dirty entries. Returns newly stuck DirtyMeta[]. */
|
|
127
|
+
incrementDirtyUploadAttempts(collection: string, ids: Id[]): Promise<DirtyMeta[]>;
|
|
113
128
|
/** Clear dirty change for a record (after successful sync) */
|
|
114
129
|
clearDirtyChange(collection: string, id: Id): Promise<void>;
|
|
115
130
|
/** Clear dirty changes for multiple records (batch) */
|
|
@@ -91,6 +91,21 @@ export interface SaveIdMismatchInfo {
|
|
|
91
91
|
/** Timestamp when mismatch detected */
|
|
92
92
|
timestamp: Date;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Payload za `onDirtyItemStuck` callback. Sproži se, ko `incrementDirtyUploadAttempts`
|
|
96
|
+
* nastavi `stuckSince` na DirtyChange entry-ju — pomeni, da je item presegel
|
|
97
|
+
* `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` neuspelih nalaganj.
|
|
98
|
+
*/
|
|
99
|
+
export interface DirtyItemStuckInfo {
|
|
100
|
+
/** Collection name */
|
|
101
|
+
collection: string;
|
|
102
|
+
/** Meta podatki stuck itemov (vsebujejo `stuckSince`, `numUploadAttempts`, itd.) */
|
|
103
|
+
items: import("./I_DexieDb").DirtyMeta[];
|
|
104
|
+
/** Optional caller tag */
|
|
105
|
+
calledFrom?: string;
|
|
106
|
+
/** Timestamp when stuck was detected */
|
|
107
|
+
timestamp: Date;
|
|
108
|
+
}
|
|
94
109
|
/**
|
|
95
110
|
* Callback payload fired by `clearDirty()` when called WITHOUT a
|
|
96
111
|
* `collection` argument (clear-all path). One invocation per
|
|
@@ -741,6 +756,19 @@ export interface SyncedDbConfig {
|
|
|
741
756
|
* reveals which dirty entries hit which skip path.
|
|
742
757
|
*/
|
|
743
758
|
onUploadSkip?: (info: UploadSkipInfo) => void;
|
|
759
|
+
/**
|
|
760
|
+
* Fired when a dirty item exceeds `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS`
|
|
761
|
+
* upload attempts without server acknowledgement. The item's `stuckSince`
|
|
762
|
+
* is set and it will remain dirty until manually cleared via
|
|
763
|
+
* `discardStuckItems()` or `clearDirty()`, or until a future upload
|
|
764
|
+
* succeeds (which clears the dirty entry normally).
|
|
765
|
+
*
|
|
766
|
+
* Fires once per collection per `uploadDirtyItems` cycle, with ALL items
|
|
767
|
+
* that newly became stuck in that cycle (not previously stuck items).
|
|
768
|
+
*
|
|
769
|
+
* Use for alerting / syslog / automatic recovery flows.
|
|
770
|
+
*/
|
|
771
|
+
onDirtyItemStuck?: (info: DirtyItemStuckInfo) => void;
|
|
744
772
|
/**
|
|
745
773
|
* Callback when `save(collection, id, update)` is called with `update._id`
|
|
746
774
|
* that does NOT match `id`. Library auto-strips `_id` from update to prevent
|
|
@@ -1228,6 +1256,20 @@ export interface I_SyncedDb {
|
|
|
1228
1256
|
* ki imajo vsaj en dirty zapis. Kolekcije brez dirty vnosov niso vključene.
|
|
1229
1257
|
*/
|
|
1230
1258
|
getDirtyMeta(): Promise<Readonly<Record<string, readonly DirtyMeta[]>>>;
|
|
1259
|
+
/**
|
|
1260
|
+
* Return dirty entries whose `stuckSince` is set across all collections.
|
|
1261
|
+
* A stuck entry has exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload
|
|
1262
|
+
* attempts without success. Collections with no stuck items are omitted.
|
|
1263
|
+
*/
|
|
1264
|
+
getStuckItems(): Promise<Readonly<Record<string, readonly DirtyMeta[]>>>;
|
|
1265
|
+
/**
|
|
1266
|
+
* Discard all stuck dirty entries across ALL collections without touching
|
|
1267
|
+
* the main row data. Fires `onBeforeDirtyClearAll` per collection with
|
|
1268
|
+
* `reason: "discard-stuck"`. Returns the `DirtyMeta[]` of removed entries.
|
|
1269
|
+
*
|
|
1270
|
+
* @param calledFrom Diagnostic tag threaded through to the callback.
|
|
1271
|
+
*/
|
|
1272
|
+
discardStuckItems(calledFrom?: string): Promise<DirtyMeta[]>;
|
|
1231
1273
|
/**
|
|
1232
1274
|
* Drops a collection, ensuring no data loss.
|
|
1233
1275
|
* - Throws if offline or forcedOffline (unless force=true)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cry-synced-db-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.200",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -30,16 +30,16 @@
|
|
|
30
30
|
"@types/bun": "latest",
|
|
31
31
|
"bson": "^7.2.0",
|
|
32
32
|
"cry-ebus-proxy": "^2.0.0",
|
|
33
|
-
"dexie": "^4.4.
|
|
34
|
-
"esbuild": "^0.28.
|
|
33
|
+
"dexie": "^4.4.3",
|
|
34
|
+
"esbuild": "^0.28.1",
|
|
35
35
|
"fake-indexeddb": "^6.2.5",
|
|
36
36
|
"typescript": "^6",
|
|
37
|
-
"vitest": "^4.1.
|
|
37
|
+
"vitest": "^4.1.8"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"cry-db": "^2.5.0",
|
|
41
|
-
"cry-helpers": "^2.1.
|
|
42
|
-
"msgpackr": "^2.0.
|
|
41
|
+
"cry-helpers": "^2.1.205",
|
|
42
|
+
"msgpackr": "^2.0.4",
|
|
43
43
|
"superjson": "^2.2.6"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|