go-go-scope leverages TypeScript’s Explicit Resource Management proposal (ES2022) to provide automatic cleanup using the using and await using keywords.
import { Task } from 'go-go-scope';// Task implements Disposablefunction processData() { using task = scope().task(async () => { return await fetchData(); }); // Task is NOT started yet (lazy execution) // When this block exits, task[Symbol.dispose]() is automatically called}
For asynchronous cleanup (most common with scopes), use await using:
import { scope } from 'go-go-scope';// ✅ Recommended patternasync function handleRequest() { await using s = scope(); const task = s.task(async ({ signal }) => { return await fetch('/api/data', { signal }); }); const [err, data] = await task; // When this block exits, s[Symbol.asyncDispose]() is called // This cancels all tasks and cleans up all resources}
The await using keyword ensures that cleanup happens even if an exception is thrown, similar to a try...finally block.
The Scope class implements Symbol.asyncDispose for automatic cleanup:Source location:/home/daytona/workspace/source/packages/go-go-scope/src/scope.ts:1830-1917
class Scope implements AsyncDisposable { async [Symbol.asyncDispose](): Promise<void> { // 1. Mark as disposed this.disposed = true; // 2. Abort all tasks this.abortController.abort(new Error('Scope disposed')); // 3. Wait for active tasks to settle await Promise.allSettled(this.activeTasks); // 4. Dispose resources in LIFO order (reverse of creation) for (let i = this.disposables.length - 1; i >= 0; i--) { const disposable = this.disposables[i]; if (Symbol.asyncDispose in disposable) { await disposable[Symbol.asyncDispose](); } else if (Symbol.dispose in disposable) { disposable[Symbol.dispose](); } } // 5. Dispose child scopes in LIFO order for (let i = this.childScopes.length - 1; i >= 0; i--) { await this.childScopes[i][Symbol.asyncDispose](); } }}
await using s = scope();// Resources created in order: A, B, Cs.onDispose(() => console.log('Cleanup A'));s.onDispose(() => console.log('Cleanup B'));s.onDispose(() => console.log('Cleanup C'));// When scope exits, disposal happens in reverse order:// Cleanup C// Cleanup B// Cleanup A
Visual representation:
Creation Order: A → B → CDisposal Order: C → B → A (LIFO)
Register any object implementing Disposable or AsyncDisposable:
await using s = scope();const channel = new Channel();s.registerDisposable(channel); // Automatically disposed with scopeconst semaphore = new Semaphore(5);s.registerDisposable(semaphore); // Automatically disposed with scope
Provide cleanup functions when registering services:
await using s = scope();const db = await createDatabase();// Provide service with disposal functions.provide('database', db, async (db) => { console.log('Closing database...'); await db.close();});// When scope is disposed, database is automatically closed
Many go-go-scope primitives auto-register for cleanup:
await using s = scope();// These are automatically cleaned up when scope exits:const ch = s.channel(); // Channel closedconst bc = s.broadcast(); // Broadcast channel closed const sem = s.semaphore(5); // Semaphore releasedconst pool = s.pool({ ... }); // Pool drained and closed
await using s = scope();s.onDispose(() => console.log('Cleanup always runs'));throw new Error('Something went wrong');// Output: "Cleanup always runs"// Cleanup executes before error propagates
await using s = scope();const task = s.task(async ({ signal }) => { signal.addEventListener('abort', () => { console.log('Task cancelled'); }); await infiniteLoop();});// When scope exits:// 1. All tasks receive abort signal ("Task cancelled")// 2. Scope waits for tasks to settle// 3. Resources are disposed in LIFO order