mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: wait for the saved session check instead of loading the account beside it when the network comes back at start
This commit is contained in:
@@ -391,6 +391,55 @@ describe('auth-store', () => {
|
|||||||
expect(useAccountStore.getState().getAccountById('acc-1')?.needsSignIn).toBeFalsy();
|
expect(useAccountStore.getState().getAccountById('acc-1')?.needsSignIn).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The network coming back (App.tsx) retries the session while the
|
||||||
|
// start from the saved session is still checking it: the retry used to
|
||||||
|
// load the account beside the check, and check the server while the
|
||||||
|
// start's reads were still landing.
|
||||||
|
it('waits for the check of the saved session when the network comes back during it', async () => {
|
||||||
|
registerAccount();
|
||||||
|
mockLoadFromCache.mockResolvedValueOnce(true);
|
||||||
|
const check = deferred<unknown>();
|
||||||
|
mockRevalidate.mockReturnValueOnce(check.promise);
|
||||||
|
await useAuthStore.getState().restoreSession();
|
||||||
|
|
||||||
|
let retried: boolean | undefined;
|
||||||
|
const retry = useAuthStore.getState().retrySession().then((ok) => { retried = ok; });
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
expect(mockLoadAccount).not.toHaveBeenCalled();
|
||||||
|
expect(retried).toBeUndefined();
|
||||||
|
|
||||||
|
check.resolve({ apiUrl: 'https://mail.example.com/jmap/' });
|
||||||
|
await retry;
|
||||||
|
expect(retried).toBe(true);
|
||||||
|
expect(useAuthStore.getState().session).not.toBeNull();
|
||||||
|
expect(mockLoadAccount).not.toHaveBeenCalled();
|
||||||
|
expect(mockRevalidate).toHaveBeenCalledTimes(1);
|
||||||
|
expect((resume.mock.calls as unknown[][]).filter(([id]) => id === 'acc-1')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the account once the check of the saved session failed without network', async () => {
|
||||||
|
registerAccount();
|
||||||
|
mockLoadFromCache.mockResolvedValueOnce(true);
|
||||||
|
const check = deferred<unknown>();
|
||||||
|
mockRevalidate.mockReturnValueOnce(check.promise);
|
||||||
|
await useAuthStore.getState().restoreSession();
|
||||||
|
|
||||||
|
mockLoadAccount.mockResolvedValueOnce(true);
|
||||||
|
const retry = useAuthStore.getState().retrySession();
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
expect(mockLoadAccount).not.toHaveBeenCalled();
|
||||||
|
check.reject(new NetworkError('offline'));
|
||||||
|
|
||||||
|
expect(await retry).toBe(true);
|
||||||
|
expect(mockLoadAccount).toHaveBeenCalledTimes(1);
|
||||||
|
expect(useAuthStore.getState().session).not.toBeNull();
|
||||||
|
// The start is confirmed: a failed switch away does not check it again.
|
||||||
|
mockLoadAccount.mockRejectedValueOnce(new NetworkError('offline'));
|
||||||
|
useAccountStore.setState({ accounts: [...useAccountStore.getState().accounts, account('acc-2', false)] });
|
||||||
|
await useAuthStore.getState().switchAccount('acc-2');
|
||||||
|
expect(mockRevalidate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
// A switch that fails puts the account back. Its saved session was
|
// A switch that fails puts the account back. Its saved session was
|
||||||
// never confirmed (the switch overtook the check), so the check runs
|
// never confirmed (the switch overtook the check), so the check runs
|
||||||
// again, and the outbox waits for it.
|
// again, and the outbox waits for it.
|
||||||
|
|||||||
@@ -323,6 +323,26 @@ let restoreUnderWay: Promise<void> | null = null;
|
|||||||
// connect, until the server confirms it (revalidateRestoredSession). Its
|
// connect, until the server confirms it (revalidateRestoredSession). Its
|
||||||
// session is not live yet: its server was not checked and its outbox waits.
|
// session is not live yet: its server was not checked and its outbox waits.
|
||||||
let unconfirmedStart: string | null = null;
|
let unconfirmedStart: string | null = null;
|
||||||
|
/** The reads that start sent with the saved session. */
|
||||||
|
let unconfirmedStartReads: { accountId: string; promise: Promise<void> } | null = null;
|
||||||
|
/**
|
||||||
|
* The check of the saved session under way. A session retry (the network
|
||||||
|
* coming back) during a start from the saved session waits for it instead of
|
||||||
|
* loading the account beside it.
|
||||||
|
*/
|
||||||
|
let revalidation: { accountId: string; promise: Promise<void> } | null = null;
|
||||||
|
|
||||||
|
function readsOfStart(accountId: string): Promise<void> | undefined {
|
||||||
|
return unconfirmedStartReads?.accountId === accountId ? unconfirmedStartReads.promise : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRevalidation(accountId: string, reads?: Promise<void>): Promise<void> {
|
||||||
|
const promise: Promise<void> = revalidateRestoredSession(accountId, reads).finally(() => {
|
||||||
|
if (revalidation?.promise === promise) revalidation = null;
|
||||||
|
});
|
||||||
|
revalidation = { accountId, promise };
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
async function switchesSettled(): Promise<void> {
|
async function switchesSettled(): Promise<void> {
|
||||||
while (pendingSwitch) await pendingSwitch.settled;
|
while (pendingSwitch) await pendingSwitch.settled;
|
||||||
@@ -627,6 +647,7 @@ function applyConnectedState(
|
|||||||
// A session from the server, or a start from the saved one, which sets
|
// A session from the server, or a start from the saved one, which sets
|
||||||
// this again right after.
|
// this again right after.
|
||||||
unconfirmedStart = null;
|
unconfirmedStart = null;
|
||||||
|
unconfirmedStartReads = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||||
@@ -1057,7 +1078,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
// still in the snapshot taken then, and its server was checked.
|
// still in the snapshot taken then, and its server was checked.
|
||||||
jmapClient.restoreSnapshot(origin.client);
|
jmapClient.restoreSnapshot(origin.client);
|
||||||
if (origin.live) refetchFeatureStores(accountId);
|
if (origin.live) refetchFeatureStores(accountId);
|
||||||
else if (unconfirmedStart === accountId) void revalidateRestoredSession(accountId);
|
else if (unconfirmedStart === accountId) void startRevalidation(accountId);
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1093,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
if (origin.accountId && origin.live) resumeOutbox(origin.accountId);
|
if (origin.accountId && origin.live) resumeOutbox(origin.accountId);
|
||||||
// A start from the saved session whose check this switch overtook.
|
// A start from the saved session whose check this switch overtook.
|
||||||
else if (origin.accountId && unconfirmedStart === origin.accountId) {
|
else if (origin.accountId && unconfirmedStart === origin.accountId) {
|
||||||
void revalidateRestoredSession(origin.accountId);
|
void startRevalidation(origin.accountId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -1245,7 +1266,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
unconfirmedStart = target.id;
|
unconfirmedStart = target.id;
|
||||||
perfMark('restore-done', { cached: true });
|
perfMark('restore-done', { cached: true });
|
||||||
const reads = reloadFeatureStores();
|
const reads = reloadFeatureStores();
|
||||||
void revalidateRestoredSession(target.id, reads);
|
unconfirmedStartReads = { accountId: target.id, promise: reads };
|
||||||
|
void startRevalidation(target.id, reads);
|
||||||
void syncAccountDisplayName(target.id);
|
void syncAccountDisplayName(target.id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1358,6 +1380,20 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
// The server refused its sign-in: only a new one brings it back.
|
// The server refused its sign-in: only a new one brings it back.
|
||||||
if (target.needsSignIn) return false;
|
if (target.needsSignIn) return false;
|
||||||
|
// A start from the saved session: its check brings the session up, after
|
||||||
|
// the reads that start sent. Loading the account beside it checked the
|
||||||
|
// server while those reads were still landing. Only a check that ended
|
||||||
|
// without network leaves the load below to try.
|
||||||
|
if (unconfirmedStart === activeAccountId) {
|
||||||
|
const running = revalidation?.accountId === activeAccountId
|
||||||
|
? revalidation.promise
|
||||||
|
: startRevalidation(activeAccountId, readsOfStart(activeAccountId));
|
||||||
|
await running;
|
||||||
|
if (!isCurrent() || get().activeAccountId !== activeAccountId) return false;
|
||||||
|
if (get().session) return true;
|
||||||
|
if (get().reauth || unconfirmedStart === activeAccountId) return false;
|
||||||
|
}
|
||||||
|
const startReads = readsOfStart(activeAccountId);
|
||||||
try {
|
try {
|
||||||
const ok = await jmapClient.loadAccount(activeAccountId);
|
const ok = await jmapClient.loadAccount(activeAccountId);
|
||||||
if (!ok || !isCurrent()) return false;
|
if (!ok || !isCurrent()) return false;
|
||||||
@@ -1369,6 +1405,13 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
const fresh = jmapClient.currentSession!;
|
const fresh = jmapClient.currentSession!;
|
||||||
followStoredServer(activeAccountId);
|
followStoredServer(activeAccountId);
|
||||||
|
if (startReads) {
|
||||||
|
// What the start read from the saved session lands before a server
|
||||||
|
// that was replaced drops the caches.
|
||||||
|
const recorded = await readServerFingerprint(activeAccountId).catch(() => null);
|
||||||
|
if (recorded !== sessionFingerprint(fresh)) await startReads;
|
||||||
|
if (!isCurrent()) return false;
|
||||||
|
}
|
||||||
await checkServerChange(activeAccountId, fresh);
|
await checkServerChange(activeAccountId, fresh);
|
||||||
if (!isCurrent()) return false;
|
if (!isCurrent()) return false;
|
||||||
applyConnectedState(set, fresh, target.serverUrl, target.username, activeAccountId);
|
applyConnectedState(set, fresh, target.serverUrl, target.username, activeAccountId);
|
||||||
@@ -1449,6 +1492,7 @@ async function revalidateRestoredSession(accountId: string, reads?: Promise<void
|
|||||||
const live = jmapClient.currentSession;
|
const live = jmapClient.currentSession;
|
||||||
if (!isCurrent() || !live) return;
|
if (!isCurrent() || !live) return;
|
||||||
unconfirmedStart = null;
|
unconfirmedStart = null;
|
||||||
|
unconfirmedStartReads = null;
|
||||||
useAccountStore.getState().updateAccount(accountId, {
|
useAccountStore.getState().updateAccount(accountId, {
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
hasError: false,
|
hasError: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user