Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .agents/policy-officer.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,15 @@ Keep feedback **concise and actionable**. Lead with violations that need attenti
No violations found.
```

That's it. Do not list compliant items. Do not summarize what was checked. Just "No violations found."

**Guidelines:**
- Only list violations, not compliant items
- One bullet per violation with file path, policy link, and fix
- Keep explanations brief - the policy document has details
- No summary tables or counts - just the actionable items
- No "Advisory Notes" section unless there's something genuinely useful to add
- No "Summary" sections listing what passed
- No explanations of why things are compliant

## Constraints

Expand Down
3 changes: 3 additions & 0 deletions .policies/stateless-streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ This document defines the recommended policy for implementing stateless stream o
| Scenario | Required Approach |
|----------|-------------------|
| Functions returning `Operation<T>` that operate on streams | Return object with `*[Symbol.iterator]()` method |
| Resource/hook functions (prefixed with `use`) | Regular `function*` generator is acceptable |
| Simple utility functions | Regular `function*` generator is acceptable |

**Key distinction:** Calling `drain(stream)` should return an object, not start execution. Execution only begins when you `yield*` the result.

**Exception:** Resource functions like `useScope()`, `useAbortSignal()`, `useEvalScope()` are designed to set up state within a scope and are not meant to be stored and reused. These follow Effection's `use*` convention and may use `function*` directly.

## Examples

### Compliant: Using *[Symbol.iterator] for stream operations
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ packages:
- "node"
- "process"
- "raf"
- "scope-eval"
- "signals"
- "stream-helpers"
- "stream-yaml"
Expand Down
101 changes: 101 additions & 0 deletions scope-eval/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# @effectionx/scope-eval

Evaluate Effection operations in a scope while retaining resources.

---

While `Scope.run` and `Scope.spawn` can evaluate operations in isolated scopes, resources are torn down once operations return. `useEvalScope` allows you to invoke operations in an existing scope, receive the result of evaluations, while retaining resources for the lifecycle of that scope.

## Usage

### useEvalScope

Create a scope that evaluates operations and retains their resources:

```typescript
import { main, createContext } from "effection";
import { useEvalScope } from "@effectionx/scope-eval";

await main(function*() {
const context = createContext<string>("my-context");

const evalScope = yield* useEvalScope();

// Context not set yet
evalScope.scope.get(context); // => undefined

// Evaluate an operation that sets context
yield* evalScope.eval(function*() {
yield* context.set("Hello World!");
});

// Now the context is visible via the scope
evalScope.scope.get(context); // => "Hello World!"
});
```

### Error Handling

Operations are executed safely and return a `Result<T>`:

```typescript
import { main } from "effection";
import { useEvalScope } from "@effectionx/scope-eval";

await main(function*() {
const evalScope = yield* useEvalScope();

const result = yield* evalScope.eval(function*() {
throw new Error("something went wrong");
});

if (result.ok) {
console.log("Success:", result.value);
} else {
console.log("Error:", result.error.message);
}
});
```

### box / unbox

Utilities for capturing operation results as values:

```typescript
import { main } from "effection";
import { box, unbox } from "@effectionx/scope-eval";

