cry-synced-db-client 0.1.144 → 0.1.146

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
@@ -108,6 +108,67 @@ Signature is identical. No other callback changes.
108
108
  - Noop when offline or on writeOnly collections.
109
109
  - Ignored on `find` / `findOne` (use `referToServer` there).
110
110
 
111
+ ## 0.1.146 (2026-04-25)
112
+
113
+ ### Fix: INITIAL SYNC re-fetched whole dataset on every reload (cursor race)
114
+
115
+ `syncMetaCache` was populated lazily inside `loadCollectionToInMem` — one
116
+ collection at a time, only after that collection's records finished hydrating
117
+ into in-memory state. When `ConnectionManager.tryGoOnline` fired
118
+ `sync("INITIAL SYNC")` on WS connect (or `setSyncOnlyTheseCollections`
119
+ expanded the allowed set under a fresh login), the sync engine read
120
+ `syncMetaCache.get(collection)?.lastSyncTs` for each collection — and for
121
+ collections whose hydration hadn't completed yet, it found nothing and fell
122
+ back to `timestamp: 0` (`SyncEngine.ts:94`). The server then returned every
123
+ matching row since epoch, not a delta.
124
+
125
+ In one observed reproducer with 62 sync'd collections, 58 of them arrived at
126
+ the server with `timestamp: 0` and the server replied with 60 131 rows on a
127
+ session that already had 58 745 rows cached locally — i.e. ~full re-fetch
128
+ every reload, regardless of how recently the client had synced.
129
+
130
+ `init()` now eagerly preloads sync cursors for every registered collection
131
+ into `syncMetaCache` before `connectionManager.startTimers()` runs, via a
132
+ parallel fan-out of `getSyncMeta` reads. Cursor cache availability is
133
+ decoupled from in-mem record hydration: a sync triggered the moment WS comes
134
+ up reads a fully populated cache, regardless of where hydration is. One Dexie
135
+ point-lookup per registered collection — cheap.
136
+
137
+ The lazy populate inside `loadCollectionToInMem` is retained as a defensive
138
+ overwrite for collections registered after init (e.g. via dynamic
139
+ `addCollection`).
140
+
141
+ ## 0.1.145 (2026-04-25)
142
+
143
+ ### Fix: `onSyncProgress` back-track during initial sync
144
+
145
+ `onSyncProgress` is fired from two distinct phases that can run **concurrently**
146
+ during initial sync — Dexie → in-mem hydration (`SyncedDb.loadCollectionsToInMem`)
147
+ and server → Dexie download (`SyncEngine.findNewerManyStream`). Each phase
148
+ carries its own `loaded`/`total`, so consumers wiring the callback into a single
149
+ progress bar saw the percentage back-track every time a tick from the other
150
+ phase arrived (e.g. dexie 9/58 → server 1/62 → dexie 10/58 → server 2/62 …).
151
+
152
+ The payload now carries a `phase: 'dexie' | 'server'` discriminator so consumers
153
+ can attribute each tick to its source and either filter or render the two
154
+ streams separately. **Non-breaking**: consumers that destructure
155
+ `{ collection, loaded, total }` and ignore `phase` keep working unchanged.
156
+
157
+ Type change in `I_SyncedDb.SyncedDbConfig` and internal `SyncEngineCallbacks`:
158
+ ```ts
159
+ onSyncProgress?: (info: {
160
+ phase: 'dexie' | 'server';
161
+ collection: string;
162
+ loaded: number;
163
+ total: number;
164
+ items: number;
165
+ }) => void;
166
+ ```
167
+
168
+ The JSDoc on `onSyncProgress` previously claimed "Fires only during
169
+ init/setSyncOnlyTheseCollections" — that was wrong; it also fires during server
170
+ sync. Doc corrected.
171
+
111
172
  ## 0.1.136 (2026-04-20)
112
173
 
113
174
  - `DexieDb.saveMany` is now fail-safe:
