1interface ThreadDismissal {
2 readonly finished: Promise<void>;
3 readonly restore: () => void;
4}
5
6const rows = new Map<string, Set<() => ThreadDismissal>>();
7
8/** A thread can be visible in Home and the navigation sidebar at once. */
9export function registerThreadDismissal(key: string, dismiss: () => ThreadDismissal) {
10 const registrations = rows.get(key) ?? new Set();
11 registrations.add(dismiss);
12 rows.set(key, registrations);
13 return () => {
14 registrations.delete(dismiss);
15 if (registrations.size === 0) rows.delete(key);
16 };
17}
18
19/** Finish the exit before mutating the list; failed commands put the rows back. */
20export async function withThreadDismissal<T>(
21 key: string,
22 action: () => Promise<T>,
23 succeeded: (result: T) => boolean,
24): Promise<T> {
25 const dismissals = Array.from(rows.get(key) ?? [], (dismiss) => dismiss());
26 let committed = false;
27 try {
28 await Promise.all(dismissals.map(({ finished }) => finished));
29 const result = await action();
30 committed = succeeded(result);
31 return result;
32 } finally {
33 if (!committed) dismissals.forEach(({ restore }) => restore());
34 }
35}