cry-synced-db-client 0.1.197 → 0.1.199

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.198 (2026-06-13)
4
+
5
+ ### Stuck-item tracking (`getStuckItems` / `discardStuckItems` / `onDirtyItemStuck`)
6
+
7
+ Nov mehanizem za detekcijo dirty itemov, ki jih server vztrajno zavrača.
8
+
9
+ **Fields na `DirtyChange` / `DirtyMeta`:**
10
+ - `firstUploadAttempt?`, `lastUploadAttempt?`, `numUploadAttempts?`, `stuckSince?`
11
+ - `stuckSince` se nastavi ko `numUploadAttempts > DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` (2)
12
+
13
+ **Nove metode na `SyncedDb`:**
14
+ - `getStuckItems()` — vrne samo stuck dirty iteme po kolekcijah (`Record<collection, DirtyMeta[]>`)
15
+ - `discardStuckItems(calledFrom?)` — zbriše vse stuck dirty iteme, sproži `onBeforeDirtyClearAll` z `reason: "discard-stuck"`
16
+
17
+ **Nov callback:**
18
+ - `onDirtyItemStuck(info: DirtyItemStuckInfo)` — sproži se ko item prvič postane stuck (po 3. neuspelem uploadu). Vsebuje `collection`, `items: DirtyMeta[]`, `calledFrom`, `timestamp`.
19
+
20
+ **Internal:**
21
+ - `I_DexieDb.incrementDirtyUploadAttempts(collection, ids)` — nova metoda, vrača `DirtyMeta[]` na novo stuck itemov
22
+ - Vgrajena v `SyncEngine.uploadDirtyItems` in `uploadDirtyItemsForCollection` — per-collection result (errored ids) in catch block (network/timeout)
23
+ - `DexieDb.getDirtyMeta` sedaj vključuje nova polja v izhod
24
+
25
+ **Tests:** 17 testov v `test/stuckItems.test.ts` — unit testi za increment, integracijski za get/discard, e2e za callback skozi upload pipeline.
26
+
3
27
  ## 0.1.196 (2026-06-10)
4
28
 
5
29
  ### `refreshImmediately` — blokirni server-fetch z verzijsko primerjavo