package/dist/index.js CHANGED
@@ -2502,6 +2502,7 @@ var _SyncEngine = class _SyncEngine {
2502
2502
  if (!completedCollections.has(collection)) {
2503
2503
  completedCollections.add(collection);
2504
2504
  this.callbackSafe(this.callbacks.onSyncProgress, {
2505
+ phase: "server",
2505
2506
  collection,
2506
2507
  loaded: completedCollections.size,
2507
2508
  total: syncSpecs.length,
@@ -3800,6 +3801,7 @@ var _SyncedDb = class _SyncedDb {
3800
3801
  }
3801
3802
  }
3802
3803
  await this.pendingChanges.recoverPendingWrites();
3804
+ await this.preloadAllSyncMetas();
3803
3805
  const allowedColls = [...this.collections.keys()].filter((n) => this.isSyncAllowed(n));
3804
3806
  await this.loadCollectionsToInMem(allowedColls, "init");
3805
3807
  this.leaderElection.init();
@@ -4983,6 +4985,7 @@ var _SyncedDb = class _SyncedDb {
4983
4985
  totalItems += items;
4984
4986
  loaded++;
4985
4987
  this.safeCallback(this.onSyncProgress, {
4988
+ phase: "dexie",
4986
4989
  collection: name,
4987
4990
  loaded,
4988
4991
  total: names.length,
@@ -5017,6 +5020,25 @@ var _SyncedDb = class _SyncedDb {
5017
5020
  }
5018
5021
  return allItems.length;
5019
5022
  }
5023
+ /**
5024
+ * Bulk-read sync cursors for every registered collection into syncMetaCache.
5025
+ * Called once during init() before sync can fire. Decouples cursor cache
5026
+ * availability from in-mem record hydration, eliminating the race where a
5027
+ * sync triggered by ConnectionManager.tryGoOnline (or by setSyncOnlyTheseCollections
5028
+ * expanding the allowed set) reads an unpopulated cache and sends timestamp:0
5029
+ * for un-hydrated collections.
5030
+ */
5031
+ async preloadAllSyncMetas() {
5032
+ const names = [...this.collections.keys()];
5033
+ const results = await Promise.all(
5034
+ names.map(
5035
+ (name) => this.dexieDb.getSyncMeta(name).then((meta) => ({ name, meta }))
5036
+ )
5037
+ );
5038
+ for (const { name, meta } of results) {
5039
+ if (meta) this.syncMetaCache.set(name, meta);
5040
+ }
5041
+ }
5020
5042
  assertCollection(name) {
5021
5043
  if (!this.collections.has(name)) {
5022
5044
  throw new Error(`SyncedDb: Collection "${(name == null ? void 0 : name.toString()) || "?"}" not configured`);
@@ -291,6 +291,15 @@ export declare class SyncedDb implements I_SyncedDb {
291
291
  */
292
292
  private loadCollectionsToInMem;
293
293
  private loadCollectionToInMem;
294
+ /**
295
+ * Bulk-read sync cursors for every registered collection into syncMetaCache.
296
+ * Called once during init() before sync can fire. Decouples cursor cache
297
+ * availability from in-mem record hydration, eliminating the race where a
298
+ * sync triggered by ConnectionManager.tryGoOnline (or by setSyncOnlyTheseCollections
299
+ * expanding the allowed set) reads an unpopulated cache and sends timestamp:0
300
+ * for un-hydrated collections.
301
+ */
302
+ private preloadAllSyncMetas;
294
303
  private assertCollection;
295
304
  private static readonly STRINGIFIED_FALSY;
296
305
  /** Stringify an Id parameter (ObjectId → hex string). */
@@ -235,6 +235,7 @@ export interface SyncEngineCallbacks {
235
235
  }) => void;
236
236
  onSyncEnd?: (info: SyncInfo) => void;
237
237
  onSyncProgress?: (info: {
238
+ phase: 'dexie' | 'server';
238
239
  collection: string;
239
240
  loaded: number;
240
241
  total: number;
@@ -345,8 +345,15 @@ export interface SyncedDbConfig {
345
345
  totalItems: number;
346
346
  durationMs: number;
347
347
  }) => void;
348
- /** Callback after each collection is loaded during full sync (Dexie→inMem). Fires only during init/setSyncOnlyTheseCollections. */
348
+ /**
349
+ * Callback after each collection completes loading. Fires from two distinct phases that can run
350
+ * concurrently — `phase` discriminates the source so consumers can render progress coherently:
351
+ * - `'dexie'` — Dexie → in-memory hydration during init/setSyncOnlyTheseCollections
352
+ * - `'server'` — server → Dexie download during full/initial sync (findNewerManyStream)
353
+ * `loaded`/`total` are scoped to the phase that emitted the event.
354
+ */
349
355
  onSyncProgress?: (info: {
356
+ phase: 'dexie' | 'server';
350
357
  collection: string;
351
358
  loaded: number;
352
359
  total: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.144",
3
+ "version": "0.1.146",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",