effection icon indicating copy to clipboard operation
effection copied to clipboard

Add AI friendly TL;DR to the docs

Open taras opened this issue 4 months ago • 0 comments

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 main or run for proper execution context
  • Use resources only when return values need to be tied to parent operation lifecycle
  • Always use try/finally blocks around provide in 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* or run()
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

  1. Automatic Cleanup - No leaked timeouts, requests, or resources
  2. Predictable Cancellation - Operations can always be interrupted
  3. Composability - Operations compose like UI components
  4. Familiar Syntax - Uses standard JavaScript constructs
  5. 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.

taras avatar Sep 05 '25 23:09 taras