fix(mobile): reconcile dismissed pushes on first connection

This commit is contained in:
Jinwoo-H
2026-09-07 14:26:13 -04:00
parent 1bb74e4599
commit 8da335d778
3 changed files with 44 additions and 5 deletions
@@ -246,11 +246,13 @@ export function subscribeToDesktopNotifications(client: RpcClient, hostId: strin
// left over from a previous desktop lifetime, so the catch-up request carries
// a watermark that means something against the counter now answering it.
adoptNotificationEpoch(session, hostId, readyEpoch)
// A reconnect always catches up. A cold open catches up only when this device
// has delivered for this host before — a first-ever pairing must not be handed
// the desktop's whole retained buffer.
// First pairing recovers tray dismissals without replaying historical alerts.
if (isReconnect || session.hadStoredWatermark) {
await fetchMissed()
} else {
await requestNotificationCatchup(client, hostId, undefined, () => disposed).catch(
() => {}
)
}
})()
return
@@ -148,6 +148,39 @@ beforeEach(() => {
})
describe('reopen after a push the OS showed while Orca was closed', () => {
it('recovers a dismissed push without a stored watermark or replaying historical alerts', async () => {
const identity = { notificationId: 'a:6', notificationSeq: 6, notificationEpoch: 'epoch-1' }
presentTray([{ hostFingerprint, ...identity }])
const { client, ready } = catchUpClient()
vi.mocked(client.sendRequest).mockResolvedValue({
ok: true,
result: {
dismissedPushes: [identity],
notifications: [{ type: 'notification', title: 'Historical alert', notificationSeq: 5 }]
}
} as never)
const dispose = subscribeToDesktopNotifications(client, 'host-1')
ready()
await flushAsync()
expect(client.sendRequest).toHaveBeenCalledWith('notifications.getMissedSince', {
lastSeenSeq: Number.MAX_SAFE_INTEGER,
deliveredPushes: [identity]
})
expect(Notifications.dismissNotificationAsync).toHaveBeenCalledExactlyOnceWith('tray-0')
expect(shownTitles()).toEqual([])
expect(persistedSeq()).toBe(0)
dispose()
})
it('does not request history on a first pairing with an empty tray', async () => {
const { client, ready } = catchUpClient()
const dispose = subscribeToDesktopNotifications(client, 'host-1')
ready()
await flushAsync()
expect(client.sendRequest).not.toHaveBeenCalled()
dispose()
})
it('replays only the events still missing from the tray', async () => {
presentTray([
{ hostFingerprint, notificationId: 'a:6', notificationSeq: 6, notificationEpoch: 'epoch-1' }
@@ -61,12 +61,16 @@ async function readDelivered(hostId: string): Promise<Map<string, OrcaPushPayloa
export async function requestNotificationCatchup(
client: Pick<RpcClient, 'sendRequest'>,
hostId: string,
params: { lastSeenSeq: number; epoch?: string; includeDesktopSuppressed?: boolean },
params: { lastSeenSeq: number; epoch?: string; includeDesktopSuppressed?: boolean } | undefined,
isDisposed: () => boolean
) {
const delivered = await readDelivered(hostId)
if (!params && (delivered.size === 0 || isDisposed())) {
return { ok: true, result: { notifications: [] } }
}
const response = await client.sendRequest('notifications.getMissedSince', {
...params,
// First pairing reconciles the tray without requesting historical alerts.
...(params ?? { lastSeenSeq: Number.MAX_SAFE_INTEGER }),
...(delivered.size
? { deliveredPushes: [...delivered.values()].map((payload) => identity(payload)!) }
: {})