Effection Documentation

repository·v4·Indexed 21 days ago

https://github.com/thefrontside/effection

A 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.

Tokens
31.2K
Snippets
96
Records
119
Agent score
73%

What's inside Effection

  1. What is Effection?

    v4
    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.
  2. What is Effection Context and when to use it?

    v4

    Effection Context is 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.
  3. What is Structured Concurrency in Effection?

    v4

    Effection 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:

    1. No operation runs longer than its parent: When a parent scope exits, all child operations are automatically halted.
    2. 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 and yield* to manage control flow. This allows the library to intercept the execution and manage the lifecycle of concurrent tasks.

  4. Understand Task Execution Priority in v4

    v4

    Effection 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
    }
  5. Understand Strict Structured Concurrency in Effection

    v4

    Effection 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 its return statement 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.

  6. How structured concurrency works in Effection

    v4

    Effection 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:

    1. If a child task fails, the error is sent to the parent.
    2. The parent task fails and automatically halts all its other children.
    3. 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.

  7. Mapping Async/Await concepts to Effection

    v4

    Effection provides direct equivalents for standard JavaScript async/await and Async Iteration protocols. Understanding these mappings helps in transitioning from standard asynchronous code to Effection's generator-based model.

    AsyncEffection
    PromiseOperation
    awaityield*
    async functionfunction*
    AsyncIteratorSubscription
    AsyncIterableStream
    for awaitfor yield* each
  8. How Operations differ from Promises

    v4

    Unlike 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();
  9. Prevent leaked effects with automatic cleanup

    v4

    A 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 setTimeout callbacks) 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)]);
    });
  10. How cancellation and abort signals work in Effection

    v4

    Effection automates cancellation by embedding disposal logic within every operation. Unlike standard async/await or Promises, you do not need to manually pass AbortSignal or AbortController through your function arguments.

    When an operation is cancelled (for example, by hitting CTRL-C in a Node.js process), Effection automatically triggers the necessary cleanup for in-flight operations. You can access the current cancellation signal within a generator using yield* useAbortSignal() to wire up external APIs (like fetch) 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;
    }
  11. Prevent resource leaks with action cleanup functions

    v4

    When using action(), you can prevent side effects from leaking by returning a cleanup function from the callback. This is particularly useful for operations like setTimeout or 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);
      });
    }