fix(puter-js): push cleanup runs without the lock where Web Locks refuse

In a frame where navigator.locks.request rejects, such as a sandboxed
frame, sign-out, disable(), the load-time check and syncs now run their
step without the lock instead of rejecting, so signOut() still resolves
and the subscription is still ended.
This commit is contained in:
Juan Castro committed 2026-11-02 10:22:57 -05:00
1 parent e15c242f5a
commit d9fd40daed
2 files changed
+54 -6

No files matched your search

@@ -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. */
@@ -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`. */