Effection Documentation
repository·v4·Indexed 21 days ago
https://github.com/thefrontside/effectionA JavaScript library for managing structured concurrency and effects. Effection prevents resource leaks and ensures reliable cancellation across Node.js, Browser, and Deno. It provides a generator-based model where `yield*` replaces `await`, `function*` replaces `async function`, and `Operation` replaces `Promise`. Key features include the `action()` constructor for safe operations with mandatory cleanup functions, `Streams` and `Subscriptions` for asynchronous iteration, and `createSignal` for bridging external events.
What's inside Effection
- Effection is a library for JavaScript that provides structured concurrency and effects management. It is designed to ensure that resources and effects are never leaked and that cancellation is always handled properly, allowing for robust concurrent code that scales while maintaining a standard JavaScript developer experience.
What is Effection Context and when to use it?
v4Effection
Contextis a mechanism for storing ambient values that are needed by operations in your application. It allows a parent operation to make information available to any operation in its subtree—no matter how deep—without having to pass that information explicitly through function arguments (a problem known as "argument drilling") or relying on lexical scope.Common use cases include:
- Configuration: Accessing environment variables or configuration files from any operation.
- Client APIs: Creating a client instance once and sharing it across all child operations.
- Services: Sharing stateful objects like database connections or WebSockets. When combined with the [resource API], context ensures these services are properly disposed of when no longer needed.
What is Structured Concurrency in Effection?
v4Effection brings structured concurrency to JavaScript by ensuring that asynchronous work is bound to the lifetime of the scope that started it. This solves the problem of 'leaked' async work (like orphaned processes, unclosed sockets, or running timers) that continues to execute after a parent function or component has finished or been destroyed.
Effection provides two core guarantees:
- No operation runs longer than its parent: When a parent scope exits, all child operations are automatically halted.
- Every operation exits fully: Cleanup logic (such as
finally {}blocks) is guaranteed to run when an operation is halted, preventing resource leaks.
In Effection, instead of using
async/await, you use generator functions andyield*to manage control flow. This allows the library to intercept the execution and manage the lifecycle of concurrent tasks.Understand Task Execution Priority in v4
v4Effection v4 introduces a change in task scheduling: a parent task always has priority over its children.
In v3, child tasks spawned in the background could start executing immediately, even while the parent was running synchronous code. In v4, a child task will not run until the parent task yields control to a truly asynchronous operation (e.g.,
yield* sleep(1)or similar).Implications:
- Purely synchronous loops in a parent task will block child tasks from starting.
- If your code relies on child tasks starting immediately, you must explicitly yield control.
How to yield control to children: Use an asynchronous operation like
yield* sleep(0)to allow the scheduler to run pending child tasks.function* parent() { yield* spawn(childTask); yield* sleep(0); // Yields control to child immediately // Continue with parent work }Understand Strict Structured Concurrency in Effection
v4Effection implements Strict Structured Concurrency, a refinement of standard structured concurrency.
In standard structured concurrency, a parent task cannot finish until all its child tasks have finished. Effection adds a second guarantee: background tasks are automatically halted and reclaimed as soon as the foreground task completes.
Key Concepts:
- Foreground: Operations that are part of the core algorithm and whose results are required to produce the function's return value (e.g.,
yield* all([...])). The lifetime of the foreground is naturally aligned with the scope. - Background: Tasks that provide side-effects to support the foreground but do not contribute to the return value (e.g., a UI spinner or a timeout timer).
In Effection, you do not need to manually call
.halt()on background tasks. Once the foreground logic reaches itsreturnstatement or finishes its execution, Effection automatically triggers the shutdown of all background tasks spawned within that scope, ensuring no resources are wasted and preventing deadlocks.- Foreground: Operations that are part of the core algorithm and whose results are required to produce the function's return value (e.g.,
Use `yield*` instead of `await`
v4In Effection, use
yield*to pause a computation and resume it when the value represented by the right-hand side (anOperation) becomes available. This is the direct counterpart to JavaScript'sawait.JavaScript:
await promise;Effection:
yield* operation;yield* operation;How structured concurrency works in Effection
v4Effection implements structured concurrency by creating a hierarchy of tasks. When you use
spawn, the new task becomes a child of the current task.Error Propagation & Lifecycle:
- If a child task fails, the error is sent to the parent.
- The parent task fails and automatically
halts all its other children. - No task can outlive its parent, ensuring that resources are cleaned up and no 'dangling' operations remain.
This hierarchy ensures that the lifetime of concurrent operations is clearly defined and tied to the scope in which they were started.
Mapping Async/Await concepts to Effection
v4Effection provides direct equivalents for standard JavaScript
async/awaitandAsync Iterationprotocols. Understanding these mappings helps in transitioning from standard asynchronous code to Effection's generator-based model.Async Effection PromiseOperationawaityield*async functionfunction*AsyncIteratorSubscriptionAsyncIterableStreamfor awaitfor yield* eachHow Operations differ from Promises
v4Unlike Promises, which are stateful and begin executing immediately upon creation, Effection Operations are stateless. An Operation is a description (a recipe) of what should be done, but it does not execute until it is explicitly run. This distinction allows for better control over asynchrony and enables graceful cancellation.
function *sayHello() { console.log("Hello World!"); }; // This does nothing by itself: sayHello();Prevent leaked effects with automatic cleanup
v4A major advantage of Operations over Promises is the ability to be interrupted. When an operation is cancelled or passes out of scope, Effection ensures that any associated side effects (like
setTimeoutcallbacks) are cleaned up. This prevents "leaked effects" that can cause processes to hang.In Effection, the logic to enter an effect is bundled with the logic to exit it (teardown).
import { sleep, race, main } from "effection"; // This will exit immediately after 10ms, cleaning up the 1000ms timer await main(function*() { yield* race([sleep(10), sleep(1000)]); });How cancellation and abort signals work in Effection
v4Effection automates cancellation by embedding disposal logic within every operation. Unlike standard
async/awaitor Promises, you do not need to manually passAbortSignalorAbortControllerthrough your function arguments.When an operation is cancelled (for example, by hitting
CTRL-Cin a Node.js process), Effection automatically triggers the necessary cleanup for in-flight operations. You can access the current cancellation signal within a generator usingyield* useAbortSignal()to wire up external APIs (likefetch) to Effection's cancellation system.import { until, useAbortSignal } from "effection"; export function* fetchWeekDay(timezone) { // Retrieve the signal from the current Effection context let signal = yield* useAbortSignal(); // Pass the signal to standard APIs like fetch let response = yield* until(fetch(`http://worldclockapi.com/api/json/${timezone}/now`, { signal })); let time = yield* until(response.json()); return time.dayOfTheWeek; }Prevent resource leaks with action cleanup functions
v4When using
action(), you can prevent side effects from leaking by returning a cleanup function from the callback. This is particularly useful for operations likesetTimeoutor network requests.In a standard Promise implementation, you would need to manually pass and manage an
AbortSignal. In Effection, the cleanup function is invoked automatically when the operation is no longer needed (e.g., if a parent scope is aborted or a race condition occurs), ensuring resources are released without explicit signal plumbing.export function sleep(duration: number): Operation<void> { return action((resolve) => { let timeoutId = setTimeout(resolve, duration); // This cleanup function ensures the timeout is cleared if the action is aborted return () => clearTimeout(timeoutId); }); }