cry-synced-db-client 0.1.187 → 0.1.188

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,89 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.188 (2026-05-15)
4
+
5
+ ### `clearDirty()` — manual dirty drain + `onBeforeDirtyClearAll` callback
6
+
7
+ Public counterpart to `getDirty` / `getDirtyMeta` for the "stuck dirty
8
+ drain" recovery flow. Use case: a known-bad dirty entry can't upload
9
+ (e.g. pre-fix `sestevki` parent+descendant producing repeated 500-loops
10
+ on klikvet tabs running pre-0.1.185 bundles) and the operator wants to
11
+ forfeit the pending local intent in favor of server state — without
12
+ calling the heavier `dropCollection` / `dropDatabase` which also wipes
13
+ the main row data.
14
+
15
+ ```typescript
16
+ clearDirty(
17
+ collection?: string,
18
+ ids?: Id[],
19
+ calledFrom?: string,
20
+ ): Promise<DirtyMeta[]>
21
+ ```
22
+
23
+ Returns the `DirtyMeta[]` of every entry that was actually removed, so
24
+ the caller can archive what was lost.
25
+
26
+ | Call shape | Effect | Fires `onBeforeDirtyClearAll` |
27
+ |---|---|---|
28
+ | `clearDirty()` / `clearDirty(undefined)` | Clear ALL dirty in EVERY collection | ✅ once per collection that has dirty |
29
+ | `clearDirty(coll)` | Clear all dirty in one collection | ❌ |
30
+ | `clearDirty(coll, ids)` | Clear specific ids in that collection | ❌ |
31
+ | `clearDirty(undefined, ids)` | Throws — ambiguous | — |
32
+
33
+ The new config callback:
34
+
35
+ ```typescript
36
+ onBeforeDirtyClearAll?: (info: BeforeDirtyClearAllInfo) => void;
37
+
38
+ interface BeforeDirtyClearAllInfo {
39
+ reason: string; // `calledFrom` if provided, else "manual"
40
+ collection: string;
41
+ items: DirtyMeta[]; // every entry in that collection (no `changes` payload)
42
+ calledFrom?: string; // passthrough of the third arg
43
+ timestamp: Date;
44
+ }
45
+ ```
46
+
47
+ Fires **only** on the clear-all path, **before** the Dexie delete runs,
48
+ **once per collection** with dirty. Use to archive the dirty state to
49
+ syslog / audit trail before it disappears. Per-collection /
50
+ per-id `clearDirty` calls are intentionally silent — the caller already
51
+ knows what they're touching.
52
+
53
+ ```typescript
54
+ new SyncedDb({
55
+ // ...
56
+ onBeforeDirtyClearAll: (info) => {
57
+ syslog.warn("dirty cleared", {
58
+ collection: info.collection,
59
+ count: info.items.length,
60
+ reason: info.reason,
61
+ ids: info.items.map((m) => m.id),
62
+ });
63
+ },
64
+ });
65
+
66
+ // Nuke everything across collections, tagged for the audit log.
67
+ const cleared = await syncedDb.clearDirty(undefined, undefined, "stuck-drain");
68
+
69
+ // Targeted: drop two known-bad rows in `obiski` (silent — no callback).
70
+ await syncedDb.clearDirty("obiski", ["6a032a59...", "6a05a1ba..."]);
71
+
72
+ // Inspect first, then decide.
73
+ const meta = await syncedDb.getDirtyMeta();
74
+ for (const [coll, items] of Object.entries(meta)) {
75
+ if (items.length > 100) {
76
+ await syncedDb.clearDirty(coll, undefined, `over-threshold:${items.length}`);
77
+ }
78
+ }
79
+ ```
80
+
81
+ `clearDirty` does NOT await in-flight uploads — call `flushToServer()`
82
+ first if a last-chance upload is desired. It also does not touch the
83
+ main row data; in-mem and Dexie rows are preserved at whatever state
84
+ they're at (typically: the server's view if WS-push has landed
85
+ recently, otherwise the pre-rollback local intent).
86
+
3
87
  ## 0.1.187 (2026-05-16)
4
88
 
5
89
  ### Fix: `setByPath` auto-creates plain-object intermediates (root cause of `sestevki` parent+child conflict)
