cry-synced-db-client 0.1.145 → 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,36 @@ 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
+
111
141
  ## 0.1.145 (2026-04-25)
112
142
 
113
143
  ### Fix: `onSyncProgress` back-track during initial sync
package/dist/index.js CHANGED
@@ -3801,6 +3801,7 @@ var _SyncedDb = class _SyncedDb {
3801
3801
  }
3802
3802
  }
3803
3803
  await this.pendingChanges.recoverPendingWrites();
3804
+ await this.preloadAllSyncMetas();
3804
3805
  const allowedColls = [...this.collections.keys()].filter((n) => this.isSyncAllowed(n));
3805
3806
  await this.loadCollectionsToInMem(allowedColls, "init");
3806
3807
  this.leaderElection.init();
@@ -5019,6 +5020,25 @@ var _SyncedDb = class _SyncedDb {
5019
5020
  }
5020
5021
  return allItems.length;
5021
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
+ }
5022
5042
  assertCollection(name) {
5023
5043
  if (!this.collections.has(name)) {
5024
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). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.145",
3
+ "version": "0.1.146",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",