diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0a4a1d42 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,373 @@ +# AGENTS.md — Effection agent contract + +This file is the behavioral contract for AI agents working with the Effection +codebase. + +Agents must not invent APIs, must not infer semantics from other ecosystems, and +must ground claims in the public API and repository code. + +If you are unsure whether something exists, consult the API reference: +https://frontside.com/effection/api/ + +## Core invariants (do not violate) + +### Operations vs Promises + +- **Operations** are lazy. They execute only when interpreted (e.g. `yield*`, + `run()`, `Scope.run()`, `spawn()`). +- **Promises** are eager. Creating a promise (or calling an `async` function) + starts work; `await` only observes completion. +- You must not claim that a promise is "inert until awaited". +- You must not use `await` inside a generator function (`function*`). Use + `yield*` with an operation instead (e.g. `yield* until(promise)`). + +### Structured concurrency is scope-owned + +- Scope hierarchy is created automatically by the interpreter; application code + should not manage scopes manually. +- "Lexical" in Effection: scope hierarchy follows the lexical structure of + operation invocation sites (e.g. `yield*`, `spawn`, `Scope.run`), not where + references are stored or later used. +- Work is owned by **Scopes**. +- When a scope exits, all work created in that scope is halted. +- References do not extend lifetimes. Returning a `Task`, `Scope`, `Stream`, or + `AbortSignal` does not keep it alive. + +### Effects do not escape scopes + +- Values may escape scopes. +- Ongoing effects must not escape: tasks, resources, streams/subscriptions, and + context mutations must remain scope-bound. + +## Operations, Futures, Tasks + +### Operation + +- An `Operation` is a recipe for work. It does nothing by itself. +- Operations are typically created by invoking a generator function + (`function*`). + +### Future + +- A `Future` is both: + - an Effection operation (`yield* future`) + - a Promise (`await future`) + +### Task + +- A `Task` is a `Future` representing a concurrently running operation. +- A task does not own lifetime or context; its scope does. + +## Entry points and scope creation + +### `main()` + +- You should prefer `main()` when writing an entire program in Effection. +- Inside `main()`, prefer `yield* exit(status, message?)` for termination; do + not call `process.exit()` / `Deno.exit()` directly (it bypasses orderly + shutdown). + +### `exit()` + +- `exit()` is an operation intended to be used from within `main()` to initiate + shutdown. + +### `run()` + +- You may use `run()` to embed Effection into existing async code. +- `run()` starts execution immediately; awaiting the returned task only observes + completion. + +### `createScope()` + +- You must not use `createScope()` for normal Effection application code. +- You may use `createScope()` only for **integration** between Effection and + non-Effection lifecycle management (frameworks/hosts/embedders). +- You must observe `destroy()` (`await` / `yield*`) to complete teardown. + Calling `destroy()` without observation does not guarantee shutdown + completion. + +### `useScope()` + +- Use `yield* useScope()` to capture the current `Scope` for integration (e.g. + callbacks) and re-enter Effection with `scope.run(() => operation)`. + +## `spawn()` + +**Shape (canonical)** + +```ts +const op = spawn(myOperation); // returns an OPERATION +const task = yield * op; // returns a TASK (Future) and starts it +``` + +**Rules** + +- `spawn()` does not start work by itself. Yielding the spawn operation starts + work. +- A spawned task must not outlive its parent scope. + +## `Task.halt()` + +**Rules** + +- `task.halt()` returns a `Future`. You must observe it (`await` / + `yield*` / `.then()`), or shutdown is not guaranteed to complete. +- `halt()` represents teardown. It can succeed even if the task failed. +- If a task is halted before completion, consuming its value (`yield* task` / + `await task`) fails with `Error("halted")`. + +## Scope vs Task (ownership) + +| Concept | Owns lifetime | Owns context | +| ------- | ------------: | -----------: | +| `Scope` | ✅ | ✅ | +| `Task` | ❌ | ❌ | + +## Context API (strict) + +**Valid APIs** + +- `createContext(name, defaultValue?)` +- `yield* Context.get()` +- `yield* Context.expect()` +- `yield* Context.set(value)` +- `yield* Context.delete()` +- `yield* Context.with(value, operation)` + +**Rules** + +- You must treat context as scope-local. Children inherit from parents; children + may override without mutating ancestors. +- You must not treat context as global mutable state. + +## `race()` + +**Rules** + +- `race()` accepts an array of operations. +- It returns the value of the first operation to complete. +- It halts all losing operations. + +## `all()` + +**Rules** + +- `all()` accepts an array of operations and evaluates them concurrently. +- It returns an array of results in input order. +- If any member errors, `all()` errors and halts the other members. +- If you need "all operations either complete or error" (no fail-fast), wrap + each member to return a railway-style result (e.g. `{ ok: true, value }` / + `{ ok: false, error }`) instead of letting errors escape. + +## `call()` + +**Rules** + +- `call()` invokes a function that returns a value, promise, or operation. +- `call()` does not create a scope boundary and does not delimit concurrency. +- If you need to report failures without throwing (e.g. so other work can + continue), catch errors and return a railway-style result object instead of + letting the error escape. + +## `lift()` + +**Rules** + +- `lift(fn)` returns a function that produces an `Operation` which calls `fn` + when interpreted (`yield*`), not when created. + +## `action()` + +**Rules** + +- Use `action()` to wrap callback-style APIs when you can provide a cleanup + function. +- You must not claim `action()` creates an error or concurrency boundary; it + does not. + +## `until()` + +**Rules** + +- `until(promise)` adapts an already-created `Promise` into an `Operation`. +- Prefer `until(promise)` over `call(() => promise)` when you have a promise—it + is shorter and clearer. +- It does not make the promise cancellable; for cancellable interop, prefer + `useAbortSignal()` with APIs that accept `AbortSignal`. + +## `scoped()` + +**Rules** + +- Use `scoped()` to create a boundary such that effects created inside do not + persist after it returns. +- You must use `scoped()` (not `call()`/`action()`) when you need boundary + semantics. + +## `resource()` + +**Shape (ordering matters)** + +```ts +resource(function* (provide) { + try { + yield* provide(value); + } finally { + cleanup(); + } +}); +``` + +**Rules** + +- Setup happens before `provide()`. +- Cleanup must be in `finally` (or after `provide()` guarded by `finally`) so it + runs on return/error/halt. +- Teardown can be asynchronous. If cleanup needs async work, express it as an + `Operation` and `yield*` it inside `finally` (wait for teardown to finish)—do + not fire-and-forget cleanup. + +## `ensure()` + +**Rules** + +- `ensure(fn)` registers cleanup to run when the current operation shuts down. +- `fn` may return `void` (sync cleanup) or an `Operation` (async cleanup). +- You should wrap sync cleanup bodies in braces so the function returns `void`. + +## `useAbortSignal()` + +**Rules** + +- `useAbortSignal()` is an interop escape hatch for non-Effection APIs that + accept `AbortSignal`. +- The returned signal is bound to the current scope and aborts when that scope + exits (return, error, or halt). +- You should pass the signal to a **leaf** async API call, not thread it through + a nested async stack. +- If the choice is "thread an AbortSignal through a nested async stack" vs + "rewrite in Effection", you should prefer rewriting in Effection. + +**Gotchas** + +- You must not assume AbortController provides structured-concurrency + guarantees. See: + https://frontside.com/blog/2025-08-04-the-heartbreaking-inadequacy-of-abort-controller/ + +## Streams, Subscriptions, Channels, Signals, Queues + +### Stream and Subscription + +- A `Stream` is an operation that yields a `Subscription`. +- A `Subscription` is stateful; values are observed via + `yield* subscription.next()`. + +### `on(target, name)` and `once(target, name)` (EventTarget adapters) + +**Rules** + +- `on()` creates a `Stream` of events from an `EventTarget`; listeners are + removed on scope exit. +- `once()` yields the next matching event as an `Operation` (it is equivalent to + subscribing to `on()` and taking one value). + +### `sleep()`, `interval()`, `suspend()` + +**Rules** + +- `sleep(ms)` is cancellable: if the surrounding scope exits, the timer is + cleared. +- `interval(ms)` is a `Stream` that ticks until the surrounding scope exits + (cleanup clears the interval). +- `suspend()` pauses indefinitely and only resumes when its enclosing scope is + destroyed. + +### `each(stream)` (loop consumption) + +**Rules** + +- You must call `yield* each.next()` exactly once at the end of every loop + iteration. +- You must call `yield* each.next()` even if the iteration ends with `continue`. + +**Gotchas** + +- If you do not call `each.next()`, the loop throws `IterationError` on the next + iteration. + +**Shape (ordering matters)** + +```ts +for (let value of yield * each(stream)) { + // ... + yield * each.next(); +} +``` + +### Channel vs Signal vs Queue + +| Concept | Send from | Send API | Requires subscribers | Buffering | +| --------- | ------------------------------ | ------------------------- | ----------------------- | ------------------------------- | +| `Channel` | inside operations | `send(): Operation` | yes (otherwise dropped) | per-subscriber while subscribed | +| `Signal` | outside operations (callbacks) | `send(): void` | yes (otherwise no-op) | per-subscriber while subscribed | +| `Queue` | anywhere (single consumer) | `add(): void` | no | buffered (single subscription) | + +### `Channel` + +**Rules** + +- Use `createChannel()` to construct a `Channel`. +- Use `Channel` for communication between operations. +- You must `yield* channel.send(...)` / `yield* channel.close(...)`. +- You must assume sends are dropped when there are no active subscribers. + +### `Signal` + +**Rules** + +- Use `createSignal()` to construct a `Signal`. +- Use `Signal` only as a bridge from synchronous callbacks into an Effection + stream. +- You must not use `Signal` for in-operation messaging; use `Channel` instead. +- You must assume `signal.send(...)` is a no-op if nothing is subscribed. + +### `Queue` + +**Rules** + +- Use `createQueue()` to construct a `Queue`. +- You may use `Queue` when you need buffering independent of subscriber timing + (single consumer). +- You must consume via `yield* queue.next()`. + +## `subscribe()` and `stream()` (async iterable adapters) + +**Rules** + +- Use `subscribe(asyncIterator)` to adapt an `AsyncIterator` to an Effection + `Subscription`. +- Use `stream(asyncIterable)` to adapt an `AsyncIterable` to an Effection + `Stream`. +- You must not treat JavaScript async iterables as Effection streams without + wrapping. +- You must not use `for await` inside a generator function. Use `stream()` to + adapt the async iterable, then `each()` to iterate. + +**Shape (async iterable consumption)** + +```ts +for (const item of yield * each(stream(asyncIterable))) { + // ... + yield * each.next(); +} +``` + +## `withResolvers()` + +**Rules** + +- `withResolvers()` creates an `operation` plus synchronous `resolve(value)` / + `reject(error)` functions. +- After resolve/reject, yielding the `operation` always produces the same + outcome; calling resolve/reject again has no effect. diff --git a/deno.json b/deno.json index b38a09a8..79a6226b 100644 --- a/deno.json +++ b/deno.json @@ -15,7 +15,7 @@ "rules": { "exclude": ["prefer-const", "require-yield"] }, "exclude": ["build", "www", "docs", "tasks"] }, - "fmt": { "exclude": ["build", "www", "CODE_OF_CONDUCT.md", "README.md"] }, + "fmt": { "exclude": ["build", "www"] }, "test": { "exclude": ["build"] }, "compilerOptions": { "lib": ["deno.ns", "esnext", "dom", "dom.iterable"] }, "imports": { diff --git a/llms-full.txt b/llms-full.txt deleted file mode 100644 index 70e2344f..00000000 --- a/llms-full.txt +++ /dev/null @@ -1,1065 +0,0 @@ -# Effection - Complete Reference for LLMs - -Structured concurrency and effects for JavaScript. This comprehensive guide provides all essential information for helping users write Effection code. - ---- - -## 1. Core Guarantees - -When we say that Effection is "Structured Concurrency and Effects for JavaScript" we mean three things: - -1. **No operation runs longer than its parent** -2. **Every operation exits fully** -3. **It's just JavaScript** - except for the guarantees derived from (1) and (2), it should feel familiar - -### The Mental Shift - -**Before:** An asynchronous operation will run as long as it needs to -**After:** An asynchronous operation runs only as long as it's needed - -Effection binds asynchrony to scope, just like JavaScript binds memory to scope. When an operation completes, none of its child operations are left around to pollute your runtime. - -### Guaranteed Cleanup (The Await Event Horizon Problem) - -**With async/await - NO GUARANTEE:** -```js -async function main() { - try { - await new Promise((resolve) => setTimeout(resolve, 100000)); - } finally { - // code here is NOT GUARANTEED to run - } -} -``` - -Once an async function begins execution, the code in its `finally{}` blocks may never run. This limitation is called the "Await Event Horizon" and causes common errors like EADDRINUSE. - -**With Effection - GUARANTEED:** -```js -import { main, action } from "effection"; - -await main(function*() { - try { - yield* action(function*(resolve) { setTimeout(resolve, 100000) }); - } finally { - // code here is GUARANTEED to run - } -}); -``` - -Effection uses generator functions (`function*`) instead of `async/await` because async/await cannot model structured concurrency. - ---- - -## 2. Async Rosetta Stone - -Complete translation table: - -| Async/Await | Effection | -|---------------------------|--------------------------| -| `await` | `yield*` | -| `async function` | `function*` | -| `Promise` | `Operation` | -| `new Promise()` | `action()` | -| `Promise.withResolvers()` | `withResolvers()` | -| `for await...of` | `for (yield* each(...))` | -| `AsyncIterable` | `Stream` | -| `AsyncIterator` | `Subscription` | - -### Key Examples - -**Countdown:** -```js -// async/await -async function countdown() { - for (let i = 5; i > 1; i--) { - console.log(`${i}`); - await sleep(1000); - } - console.log('blastoff!'); -} - -// Effection -function* countdown() { - for (let i = 5; i > 1; i--) { - console.log(`${i}`); - yield* sleep(1000); - } - console.log('blastoff!'); -} -``` - -**Integration between async/await and Effection:** -```js -// Call async function from Effection: -yield* call(asyncFunction); - -// Run Effection operation from async context: -await run(operation); -``` - -**Converting between Promise and Operation:** -```js -// Inside an operation, convert promise to operation: -function* myOperation() { - let result = yield* until(promise); // Use until() -} - -// Outside operations (in async context), convert operation to promise: -let promise = run(operation); // Use run() -``` - -**Creating operations with cleanup:** Use `action()` with a cleanup function (see Section 3 for full example). - -**Looping over streams:** -```js -// for await -for await (let item of iterable) { - // process item -} - -// for yield* each -for (let item of yield* each(stream)) { - // process item - yield* each.next(); // MUST call this -} -``` - ---- - -## 3. Operations: Stateless & Lazy - -### Operations Are Recipes, Not Promises - -```js -// Promise starts immediately -async function sayHello() { - console.log("Hello World!"); -} -sayHello(); // Prints "Hello World!" immediately - -// Operation does nothing until run -function* sayHello() { - console.log("Hello World!"); -} -sayHello(); // Prints NOTHING - just creates a recipe -``` - -Operations only execute when passed to `yield*`, `run()`, or `spawn()`. - -**Synchronous execution:** Operations execute synchronously (no event loop tick delay), unlike async/await. This enables immediate cleanup when operations halt. - -### Entry Points - -```js -// main() - for applications (handles errors, ensures shutdown cleanup) -await main(function*() { - console.log("Hello World!"); -}); - -// run() - for integration with async code (returns promise) -let promise = run(function*() { - console.log("Hello World!"); -}); -``` - -### Callback Integration - -For integrating callback-based APIs: -- `withResolvers()` - create operations from callbacks (like `Promise.withResolvers()`) -- `action()` - like `withResolvers()` but with required cleanup function for guaranteed teardown -- `resource()` - long-running operations with setup/teardown via `provide()` - -### Cleanup & Interruption - -Promises cannot be cancelled. Operations can be interrupted, preventing "leaked effects." - -**Promise.race() takes 1000ms (leaked effect):** -```js -async function sleep(ms) { - await new Promise(resolve => setTimeout(resolve, ms)); -} -await Promise.race([sleep(10), sleep(1000)]); -// Takes 1000ms! The setTimeout callback outlives its purpose -``` - -**race() with Operations takes 10ms:** -```js -await main(function*() { - yield* race([sleep(10), sleep(1000)]); - // Takes 10ms - losing operation is cleaned up -}); -``` - -**Implementation showing cleanup pattern:** -```js -function sleep(duration) { - return action((resolve) => { - let timeoutId = setTimeout(resolve, duration); - return () => clearTimeout(timeoutId); // Called on interruption - }); -} -``` - ---- - -## 4. Concurrent Operations with spawn() - -### Problem: Dangling Promises - -With async/await, concurrent operations aren't connected. If one fails, the other continues unnecessarily. - -### Solution: spawn() - -`spawn()` creates a parent-child relationship: - -```js -main(function*() { - let dayUS = yield* spawn(() => fetchWeekDay('est')); - let daySweden = yield* spawn(() => fetchWeekDay('cet')); - console.log(`It is ${yield* dayUS}, in the US and ${yield* daySweden} in Sweden!`); -}); -``` - -Hierarchy: -``` -+-- main - | - +-- fetchWeekDay('est') - | - +-- fetchWeekDay('cet') -``` - -If `fetchWeekDay('cet')` fails: -``` -+-- main [FAILED] - | - +-- fetchWeekDay('est') [HALTED] ← Automatically stopped - | - +-- fetchWeekDay('cet') [FAILED] -``` - -**Key properties:** -- Returns a `Task` (both operation and promise) -- Impossible for child to outlive parent -- Error in child causes parent to fail -- Parent failure halts all children - -**Combinators:** -```js -// all() - wait for all (like Promise.all) -let [dayUS, daySweden] = yield* all([ - fetchWeekDay('est'), - fetchWeekDay('cet') -]); - -// race() - first to complete wins -yield* race([sleep(10), sleep(1000)]); -``` - ---- - -## 5. Scope & Lifecycle - -Every operation runs within a scope that enforces structured concurrency. **No operation may outlive its scope.** - -### Three Outcomes - -Every scope has one of three outcomes: -1. **Return** - operation completes successfully with a value -2. **Error** - operation fails and exits with an exception -3. **Halt** - operation is canceled (parent returned, errored, or halted) - -```js -import { main, sleep, spawn } from "effection"; - -await main(function* () { // <- parent scope - yield* spawn(function* () { // <- child scope - for (let i = 1; i <= 10; i++) { - yield* sleep(1000); - console.log(i); - } - }); - - yield* sleep(5000); - // main completes after 5s, child scope halts - // Only 5 numbers print -}); -``` - -### Cleanup Always Runs - -Halting a task calls `return()` on the generator, ensuring `try/finally` blocks always execute (see Section 1 for details). - -### Integration with Callback-Based Frameworks - -Use `useScope()` to integrate with Express, callbacks, etc.: - -```js -import { main, useScope } from "effection"; -import express from "express"; - -await main(function*() { - let scope = yield* useScope(); - - express().get("/", async (req, res) => { - // Run operation within main scope for each request - return await scope.run(function*() { - let data = yield* fetchData(); - res.json(data); - }); - }); -}); -``` - -### AbortSignal Integration - -Use `useAbortSignal()` for Promise integration with automatic cancellation: - -```js -import { useAbortSignal, main } from "effection"; - -await main(function*() { - let signal = yield* useAbortSignal(); - - // Signal automatically aborts when operation halts - let response = yield* until(fetch('/api', { signal })); -}); -``` - -**Cleanup helper:** `ensure()` provides cleaner syntax than try/finally for guaranteed cleanup. - ---- - -## 6. Long-Running Operations with resource() - -### Problem - -Operations that are: -1. Long running -2. Need interaction while running - -Example: Socket that needs messages sent while open. - -### Solution: resource() with provide() - -```js -export function useSocket(port, host) { - return resource(function* (provide) { - let socket = new Socket(); - socket.connect(port, host); - yield* once(socket, 'connect'); - - try { - yield* provide(socket); // Returns control to caller with socket - } finally { - socket.close(); // Guaranteed cleanup - } - }); -} - -await main(function*() { - let socket = yield* useSocket(1337, '127.0.0.1'); - socket.write('hello'); // Works! - // socket.close() called automatically when main finishes -}); -``` - -**Mantra: Resources _provide_ values** - -**How it works:** -1. Code before `provide()` = setup -2. `provide(value)` = pass control back to caller with value -3. `provide()` remains suspended until resource goes out of scope -4. Code after `provide()` (or in finally) = guaranteed cleanup - -**Composing resources:** -```js -function useHeartSocket(port, host) { - return resource(function* (provide) { - let socket = yield* useSocket(port, host); // Use another resource - - yield* spawn(function*() { - while (true) { - yield* sleep(10_000); - socket.send(JSON.stringify({ type: "heartbeat" })); - } - }); - - yield* provide(socket); - // Cleanup automatic: heartbeat stopped, socket closed - }); -} -``` - ---- - -## 7. Streams and Subscriptions - -### Stream vs Subscription - -- **`Subscription`** = Stateful, active queue of values (like `AsyncIterator`) -- **`Stream`** = Stateless recipe for creating subscriptions (like `AsyncIterable`) - -A Stream can have **multiple subscriptions**, each receiving the same values. - -### Creating Streams with Channel - -```js -await main(function*() { - let channel = createChannel(); - - yield* channel.send('too early'); // Does nothing - no subscribers! - - let subscription1 = yield* channel; - let subscription2 = yield* channel; - - yield* channel.send('hello'); - yield* channel.send('world'); - - console.log(yield* subscription1.next()); //=> { done: false, value: "hello" } - console.log(yield* subscription2.next()); //=> { done: false, value: "hello" } -}); -``` - -**Important:** Sending to a channel with no subscribers does nothing. - -### for yield* each Loop - -```js -await main(function*() { - let channel = createChannel(); - - yield* spawn(function*() { - yield* sleep(1000); - yield* channel.send('hello'); - yield* sleep(1000); - yield* channel.send('world'); - }); - - for (let value of yield* each(channel)) { - console.log('got value:', value); - yield* each.next(); // MUST call this at loop end - } -}); -``` - -**Why spawn()?** Need to send values concurrently with consuming them. Can't send before loop (no subscribers), can't send after (loop never completes). - -### Never Missing Messages - -Subscriptions guarantee no message loss: - -```js -await main(function*() { - let channel = createChannel(); - let subscription = yield* channel; - - yield* spawn(function*() { - yield* sleep(1000); - yield* channel.send('hello'); - yield* channel.send('world'); // Both sent while consumer busy - }); - - let { value: firstValue } = yield* subscription.next(); - console.log(firstValue); // 'hello' - - yield* sleep(1000); // Consumer busy here - - let { value: secondValue } = yield* subscription.next(); - console.log(secondValue); // 'world' - not missed! -}); -``` - ---- - -## 8. Events: on() and once() - -### Single Events with once() - -Wait for a single event from any [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) (DOM elements, WebSockets, etc.): - -```js -import { main, once } from 'effection'; - -await main(function*() { - let socket = new WebSocket('ws://localhost:1234'); - - yield* once(socket, 'open'); - console.log('socket is open!'); - - let closeEvent = yield* once(socket, 'close'); - console.log('closed with code', closeEvent.code); -}); -``` - -### Event Streams with on() - -For recurring events, use `on()` which returns a Stream (avoids missing events between `once()` calls): - -```js -import { main, on, each } from 'effection'; - -await main(function*() { - let socket = new WebSocket('ws://localhost:1234'); - - for (let message of yield* each(on(socket, 'message'))) { - console.log('message:', message.data); - yield* each.next(); - } -}); -``` - -**Note:** `on()` and `once()` work with the DOM `EventTarget` API only. For Node.js EventEmitters, you'll need to wrap them or use a different approach. - -**Type inference:** Both functions infer the correct event type from the target (e.g., `CloseEvent` for `'close'` on WebSocket). - ---- - -## 9. Utility Operations - -### suspend() - Pause Indefinitely - -Pause execution until the scope is destroyed. Useful for "wait forever" patterns: - -```js -import { main, suspend } from "effection"; - -await main(function*() { - try { - console.log('suspending'); - yield* suspend(); // Waits here until scope ends - } finally { - console.log('done!'); // Runs when scope is destroyed - } -}); -``` - -Common use: Keep a resource alive until parent scope ends. - -### ensure() - Guaranteed Cleanup - -Cleaner alternative to `try/finally` for cleanup. Avoids rightward drift: - -```js -import { main, ensure } from 'effection'; -import { createServer } from 'http'; - -await main(function*() { - let server = createServer(...); - yield* ensure(() => { server.close() }); // Runs when scope ends - - // ... rest of your code -}); -``` - -For async cleanup, return an operation: - -```js -yield* ensure(function*() { - server.close(); - yield* once(server, 'close'); // Wait for close to complete -}); -``` - -### interval() - Repeated Timed Stream - -Create an infinite stream that emits at regular intervals: - -```js -import { main, each, interval } from 'effection'; - -await main(function*() { - let startTime = Date.now(); - - for (let _ of yield* each(interval(1000))) { - let elapsed = Date.now() - startTime; - console.log(`elapsed: ${elapsed}ms`); - yield* each.next(); - } -}); -``` - -### createQueue() - Buffered Queue - -Unlike channels (which drop messages with no subscribers), queues buffer items. Items added before a listener exists will still be received: - -```js -import { main, createQueue } from 'effection'; - -await main(function*() { - let queue = createQueue(); - - // Add items BEFORE listening - they're queued - queue.add("first"); - queue.add("second"); - - // Now consume - items are still there - let next = yield* queue.next(); - console.log(next.value); // "first" - - next = yield* queue.next(); - console.log(next.value); // "second" - - queue.close(); // Signal end of queue -}); -``` - -**Queue vs Channel:** -- `createChannel()` - unbuffered, messages dropped if no subscriber -- `createQueue()` - buffered, messages queued until consumed - -### exit() - Exit main() Program - -Immediately halt and exit the program with a status code. Only works inside `main()`: - -```js -import { main, exit } from 'effection'; - -await main(function*() { - if (invalidArgs()) { - yield* exit(1, "invalid arguments"); // Exits with code 1 - } - - // ... normal execution - yield* exit(0); // Success -}); -``` - -**Warning:** Never use `process.exit()` or `Deno.exit()` directly—they skip cleanup. Always use `yield* exit()`. - -### subscribe() / stream() - Convert Async Iterables - -Convert JavaScript `AsyncIterator`/`AsyncIterable` to Effection types: - -```js -import { subscribe, stream } from 'effection'; - -// Convert AsyncIterator to Subscription -let subscription = subscribe(asyncIterator); -let { value } = yield* subscription.next(); - -// Convert AsyncIterable to Stream -let myStream = stream(asyncIterable); -for (let item of yield* each(myStream)) { - console.log(item); - yield* each.next(); -} -``` - ---- - -## 10. Isolating Effects with scoped() - -### Problem - -Sometimes you need to run an operation that spawns tasks or creates resources, but you want all those effects cleaned up immediately when the operation completes - not when the parent scope ends. - -### Solution: scoped() - -`scoped()` encapsulates an operation so that **no effects persist outside of it**. When the scoped operation completes, all concurrent tasks and resources are shut down, and context values are restored. - -```js -import { scoped, spawn, sleep } from "effection"; - -function* myOperation() { - // This task will be cleaned up when scoped() completes - yield* scoped(function*() { - yield* spawn(function*() { - while (true) { - yield* sleep(1000); - console.log("heartbeat"); - } - }); - - yield* sleep(3000); // After 3s, scoped() completes - return "done"; - }); - // Heartbeat task is already stopped here - console.log("continuing without heartbeat"); -} -``` - -### Return Behavior - -`scoped()` returns whatever the inner operation returns: - -```js -let result = yield* scoped(function*() { - return { value: 42 }; -}); -console.log(result); // { value: 42 } -``` - -**When halted:** If the inner operation is halted (cancelled) before it can return a value, `scoped()` returns `{ exists: false }` - a Maybe type indicating no value was produced: - -```js -import { race, scoped, suspend } from "effection"; - -let result = yield* race([ - scoped(function*() { - yield* suspend(); // Never completes - return "never reached"; - }), - sleep(100), // This wins the race -]); -// The scoped operation was halted, result from race is undefined -``` - -### Use Cases - -1. **Temporary resources** - Create resources that clean up immediately, not at end of parent scope -2. **Isolated side effects** - Run operations without polluting the parent scope with spawned tasks -3. **AbortSignal creation** - Get an AbortSignal that aborts when the scoped block exits: - -```js -let signal = yield* scoped(function*() { - return yield* useAbortSignal(); -}); -console.log(signal.aborted); // true - scope already exited -``` - -### Context Isolation - -Context values set inside `scoped()` are restored when the scope exits: - -```js -const MyContext = createContext("my-value"); - -yield* MyContext.set("outer"); -console.log(yield* MyContext.get()); // "outer" - -yield* scoped(function*() { - yield* MyContext.set("inner"); - console.log(yield* MyContext.get()); // "inner" -}); - -console.log(yield* MyContext.get()); // "outer" - restored! -``` - -### Key Difference from spawn() - -- `spawn()` - child runs concurrently, parent continues, child cleaned up when parent scope ends -- `scoped()` - child runs to completion (or halt), effects cleaned up immediately, then parent continues - ---- - -## 11. Context & Dependency Injection - -Context provides ambient values throughout the operation tree without "argument drilling" (passing arguments through many intermediate operations). - -### Three Steps Pattern - -**1. Create** a context reference: -```js -import { createContext } from 'effection'; -const GithubTokenContext = createContext("token"); -``` - -**2. Set** the context value: -```js -yield* GithubTokenContext.set("gha-1234567890"); -``` - -**3. Get** the context value: -```js -// Non-optional - throws if not set -const token = yield* GithubTokenContext.expect(); - -// Optional - returns undefined if not set -const token = yield* GithubTokenContext.get(); -``` - -### Complete Example - -```js -import { createContext, main } from 'effection'; - -const GithubTokenContext = createContext("token"); - -await main(function* () { - yield* GithubTokenContext.set("gha-1234567890"); - yield* fetchGithubData(); -}); - -function* fetchGithubData() { - yield* fetchRepositories(); // doesn't need token parameter -} - -function* fetchRepositories() { - const token = yield* GithubTokenContext.expect(); // throws if not set - console.log(token); // gha-1234567890 -} -``` - -### Context Hierarchy - -- Bound to scope - all children see parent's value -- Children can override without affecting ancestors -- Common uses: config, API clients, services with guaranteed cleanup - ---- - -## 12. Result and Maybe Types - -### Result - Success or Error - -Used for operations that can succeed or fail: - -```js -type Result = - | { ok: true, value: T } // Success - | { ok: false, error: Error } // Failure - -// Create results: -import { Ok, Err } from 'effection'; - -Ok(42) // { ok: true, value: 42 } -Ok() // { ok: true } (for void) -Err(new Error()) // { ok: false, error: Error } -``` - -### Maybe - Value or Nothing - -Used by `scoped()` when an operation is halted before returning: - -```js -type Maybe = - | { exists: true, value: T } // Has value - | { exists: false } // No value (operation was halted) - -// Example: scoped() returns Maybe when halted -let result = yield* scoped(function*() { - yield* suspend(); // Never completes - return "value"; -}); -// result = { exists: false } because operation was halted -``` - -This explains the `{ exists: false }` you may see when a scoped operation is cancelled before it can return. - ---- - -## 13. Advanced APIs - -These APIs are primarily for framework authors and advanced integration scenarios. - -### createScope() - Manual Scope Management - -Create a standalone scope outside of `main()` or `run()`. Returns a tuple of `[scope, destroy]`: - -```js -import { createScope, sleep, suspend } from "effection"; - -let [scope, destroy] = createScope(); - -// Run operations in the scope -scope.run(function*() { - yield* sleep(1000); - console.log("done sleeping"); -}); - -scope.run(function*() { - try { - yield* suspend(); - } finally { - console.log("cleaned up!"); - } -}); - -// Later: destroy the scope (halts all operations, runs cleanup) -await destroy(); // prints "cleaned up!" -``` - -**Use cases:** -- Framework integration (Express, Koa, etc.) -- Manual lifecycle management -- Testing utilities - -### global - Root Scope - -The root of all Effection scopes. All scopes created without a parent derive from `global`: - -```js -import { global } from "effection"; - -// global is a Scope - you can run operations on it -global.run(function*() { - // This runs in the global scope -}); -``` - -**Warning:** Operations in `global` are never automatically cleaned up. Prefer `main()` or `createScope()` for proper lifecycle management. - -### lift() - Convert Function to Operation - -Convert a synchronous function into an operation. Useful for making plain functions composable with `yield*`: - -```js -import { lift } from "effection"; - -// Create an operation-returning version of console.log -let log = lift((message: string) => console.log(message)); - -function* myOperation() { - yield* log("hello world"); // Now works with yield* - yield* log("done"); -} -``` - -**When to use:** -- Wrapping side-effectful functions for use in operations -- Making synchronous code interruptible at yield points -- Creating operation-based APIs from plain functions - -### useScope() - Get Current Scope - -Get the scope of the currently running operation. Useful for spawning operations from callback contexts: - -```js -import { main, useScope } from "effection"; -import express from "express"; - -await main(function*() { - let scope = yield* useScope(); - - express().get("/", async (req, res) => { - // Run operation within the main scope from callback - await scope.run(function*() { - let data = yield* fetchData(); - res.json(data); - }); - }); -}); -``` - ---- - -## 14. Critical Gotchas (Quick Reference) - -1. **Operations are lazy** - Must use `yield*`, `run()`, or `spawn()` to execute (see Section 3) -2. **`each.next()` required** - Always call `yield* each.next()` at end of `for...of` loop (see Section 7) -3. **Subscribers before send** - `channel.send()` does nothing without active subscribers (see Section 7) -4. **Promise ↔ Operation conversion** - Use `until(promise)` inside operations, `run(operation)` outside (see Section 2) -5. **No `await` with operations** - Use `yield*` inside operations, `await run()` in async context - ---- - -## 15. Upgrading from v3 to v4 - -### `call()` Simplified - -v4 `call()` only invokes functions—nothing else. Use new helpers for other cases: - -| v3 Usage | v4 Replacement | -|----------|----------------| -| `yield* call(promise)` | `yield* until(promise)` | -| `yield* call(5)` | `yield* constant(5)` | -| `yield* call(fn)` for boundaries | `yield* scoped(fn)` | - -**Important:** `call()` no longer establishes concurrency/error boundaries. Spawned tasks inside `call()` are NOT terminated when it returns. Use `scoped()` if you need boundary behavior. - -### `action()` Simplified - -v4 `action()` now mirrors `new Promise()` more closely: - -```js -// v3 - generator function with try/finally -function sleep(ms) { - return action(function*(resolve) { - let timeout = setTimeout(resolve, ms); - try { - yield* suspend(); - } finally { - clearTimeout(timeout); - } - }); -} - -// v4 - synchronous function, return cleanup -function sleep(ms) { - return action((resolve, reject) => { - let timeout = setTimeout(resolve, ms); - return () => clearTimeout(timeout); // cleanup function - }); -} -``` - -Like `call()`, `action()` no longer establishes boundaries. Wrap with `scoped()` if needed. - -### Task Execution Priority Change - -**v3:** Child tasks started immediately when spawned -**v4:** Parent tasks always have priority; children wait until parent yields to async operation - -```js -yield* spawn(childTask); -console.log("In v4, child hasn't started yet"); -yield* sleep(0); // NOW child gets to run -``` - -This makes execution more predictable but may affect timing-sensitive code. Add `yield* sleep(0)` to explicitly yield control if children need to start immediately. - ---- - -## Quick Reference Summary - -**Entry points:** -- `main()` - Application entry, handles errors and shutdown -- `run()` - Returns promise for async integration - -**Core operations:** -- `sleep(ms)` - Pause for duration -- `spawn(fn)` - Concurrent child operation -- `suspend()` - Pause indefinitely until scope ends -- `ensure(fn)` - Guaranteed cleanup (cleaner than try/finally) - -**Callback integration:** -- `withResolvers()` - Create operation from callback (like `Promise.withResolvers()`) -- `action(fn)` - Like `withResolvers()` but requires cleanup function -- `resource(fn)` - Long-running with setup/teardown via `provide()` - -**Concurrency:** -- `spawn()` - Run concurrent operation as child -- `race()` - First to complete wins -- `all()` - Wait for all to complete -- `scoped()` - Isolate effects, clean up immediately when block exits - -**Streams:** -- `createChannel()` - Create unbuffered stream with send() method -- `createQueue()` - Create buffered queue (items persist without subscribers) -- `each(stream)` - Loop over stream values (requires `yield* each.next()`) -- `createSignal()` - Stream from external events -- `interval(ms)` - Stream that emits at regular intervals - -**Events (EventTarget only):** -- `once(target, name)` - Wait for single event -- `on(target, name)` - Stream of events - -**Types:** -- `Result` - `{ ok: true, value }` or `{ ok: false, error }` -- `Maybe` - `{ exists: true, value }` or `{ exists: false }` - -**Conversion:** -- `until(promise)` - Promise → Operation (use inside operations) -- `run(operation)` - Operation → Promise (use in async context) -- `call(fn)` - Invoke function as operation (v4: no longer establishes boundaries) -- `constant(value)` - Wrap constant value as operation -- `subscribe(asyncIterator)` - AsyncIterator → Subscription -- `stream(asyncIterable)` - AsyncIterable → Stream - -**Program control (main() only):** -- `exit(code, msg?)` - Exit program with status code (use instead of process.exit) - -**Advanced (framework integration):** -- `createScope()` - Create standalone scope with destroy function -- `global` - Root scope (operations never auto-cleaned) -- `lift(fn)` - Convert sync function to operation -- `useScope()` - Get current scope for callback integration - -**Remember:** -- Use `yield*` not `await` (inside operations) -- Use `function*` not `async function` -- Operations are lazy - only run when yielded -- Cleanup is automatic via structured concurrency -- Parent completion halts all children diff --git a/llms.txt b/llms.txt deleted file mode 100644 index dad68cdc..00000000 --- a/llms.txt +++ /dev/null @@ -1,90 +0,0 @@ -# Effection — Structured Concurrency for JavaScript - -Effection is a JavaScript library for building reliable asynchronous and concurrent programs using -**structured concurrency**. It models async work as a tree of tasks with explicit lifetimes, -deterministic cancellation, and guaranteed cleanup. - -Effection uses **generator functions** instead of async/await to represent long-lived, -cancellable operations that compose naturally and enforce parent/child task relationships. - -This file provides a high-level map of the Effection concepts and documentation. -For detailed explanations, APIs, and examples, see `llms-full.txt`. - ---- - -## Core Concepts - -- **Structured Concurrency** - All concurrent work has a parent. When a parent task exits, all child tasks are cancelled. - Cleanup logic is guaranteed to run. - -- **Tasks** - Generator functions (`function*`) represent units of async work that can be started, paused, - cancelled, and composed. - -- **Cancellation & Cleanup** - Cancellation is built into the execution model. `finally` blocks always run when a task is - cancelled or completes. - -- **Scopes** - Scopes define the lifetime boundary for tasks and resources. - -- **Resources** - Resources model acquire/release lifecycles (e.g. sockets, processes, workers) and ensure cleanup - when a scope exits. - ---- - -## Core APIs (High Level) - -- `run()` / `main()` — run a task to completion -- `spawn()` — start a concurrent child task -- `all()` / `race()` — structured concurrency combinators -- `resource()` / `provide()` — model managed resources -- `scoped()` — isolate effects with immediate cleanup -- `on()` / `once()` — event streams (EventTarget only) -- `useScope()` — access the current task scope -- Utilities: `sleep`, `interval`, `ensure`, `suspend`, `createChannel`, `createQueue` - -> **Gotcha:** When iterating streams with `each()`, always call `yield* each.next()` at the end of the loop body. - ---- - -## Common Use Cases - -- Long-lived servers and daemons -- CLI tools and process orchestration -- WebSocket and streaming workflows -- Worker pools and background jobs -- Tests involving processes, signals, or concurrency -- Reactive state machines with explicit lifetimes - ---- - -## Version Notes - -- Effection v3 established the structured concurrency model -- Effection v4 refines ergonomics, scope handling, and priority semantics -- APIs are designed to work across Node.js, Deno, and browsers - ---- - -## Codebase Map - -For agents with file access: - -- **Source:** `lib/` — core implementation files -- **Docs:** `docs/` — conceptual guides (MDX) -- **Tests:** `test/` — usage examples and edge cases -- **Full LLM reference:** `llms-full.txt` - ---- - -## Detailed Reference - -See **`llms-full.txt`** for: -- Conceptual explanations with examples -- API-level documentation -- Patterns and idioms -- async/await comparisons -- v3 → v4 migration guidance diff --git a/www/assets/llms-full.txt b/www/assets/llms-full.txt deleted file mode 120000 index 4eed9671..00000000 --- a/www/assets/llms-full.txt +++ /dev/null @@ -1 +0,0 @@ -../../llms-full.txt \ No newline at end of file diff --git a/www/assets/llms.txt b/www/assets/llms.txt deleted file mode 120000 index 0ec2a763..00000000 --- a/www/assets/llms.txt +++ /dev/null @@ -1 +0,0 @@ -../../llms.txt \ No newline at end of file diff --git a/www/assets/llms.txt b/www/assets/llms.txt new file mode 100644 index 00000000..91fca11b --- /dev/null +++ b/www/assets/llms.txt @@ -0,0 +1,55 @@ +# Effection — Structured Concurrency for JavaScript + +Effection is a JavaScript library for building reliable asynchronous and +concurrent programs using **structured concurrency**. + +Effection models async work as **lazy operations** with explicit lifetimes, +deterministic cancellation, and guaranteed cleanup. It uses **generator +functions (`function*`)**, not `async` / `await`, to express long-lived, +cancellable work. + +--- + +## ⚠️ IMPORTANT FOR AI AGENTS + +If you will **write, modify, refactor, or debug Effection code**, you **must** +read [AGENTS.md] first. + +**AGENTS.md is the normative behavioral contract.** +- Do not invent APIs +- Do not infer semantics from Promises or other ecosystems +- Do not substitute primitives that “look equivalent” +- If information is missing or uncertain, consult the API reference + +If any other document conflicts with AGENTS.md, **AGENTS.md takes precedence**. + +--- + +## Where to look (routing) + +- **Behavioral rules & invariants (authoritative):** [AGENTS.md] +- **Public API reference (authoritative):** [API] +- **Conceptual guides & explanations (human-oriented):** [Guides] + - [Thinking in Effection] + - [Async Rosetta Stone] + - [Operations] + - [Scope] + - [Resources] + - [Spawn] + - [Collections] + - [Browse all guides][docs/] +- **Extension packages (process, fetch, websockets, WebWorkers):** [EffectionX] + +--- + +[AGENTS.md]: https://raw.githubusercontent.com/thefrontside/effection/v4/AGENTS.md +[API]: https://frontside.com/effection/api/ +[Guides]: https://frontside.com/effection/guides/v4 +[Thinking in Effection]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/thinking-in-effection.mdx +[Async Rosetta Stone]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/async-rosetta-stone.mdx +[Operations]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/operations.mdx +[Scope]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/scope.mdx +[Resources]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/resources.mdx +[Spawn]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/spawn.mdx +[Collections]: https://raw.githubusercontent.com/thefrontside/effection/v4/docs/collections.mdx +[EffectionX]: https://frontside.com/effection/x/ \ No newline at end of file diff --git a/www/components/footer.tsx b/www/components/footer.tsx index f66b38e0..79f22690 100644 --- a/www/components/footer.tsx +++ b/www/components/footer.tsx @@ -33,13 +33,13 @@ export function Footer(): JSX.Element {

- Resources + AI Agent Resources

- LLM Reference + llms.txt - - Full LLM Reference + + AGENTS.md