From 41be27b4107a11c8b2d2d8e8b426285da4542bc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 09:09:29 +0000 Subject: [PATCH] fix(toasts): cap items array at 50 to bound runaway error toasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Error toasts are sticky (duration: 0) so a misbehaving command that fires errors in a loop — a backend that flaps, a polling effect over a broken endpoint — accumulates toast items in the in-memory store indefinitely. The audit found no other unbounded $state arrays in the frontend stores (history caps at 500, recentNudges prunes by time, tasks/taskLists replace-on-load), but `items.push(toast)` had no upper bound. Add MAX_TOASTS = 50 with FIFO eviction. Doesn't change behaviour for realistic toast volumes; only kicks in if something is genuinely wrong. https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb --- src/lib/stores/toasts.svelte.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/stores/toasts.svelte.ts b/src/lib/stores/toasts.svelte.ts index 0d91b85..7f1ff88 100644 --- a/src/lib/stores/toasts.svelte.ts +++ b/src/lib/stores/toasts.svelte.ts @@ -18,6 +18,12 @@ import { errorMessage } from "$lib/utils/errors.js"; let nextId = 1; +// Soft cap on how many toasts can be queued at once. Error toasts are sticky +// (duration: 0) so a misbehaving command that fires errors in a loop can pile +// up indefinitely without this cap. When exceeded, the oldest toast is +// evicted to make room. +const MAX_TOASTS = 50; + function defaultDuration(severity: ToastSeverity): number { switch (severity) { case "success": return 3000; @@ -38,6 +44,9 @@ function createToastsStore() { const duration = defaultDuration(severity); const toast = { id, severity, title, body: body ?? '', duration }; items.push(toast); + if (items.length > MAX_TOASTS) { + items.splice(0, items.length - MAX_TOASTS); + } if (duration > 0) { setTimeout(() => dismiss(id), duration); }