mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
fix: keep offline edits when the app cannot read an account's server record
The app records which server filled each account's caches and checks it
with every new session (a330d77). A read of that record that failed -
a storage error, such as a busy database - answered "none recorded",
so the session counted as the first sight of the account: its queued
offline edits were dropped, with a notice that the mail server had
changed when it had not, and the record was written anew. Had the
server been replaced, that record hid the change from every later
check.
A read that fails now fails the check: nothing is dropped or written,
and the next session checks again. As with any failed check, signing
in goes on.
This commit is contained in:
@@ -151,8 +151,10 @@ async function forgetUnconfirmedServerState(accountId: string): Promise<void> {
|
||||
* (username@host) and drop what was derived from the server when it changed.
|
||||
* On the first sight of an account only what can't be confirmed to belong
|
||||
* to its server goes (forgetUnconfirmedServerState). The fingerprint is
|
||||
* written after that, so an interrupted drop is repeated next time. Resolves
|
||||
* to true when caches were dropped.
|
||||
* written after that, so an interrupted drop is repeated next time. A
|
||||
* recorded fingerprint that cannot be read rejects before anything goes or
|
||||
* is written, and the next session checks again. Resolves to true when
|
||||
* caches were dropped.
|
||||
*/
|
||||
export async function reconcileServerFingerprint(accountId: string, session: JMAPSession): Promise<boolean> {
|
||||
const next = sessionFingerprint(session);
|
||||
|
||||
@@ -48,13 +48,14 @@ export function sessionFingerprint(session: JMAPSession): string {
|
||||
return sha256Hex(new TextEncoder().encode(JSON.stringify(parts))).slice(0, 16);
|
||||
}
|
||||
|
||||
/** The fingerprint recorded for a local account (username@host), if any. */
|
||||
/**
|
||||
* The fingerprint recorded for a local account (username@host), or null when
|
||||
* none is. A read that fails throws rather than answer null: that would be
|
||||
* the first sight of the account, which drops its queued changes (see
|
||||
* reconcileServerFingerprint), on a server that may never have changed.
|
||||
*/
|
||||
export async function readServerFingerprint(accountId: string): Promise<string | null> {
|
||||
try {
|
||||
return await AsyncStorage.getItem(storageKey(accountId));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return AsyncStorage.getItem(storageKey(accountId));
|
||||
}
|
||||
|
||||
/** The primary JMAP account id recorded with the fingerprint, if any. */
|
||||
|
||||
@@ -390,4 +390,49 @@ describe('reconcileServerFingerprint', () => {
|
||||
expect(await AsyncStorage.getItem(`push:notifiedIds:v1:${ACCOUNT}`)).toBeNull();
|
||||
expect(await readServerFingerprint(ACCOUNT)).toBe(sessionFingerprint(session('1.0')));
|
||||
});
|
||||
|
||||
// A read that fails is no answer. Taken for no fingerprint, it made the
|
||||
// session the first sight of the account: the queued changes went on a
|
||||
// server that had not changed, and the fingerprint written then hid one
|
||||
// that had.
|
||||
describe('when the recorded fingerprint cannot be read', () => {
|
||||
const checkWithFailingRead = async (current: JMAPSession) => {
|
||||
const getItem = vi.mocked(AsyncStorage.getItem);
|
||||
const read = getItem.getMockImplementation()!;
|
||||
getItem.mockImplementation(async (key: string) => {
|
||||
if (key === `server-fingerprint:v1:${ACCOUNT}`) throw new Error('SQLITE_BUSY');
|
||||
return read(key);
|
||||
});
|
||||
try {
|
||||
await expect(reconcileServerFingerprint(ACCOUNT, current)).rejects.toThrow('SQLITE_BUSY');
|
||||
} finally {
|
||||
getItem.mockImplementation(read);
|
||||
}
|
||||
};
|
||||
|
||||
it('keeps the queued changes, and checks again with the next session (Stalwart 1.0)', async () => {
|
||||
await seed(ACCOUNT);
|
||||
await writeServerFingerprint(ACCOUNT, sessionFingerprint(session('1.0')), 'f');
|
||||
|
||||
await checkWithFailingRead(session('1.0'));
|
||||
|
||||
expect(useOutboxStore.getState().entries).toHaveLength(2);
|
||||
expect(useOutboxStore.getState().failed).toHaveLength(1);
|
||||
expect(useToastStore.getState().toasts).toEqual([]);
|
||||
expect(await reconcileServerFingerprint(ACCOUNT, session('1.0'))).toBe(false);
|
||||
expect(useOutboxStore.getState().entries).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('records nothing, so the next session still drops what the old server left (0.16.23 to 1.0)', async () => {
|
||||
await seed(ACCOUNT);
|
||||
|
||||
await checkWithFailingRead(session('1.0'));
|
||||
|
||||
expect(useEmailStore.getState().mailboxState).toBe('s1iaq');
|
||||
expect(await readServerFingerprint(ACCOUNT)).toBe(sessionFingerprint(session('0.16.23')));
|
||||
expect(await reconcileServerFingerprint(ACCOUNT, session('1.0'))).toBe(true);
|
||||
expect(useEmailStore.getState().mailboxState).toBeUndefined();
|
||||
expect(useOutboxStore.getState().entries).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user