fix(toasts): cap items array at 50 to bound runaway error toasts

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
This commit is contained in:
Claude
2026-04-25 09:09:29 +00:00
parent ab3bb9370c
commit 41be27b410

View File

@@ -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);
}