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 +30 -0
- package/dist/index.js +81 -1
- package/dist/src/db/SyncedDb.d.ts +18 -1
- package/dist/src/types/I_SyncedDb.d.ts +60 -0
- package/package.json +1 -1
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
|
-
|
|
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. */
|