The Scope Class
The Scope class is the foundational primitive for structured concurrency. It manages the lifetime of concurrent operations and ensures proper cleanup.
Creating Scopes
import { scope } from 'go-go-scope' ;
// Basic scope
await using s = scope ();
// Scope with timeout
await using s = scope ({
timeout: 5000 // Auto-cancel after 5 seconds
});
// Scope with name (useful for debugging)
await using s = scope ({
name: 'api-request' ,
timeout: 3000
});
// Scope with concurrency limit
await using s = scope ({
concurrency: 3 // Max 3 tasks running concurrently
});
Location in source: /home/daytona/workspace/source/packages/go-go-scope/src/scope.ts:189-373
Scope Options
Basic Options
Advanced Options
interface ScopeOptions {
// Optional timeout in milliseconds
timeout ?: number ;
// Optional name for debugging
name ?: string ;
// Optional parent AbortSignal
signal ?: AbortSignal ;
// Optional parent scope
parent ?: Scope ;
}
The AbortSignal
Every scope has an AbortSignal that propagates cancellation:
await using s = scope ({ timeout: 1000 });
console . log ( s . signal . aborted ); // false
await new Promise ( resolve => setTimeout ( resolve , 1100 ));
console . log ( s . signal . aborted ); // true (timeout exceeded)
console . log ( s . signal . reason ); // Error: Scope timeout after 1000ms
The signal property is the core mechanism for cancellation propagation. Always pass it to async operations that support cancellation.
The Task Class
The Task class represents a single unit of concurrent work. It’s a lazy, disposable promise that integrates with the scope lifecycle.
Location in source: /home/daytona/workspace/source/packages/go-go-scope/src/task.ts:25-182
Creating Tasks
Tasks are created via the scope.task() method:
await using s = scope ();
// Simple task
const task = s . task ( async ({ signal }) => {
const response = await fetch ( 'https://api.example.com/data' , { signal });
return response . json ();
});
// Task returns Result tuple: [error, value]
const [ err , data ] = await task ;
if ( err ) {
console . error ( 'Task failed:' , err );
} else {
console . log ( 'Data:' , data );
}
Task Context
Every task receives a context object with useful properties:
await using s = scope ();
const task = s . task ( async ( ctx ) => {
// AbortSignal for cancellation
ctx . signal . addEventListener ( 'abort' , () => {
console . log ( 'Task was cancelled!' );
});
// Services from dependency injection
const db = ctx . services . database ;
// Logger with task context
ctx . logger . info ( 'Task started' );
// Additional context
console . log ( ctx . context ); // Custom data from scope
return await someOperation ();
});
Context structure:
interface TaskContext < Services > {
// AbortSignal for cancellation
signal : AbortSignal ;
// Services provided via DI
services : Services ;
// Logger with task name/index
logger : Logger ;
// User-defined context data
context : Record < string , unknown >;
// Checkpoint utilities (if configured)
checkpoint ?: {
save : ( data : unknown ) => Promise < void >;
data ?: unknown ;
};
// Progress tracking (if configured)
progress ?: {
update : ( percentage : number ) => void ;
get : () => { percentage : number };
};
}
Task Lifecycle
Tasks have a specific lifecycle:
await using s = scope ();
const task = s . task ( async ({ signal }) => {
console . log ( 'Task executing' );
await delay ( 100 );
return 'done' ;
});
console . log ( task . isStarted ); // false (lazy execution)
console . log ( task . isSettled ); // false
// Task starts when awaited or .then() is called
const [ err , result ] = await task ;
console . log ( task . isStarted ); // true
console . log ( task . isSettled ); // true
Tasks use lazy execution - they only start when you await them or call .then(). This allows for efficient task composition.
Parent-Child Relationships
Scope Hierarchy
Scopes can create child scopes, forming a tree structure:
await using parent = scope ({ name: 'parent' });
// Create child scopes
const child1 = parent . createChild ({ name: 'child-1' });
const child2 = parent . createChild ({ name: 'child-2' });
// Children inherit parent's signal
parent . signal . addEventListener ( 'abort' , () => {
console . log ( 'Parent aborted' );
});
child1 . signal . addEventListener ( 'abort' , () => {
console . log ( 'Child 1 aborted' );
});
// Aborting parent automatically aborts children
await parent [ Symbol . asyncDispose ]();
// Output:
// Parent aborted
// Child 1 aborted
Hierarchy diagram:
await using root = scope ({ name: 'root' });
const api = root . createChild ({ name: 'api' });
const db = root . createChild ({ name: 'database' });
const cache = api . createChild ({ name: 'cache' });
console . log ( root . debugTree ());
// Output:
// 📦 root (id: 1)
// ├─ 📦 api (id: 2)
// │ └─ 📦 cache (id: 3)
// └─ 📦 database (id: 4)
Signal Inheritance
Child scopes inherit cancellation from parents:
await using parent = scope ({ timeout: 1000 });
const child = parent . createChild ();
child . task ( async ({ signal }) => {
// This signal will abort when parent times out
await longRunningOperation ( signal );
});
// After 1000ms, parent timeout triggers:
// 1. Parent signal aborts
// 2. Child signal aborts (inherited)
// 3. All tasks in both scopes are cancelled
Task Options
Tasks support various options for advanced control:
await using s = scope ();
// Task with retry
const task1 = s . task ( async () => {
return await flakeyOperation ();
}, {
retry: {
maxRetries: 3 ,
delay: 1000 // 1 second between retries
}
});
// Task with timeout
const task2 = s . task ( async () => {
return await slowOperation ();
}, {
timeout: 5000 // Task-specific timeout
});
// Task with deduplication
const task3 = s . task ( async () => {
return await fetch ( '/api/user/1' );
}, {
dedupe: 'user:1' // Share result with other tasks using same key
});
// Task with memoization
const task4 = s . task ( async () => {
return await expensiveComputation ();
}, {
memo: {
key: 'computation:1' ,
ttl: 60000 // Cache result for 1 minute
}
});
Retry Options
Checkpoint Options
interface RetryOptions {
maxRetries ?: number ;
delay ?: number | (( attempt : number , error : unknown ) => number );
retryCondition ?: ( error : unknown ) => boolean ;
onRetry ?: ( error : unknown , attempt : number ) => void ;
}
// Shorthand options
const task = s . task ( async () => {
// ...
}, {
retry: 'exponential' // Built-in exponential backoff
});
Service Injection
Scopes support dependency injection for sharing resources:
// Provide services
await using s = scope ();
s . provide ( 'database' , createDatabase ());
s . provide ( 'cache' , createCache ());
s . provide ( 'logger' , createLogger ());
// Tasks can access services
const task = s . task ( async ({ services }) => {
const user = await services . database . query ( 'SELECT * FROM users WHERE id = ?' , [ 1 ]);
await services . cache . set ( 'user:1' , user );
services . logger . info ( 'User loaded' , { userId: 1 });
return user ;
});
Service disposal:
await using s = scope ();
const db = createDatabase ();
// Provide service with disposal function
s . provide ( 'database' , db , async ( db ) => {
await db . close (); // Called when scope is disposed
});
// When scope exits, database is automatically closed
Real-World Example: API Request Handler
import { scope } from 'go-go-scope' ;
class ApiClient {
async getUserWithPosts ( userId : string ) {
// Create scope for this operation
await using s = scope ({
name: `get-user- ${ userId } ` ,
timeout: 10000 // 10 second timeout for entire operation
});
// Spawn parallel tasks
const userTask = s . task ( async ({ signal }) => {
const res = await fetch ( `/api/users/ ${ userId } ` , { signal });
return res . json ();
}, {
retry: 'exponential' , // Retry with backoff on failure
dedupe: `user: ${ userId } ` // Deduplicate concurrent requests
});
const postsTask = s . task ( async ({ signal }) => {
const res = await fetch ( `/api/users/ ${ userId } /posts` , { signal });
return res . json ();
}, {
retry: 'exponential'
});
// Wait for both tasks
const [ userErr , user ] = await userTask ;
const [ postsErr , posts ] = await postsTask ;
// Handle errors
if ( userErr ) throw new Error ( `Failed to load user: ${ userErr . message } ` );
if ( postsErr ) throw new Error ( `Failed to load posts: ${ postsErr . message } ` );
return { ... user , posts };
// ✅ All tasks automatically cancelled if scope exits early
// ✅ All resources cleaned up in LIFO order
}
}
Always handle task errors properly. Unhandled errors in tasks are returned as part of the Result tuple, not thrown.
Debugging Scopes
Use the built-in debugging utilities:
await using s = scope ({ name: 'root' });
const child1 = s . createChild ({ name: 'api' });
const child2 = s . createChild ({ name: 'db' });
// ASCII tree
console . log ( s . debugTree ());
// 📦 root (id: 1)
// ├─ 📦 api (id: 2)
// └─ 📦 db (id: 3)
// Mermaid diagram
console . log ( s . debugTree ({ format: 'mermaid' }));
// graph TD
// scope_1[📦 root]
// scope_1 --> scope_2[📦 api]
// scope_1 --> scope_3[📦 db]
Next Steps
Automatic Cleanup Learn about disposal patterns and LIFO cleanup
Cancellation Master cancellation propagation with AbortSignal
Parallel Execution Run multiple tasks concurrently
Error Handling Handle errors with Result tuples