diff --git a/src/puter-js/src/modules/notifications/index.js b/src/puter-js/src/modules/notifications/index.js index ed4dc7af3..d2342c13e 100644 --- a/src/puter-js/src/modules/notifications/index.js +++ b/src/puter-js/src/modules/notifications/index.js @@ -78,12 +78,19 @@ const warnStorage = (error) => { ); }; -/** Runs `run` alone among the site's tabs, where the browser has Web Locks. */ -const withPushLock = (run) => { - const locks = globalThis.navigator?.locks; - return typeof locks?.request === 'function' - ? locks.request(PUSH_LOCK, () => run()) - : run(); +/** Runs `run` alone among the site's tabs, or as is where Web Locks refuse. */ +const withPushLock = async (run) => { + let started = false; + const start = () => { + started = true; + return run(); + }; + try { + return await globalThis.navigator.locks.request(PUSH_LOCK, start); + } catch (error) { + if (started) throw error; + return await start(); + } }; /** Hidden, or in a window without focus. */ diff --git a/src/puter-js/src/modules/notifications/webPush.test.js b/src/puter-js/src/modules/notifications/webPush.test.js index d3d8ba374..a9d96296d 100644 --- a/src/puter-js/src/modules/notifications/webPush.test.js +++ b/src/puter-js/src/modules/notifications/webPush.test.js @@ -2250,6 +2250,47 @@ describe('sign-out', () => { }); }); +describe('where the browser refuses Web Locks, as in a sandboxed frame', () => { + beforeEach(() => { + Object.defineProperty(globalThis.navigator, 'locks', { + value: { + request: vi.fn(async () => { + throw new Error('The request was denied'); + }), + }, + configurable: true, + }); + }); + afterEach(() => { + delete globalThis.navigator.locks; + }); + + it('signs out without rejecting and still ends the subscription', async () => { + store.set(OPT_IN, true); + store.set(SERVER_KEY, KEY); + const subscription = makeSubscription(); + withBrowser(makeNav({ existing: subscription })); + mockRequest.mockResolvedValue({ removed: true }); + const { module } = makeModule(); + await expect( + module._forgetPush('leaving-token'), + ).resolves.toBeUndefined(); + expect(subscription.unsubscribe).toHaveBeenCalled(); + expect(store.has(OPT_IN)).toBe(false); + }); + + it('still ends a subscription left here without an opt-in on load', async () => { + store.set(SERVER_KEY, KEY); + const subscription = makeSubscription(); + withBrowser(makeNav({ existing: subscription })); + const { module } = makeModule(); + await module.onPush('chat', () => {}, { onError: () => {} }); + await vi.waitFor(() => + expect(subscription.unsubscribe).toHaveBeenCalled(), + ); + }); +}); + describe('a different user signing in', () => { const USERS = { 'token-a': 'u-1', 'token-a2': 'u-1', 'token-b': 'u-2' }; /** A page signed in as u-1, whose tokens name their user in `uu`. */