@pylonsync/sync 0.3.200 → 0.3.202
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/package.json +1 -1
- package/src/index.ts +61 -5
package/package.json
CHANGED
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
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
|
|
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
|
|
@@ -1459,7 +1495,27 @@ export class SyncEngine {
|
|
|
1459
1495
|
this.lastSeenTenant !== undefined &&
|
|
1460
1496
|
this.lastSeenTenant !== tenantNow
|
|
1461
1497
|
) {
|
|
1462
|
-
|
|
1498
|
+
// Two flavors of "tenant changed":
|
|
1499
|
+
//
|
|
1500
|
+
// - null → X : session first-resolution. The engine started
|
|
1501
|
+
// before the app called /api/auth/select-org;
|
|
1502
|
+
// /api/auth/me returned tenant_id=null at start
|
|
1503
|
+
// and is only now reporting the real value.
|
|
1504
|
+
// The cached IndexedDB rows ARE for tenant X
|
|
1505
|
+
// (that's where the user's data lives), so
|
|
1506
|
+
// wiping them would tombstone valid state and
|
|
1507
|
+
// produce the "rows render then flash away"
|
|
1508
|
+
// symptom multi-tenant apps were hitting. Skip
|
|
1509
|
+
// the reset; pull under the new tenant fills
|
|
1510
|
+
// any gaps via the existing cursor catch-up.
|
|
1511
|
+
//
|
|
1512
|
+
// - X → Y : actual org switch. Cached rows belong to
|
|
1513
|
+
// the OLD tenant and must not bleed into the
|
|
1514
|
+
// new context, so resetReplica is correct.
|
|
1515
|
+
const firstResolution = this.lastSeenTenant === null && tenantNow !== null;
|
|
1516
|
+
if (!firstResolution) {
|
|
1517
|
+
await this.resetReplica();
|
|
1518
|
+
}
|
|
1463
1519
|
await this.pull();
|
|
1464
1520
|
}
|
|
1465
1521
|
this.lastSeenTenant = tenantNow;
|