package/dist/index.js CHANGED
@@ -4484,6 +4484,7 @@ var _SyncedDb = class _SyncedDb {
4484
4484
  this.onEviction = config.onEviction;
4485
4485
  this.onSaveIdMismatch = config.onSaveIdMismatch;
4486
4486
  this.onUploadSkip = config.onUploadSkip;
4487
+ this.onBeforeDirtyClearAll = config.onBeforeDirtyClearAll;
4487
4488
  this.evictStaleRecordsEveryHrs = (_e = config.evictStaleRecordsEveryHrs) != null ? _e : 0;
4488
4489
  this.scopeExitLookbehindMs = (_f = config.scopeExitLookbehindMs) != null ? _f : 0;
4489
4490
  this.evictOnWake = (_g = config.evictOnWake) != null ? _g : false;
@@ -5759,6 +5760,68 @@ var _SyncedDb = class _SyncedDb {
5759
5760
  }
5760
5761
  return result;
5761
5762
  }
5763
+ /**
5764
+ * Manually clear pending dirty changes WITHOUT touching the main
5765
+ * row data. Counterpart to `getDirty` / `getDirtyMeta` for the
5766
+ * "stuck dirty drain" recovery flow — when a known-bad dirty entry
5767
+ * can't be uploaded (e.g. pre-fix sestevki parent+descendant
5768
+ * conflict producing repeated 500s) and the operator wants to
5769
+ * forfeit the pending local intent in favor of server state.
5770
+ *
5771
+ * Three call shapes:
5772
+ *
5773
+ * | Call | Effect |
5774
+ * |---|---|
5775
+ * | `clearDirty()` / `clearDirty(undefined)` | Clear ALL dirty in EVERY collection. Fires `onBeforeDirtyClearAll` once per collection that has dirty (BEFORE the Dexie delete). |
5776
+ * | `clearDirty(coll)` | Clear all dirty in one collection. No callback. |
5777
+ * | `clearDirty(coll, ids)` | Clear specific ids in that collection. No callback. |
5778
+ * | `clearDirty(undefined, ids)` | Throws — ambiguous (which collection do the ids belong to?). |
5779
+ *
5780
+ * Returns the `DirtyMeta[]` of every entry that was actually
5781
+ * removed so the caller can log / archive what was lost.
5782
+ *
5783
+ * Pairs naturally with `getDirtyMeta()` for the inspect-then-clear
5784
+ * pattern. Doesn't await in-flight uploads — caller's
5785
+ * responsibility to call `flushToServer()` first if a last-chance
5786
+ * upload is desired.
5787
+ */
5788
+ async clearDirty(collection, ids, calledFrom) {
5789
+ if (!collection && ids && ids.length > 0) {
5790
+ throw new Error(
5791
+ `[SyncedDb] clearDirty: 'ids' requires a 'collection' \u2014 ids alone are ambiguous across collections.`
5792
+ );
5793
+ }
5794
+ const cleared = [];
5795
+ if (!collection) {
5796
+ for (const [name] of this.collections) {
5797
+ const metas2 = await this.dexieDb.getDirtyMeta(name);
5798
+ if (metas2.length === 0) continue;
5799
+ this.safeCallback(this.onBeforeDirtyClearAll, {
5800
+ reason: calledFrom != null ? calledFrom : "manual",
5801
+ collection: name,
5802
+ items: metas2,
5803
+ calledFrom,
5804
+ timestamp: /* @__PURE__ */ new Date()
5805
+ });
5806
+ await this.dexieDb.clearDirtyChanges(name);
5807
+ for (const m of metas2) cleared.push(m);
5808
+ }
5809
+ return cleared;
5810
+ }
5811
+ this.assertCollection(collection);
5812
+ if (ids && ids.length > 0) {
5813
+ const idStrings = new Set(ids.map((id) => String(id)));
5814
+ const allMetas = await this.dexieDb.getDirtyMeta(collection);
5815
+ for (const m of allMetas) {
5816
+ if (idStrings.has(String(m.id))) cleared.push(m);
5817
+ }
5818
+ await this.dexieDb.clearDirtyChangesBatch(collection, ids);
5819
+ return cleared;
5820
+ }
5821
+ const metas = await this.dexieDb.getDirtyMeta(collection);
5822
+ await this.dexieDb.clearDirtyChanges(collection);
5823
+ return metas;
5824
+ }
5762
5825
  // ==================== Data Deletion ====================
