diff --git a/lib/__tests__/web-push.test.ts b/lib/__tests__/web-push.test.ts index ea36d69ce..ec5434d5a 100644 --- a/lib/__tests__/web-push.test.ts +++ b/lib/__tests__/web-push.test.ts @@ -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 = Promise.resolve(); + let release = () => {}; + turn = new Promise((resolve) => { release = resolve; }); + Object.defineProperty(navigator, 'locks', { + configurable: true, + value: { request: (_n: string, run: () => Promise) => { 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)); diff --git a/lib/web-push.ts b/lib/web-push.ts index b399c6edf..d583ee993 100644 --- a/lib/web-push.ts +++ b/lib/web-push.ts @@ -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 { + 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,