1
9
10export type ToastKind = "info" | "success" | "warning" | "error";
11
12export interface Toast {
13 id: string;
14 kind: ToastKind;
15 message: string;
16
17 expiresAt: number;
18}
19
20export interface ToastClock {
21 now(): number;
22 schedule(fn: () => void, delayMs: number): () => void;
23}
24
25export interface ToastStoreOptions {
26
27 ttl?: number;
28 clock?: ToastClock;
29}
30
31let nextId = 0;
32
33
42export class ToastStore {
43 readonly defaultTtl: number;
44 private readonly clock: ToastClock;
45 private readonly toasts: Toast[] = [];
46 private readonly timers = new Map<string, () => void>();
47 private readonly subscribers = new Set<(items: readonly Toast[]) => void>();
48
49 constructor(options: ToastStoreOptions = {}) {
50 this.defaultTtl = options.ttl ?? 5000;
51 this.clock = options.clock ?? {
52 now: () => 0,
53 schedule: (fn) => {
54 fn();
55 return () => {};
56 },
57 };
58 }
59
60
61 items(): readonly Toast[] {
62 const nowMs = this.clock.now();
63 for (let i = this.toasts.length - 1; i >= 0; i -= 1) {
64 const t = this.toasts[i];
65 if (t !== undefined && t.expiresAt > 0 && t.expiresAt <= nowMs) {
66 this.toasts.splice(i, 1);
67 this.timers.delete(t.id);
68 }
69 }
70 return this.toasts;
71 }
72
73
74 push(kind: ToastKind, message: string, ttl?: number): Toast {
75 const life = ttl ?? this.defaultTtl;
76 const id = `toast-${nextId++}`;
77 const expiresAt = life > 0 ? this.clock.now() + life : 0;
78 const toast: Toast = { id, kind, message, expiresAt };
79 this.toasts.push(toast);
80 if (life > 0) {
81 this.timers.set(
82 id,
83 this.clock.schedule(() => this.dismiss(id), life),
84 );
85 }
86 this.notify();
87 return toast;
88 }
89
90
91 dismiss(id: string): boolean {
92 const idx = this.toasts.findIndex((t) => t.id === id);
93 if (idx < 0) return false;
94 this.toasts.splice(idx, 1);
95 const cancel = this.timers.get(id);
96 this.timers.delete(id);
97 cancel?.();
98 this.notify();
99 return true;
100 }
101
102
103 subscribe(cb: (items: readonly Toast[]) => void): () => void {
104 this.subscribers.add(cb);
105 return () => {
106 this.subscribers.delete(cb);
107 };
108 }
109
110 private notify(): void {
111 const snapshot = this.items();
112 for (const cb of this.subscribers) cb(snapshot);
113 }
114}
115