@pylonsync/sync 0.3.200 → 0.3.201

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +40 -4
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.200",
6
+ "version": "0.3.201",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -165,6 +165,23 @@ export class SyncEngine {
165
165
  private reconnectAttempts = 0;
166
166
  private persistence: import("./persistence").IndexedDBPersistence | null = null;
167
167
 
168
+ /**
169
+ * Flips true once `start()` has either:
170
+ * - drained IndexedDB into the in-memory store (data path), OR
171
+ * - decided the engine has no persistence layer (test / SSR / explicit opt-out).
172
+ *
173
+ * `useQuery`'s `loading` flag consumes this so apps don't render a
174
+ * "Loading…" flash on every page refresh when the disk replica
175
+ * already has the data. Without it, even returning visits show the
176
+ * spinner for the 50–200ms gap between component mount and IndexedDB
177
+ * `loadAllEntities()` resolving — visually identical to a true
178
+ * cold-start fetch.
179
+ */
180
+ private _hydrated = false;
181
+ isHydrated(): boolean {
182
+ return this._hydrated;
183
+ }
184
+
168
185
  readonly store: LocalStore;
169
186
  readonly mutations: MutationQueue;
170
187
 
@@ -395,7 +412,10 @@ export class SyncEngine {
395
412
  // subscribers re-read. Without this, if the subsequent pull returns
396
413
  // no changes (replica already at cursor), subscribers stay stuck on
397
414
  // their initial empty snapshot until the first WS event arrives.
415
+ this._hydrated = true;
398
416
  if (hydrated) this.store.notify();
417
+ else this.store.notify(); // notify even on empty cache so useQuery
418
+ // sees `isHydrated()` flip and can drop its initial loading state.
399
419
 
400
420
  // Load cursor.
401
421
  const savedCursor = await this.persistence.loadCursor();
@@ -439,6 +459,11 @@ export class SyncEngine {
439
459
  // IndexedDB not available — continue without persistence.
440
460
  }
441
461
  }
462
+ // Always flip `_hydrated` true by this point — even when persist
463
+ // was off or IndexedDB threw. useQuery's loading semantics depend
464
+ // on it; leaving false would pin every app with persist:false
465
+ // into a permanent "Loading…" state.
466
+ this._hydrated = true;
442
467
 
443
468
  // Seed the server-resolved session before the first pull so
444
469
  // `useSession` subscribers see the right tenant from frame one, and
@@ -458,10 +483,21 @@ export class SyncEngine {
458
483
  // the local IndexedDB has rows the server doesn't (deletes made by
459
484
  // another surface while this tab was closed, or events that fell
460
485
  // off the in-memory ChangeLog before this tab's cursor caught up).
461
- // Fires after pull so we don't reconcile against rows that pull
462
- // would have applied anyway. Errors are swallowed inside
463
- // reconcileInner so a failed reconcile doesn't take down startup.
464
- void this.reconcile();
486
+ //
487
+ // Deliberately NOT fired here anymore. Apps that select-org from
488
+ // a bootstrap effect race against this reconcile pass: the engine
489
+ // resolves /api/auth/me before selectOrg lands, tenant=null, the
490
+ // entity fetch returns 0 rows, every cached row gets tombstoned,
491
+ // and the user sees a "rows render then flash away" gap until
492
+ // selectOrg fires the session-changed envelope and the engine
493
+ // re-pulls. The visibility-change + WS-reconnect reconcile triggers
494
+ // below STILL run, so deletes made while the tab was closed
495
+ // converge on the next focus / reconnect — just not in the narrow
496
+ // window where the session might still be unresolved. Net effect:
497
+ // identical safety, no flash.
498
+ //
499
+ // The session-flip guard in reconcileInner is the second line of
500
+ // defense; this is the first. Belt + braces.
465
501
 
466
502
  // Wire the visibility-change reconcile so a tab that returns from
467
503
  // the background (laptop wakes, tab unhidden) catches up against