refactor(packages): replace disposer with abort controller (#449)

This commit is contained in:
rahim
2026-02-04 15:32:59 +11:00
committed by GitHub
parent 05af832632
commit b75316d23f
10 changed files with 93 additions and 281 deletions
-48
View File
@@ -1,48 +0,0 @@
/**
* A cleanup function that may be sync or async.
*/
export type CleanupFn = () => void | Promise<void>;
/**
* A collector for cleanup functions.
*
* Allows registering multiple cleanup functions and disposing them all at once.
*
* @example
* ```ts
* const disposer = new Disposer();
*
* disposer.add(listen(video, 'play', handlePlay));
* disposer.add(listen(video, 'pause', handlePause));
* disposer.add(animationFrame(render));
*
* // Later, clean up everything at once
* disposer.dispose();
* // or for async cleanups:
* await disposer.disposeAsync();
* ```
*/
export class Disposer {
#cleanups = new Set<CleanupFn>();
get size(): number {
return this.#cleanups.size;
}
add(cleanup: CleanupFn): void {
this.#cleanups.add(cleanup);
}
/** Run all cleanups sync. Use `disposeAsync()` for async cleanups. */
dispose(): void {
for (const cleanup of this.#cleanups) {
cleanup();
}
this.#cleanups.clear();
}
async disposeAsync(): Promise<void> {
await Promise.all([...this.#cleanups].map((cleanup) => cleanup()));
this.#cleanups.clear();
}
}
-1
View File
@@ -1,3 +1,2 @@
export * from './abort';
export * from './disposer';
export * from './event-like';
@@ -1,212 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import { Disposer } from '../disposer';
describe('disposer', () => {
describe('constructor', () => {
it('creates a disposer with size 0', () => {
const disposer = new Disposer();
expect(disposer.size).toBe(0);
});
it('tracks size as cleanups are added', () => {
const disposer = new Disposer();
disposer.add(() => {});
expect(disposer.size).toBe(1);
disposer.add(() => {});
expect(disposer.size).toBe(2);
});
});
describe('add', () => {
it('adds cleanup functions', () => {
const disposer = new Disposer();
const cleanup1 = vi.fn();
const cleanup2 = vi.fn();
disposer.add(cleanup1);
disposer.add(cleanup2);
expect(disposer.size).toBe(2);
});
it('does not call cleanups when adding', () => {
const disposer = new Disposer();
const cleanup = vi.fn();
disposer.add(cleanup);
expect(cleanup).not.toHaveBeenCalled();
});
});
describe('dispose', () => {
it('calls all cleanup functions', () => {
const disposer = new Disposer();
const cleanup1 = vi.fn();
const cleanup2 = vi.fn();
const cleanup3 = vi.fn();
disposer.add(cleanup1);
disposer.add(cleanup2);
disposer.add(cleanup3);
disposer.dispose();
expect(cleanup1).toHaveBeenCalledOnce();
expect(cleanup2).toHaveBeenCalledOnce();
expect(cleanup3).toHaveBeenCalledOnce();
});
it('clears the disposer after dispose', () => {
const disposer = new Disposer();
disposer.add(() => {});
disposer.add(() => {});
expect(disposer.size).toBe(2);
disposer.dispose();
expect(disposer.size).toBe(0);
});
it('can be called multiple times safely', () => {
const disposer = new Disposer();
const cleanup = vi.fn();
disposer.add(cleanup);
disposer.dispose();
disposer.dispose();
expect(cleanup).toHaveBeenCalledOnce();
});
it('allows adding new cleanups after dispose', () => {
const disposer = new Disposer();
const cleanup1 = vi.fn();
const cleanup2 = vi.fn();
disposer.add(cleanup1);
disposer.dispose();
disposer.add(cleanup2);
disposer.dispose();
expect(cleanup1).toHaveBeenCalledOnce();
expect(cleanup2).toHaveBeenCalledOnce();
});
});
describe('disposeAsync', () => {
it('calls all cleanup functions', async () => {
const disposer = new Disposer();
const cleanup1 = vi.fn();
const cleanup2 = vi.fn();
disposer.add(cleanup1);
disposer.add(cleanup2);
await disposer.disposeAsync();
expect(cleanup1).toHaveBeenCalledOnce();
expect(cleanup2).toHaveBeenCalledOnce();
});
it('awaits async cleanup functions', async () => {
const disposer = new Disposer();
const order: string[] = [];
disposer.add(async () => {
await Promise.resolve();
order.push('async1');
});
disposer.add(() => {
order.push('sync');
});
disposer.add(async () => {
await Promise.resolve();
order.push('async2');
});
await disposer.disposeAsync();
expect(order).toContain('async1');
expect(order).toContain('sync');
expect(order).toContain('async2');
});
it('clears the disposer after disposeAsync', async () => {
const disposer = new Disposer();
disposer.add(async () => {});
expect(disposer.size).toBe(1);
await disposer.disposeAsync();
expect(disposer.size).toBe(0);
});
it('handles mixed sync and async cleanups', async () => {
const disposer = new Disposer();
const results: number[] = [];
disposer.add(() => {
results.push(1);
});
disposer.add(async () => {
await new Promise((r) => setTimeout(r, 10));
results.push(2);
});
disposer.add(() => {
results.push(3);
});
await disposer.disposeAsync();
expect(results).toHaveLength(3);
expect(results).toContain(1);
expect(results).toContain(2);
expect(results).toContain(3);
});
});
describe('integration', () => {
it('works with real-world cleanup patterns', () => {
const disposer = new Disposer();
// Simulating event listener cleanup
const listeners = new Map<string, () => void>();
const addEventListener = (type: string, handler: () => void) => {
listeners.set(type, handler);
return () => {
listeners.delete(type);
};
};
disposer.add(addEventListener('click', () => {}));
disposer.add(addEventListener('keydown', () => {}));
expect(listeners.size).toBe(2);
disposer.dispose();
expect(listeners.size).toBe(0);
});
it('works with timer cleanup patterns', () => {
vi.useFakeTimers();
const disposer = new Disposer();
let timerFired = false;
const id = setTimeout(() => {
timerFired = true;
}, 1000);
disposer.add(() => clearTimeout(id));
disposer.dispose();
vi.advanceTimersByTime(2000);
expect(timerFired).toBe(false);
vi.useRealTimers();
});
});
});