-
Notifications
You must be signed in to change notification settings - Fork 35
Description
This TL;DR was generated (by Claude Code) from the complete Effection documentation (22,242 tokens) with the following requirements:
- Keep under 5,000 tokens for conciseness
- Wrap all code examples in
mainorrunfor proper execution context - Use resources only when return values need to be tied to parent operation lifecycle
- Always use try/finally blocks around
providein resource examples - Order content in learning sequence (getting started β core concepts β advanced patterns)
- Simplify resource pool example to avoid explicit release management
The original documentation spans 15 MDX files covering installation, TypeScript usage, core concepts, operations, spawning, resources, collections/streams, events, error handling, context, actions, scope management, and process handling.
Effection TL;DR
Structured Concurrency and Effects for JavaScript - A library that makes async JavaScript code more predictable, composable, and cancellable.
Getting Started
Installation
# Node.js/Browser
npm install effection
# Deno
import { main } from "jsr:@effection/effection@3";Your First Program
import { main, sleep } from 'effection';
await main(function*() {
console.log('Hello');
yield* sleep(1000);
console.log('World!');
});Core Concepts
1. Operations vs Promises
- Operations are stateless recipes that describe what to do
- Promises are stateful and execute immediately
- Operations only run when evaluated with
yield*orrun()
import { run, until } from 'effection';
// Promise - runs immediately
const promise = fetch('/api');
// Operation - only runs when evaluated
function* fetchData() {
return yield* until(fetch('/api'));
}
await run(fetchData);2. The Rosetta Stone
| Async/Await | Effection |
|---|---|
await |
yield* |
async function |
function* |
Promise |
Operation |
new Promise() |
action() |
for await |
for yield* each |
AsyncIterable |
Stream |
3. Structured Concurrency
- No operation runs longer than its parent
- Every operation exits fully (guaranteed cleanup)
- Child operations are automatically cancelled when parent completes
import { main, spawn, sleep } from 'effection';
await main(function*() {
yield* spawn(function*() {
// This will be cancelled after 5 seconds
for (let i = 0; i < 100; i++) {
yield* sleep(1000);
console.log(i);
}
});
yield* sleep(5000); // Parent exits after 5s
});Basic Operations
Entry Points
import { main, run } from 'effection';
// For applications
await main(function*() {
// Your code here
});
// For integration with existing async code
await run(function*() {
// Your code here
});Fundamental Operations
import { main, sleep, suspend, action } from 'effection';
await main(function*() {
// Wait for duration
yield* sleep(1000);
// Wait forever (until cancelled)
yield* suspend();
// Create custom operation
function myOperation() {
return action(function*(resolve) {
let timeoutId = setTimeout(resolve, 1000);
return () => clearTimeout(timeoutId); // cleanup
});
}
yield* myOperation();
});Concurrency
Spawning Operations
import { main, spawn, all, race } from 'effection';
await main(function*() {
// Spawn concurrent operation
let task = yield* spawn(longRunningOperation);
let result = yield* task;
// Wait for all to complete
let [a, b, c] = yield* all([op1(), op2(), op3()]);
// Wait for first to complete
let winner = yield* race([op1(), op2(), op3()]);
});Error Handling
import { main, call, spawn, suspend } from 'effection';
await main(function*() {
try {
yield* riskyOperation();
} catch (error) {
console.log('Caught:', error);
}
// Error boundaries for background tasks
try {
yield* call(function*() {
yield* spawn(riskyBackgroundTask); // Errors bubble up
yield* suspend();
});
} catch (error) {
console.log('Background task failed:', error);
}
});Events
import { main, on, once, each } from 'effection';
await main(function*() {
// Single event
let event = yield* once(button, 'click');
// Stream of events
for (let click of yield* each(on(button, 'click'))) {
console.log('clicked!');
yield* each.next();
}
});Streams & Channels
import { main, createChannel, each, spawn } from 'effection';
await main(function*() {
// Channels for custom streams
let channel = createChannel();
yield* spawn(function*() {
yield* channel.send('message 1');
yield* channel.send('message 2');
});
for (let msg of yield* each(channel)) {
console.log(msg);
yield* each.next();
}
});Resources
Use resources when the return value needs to be tied to the lifecycle of the parent operation:
import { main, resource, once } from 'effection';
function useSocket(url) {
return resource(function*(provide) {
let socket = new WebSocket(url);
// Setup
yield* once(socket, 'open');
try {
yield* provide(socket); // Give socket to caller
} finally {
socket.close(); // Cleanup guaranteed
}
});
}
// Usage
await main(function*() {
let socket = yield* useSocket('ws://localhost');
socket.send('hello');
// Socket automatically closed when scope exits
});Context
Share values across operation tree without passing parameters:
import { main, createContext } from 'effection';
const DatabaseContext = createContext('db');
await main(function*() {
yield* DatabaseContext.set(database);
yield* processUsers();
});
function* processUsers() {
let db = yield* DatabaseContext.expect();
// Use db here
}Common Patterns
Timeout Any Operation
import { main, race, sleep } from 'effection';
function withTimeout(operation, ms) {
return race([
operation,
function*() {
yield* sleep(ms);
throw new Error('Timeout');
}
]);
}
await main(function*() {
yield* withTimeout(slowOperation(), 5000);
});Retry with Backoff
import { main, sleep } from 'effection';
function* retry(operation, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return yield* operation();
} catch (error) {
if (attempt === maxAttempts) throw error;
yield* sleep(1000 * attempt); // Exponential backoff
}
}
}
await main(function*() {
let result = yield* retry(unreliableOperation);
});Resource Pool
import { main, resource, sleep } from 'effection';
function usePool(createResource, size = 5) {
return resource(function*(provide) {
let pool = [];
let inUse = new Set();
// Pre-create resources
for (let i = 0; i < size; i++) {
pool.push(yield* createResource());
}
try {
yield* provide({
*acquire() {
while (pool.length === 0) {
yield* sleep(10);
}
let resource = pool.pop();
inUse.add(resource);
return resource;
}
});
} finally {
// Clean up all resources
for (let resource of [...pool, ...inUse]) {
resource.cleanup();
}
}
});
}
await main(function*() {
let pool = yield* usePool(createDatabaseConnection, 10);
let conn = yield* pool.acquire();
// Use connection - it's automatically cleaned up when scope exits
});Key Benefits
- Automatic Cleanup - No leaked timeouts, requests, or resources
- Predictable Cancellation - Operations can always be interrupted
- Composability - Operations compose like UI components
- Familiar Syntax - Uses standard JavaScript constructs
- Type Safety - Full TypeScript support
That's Effection in a nutshell! It brings structured concurrency to JavaScript, making async code more reliable and easier to reason about.