package/dist/index.js CHANGED
@@ -3508,7 +3508,7 @@ var _SyncEngine = class _SyncEngine {
3508
3508
  * Upload dirty items for all collections.
3509
3509
  */
3510
3510
  async uploadDirtyItems(calledFrom) {
3511
- var _a;
3511
+ var _a, _b;
3512
3512
  const collectionBatches = [];
3513
3513
  for (const [collectionName] of this.collections) {
3514
3514
  const dirtyChanges = await this.dexieDb.getDirty(collectionName);
@@ -3664,6 +3664,15 @@ var _SyncEngine = class _SyncEngine {
3664
3664
  writeStartedAt,
3665
3665
  calledFrom
3666
3666
  );
3667
+ for (const batch of collectionBatches) {
3668
+ for (const b of batch) {
3669
+ const allIds = [];
3670
+ for (const u of b.batch.updates) allIds.push(u._id);
3671
+ for (const d of b.batch.deletes) allIds.push((_a = d._id) != null ? _a : d);
3672
+ const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(b.collection, allIds);
3673
+ this.callOnDirtyItemStuck(b.collection, newlyStuck, calledFrom);
3674
+ }
3675
+ }
3667
3676
  throw err;
3668
3677
  }
3669
3678
  let sentCount = 0;
@@ -3717,8 +3726,26 @@ var _SyncEngine = class _SyncEngine {
3717
3726
  if (allSuccessIds.length > 0) {
3718
3727
  await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
3719
3728
  }
3729
+ const sentIdsForCollection = /* @__PURE__ */ new Set();
3730
+ for (const batch of collectionBatches) {
3731
+ for (const b of batch) {
3732
+ if (b.collection !== collection) continue;
3733
+ for (const u of b.batch.updates) sentIdsForCollection.add(String(u._id));
3734
+ for (const d of b.batch.deletes) sentIdsForCollection.add(String(d._id));
3735
+ }
3736
+ }
3737
+ const retainedIds = [];
3738
+ for (const sid of sentIdsForCollection) {
3739
+ if (!allSuccessIds.find((aid) => String(aid) === sid)) {
3740
+ retainedIds.push(sid);
3741
+ }
3742
+ }
3743
+ if (retainedIds.length > 0) {
3744
+ const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(collection, retainedIds);
3745
+ this.callOnDirtyItemStuck(collection, newlyStuck, calledFrom);
3746
+ }
3720
3747
  let collectionSentCount = 0;
3721
- const isWriteOnly = (_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly;
3748
+ const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
3722
3749
  const insertedAndUpdated = inserted.concat(updated);
3723
3750
  if (insertedAndUpdated.length > 0) {
3724
3751
  const idsToCheck = [];
@@ -3857,7 +3884,7 @@ var _SyncEngine = class _SyncEngine {
3857
3884
  /**
3858
3885
  * Upload dirty items for a specific collection.
3859
3886
  */
3860
- async uploadDirtyItemsForCollection(collection) {
3887
+ async uploadDirtyItemsForCollection(collection, calledFrom) {
3861
3888
  const dirtyItems = await this.dexieDb.getDirty(collection);
3862
3889
  if (dirtyItems.length === 0) {
3863
3890
  return { sentCount: 0 };
@@ -3955,6 +3982,24 @@ var _SyncEngine = class _SyncEngine {
3955
3982
  if (allSuccessIds.length > 0) {
3956
3983
  await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
3957
3984
  }
3985
+ const sentIdsForCollection = /* @__PURE__ */ new Set();
3986
+ for (const batch of collectionBatches) {
3987
+ for (const b of batch) {
3988
+ if (b.collection !== collection) continue;
3989
+ for (const u of b.batch.updates) sentIdsForCollection.add(String(u._id));
3990
+ for (const d of b.batch.deletes) sentIdsForCollection.add(String(d._id));
3991
+ }
3992
+ }
3993
+ const retainedIds = [];
3994
+ for (const sid of sentIdsForCollection) {
3995
+ if (!allSuccessIds.find((aid) => String(aid) === sid)) {
3996
+ retainedIds.push(sid);
3997
+ }
3998
+ }
3999
+ if (retainedIds.length > 0) {
4000
+ const newlyStuck = await this.dexieDb.incrementDirtyUploadAttempts(collection, retainedIds);
4001
+ this.callOnDirtyItemStuck(collection, newlyStuck, calledFrom);
4002
+ }
3958
4003
  if (mustRefresh && mustRefresh.length > 0) {
3959
4004
  await this.adoptMustRefresh(
3960
4005
  collection,
@@ -4192,6 +4237,19 @@ var _SyncEngine = class _SyncEngine {
4192
4237
  }
4193
4238
  }
4194
4239
  }
4240
+ callOnDirtyItemStuck(collection, stuckMetas, calledFrom) {
4241
+ if (!this.callbacks.onDirtyItemStuck || stuckMetas.length === 0) return;
4242
+ try {
4243
+ this.callbacks.onDirtyItemStuck({
4244
+ collection,
4245
+ items: stuckMetas,
4246
+ calledFrom,
4247
+ timestamp: /* @__PURE__ */ new Date()
4248
+ });
4249
+ } catch (err) {
4250
+ console.error(`[SyncEngine] onDirtyItemStuck callback failed: ${err}`, err);
4251
+ }
4252
+ }
4195
4253
  callOnServerSyncWrite(request, response, error, startTime, timestamp, calledFrom) {
4196
4254
  if (!this.callbacks.onServerSyncWrite) return;
4197
4255
  try {
@@ -4867,7 +4925,8 @@ var _SyncedDb = class _SyncedDb {
4867
4925
  onServerSyncWrite: config.onServerSyncWrite,
4868
4926
  onFindNewerManyCall: config.onFindNewerManyCall,
4869
4927
  onFindNewerManyResult: config.onFindNewerManyResult,
4870
- onUploadSkip: config.onUploadSkip
4928
+ onUploadSkip: config.onUploadSkip,
4929
+ onDirtyItemStuck: config.onDirtyItemStuck
4871
4930
  },
4872
4931
  deps: {
4873
4932
  getSyncMetaCache: () => this.syncMetaCache,
@@ -6330,6 +6389,59 @@ var _SyncedDb = class _SyncedDb {
6330
6389
  await this.dexieDb.clearDirtyChanges(collection);
6331
6390
  return metas;
6332
6391
  }
6392
+ /**
6393
+ * Return dirty entries whose `stuckSince` is set across all collections.
6394
+ * A stuck entry has exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload
6395
+ * attempts without success — typically a server-side error that prevents
6396
+ * the record from being accepted.
6397
+ *
6398
+ * Same shape as `getDirtyMeta()` but filtered to stuck items only.
6399
+ * Collections with no stuck items are omitted from the result.
6400
+ */
6401
+ async getStuckItems() {
6402
+ const result = {};
6403
+ for (const [collectionName] of this.collections) {
6404
+ const metas = await this.dexieDb.getDirtyMeta(collectionName);
6405
+ const stuck = metas.filter((m) => m.stuckSince !== void 0);
6406
+ if (stuck.length > 0) {
6407
+ result[collectionName] = stuck;
6408
+ }
6409
+ }
6410
+ return result;
6411
+ }
6412
+ /**
6413
+ * Discard all stuck dirty entries across ALL collections without touching
6414
+ * the main row data. A stuck entry is one where `stuckSince` is set
6415
+ * (exceeded `DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS` upload attempts).
6416
+ *
6417
+ * Fires `onBeforeDirtyClearAll` once per collection that has stuck items,
6418
+ * with `reason: "discard-stuck"`. Returns the `DirtyMeta[]` of every
6419
+ * entry that was removed.
6420
+ *
6421
+ * Does NOT await in-flight uploads — call `flushToServer()` first if a
6422
+ * last-chance upload is desired.
6423
+ *
6424
+ * @param calledFrom Diagnostic tag threaded through to the callback.
6425
+ */
6426
+ async discardStuckItems(calledFrom) {
6427
+ const cleared = [];
6428
+ for (const [collectionName] of this.collections) {
6429
+ const metas = await this.dexieDb.getDirtyMeta(collectionName);
6430
+ const stuck = metas.filter((m) => m.stuckSince !== void 0);
6431
+ if (stuck.length === 0) continue;
6432
+ this.safeCallback(this.onBeforeDirtyClearAll, {
6433
+ reason: "discard-stuck",
6434
+ collection: collectionName,
6435
+ items: stuck,
6436
+ calledFrom,
6437
+ timestamp: /* @__PURE__ */ new Date()
6438
+ });
6439
+ const ids = stuck.map((m) => m.id);
6440
+ await this.dexieDb.clearDirtyChangesBatch(collectionName, ids);
6441
+ for (const m of stuck) cleared.push(m);
6442
+ }
6443
+ return cleared;
6444
+ }
6333
6445
  // ==================== Data Deletion ====================
6334
6446
  async dropCollection(collection, force = false) {
6335
6447
  this.assertCollection(collection);
@@ -7359,6 +7471,9 @@ var SyncedDb = _SyncedDb;
7359
7471
  // src/db/DexieDb.ts
7360
7472
  import Dexie from "dexie";
7361
7473
 
7474
+ // src/types/I_DexieDb.ts
7475
+ var DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS = 2;
7476
+
7362
7477
  // src/utils/computeDiff.ts
7363
7478
  function isDescendantOrEqual(path, candidate) {
7364
7479
  if (path === candidate) return true;
@@ -7645,7 +7760,11 @@ var DexieDb = class extends Dexie {
7645
7760
  baseTs: entry.baseTs,
7646
7761
  baseRev: entry.baseRev,
7647
7762
  createdAt: entry.createdAt,
7648
- updatedAt: entry.updatedAt
7763
+ updatedAt: entry.updatedAt,
7764
+ firstUploadAttempt: entry.firstUploadAttempt,
7765
+ lastUploadAttempt: entry.lastUploadAttempt,
7766
+ numUploadAttempts: entry.numUploadAttempts,
7767
+ stuckSince: entry.stuckSince
7649
7768
  });
7650
7769
  }
7651
7770
  return result;
@@ -7723,6 +7842,45 @@ var DexieDb = class extends Dexie {
7723
7842
  }
7724
7843
  return result;
7725
7844
  }
7845
+ async incrementDirtyUploadAttempts(collection, ids) {
7846
+ var _a;
7847
+ if (ids.length === 0) return [];
7848
+ const keys = [];
7849
+ for (const id of ids) keys.push([collection, this.idToString(id)]);
7850
+ const entries = await this.dirtyChanges.bulkGet(keys);
7851
+ const now = Date.now();
7852
+ const toUpdate = [];
7853
+ const newlyStuck = [];
7854
+ for (let i = 0; i < entries.length; i++) {
7855
+ const entry = entries[i];
7856
+ if (!entry) continue;
7857
+ entry.numUploadAttempts = ((_a = entry.numUploadAttempts) != null ? _a : 0) + 1;
7858
+ if (entry.firstUploadAttempt === void 0) {
7859
+ entry.firstUploadAttempt = now;
7860
+ }
7861
+ entry.lastUploadAttempt = now;
7862
+ if (entry.numUploadAttempts > DIRTY_STUCK_AFTER_UPLOAD_ATTEMPTS && entry.stuckSince === void 0) {
7863
+ entry.stuckSince = now;
7864
+ newlyStuck.push({
7865
+ collection: entry.collection,
7866
+ id: entry.id,
7867
+ baseTs: entry.baseTs,
7868
+ baseRev: entry.baseRev,
7869
+ createdAt: entry.createdAt,
7870
+ updatedAt: entry.updatedAt,
7871
+ firstUploadAttempt: entry.firstUploadAttempt,
7872
+ lastUploadAttempt: entry.lastUploadAttempt,
7873
+ numUploadAttempts: entry.numUploadAttempts,
7874
+ stuckSince: entry.stuckSince
7875
+ });
7876
+ }
7877
+ toUpdate.push(entry);
7878
+ }
7879
+ if (toUpdate.length > 0) {
7880
+ await this.dirtyChanges.bulkPut(toUpdate);
7881
+ }
7882
+ return newlyStuck;
7883
+ }
7726
7884
  async clearDirtyChange(collection, id) {
7727
7885
  const stringId = this.idToString(id);
7728
7886
  await this.dirtyChanges.delete([collection, stringId]);
@@ -8231,6 +8389,7 @@ function createStructureReader(structure, firstId) {
8231
8389
  inlineObjectReadThreshold = Infinity;
8232
8390
  return readObject();
8233
8391
  }
8392
+ structure.read0 = optimizedReadObject;
8234
8393
  if (structure.highByte === 0)
8235
8394
  structure.read = createSecondByteReader(firstId, structure.read);
8236
8395
  return optimizedReadObject();
@@ -8247,6 +8406,7 @@ function createStructureReader(structure, firstId) {
8247
8406
  return object;
8248
8407
  }
8249
8408
  readObject.count = 0;
8409
+ structure.read0 = readObject;
8250
8410
  if (structure.highByte === 0) {
8251
8411
  return createSecondByteReader(firstId, readObject);
8252
8412
  }
@@ -8646,7 +8806,7 @@ var recordDefinition = (id, highByte) => {
8646
8806
  }
8647
8807
  currentStructures[id] = structure;
8648
8808
  structure.read = createStructureReader(structure, firstByte);
8649
- return structure.read();
8809
+ return (structure.read0 || structure.read)();
8650
8810
  };
8651
8811
  currentExtensions[0] = () => {
8652
8812
  };
@@ -9024,6 +9184,7 @@ var Packr = class extends Unpackr {
9024
9184
  let newSharedData = (packr3._prepareStructures || prepareStructures)(structures, packr3);
9025
9185
  if (!encodingError) {
9026
9186
  if (packr3.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
9187
+ structures.uninitialized = true;
9027
9188
  return packr3.pack(value, encodeOptions);
9028
9189
  }
9029
9190
  packr3.lastNamedStructuresLength = sharedLength;
@@ -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.197",
3
+ "version": "0.1.199",
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.2",
34
- "esbuild": "^0.28.0",
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.6"
37
+ "vitest": "^4.1.8"
38
38
  },
39
39
  "dependencies": {
40
40
  "cry-db": "^2.5.0",
41
- "cry-helpers": "^2.1.194",
42
- "msgpackr": "^2.0.1",
41
+ "cry-helpers": "^2.1.205",
42
+ "msgpackr": "^2.0.4",
43
43
  "superjson": "^2.2.6"
44
44
  },
45
45
  "peerDependencies": {