fix(gui): a tap delivered twice routes once

The page remembers the notifications it handed over in the last ten
seconds and drops a repeat, whichever way it arrives.
This commit is contained in:
Juan Castro committed 2026-10-27 14:17:57 -04:00
1 parent 5f7ab80721
commit 7aa4d91ae9
2 files changed
+35 -1

No files matched your search

+14 -1
View File
@@ -27,6 +27,8 @@ export const PUSH_TAP_PARAM = 'puter.notification';
const UID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const UID_MAX = 64;
// A tap for a uid acted on this recently is the same tap again.
const TAP_REPEAT_MS = 10_000;
const nonEmpty = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
@@ -200,6 +202,8 @@ export const storedTapHandler = ({ fetchRow, surface, found, missing }) => async
let pushTapHandler = null;
/** @type {PushTap[]} */
const pendingPushTaps = [];
/** @type {Map<string, number>} uid → when it was last handed over */
const recentTaps = new Map();
/** @param {(tap: PushTap) => unknown} handler */
export const setPushTapHandler = (handler) => {
@@ -207,9 +211,18 @@ export const setPushTapHandler = (handler) => {
for ( const tap of pendingPushTaps.splice(0) ) void handler(tap);
};
const seenLately = (uid, now) => {
for ( const [seen, at] of recentTaps ) {
if ( now - at >= TAP_REPEAT_MS ) recentTaps.delete(seen);
}
if ( recentTaps.has(uid) ) return true;
recentTaps.set(uid, now);
return false;
};
/** @param {PushTap|null} tap */
const deliverPushTap = (tap) => {
if ( ! tap ) return;
if ( ! tap || seenLately(tap.uid, Date.now()) ) return;
if ( pushTapHandler ) void pushTapHandler(tap);
else pendingPushTaps.push(tap);
};
@@ -282,6 +282,10 @@ describe('storedTapHandler', () => {
});
describe('push tap dispatch', () => {
afterEach(() => {
vi.useRealTimers();
});
it('holds taps until a handler is set, then hands them over in order', async () => {
const { dispatchPushTap, setPushTapHandler } = await import('./notificationTap.js');
dispatchPushTap({ uid: UID });
@@ -305,4 +309,21 @@ describe('push tap dispatch', () => {
expect(fetchRow).toHaveBeenCalledWith(OTHER_UID);
expect(openApp).not.toHaveBeenCalled();
});
it('hands the same notification over once within ten seconds', async () => {
vi.useFakeTimers();
const { dispatchPushTap, setPushTapHandler } = await import('./notificationTap.js');
const handler = vi.fn();
setPushTapHandler(handler);
dispatchPushTap({ uid: UID });
dispatchPushTap({ uid: UID });
dispatchPushTap({ uid: OTHER_UID });
expect(handler.mock.calls).toEqual([[{ uid: UID }], [{ uid: OTHER_UID }]]);
vi.advanceTimersByTime(9_999);
dispatchPushTap({ uid: UID });
expect(handler).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(1);
dispatchPushTap({ uid: UID });
expect(handler).toHaveBeenCalledTimes(3);
});
});