Skip to content

Commit f20f6c7

Browse files
committed
Add DisposeManager class for centralized management of disposers
1 parent b1fe7a3 commit f20f6c7

File tree

2 files changed

+455
-0
lines changed

2 files changed

+455
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
export type Disposer = () => unknown | PromiseLike<unknown>;
2+
3+
export interface DisposeManagerOptions {
4+
/**
5+
* Initial set of disposers to add to the manager.
6+
* Add more disposers later using {@link DisposeManager.add}.
7+
*/
8+
disposers: Array<Disposer>;
9+
}
10+
11+
/**
12+
* Use a DisposeManager to centralize the management of disposers.
13+
* Add one or more disposers to the manager, and use the manager's
14+
* dispose function to invoke them all in order.
15+
*
16+
* Once disposed, trying to dispose again is a no-op.
17+
*/
18+
export class DisposeManager {
19+
private disposed: boolean;
20+
private disposers: Array<Disposer>;
21+
22+
constructor(options?: DisposeManagerOptions) {
23+
this.disposed = false;
24+
this.disposers = [...(options?.disposers ?? [])];
25+
}
26+
27+
public isDisposed(): boolean {
28+
return this.disposed;
29+
}
30+
31+
/**
32+
* Add a callback to be invoked when the manager is disposed.
33+
*/
34+
public add(disposer: Disposer): void {
35+
if (this.disposed || !disposer) {
36+
return;
37+
}
38+
this.disposers.push(disposer);
39+
}
40+
41+
/**
42+
* Add one or more signals that when any abort then disposes the manager.
43+
*/
44+
public disposeOnAbort(signal: AbortSignal): void {
45+
if (this.disposed || !signal) {
46+
return;
47+
}
48+
49+
if (signal.aborted) {
50+
this.dispose();
51+
return;
52+
}
53+
54+
const dispose = () => this.dispose();
55+
56+
signal.addEventListener('abort', dispose, { once: true });
57+
58+
// Add cleanup for the event listener itself to avoid memory leaks
59+
this.add(() => {
60+
signal.removeEventListener('abort', dispose);
61+
});
62+
}
63+
64+
/**
65+
* Invokes each disposer in order.
66+
* If any are async then they are not awaited.
67+
*/
68+
public dispose(): void {
69+
return this.disposeInternal('sync');
70+
}
71+
72+
/**
73+
* Invokes each disposer in order.
74+
* If any are async then they are awaited before calling the next disposer.
75+
*/
76+
public async disposeAsync(): Promise<void> {
77+
return this.disposeInternal('async');
78+
}
79+
80+
private disposeInternal(mode: 'sync'): void;
81+
private disposeInternal(mode: 'async'): Promise<void>;
82+
private disposeInternal(mode: 'sync' | 'async'): void | Promise<void> {
83+
if (this.disposed) {
84+
return mode === 'async' ? Promise.resolve() : undefined;
85+
}
86+
this.disposed = true;
87+
if (mode === 'async') {
88+
return Promise.resolve()
89+
.then(async () => {
90+
for (const disposer of this.disposers) {
91+
await disposer();
92+
}
93+
})
94+
.finally(() => {
95+
this.disposers.length = 0;
96+
});
97+
} else {
98+
try {
99+
for (const disposer of this.disposers) {
100+
disposer();
101+
}
102+
} finally {
103+
this.disposers.length = 0;
104+
}
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)