cry-synced-db-client 0.1.191 → 0.1.194

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,37 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.193 (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` (informational), and a derived `ready` flag
15
+ (`ready = (state === "hydrated")` — the local cache is loaded into in-mem;
16
+ empty is valid). Data *freshness* is a separate, global concern
17
+ (`lastSuccessfulServerSync`), deliberately **not** folded into `ready`:
18
+ gating on a per-collection cursor wedged the aggregate at `partial` for
19
+ genuinely-empty, never-synced collections (every tenant has several).
20
+ - Rolled-up `aggregate`: `idle` / `full` / `partial` / `failed`.
21
+
22
+ The callback fires on each hydration transition (success/failure), on scope
23
+ changes (`setSyncOnlyTheseCollections`), and at sync end (a download that
24
+ advances a cursor can flip a hydrated-empty collection to ready).
25
+
26
+ ### Per-collection hydration failures no longer abort the batch
27
+
28
+ `loadCollectionsToInMem`'s worker pool now isolates a single collection's
29
+ Dexie hydration failure (try/catch per collection) instead of rejecting the
30
+ whole `Promise.all` — matching the existing `Promise.allSettled` tolerance in
31
+ `addCollectionsToSync`. The failure is recorded as `state: "failed"` and
32
+ surfaced via `getPreloadStatus()` (`aggregate: "partial"`) rather than throwing
33
+ out of `init()`.
34
+
3
35
  ## 0.1.191 (2026-05-22)
4
36
 
5
37
  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";
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,55 @@ 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 local cache is loaded into in-mem (`state === "hydrated"`).
415
+ * Empty is valid — a genuinely empty, never-synced collection is still ready.
416
+ * Data *freshness* is a separate, global concern (`lastSuccessfulServerSync`),
417
+ * not tracked per-collection.
418
+ */
419
+ ready: boolean;
420
+ /** Timestamp of the last successful Dexie→in-mem hydration. */
421
+ hydratedAt?: Date;
422
+ /** Error message from the last failed hydration (set when `state === "failed"`). */
423
+ lastError?: string;
424
+ /**
425
+ * Informational: true once a server download has advanced this collection's
426
+ * cursor (`lastSyncTs` set). NOT part of `ready` — an unused collection may
427
+ * never get a cursor yet is perfectly loadable.
428
+ */
429
+ everDownloaded: boolean;
430
+ }
431
+ /** Rolled-up preload status across all readable, in-scope collections. */
432
+ export type PreloadAggregate = "idle" | "full" | "partial" | "failed";
433
+ /** Snapshot of preload/hydration status — see {@link I_SyncedDb.getPreloadStatus}. */
434
+ export interface PreloadStatus {
435
+ /**
436
+ * `full` = every expected collection ready; `failed` = all expected failed;
437
+ * `partial` = anything in between (some ready and/or pending and/or failed);
438
+ * `idle` = no readable, in-scope collections expected.
439
+ */
440
+ aggregate: PreloadAggregate;
441
+ /** Per-collection detail for every readable, in-scope collection. */
442
+ collections: CollectionPreloadStatus[];
443
+ /** Count of readable, in-scope collections (the denominator for `aggregate`). */
444
+ expectedCount: number;
445
+ /** Number of `ready` collections. */
446
+ readyCount: number;
447
+ /** Number of `failed` collections. */
448
+ failedCount: number;
449
+ /** Number of `pending` (not-yet-ready, not-failed) collections. */
450
+ pendingCount: number;
451
+ }
403
452
  /** Per-collection eviction statistics. */
404
453
  export interface EvictionCollectionInfo {
405
454
  /** Collection name. */
@@ -590,6 +639,12 @@ export interface SyncedDbConfig {
590
639
  total: number;
591
640
  items: number;
592
641
  }) => void;
642
+ /**
643
+ * Callback when per-collection hydration status changes (hydration end,
644
+ * hydration failure, scope change, or sync end — when a download advances a
645
+ * cursor). Receives the same snapshot as {@link I_SyncedDb.getPreloadStatus}.
646
+ */
647
+ onPreloadStatusChange?: (status: PreloadStatus) => void;
593
648
  /** Callback when server download starts (findNewerManyStream) */
594
649
  onServerSyncStart?: (info: {
595
650
  calledFrom?: string;
@@ -1006,6 +1061,14 @@ export interface I_SyncedDb {
1006
1061
  * Persisted in Dexie via syncMeta key `__lastInitialSync`.
1007
1062
  */
1008
1063
  lastInitialSync(): Date | undefined;
1064
+ /**
1065
+ * Snapshot of per-collection hydration status plus a rolled-up aggregate
1066
+ * (`full` / `partial` / `failed` / `idle`). Computed over readable
1067
+ * (non-`writeOnly`), in-scope collections — so during a
1068
+ * `setSyncOnlyTheseCollections` subset only those collections are reported.
1069
+ * Synchronous; safe to call any time after construction.
1070
+ */
1071
+ getPreloadStatus(): PreloadStatus;
1009
1072
  /** Ali je povezan na server */
1010
1073
  isOnline(): boolean;
1011
1074
  /** 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.194",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",