fix: keep a waiting re-sync from undoing what another tab turned off, and note the relay on refresh

This commit is contained in:
Linus Rath
2026-11-04 03:13:59 +01:00
parent 9432345440
commit d8e2a4f42c
2 changed files with 52 additions and 0 deletions
+44
View File
@@ -958,6 +958,50 @@ describe('a setup that gets stuck', () => {
});
});
describe('a re-sync waiting its turn', () => {
afterEach(() => { delete (navigator as unknown as { locks?: unknown }).locks; });
it('does not bring back what another tab turned off meanwhile', async () => {
localStorage.setItem(DEVICE_KEY, THIS_DEVICE);
localStorage.setItem(SUB_KEY, 'push-mine');
localStorage.setItem(RELAY_BASE_KEY, RELAY);
const client = makeClient([sub('push-mine', THIS_DEVICE)]);
installFetch({ [THIS_DEVICE]: true });
let turn: Promise<unknown> = Promise.resolve();
let release = () => {};
turn = new Promise<void>((resolve) => { release = resolve; });
Object.defineProperty(navigator, 'locks', {
configurable: true,
value: { request: (_n: string, run: () => Promise<unknown>) => { const next = turn.then(run, run); turn = next.catch(() => undefined); return next; } },
});
const resync = resyncWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false });
await vi.waitFor(() => expect((navigator as unknown as { locks: unknown }).locks).toBeTruthy());
await new Promise((resolve) => setTimeout(resolve, 0));
localStorage.removeItem(SUB_KEY); // The other tab, holding the lock.
release();
await resync;
expect(client.updatePushSubscription).not.toHaveBeenCalled();
expect(client.createPushSubscription).not.toHaveBeenCalled();
expect(localStorage.getItem(SUB_KEY)).toBeNull();
});
});
describe('a registration from before the relay was remembered', () => {
it('gets the relay noted when it is refreshed', async () => {
localStorage.setItem(DEVICE_KEY, THIS_DEVICE);
localStorage.setItem(SUB_KEY, 'push-mine');
const client = makeClient([sub('push-mine', THIS_DEVICE)]);
installFetch({ [THIS_DEVICE]: true });
await resyncWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false });
expect(localStorage.getItem(RELAY_BASE_KEY)).toBe(RELAY);
expect(client.createPushSubscription).not.toHaveBeenCalled();
});
});
describe('resyncWebPush when the relay changed (#903)', () => {
afterEach(() => setNoThirdPartyTraffic(false));
+8
View File
@@ -428,6 +428,8 @@ export interface EnableWebPushParams {
// that outlives a permission change keeps pushing for mailboxes the user can
// no longer read - recreating is the only client-side remedy (#841).
forceRecreate?: boolean;
// For a re-sync: stop if another tab turned push off while this waited its turn.
onlyIfStillOn?: boolean;
// Scope OS push notifications to Inbox-only mail. Mirrors settings-store's
// pushNotifyInboxOnly. Required, not optional: it changes the server-side
// delivery filter, so every caller must decide explicitly.
@@ -813,6 +815,9 @@ async function enableWebPushForThisTab(
params: EnableWebPushParams,
relayBaseUrl: string,
): Promise<EnableWebPushResult> {
if (params.onlyIfStillOn && !localStorage.getItem(subscriptionIdKey(params.client.getAccountId()))) {
return { subscriptionId: '' };
}
const registration = await ensureServiceWorker();
const vapidPublicKey = await fetchVapidPublicKey(relayBaseUrl);
@@ -914,6 +919,8 @@ async function enableWebPushForThisTab(
const refreshed = await refreshSubscriptionExpires(params.client, match, emailPush);
if (refreshed) {
rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, refreshed.emailPush);
// Unless it moved, which goes the other way, this is where it lives.
if (!localStorage.getItem(relayBaseKey(accountId))) localStorage.setItem(relayBaseKey(accountId), relayBaseUrl);
return { subscriptionId: storedServerId };
}
}
@@ -1317,6 +1324,7 @@ async function runResync(accountId: string, params: ResyncWebPushParams): Promis
if (!moved && !params.force && !(await resyncDue(accountId))) return false;
await enableWebPush({
client: params.client,
onlyIfStillOn: true,
forceRecreate: moved,
relayBaseUrl: params.relayBaseUrl,
accountLabel: params.accountLabel,