| 1 | type ListenerSlot<Args extends unknown[]> = { listener?: (...args: Args) => void }; |
| 2 | |
| 3 | function bindListener<Args extends unknown[]>(slot: ListenerSlot<Args>, lifecycle: { disposed: boolean }) { |
| 4 | return (...args: Args) => { if (!lifecycle.disposed) slot.listener?.(...args); }; |
| 5 | } |
| 6 | |
| 7 | /** A disposed subscription is inert even if its source already queued delivery. */ |
| 8 | export function createSubscriptionScope(track: (delta: 1 | -1) => void = () => {}) { |
| 9 | const cleanups = new Set<() => void>(); |
| 10 | const lifecycle = { disposed: false }; |
| 11 | return { |
| 12 | listen<Args extends unknown[]>(register: (listener: (...args: Args) => void) => () => void, |
| 13 | listener: (...args: Args) => void): void { |
| 14 | if (lifecycle.disposed) return; |
| 15 | const slot: ListenerSlot<Args> = { listener }; |
| 16 | let unsubscribe: () => void; |
| 17 | try { unsubscribe = register(bindListener(slot, lifecycle)); } |
| 18 | catch (error) { slot.listener = undefined; throw error; } |
| 19 | track(1); |
| 20 | const cleanup = () => { |
| 21 | slot.listener = undefined; |
| 22 | try { unsubscribe(); } finally { track(-1); } |
| 23 | }; |
| 24 | if (lifecycle.disposed) cleanup(); |
| 25 | else cleanups.add(cleanup); |
| 26 | }, |
| 27 | dispose(): void { |
| 28 | if (lifecycle.disposed) return; |
| 29 | lifecycle.disposed = true; |
| 30 | const errors: unknown[] = []; |
| 31 | for (const cleanup of cleanups) { |
| 32 | try { cleanup(); } catch (error) { errors.push(error); } |
| 33 | } |
| 34 | cleanups.clear(); |
| 35 | if (errors.length) throw errors[0]; |
| 36 | }, |
| 37 | get size() { return cleanups.size; }, |
| 38 | }; |
| 39 | } |
| 40 |