5763
5826
  async dropCollection(collection, force = false) {
5764
5827
  this.assertCollection(collection);
@@ -56,6 +56,7 @@ export declare class SyncedDb implements I_SyncedDb {
56
56
  private readonly onEviction?;
57
57
  private readonly onSaveIdMismatch?;
58
58
  private readonly onUploadSkip?;
59
+ private readonly onBeforeDirtyClearAll?;
59
60
  private readonly evictStaleRecordsEveryHrs;
60
61
  private readonly scopeExitLookbehindMs;
61
62
  private readonly evictOnWake;
@@ -238,6 +239,32 @@ export declare class SyncedDb implements I_SyncedDb {
238
239
  getOnWakeSync(): ((info: import("./types/managers").WakeSyncInfo) => void) | undefined;
239
240
  getDirty<T extends DbEntity>(): Promise<Readonly<Record<string, readonly T[]>>>;
240
241
  getDirtyMeta(): Promise<Readonly<Record<string, readonly DirtyMeta[]>>>;
242
+ /**
243
+ * Manually clear pending dirty changes WITHOUT touching the main
244
+ * row data. Counterpart to `getDirty` / `getDirtyMeta` for the
245
+ * "stuck dirty drain" recovery flow — when a known-bad dirty entry
246
+ * can't be uploaded (e.g. pre-fix sestevki parent+descendant
247
+ * conflict producing repeated 500s) and the operator wants to
248
+ * forfeit the pending local intent in favor of server state.
249
+ *
250
+ * Three call shapes:
251
+ *
252
+ * | Call | Effect |
253
+ * |---|---|
254
+ * | `clearDirty()` / `clearDirty(undefined)` | Clear ALL dirty in EVERY collection. Fires `onBeforeDirtyClearAll` once per collection that has dirty (BEFORE the Dexie delete). |
255
+ * | `clearDirty(coll)` | Clear all dirty in one collection. No callback. |
256
+ * | `clearDirty(coll, ids)` | Clear specific ids in that collection. No callback. |
257
+ * | `clearDirty(undefined, ids)` | Throws — ambiguous (which collection do the ids belong to?). |
258
+ *
259
+ * Returns the `DirtyMeta[]` of every entry that was actually
260
+ * removed so the caller can log / archive what was lost.
261
+ *
262
+ * Pairs naturally with `getDirtyMeta()` for the inspect-then-clear
263
+ * pattern. Doesn't await in-flight uploads — caller's
264
+ * responsibility to call `flushToServer()` first if a last-chance
265
+ * upload is desired.
266
+ */
267
+ clearDirty(collection?: string, ids?: Id[], calledFrom?: string): Promise<DirtyMeta[]>;
241
268
  dropCollection(collection: string, force?: boolean): Promise<void>;
242
269
  dropDatabase(force?: boolean): Promise<void>;
243
270
  /**
@@ -91,6 +91,28 @@ export interface SaveIdMismatchInfo {
91
91
  /** Timestamp when mismatch detected */
92
92
  timestamp: Date;
93
93
  }
94
+ /**
95
+ * Callback payload fired by `clearDirty()` when called WITHOUT a
96
+ * `collection` argument (clear-all path). One invocation per
97
+ * collection that has ≥1 dirty entry, fired BEFORE the underlying
98
+ * Dexie delete runs — caller can archive/inspect the records.
99
+ *
100
+ * Per-collection `clearDirty(coll, ...)` calls do NOT fire this
101
+ * callback (targeted operation; caller already knows what they're
102
+ * touching).
103
+ */
104
+ export interface BeforeDirtyClearAllInfo {
105
+ /** What triggered the clear-all (e.g. "manual", custom string from caller). */
106
+ reason: string;
107
+ /** Collection whose dirty entries are about to be deleted. */
108
+ collection: string;
109
+ /** Meta of every dirty entry in this collection (no `changes` payload). */
110
+ items: import("./I_DexieDb").DirtyMeta[];
111
+ /** Optional caller tag passed through `clearDirty(undefined, undefined, calledFrom)`. */
112
+ calledFrom?: string;
113
+ /** Timestamp when the callback fires. */
114
+ timestamp: Date;
115
+ }
94
116
  /**
95
117
  * Callback payload for server write requests (before sending)
96
118
  */
@@ -649,6 +671,17 @@ export interface SyncedDbConfig {
649
671
  * where `this._id` and `this.protokol._id` had drifted apart.
650
672
  */
651
673
  onSaveIdMismatch?: (info: SaveIdMismatchInfo) => void;
674
+ /**
675
+ * Fired by `clearDirty()` (no collection argument — clear-all path)
676
+ * BEFORE the Dexie delete runs, once per collection that has
677
+ * dirty entries. Use to archive the dirty state to syslog / audit
678
+ * trail before it's gone, or to abort by throwing (the throw
679
+ * aborts only the in-flight clear, doesn't propagate).
680
+ *
681
+ * Per-collection `clearDirty(coll, ...)` calls are SILENT — they
682
+ * don't fire this callback.
683
+ */
684
+ onBeforeDirtyClearAll?: (info: BeforeDirtyClearAllInfo) => void;
652
685
  /**
653
686
  * Enable in-memory object metadata feature.
654
687
  * When true, collections with hasMetadata=true will have their metadata callbacks invoked
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.187",
3
+ "version": "0.1.188",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",