await main(function*() {
// Capture success or error as a Result
const result = yield* box(function*() {
return 42;
});

// Extract value (throws if error)
const value = unbox(result); // => 42
});
```

## API

### `useEvalScope(): Operation<EvalScope>`

Creates an isolated scope for evaluating operations.

Returns an `EvalScope` with:
- `scope: Scope` - The underlying Effection scope for inspecting context
- `eval<T>(op: () => Operation<T>): Operation<Result<T>>` - Evaluate an operation

### `box<T>(content: () => Operation<T>): Operation<Result<T>>`

Execute an operation and capture its result (success or error) as a `Result<T>`.

### `unbox<T>(result: Result<T>): T`

Extract the value from a `Result<T>`, throwing if it's an error.

## Use Cases

- **Testing**: Evaluate operations and inspect context/state without teardown
- **Resource retention**: Keep resources alive across multiple evaluations
- **Error boundaries**: Safely execute operations that might fail
42 changes: 42 additions & 0 deletions scope-eval/box.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it } from "@effectionx/bdd";
import { Err, Ok } from "effection";
import { expect } from "expect";
import { box, unbox } from "./mod.ts";

describe("box", () => {
it("returns Ok for successful operations", function* () {
const result = yield* box(function* () {
return 42;
});

expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value).toBe(42);
}
});

it("returns Err for failed operations", function* () {
const result = yield* box(function* () {
throw new Error("test error");
});

expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toBe("test error");
}
});
});

describe("unbox", () => {
it("extracts value from Ok result", function* () {
const result = Ok("hello");

expect(unbox(result)).toBe("hello");
});

it("throws error from Err result", function* () {
const result = Err(new Error("should throw"));

expect(() => unbox(result)).toThrow("should throw");
});
});
57 changes: 57 additions & 0 deletions scope-eval/box.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Err, Ok, type Operation, type Result } from "effection";

/**
* Execute an operation and capture its result (success or error) as a `Result<T>`.
*
* This is useful when you want to handle errors as values rather than exceptions.
*
* @param content - A function returning the operation to execute
* @returns An operation that yields `Ok(value)` on success or `Err(error)` on failure
*
* @example
* ```ts
* const result = yield* box(function*() {
* return yield* someOperation();
* });
*
* if (result.ok) {
* console.log("Success:", result.value);
* } else {
* console.log("Error:", result.error);
* }
* ```
*/
export function box<T>(content: () => Operation<T>): Operation<Result<T>> {
return {
*[Symbol.iterator]() {
try {
return Ok(yield* content());
} catch (error) {
return Err(error as Error);
}
},
};
}

/**
* Extract the value from a `Result<T>`, throwing if it's an error.
*
* @param result - The result to unbox
* @returns The success value
* @throws The error if the result is an `Err`
*
* @example
* ```ts
* const result = yield* box(function*() {
* return "hello";
* });
*
* const value = unbox(result); // "hello"
* ```
*/
export function unbox<T>(result: Result<T>): T {
if (result.ok) {
return result.value;
}
throw result.error;
}
75 changes: 75 additions & 0 deletions scope-eval/eval-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, it } from "@effectionx/bdd";
import { createContext } from "effection";
import { expect } from "expect";
import { unbox, useEvalScope } from "./mod.ts";

describe("useEvalScope", () => {
it("can evaluate operations in a separate scope", function* () {
const context = createContext<string>("test-context");

const evalScope = yield* useEvalScope();

// Context not set yet
expect(evalScope.scope.get(context)).toBeUndefined();

// Evaluate an operation that sets context
const result = yield* evalScope.eval(function* () {
yield* context.set("Hello World!");
return "done";
});

// Context is now visible via scope
expect(evalScope.scope.get(context)).toBe("Hello World!");
expect(unbox(result)).toBe("done");
});

it("captures errors as Result.Err", function* () {
const evalScope = yield* useEvalScope();

const result = yield* evalScope.eval(function* () {
throw new Error("boom");
});

expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toBe("boom");
}
});

it("can evaluate multiple operations in sequence", function* () {
const counter = createContext<number>("counter");

const evalScope = yield* useEvalScope();

yield* evalScope.eval(function* () {
yield* counter.set(1);
});

yield* evalScope.eval(function* () {
const current = evalScope.scope.get(counter) ?? 0;
yield* counter.set(current + 1);
});

expect(evalScope.scope.get(counter)).toBe(2);
});

it("child scope can see parent context but setting creates own value", function* () {
const context = createContext<string>("inherited");

// Set context in parent scope
yield* context.set("parent value");

const evalScope = yield* useEvalScope();

// Child scope CAN see parent's context (Effection context inheritance)
expect(evalScope.scope.get(context)).toBe("parent value");

// Set in child scope - this creates a new value in the child
yield* evalScope.eval(function* () {
yield* context.set("child value");
});

// Child now has its own value
expect(evalScope.scope.get(context)).toBe("child value");
});
});
Loading
Loading