cry-synced-db-client 0.1.191 → 0.1.193

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,35 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.192 (2026-05-25)
4
+
5
+ Adds per-collection preload status reporting (non-breaking, additive).
6
+
7
+ ### `getPreloadStatus()` + `onPreloadStatusChange`
8
+
9
+ New synchronous getter `getPreloadStatus(): PreloadStatus` and config callback
10
+ `onPreloadStatusChange?(status)` expose hydration status for every readable
11
+ (non-`writeOnly`), in-scope collection:
12
+
13
+ - Per-collection `state` (`pending` / `hydrated` / `failed`), `itemCount`,
14
+ `lastError`, `everDownloaded`, and a derived `ready` flag
15
+ (`ready = hydrated && (itemCount > 0 || everDownloaded)` — a hydrated-but-empty
16
+ collection that was never downloaded is **not** ready, so a fresh device
17
+ doesn't report "full" before its first server sync).
18
+ - Rolled-up `aggregate`: `idle` / `full` / `partial` / `failed`.
19
+
20
+ The callback fires on each hydration transition (success/failure), on scope
21
+ changes (`setSyncOnlyTheseCollections`), and at sync end (a download that
22
+ advances a cursor can flip a hydrated-empty collection to ready).
23
+
24
+ ### Per-collection hydration failures no longer abort the batch
25
+
26
+ `loadCollectionsToInMem`'s worker pool now isolates a single collection's
27
+ Dexie hydration failure (try/catch per collection) instead of rejecting the
28
+ whole `Promise.all` — matching the existing `Promise.allSettled` tolerance in
29
+ `addCollectionsToSync`. The failure is recorded as `state: "failed"` and
30
+ surfaced via `getPreloadStatus()` (`aggregate: "partial"`) rather than throwing
31
+ out of `init()`.
32
+
3
33
  ## 0.1.191 (2026-05-22)
4
34
 
5
35
  Adopts the cry-db 2.5.0 sync contract. **Requires cry-db ≥ 2.5.0 on the
package/dist/index.js CHANGED
@@ -4691,6 +4691,8 @@ var _SyncedDb = class _SyncedDb {
4691
4691
  this.syncOnlyCollections = null;
4692
4692
  // Sync metadata cache
4693
4693
  this.syncMetaCache = /* @__PURE__ */ new Map();
4694
+ // Per-collection hydration status (powers getPreloadStatus / onPreloadStatusChange)
4695
+ this.preloadStatusMap = /* @__PURE__ */ new Map();
4694
4696
  this._pendingFullResync = false;
4695
4697
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
4696
4698
  this.tenant = config.tenant;
@@ -4710,6 +4712,7 @@ var _SyncedDb = class _SyncedDb {
4710
4712
  this.onDexieSyncStart = config.onDexieSyncStart;
4711
4713
  this.onDexieSyncEnd = config.onDexieSyncEnd;
4712
4714
  this.onSyncProgress = config.onSyncProgress;
4715
+ this.onPreloadStatusChange = config.onPreloadStatusChange;
4713
4716
  this.onServerSyncStart = config.onServerSyncStart;
4714
4717
  this.onServerSyncEnd = config.onServerSyncEnd;
4715
4718
  this.onConflictResolved = config.onConflictResolved;
@@ -4729,6 +4732,7 @@ var _SyncedDb = class _SyncedDb {
4729
4732
  this.evictOnWake = (_g = config.evictOnWake) != null ? _g : false;
4730
4733
  for (const col of config.collections) {
4731
4734
  this.collections.set(col.name, col);
4735
+ if (!col.writeOnly) this.preloadStatusMap.set(col.name, { state: "pending", itemCount: 0 });
4732
4736
  }
4733
4737
  this.inMemManager = new InMemManager({
4734
4738
  inMemDb: this.inMemDb,
@@ -5029,6 +5033,9 @@ var _SyncedDb = class _SyncedDb {
5029
5033
  const existing = this.collections.get(spec.name);
5030
5034
  if (existing && !existing.temporaryConfig) continue;
5031
5035
  this.collections.set(spec.name, spec);
5036
+ if (!spec.writeOnly && !this.preloadStatusMap.has(spec.name)) {
5037
+ this.preloadStatusMap.set(spec.name, { state: "pending", itemCount: 0 });
5038
+ }
5032
5039
  if (this.syncOnlyCollections) {
5033
5040
  this.syncOnlyCollections.add(spec.name);
5034
5041
  }
@@ -5149,8 +5156,10 @@ var _SyncedDb = class _SyncedDb {
5149
5156
  for (const [name] of this.collections) {
5150
5157
  if (this.isSyncAllowed(name) && !prevAllowed.has(name)) {
5151
5158
  newlyAllowed.push(name);
5159
+ this.preloadStatusMap.set(name, { state: "pending", itemCount: 0 });
5152
5160
  }
5153
5161
  }
5162
+ this.emitPreloadStatusChange();
5154
5163
  if (newlyAllowed.length > 0) {
5155
5164
  await this.loadCollectionsToInMem(newlyAllowed, "setSyncOnlyTheseCollections");
5156
5165
  }
@@ -6079,6 +6088,7 @@ var _SyncedDb = class _SyncedDb {
6079
6088
  }
6080
6089
  } finally {
6081
6090
  this.syncLock = false;
6091
+ this.emitPreloadStatusChange();
6082
6092
  }
6083
6093
  }
6084
6094
  async processQueuedWsUpdates() {
@@ -6895,7 +6905,11 @@ var _SyncedDb = class _SyncedDb {
6895
6905
  async () => {
6896
6906
  while (queue.length > 0) {
6897
6907
  const name = queue.shift();
6898
- const items = await this.loadCollectionToInMem(name);
6908
+ let items = 0;
6909
+ try {
6910
+ items = await this.loadCollectionToInMem(name);
6911
+ } catch (e) {
6912
+ }
6899
6913
  totalItems += items;
6900
6914
  loaded++;
6901
6915
  this.safeCallback(this.onSyncProgress, {
@@ -6917,7 +6931,73 @@ var _SyncedDb = class _SyncedDb {
6917
6931
  });
6918
6932
  return totalItems;
6919
6933
  }
6934
+ /**
6935
+ * Hydrate a single collection from Dexie, recording the outcome in
6936
+ * {@link preloadStatusMap} (hydrated / failed) for {@link getPreloadStatus}.
6937
+ * Rethrows on failure so existing callers (e.g. the `Promise.allSettled` in
6938
+ * addCollectionsToSync, the per-collection try/catch in loadCollectionsToInMem)
6939
+ * keep their current behavior.
6940
+ */
6920
6941
  async loadCollectionToInMem(name) {
6942
+ var _a;
6943
+ try {
6944
+ const count = await this._hydrateCollectionFromDexie(name);
6945
+ this.preloadStatusMap.set(name, { state: "hydrated", itemCount: count, hydratedAt: /* @__PURE__ */ new Date() });
6946
+ this.emitPreloadStatusChange();
6947
+ return count;
6948
+ } catch (err) {
6949
+ const prev = this.preloadStatusMap.get(name);
6950
+ this.preloadStatusMap.set(name, {
6951
+ state: "failed",
6952
+ itemCount: (_a = prev == null ? void 0 : prev.itemCount) != null ? _a : 0,
6953
+ hydratedAt: prev == null ? void 0 : prev.hydratedAt,
6954
+ lastError: err instanceof Error ? err.message : String(err)
6955
+ });
6956
+ this.emitPreloadStatusChange();
6957
+ throw err;
6958
+ }
6959
+ }
6960
+ /**
6961
+ * Per-collection hydration status + rolled-up aggregate, over readable
6962
+ * (non-`writeOnly`), in-scope collections. See {@link I_SyncedDb.getPreloadStatus}.
6963
+ */
6964
+ getPreloadStatus() {
6965
+ var _a, _b;
6966
+ const collections = [];
6967
+ let readyCount = 0;
6968
+ let failedCount = 0;
6969
+ let pendingCount = 0;
6970
+ for (const name of this.collections.keys()) {
6971
+ if (!this.isSyncAllowed(name)) continue;
6972
+ const rec = (_a = this.preloadStatusMap.get(name)) != null ? _a : { state: "pending", itemCount: 0 };
6973
+ const everDownloaded = !!((_b = this.syncMetaCache.get(name)) == null ? void 0 : _b.lastSyncTs);
6974
+ const ready = rec.state === "hydrated" && (rec.itemCount > 0 || everDownloaded);
6975
+ if (rec.state === "failed") failedCount++;
6976
+ else if (ready) readyCount++;
6977
+ else pendingCount++;
6978
+ collections.push({
6979
+ name,
6980
+ state: rec.state,
6981
+ itemCount: rec.itemCount,
6982
+ ready,
6983
+ hydratedAt: rec.hydratedAt,
6984
+ lastError: rec.lastError,
6985
+ everDownloaded
6986
+ });
6987
+ }
6988
+ const expectedCount = collections.length;
6989
+ let aggregate;
6990
+ if (expectedCount === 0) aggregate = "idle";
6991
+ else if (readyCount === expectedCount) aggregate = "full";
6992
+ else if (failedCount === expectedCount) aggregate = "failed";
6993
+ else aggregate = "partial";
6994
+ return { aggregate, collections, expectedCount, readyCount, failedCount, pendingCount };
6995
+ }
6996
+ /** Emit onPreloadStatusChange with a fresh snapshot (skips computation when no listener). */
6997
+ emitPreloadStatusChange() {
6998
+ if (this.onPreloadStatusChange) this.safeCallback(this.onPreloadStatusChange, this.getPreloadStatus());
6999
+ }
7000
+ async _hydrateCollectionFromDexie(name) {
6921
7001
  const allItems = [];
6922
7002
  await this.dexieDb.forEachBatch(name, 2e3, async (chunk) => {
6923
7003
  for (let i = 0; i < chunk.length; i++) {
@@ -1,5 +1,5 @@
1
1
  import type { AggregateOptions } from "mongodb";
2
- import type { I_SyncedDb, SyncedDbConfig, CollectionConfig, WsNotificationInfo, EvictionInfo, EvictionCollectionInfo } from "../types/I_SyncedDb";
2
+ import type { I_SyncedDb, SyncedDbConfig, CollectionConfig, WsNotificationInfo, EvictionInfo, EvictionCollectionInfo, PreloadStatus } from "../types/I_SyncedDb";
3
3
  import type { DirtyMeta, MetaUpdateBroadcast } from "../types/I_DexieDb";
4
4
  import type { QuerySpec, QueryOpts, UpdateSpec, InsertSpec, BatchSpec } from "../types/I_RestInterface";
5
5
  import type { Id, DbEntity } from "../types/DbEntity";
@@ -34,6 +34,7 @@ export declare class SyncedDb implements I_SyncedDb {
34
34
  private readonly syncedDbInstanceId;
35
35
  private readonly dexieLoadConcurrency;
36
36
  private syncMetaCache;
37
+ private readonly preloadStatusMap;
37
38
  private unsubscribeServerUpdates?;
38
39
  private cleanupNotifierCallbacks?;
39
40
  private beforeUnloadHandler?;
@@ -46,6 +47,7 @@ export declare class SyncedDb implements I_SyncedDb {
46
47
  private readonly onDexieSyncStart?;
47
48
  private readonly onDexieSyncEnd?;
48
49
  private readonly onSyncProgress?;
50
+ private readonly onPreloadStatusChange?;
49
51
  private readonly onServerSyncStart?;
50
52
  private readonly onServerSyncEnd?;
51
53
  private readonly onConflictResolved?;
@@ -516,7 +518,22 @@ export declare class SyncedDb implements I_SyncedDb {
516
518
  * @returns total items loaded
517
519
  */
518
520
  private loadCollectionsToInMem;
521
+ /**
522
+ * Hydrate a single collection from Dexie, recording the outcome in
523
+ * {@link preloadStatusMap} (hydrated / failed) for {@link getPreloadStatus}.
524
+ * Rethrows on failure so existing callers (e.g. the `Promise.allSettled` in
525
+ * addCollectionsToSync, the per-collection try/catch in loadCollectionsToInMem)
526
+ * keep their current behavior.
527
+ */
519
528
  private loadCollectionToInMem;
529
+ /**
530
+ * Per-collection hydration status + rolled-up aggregate, over readable
531
+ * (non-`writeOnly`), in-scope collections. See {@link I_SyncedDb.getPreloadStatus}.
532
+ */
533
+ getPreloadStatus(): PreloadStatus;
534
+ /** Emit onPreloadStatusChange with a fresh snapshot (skips computation when no listener). */
535
+ private emitPreloadStatusChange;
536
+ private _hydrateCollectionFromDexie;
520
537
  /**
521
538
  * Bulk-read sync cursors for every registered collection into syncMetaCache.
522
539
  * Called once during init() before sync can fire. Decouples cursor cache
@@ -400,6 +400,52 @@ export interface SyncInfo {
400
400
  /** Per-collection sync statistics (collection name -> stats) */
401
401
  collections?: Record<string, CollectionSyncStats>;
402
402
  }
403
+ /** Per-collection hydration state for preload status reporting. */
404
+ export type CollectionHydrationState = "pending" | "hydrated" | "failed";
405
+ /** Hydration status of a single synced (readable, in-scope) collection. */
406
+ export interface CollectionPreloadStatus {
407
+ /** Collection name. */
408
+ name: string;
409
+ /** Current hydration state. */
410
+ state: CollectionHydrationState;
411
+ /** In-mem item count after the last successful hydration (0 is valid). */
412
+ itemCount: number;
413
+ /**
414
+ * Whether the collection is considered ready: hydrated AND either has data
415
+ * (`itemCount > 0`) or its cursor has advanced via a server download
416
+ * (`everDownloaded`). A hydrated-but-empty collection that was never
417
+ * downloaded is NOT ready — we can't tell "genuinely empty" from
418
+ * "not fetched yet".
419
+ */
420
+ ready: boolean;
421
+ /** Timestamp of the last successful Dexie→in-mem hydration. */
422
+ hydratedAt?: Date;
423
+ /** Error message from the last failed hydration (set when `state === "failed"`). */
424
+ lastError?: string;
425
+ /** True once a server download has advanced this collection's cursor (`lastSyncTs` set). */
426
+ everDownloaded: boolean;
427
+ }
428
+ /** Rolled-up preload status across all readable, in-scope collections. */
429
+ export type PreloadAggregate = "idle" | "full" | "partial" | "failed";
430
+ /** Snapshot of preload/hydration status — see {@link I_SyncedDb.getPreloadStatus}. */
431
+ export interface PreloadStatus {
432
+ /**
433
+ * `full` = every expected collection ready; `failed` = all expected failed;
434
+ * `partial` = anything in between (some ready and/or pending and/or failed);
435
+ * `idle` = no readable, in-scope collections expected.
436
+ */
437
+ aggregate: PreloadAggregate;
438
+ /** Per-collection detail for every readable, in-scope collection. */
439
+ collections: CollectionPreloadStatus[];
440
+ /** Count of readable, in-scope collections (the denominator for `aggregate`). */
441
+ expectedCount: number;
442
+ /** Number of `ready` collections. */
443
+ readyCount: number;
444
+ /** Number of `failed` collections. */
445
+ failedCount: number;
446
+ /** Number of `pending` (not-yet-ready, not-failed) collections. */
447
+ pendingCount: number;
448
+ }
403
449
  /** Per-collection eviction statistics. */
404
450
  export interface EvictionCollectionInfo {
405
451
  /** Collection name. */
@@ -590,6 +636,12 @@ export interface SyncedDbConfig {
590
636
  total: number;
591
637
  items: number;
592
638
  }) => void;
639
+ /**
640
+ * Callback when per-collection hydration status changes (hydration end,
641
+ * hydration failure, scope change, or sync end — when a download advances a
642
+ * cursor). Receives the same snapshot as {@link I_SyncedDb.getPreloadStatus}.
643
+ */
644
+ onPreloadStatusChange?: (status: PreloadStatus) => void;
593
645
  /** Callback when server download starts (findNewerManyStream) */
594
646
  onServerSyncStart?: (info: {
595
647
  calledFrom?: string;
@@ -1006,6 +1058,14 @@ export interface I_SyncedDb {
1006
1058
  * Persisted in Dexie via syncMeta key `__lastInitialSync`.
1007
1059
  */
1008
1060
  lastInitialSync(): Date | undefined;
1061
+ /**
1062
+ * Snapshot of per-collection hydration status plus a rolled-up aggregate
1063
+ * (`full` / `partial` / `failed` / `idle`). Computed over readable
1064
+ * (non-`writeOnly`), in-scope collections — so during a
1065
+ * `setSyncOnlyTheseCollections` subset only those collections are reported.
1066
+ * Synchronous; safe to call any time after construction.
1067
+ */
1068
+ getPreloadStatus(): PreloadStatus;
1009
1069
  /** Ali je povezan na server */
1010
1070
  isOnline(): boolean;
1011
1071
  /** Nastavi online/offline status. Returns a promise when going online. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.191",
3
+ "version": "0.1.193",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",