@@ -17,6 +17,50 @@ import { logFrontend, type LogLevel } from "@/lib/ipc/client"; |
| 17 | 17 | |
| 18 | 18 | let installed = false; |
| 19 | 19 | |
| 20 | +/** Coalesce repeated identical log entries into a single IPC call. |
| 21 | + * The Tauri bridge fires bursts of 10+ "Couldn't find callback id" |
| 22 | + * warnings in a couple of ms whenever a listener is unlistened |
| 23 | + * while events are still in flight — sending each one through |
| 24 | + * `log_frontend` turns into 10 main-thread IPC round-trips. We |
| 25 | + * dedupe over a 500 ms window and emit a summary with the count |
| 26 | + * when it finally flushes. */ |
| 27 | +const COALESCE_WINDOW_MS = 500; |
| 28 | + |
| 29 | +interface PendingLog { |
| 30 | + level: LogLevel; |
| 31 | + source: string; |
| 32 | + message: string; |
| 33 | + stack?: string; |
| 34 | + count: number; |
| 35 | + timer: ReturnType<typeof setTimeout>; |
| 36 | +} |
| 37 | + |
| 38 | +const pendingLogs = new Map<string, PendingLog>(); |
| 39 | + |
| 40 | +function queueLog( |
| 41 | + level: LogLevel, |
| 42 | + source: string, |
| 43 | + message: string, |
| 44 | + stack?: string, |
| 45 | +) { |
| 46 | + const key = `${level}::${source}::${message}`; |
| 47 | + const existing = pendingLogs.get(key); |
| 48 | + if (existing) { |
| 49 | + existing.count += 1; |
| 50 | + return; |
| 51 | + } |
| 52 | + const timer = setTimeout(() => { |
| 53 | + const entry = pendingLogs.get(key); |
| 54 | + pendingLogs.delete(key); |
| 55 | + if (!entry) return; |
| 56 | + const msg = entry.count > 1 |
| 57 | + ? `${entry.message} (×${entry.count})` |
| 58 | + : entry.message; |
| 59 | + void logFrontend(entry.level, entry.source, msg, entry.stack); |
| 60 | + }, COALESCE_WINDOW_MS); |
| 61 | + pendingLogs.set(key, { level, source, message, stack, count: 1, timer }); |
| 62 | +} |
| 63 | + |
| 20 | 64 | export function installDebugBridge(): void { |
| 21 | 65 | if (installed) return; |
| 22 | 66 | installed = true; |
@@ -26,7 +70,7 @@ export function installDebugBridge(): void { |
| 26 | 70 | const err = ev.error as unknown; |
| 27 | 71 | const message = formatMessage(ev.message, err); |
| 28 | 72 | const stack = extractStack(err); |
| 29 | | - void logFrontend("error", "window.onerror", message, stack); |
| 73 | + queueLog("error", "window.onerror", message, stack); |
| 30 | 74 | }); |
| 31 | 75 | |
| 32 | 76 | // Unhandled promise rejections. |
@@ -34,7 +78,7 @@ export function installDebugBridge(): void { |
| 34 | 78 | const reason = ev.reason as unknown; |
| 35 | 79 | const message = formatMessage("unhandledrejection", reason); |
| 36 | 80 | const stack = extractStack(reason); |
| 37 | | - void logFrontend("error", "unhandledrejection", message, stack); |
| 81 | + queueLog("error", "unhandledrejection", message, stack); |
| 38 | 82 | }); |
| 39 | 83 | |
| 40 | 84 | // Shadow console.error / console.warn so anything that goes to |
@@ -52,7 +96,7 @@ function wrapConsole(level: "error" | "warn") { |
| 52 | 96 | try { |
| 53 | 97 | const message = args.map(stringify).join(" "); |
| 54 | 98 | const stack = args.map(extractStack).find((s): s is string => !!s); |
| 55 | | - void logFrontend(mapped, `console.${level}`, message, stack); |
| 99 | + queueLog(mapped, `console.${level}`, message, stack); |
| 56 | 100 | } catch { |
| 57 | 101 | // Never let logging fail noisily. |
| 58 | 102 | } |