Files
orca/src/main/ipc/native-notification-lifecycle.ts
T
Neil 15e1ba3f84 refactor(ipc): split main-process IPC modules under the max-lines budget (#14703)
The six oversized src/main/ipc modules each carried a file-level
`eslint-disable max-lines` and ran 427-671 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all six
suppressions and prunes their entries (341 -> 335).

Pure move, no behavior change. Each file is cut along the seams it already had:
pet splits into format allowlist / storage paths / symlink-safe copy / bundle
manifest + import; filesystem-auth into path-containment primitives, the
config-derived allow-list, and the git-registered root cache; notifications into
sound selection, native lifecycle, permission probe, and burst cooldown;
crash-reporting into renderer error reports, breadcrumbs, and sender.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 49 registrations across 49
distinct channels on both sides. filesystem-auth's security boundary keeps its
acyclic layering -- containment primitives, then allow-list, then root cache,
then path-resolution orchestration -- with no layer gaining a back-edge.

Also keeps clipboard-ipc-handlers.test.ts under the 800-line test budget. The
split had briefly added a redundant vi.mock for isENOENT (byte-identical to the
real implementation) that pushed it to 801; the mock is dropped in favor of the
real function, with realpath added to the existing node:fs/promises mock.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 617 modules,
and no lint suppression added anywhere.
2026-08-15 18:08:54 -07:00

86 lines
2.2 KiB
TypeScript

import type { Notification } from 'electron'
const NOTIFICATION_DISPLAY_CONFIRMATION_TIMEOUT_MS = 2500
const NOTIFICATION_RELEASE_FALLBACK_MS = 5 * 60 * 1000
// Why: keep a strong reference so GC can't collect notifications (and their click handlers) before the user interacts with them.
export const activeNotifications = new Set<Notification>()
export const activeNotificationsById = new Map<
string,
{ notification: Notification; release: () => void }
>()
export function retainNotificationUntilRelease(
notification: Notification,
onRelease?: () => void
): () => void {
activeNotifications.add(notification)
let released = false
let releaseTimer: ReturnType<typeof setTimeout> | null = null
function release(): void {
if (released) {
return
}
released = true
activeNotifications.delete(notification)
notification.removeListener('close', release)
if (releaseTimer) {
clearTimeout(releaseTimer)
releaseTimer = null
}
onRelease?.()
}
notification.on('close', release)
releaseTimer = setTimeout(release, NOTIFICATION_RELEASE_FALLBACK_MS)
if (typeof releaseTimer.unref === 'function') {
releaseTimer.unref()
}
return release
}
export function waitForNotificationDisplay(notification: Notification): Promise<boolean> {
return new Promise((resolve) => {
let settled = false
let timer: ReturnType<typeof setTimeout> | null = null
function cleanup(): void {
notification.removeListener('show', onShow)
notification.removeListener('failed', onFailed)
if (timer) {
clearTimeout(timer)
timer = null
}
}
function settle(displayed: boolean): void {
if (settled) {
return
}
settled = true
cleanup()
resolve(displayed)
}
function onShow(): void {
settle(true)
}
function onFailed(): void {
settle(false)
}
notification.once('show', onShow)
notification.once('failed', onFailed)
timer = setTimeout(() => settle(false), NOTIFICATION_DISPLAY_CONFIRMATION_TIMEOUT_MS)
})
}
export function logNativeNotificationFailure(context: string, error?: string): void {
console.warn(
`[notifications] ${context} notification failed to show${error ? `: ${error}` : '.'}`
)
}