Skip to content

feat: add @effectionx/durable-effects package#180

Open
taras wants to merge 53 commits intofeat/durable-streamsfrom
feat/durable-effects
Open

feat: add @effectionx/durable-effects package#180
taras wants to merge 53 commits intofeat/durable-streamsfrom
feat/durable-effects

Conversation

@taras
Copy link
Copy Markdown
Member

@taras taras commented Mar 7, 2026

Motivation

durable-streams gives you durableCall — a generic "call this function and journal the result" primitive. But real workflows need structured effects with typed results, content hashing for staleness detection, and replay guards that know what changed. Writing yield* durableCall("read-file", () => fs.readFile(path)) works but loses the structure: there's no content hash in the result for replay guards to compare, no path in the description for guards to inspect, and no typed ReadFileResult for the caller.

This package provides effects where the description and result shapes are designed together with their corresponding replay guards. durableReadFile stores the path in the description and a content hash in the result. useFileContentGuard knows to read that path, hash the current file, and compare against that stored hash. The effect and guard are two halves of the same design.

Approach

New package providing six durable effects and three replay guards for Effection workflows. This is the "leaf effects" layer that sits on top of @effectionx/durable-streams — where durable-streams provides the protocol (journal, replay index, divergence detection, durableRun, createDurableOperation), this package provides the concrete operations that workflows actually use.

DurableRuntime — the platform abstraction

Effects don't call Node or Deno APIs directly. A DurableRuntime interface provides exec(), readTextFile(), glob(), fetch(), env(), and platform() — all returning Operation<T>, not Promise<T>. Cancellation flows through Effection's structured concurrency. No AbortSignal crosses the interface.

nodeRuntime() is the concrete implementation using @effectionx/process, @effectionx/fs, and @effectionx/fetch. stubRuntime() is the test double — every I/O method throws by default, proving that replay tests never hit live execution.

The runtime is installed as an Effection context before durableRun:

yield* DurableRuntimeCtx.set(nodeRuntime());
yield* durableRun(workflow, { stream });

Effects

Effect Type field What it does
durableExec "exec" Run a subprocess, capture exit code / stdout / stderr
durableReadFile "read_file" Read a file, include content hash in result
durableGlob "glob" Scan directories, sorted matches with per-file hashes and composite scan hash
durableFetch "fetch" HTTP request, filtered response headers, body hash in result
durableEval "eval" Run caller-provided evaluator, source + bindings hashes in result
durableResolve "resolve" Capture non-deterministic values (time, UUID, random, env, platform)

All use createDurableOperation from durable-streams. All are fully replay-safe — during replay, the stored result is returned without executing anything.

Replay Guards

Guard Works with What it detects
useFileContentGuard durableReadFile File content changed since journal was recorded
useGlobContentGuard durableGlob Files added, removed, or modified in scanned directory
useCodeFreshnessGuard durableEval Source code or bindings changed for an eval cell

Guards hook into the existing ReplayGuard middleware from durable-streams. During the check phase (before replay), they gather current state through the runtime. During the decide phase (during replay), they compare against recorded hashes. Mismatch → StaleInputError.

Shared utilities

computeSHA256 returns Operation<string> using the Web Crypto API. Used by effects (to hash content into results) and guards (to hash current state for comparison). Returns "sha256:<hex>" format.

Testing

Tests use @effectionx/bdd (describe/it) and stubRuntime() for isolation. Each effect has golden-run, full-replay, and error-propagation tests. Guards have unchanged/changed/non-applicable tests. nodeRuntime has integration tests against real Node APIs.

hash.test.ts           — computeSHA256 correctness
operations.test.ts     — all 6 effects
guards.test.ts         — all 3 guards  
node-runtime.test.ts   — nodeRuntime() integration

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e42b19ee-f47a-4b52-8426-ca2cb91d9012

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new @effectionx/durable-effects package: durable runtime abstraction and Node runtime, six durable operations, three replay guards, hashing utility, tests, documentation, and workspace/package configuration for integration.

Changes

Cohort / File(s) Summary
Runtime Abstractions
durable-effects/runtime.ts, durable-effects/node-runtime.ts, durable-effects/stub-runtime.ts
Adds DurableRuntime interface and DurableRuntimeCtx; implements nodeRuntime() (Node I/O) and stubRuntime() for tests.
Durable Operations
durable-effects/operations.ts
Implements durableExec, durableReadFile, durableGlob, durableFetch, durableEval, durableResolve plus helpers (durableNow, durableUUID, durableEnv); computes hashes and records events via durable-streams.
Replay Guards & Hashing
durable-effects/guards.ts, durable-effects/hash.ts
Adds useFileContentGuard, useGlobContentGuard, useCodeFreshnessGuard with caching and StaleInputError detection; implements computeSHA256 via Web Crypto.
Public API Aggregation
durable-effects/mod.ts
Exports runtimes, operations, guards, and computeSHA256 as the package surface.
Tests
durable-effects/operations.test.ts, durable-effects/node-runtime.test.ts, durable-effects/guards.test.ts, durable-effects/hash.test.ts
Comprehensive tests for golden runs, replay behavior, guard staleness detection, node runtime behavior, and hashing correctness.
Docs & Plan
durable-effects/README.md, durable-effects/PLAN.md
Adds consumer README and implementation plan describing API, architecture, and roadmap.
Packaging & TS Config
durable-effects/package.json, durable-effects/tsconfig.json, pnpm-workspace.yaml, tsconfig.json, package.json
New package.json and tsconfig for durable-effects; workspace inclusion; pnpm override to pin effection alpha.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DurableOp as Durable Operation
    participant DurableRuntime
    participant Journal
    participant ReplayGuard

    Note over Client,Journal: First (golden) run
    Client->>DurableOp: invoke (e.g., durableExec)
    DurableOp->>DurableRuntime: perform I/O (exec/read/fetch/...)
    DurableRuntime-->>DurableOp: result
    DurableOp->>DurableOp: compute hashes
    DurableOp->>Journal: yield event with result + hashes
    Journal-->>Client: persist journal

    Note over Client,ReplayGuard: Replay run
    Client->>DurableOp: invoke same DurableOp
    DurableOp->>ReplayGuard: check current state (hashes, scans, code)
    ReplayGuard->>DurableRuntime: query runtime as needed
    DurableRuntime-->>ReplayGuard: current state
    alt unchanged
        ReplayGuard-->>DurableOp: pass
        DurableOp->>Journal: read stored result
        Journal-->>DurableOp: return recorded result
        DurableOp-->>Client: return replayed value
    else changed
        ReplayGuard-->>DurableOp: throw StaleInputError
        DurableOp-->>Client: surface error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • cowboyd
  • joshamaju
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add @effectionx/durable-effects package' clearly and concisely summarizes the main change: introducing a new package to the monorepo.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Policy Compliance ✅ Passed Pull request fully complies with all Strict and Recommended policies including proper package.json metadata and appropriate version bumping.
Description check ✅ Passed The PR description fully addresses both Motivation and Approach sections with comprehensive detail. Motivation clearly articulates the problem (structured effects with typing, content hashing, and replay guards). Approach explains the complete design including the DurableRuntime abstraction, all six effects, three guards, shared utilities, and testing strategy.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/durable-effects

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 7, 2026

Open in StackBlitz

npm i https://pkg.pr.new/thefrontside/effectionx/@effectionx/durable-effects@180

commit: 46a6855

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@durable-effects/guards.ts`:
- Around line 211-214: The bindings are hashed using JSON.stringify which is
order-dependent; change the serialization used when computing bindingsHash so it
uses the same canonical/stable serializer the recorder uses (e.g., the project's
canonicalize/stableStringify utility) instead of JSON.stringify; update the
binding hash computation in the block that calls
computeSHA256(JSON.stringify(cell.bindings)) to serialize cell.bindings
canonically (preserving deterministic key ordering and types) so computeSHA256
receives the same bytes as the recording path and prevents spurious
StaleInputError.

In `@durable-effects/mod.ts`:
- Around line 50-52: The helper durableEnv currently exposes secret journaling
via the public API (durableEnv) and should not persist sensitive env values;
either remove durableEnv from the public exports (so only durableNow and
durableUUID remain) or add explicit guardrails: modify the durableEnv
implementation to require an explicit opt-in flag and/or a redact callback
(e.g., redact: true or redactFn) and update its signature and usage checks to
refuse or redact any keys unless opted-in; also mark the symbol as
internal/private (rename to _durableEnv or add an internal JSDoc) if you choose
to keep it internal until redaction/opt-in is implemented so consumers cannot
accidentally persist secrets.

In `@durable-effects/node-runtime.test.ts`:
- Around line 13-44: Replace platform-dependent shell commands in the tests that
call runtime.exec (tests "runs a command and captures stdout", "captures
stderr", "returns non-zero exit code", "supports cwd option") with invocations
of the current Node binary via process.execPath and "-e" inline scripts so they
work cross-platform; e.g., for stdout use runtime.exec({ command:
[process.execPath, "-e", "console.log('hello world')"] }), for stderr use
console.error in the "-e" script, for non-zero exit use process.exit(N) in the
"-e" script, and for cwd verification spawn process.execPath with an "-e" script
that prints process.cwd() while passing cwd: "/tmp" (or the equivalent test
path) to runtime.exec so the test no longer relies on shell builtins like "echo"
or "pwd" or PATH resolution of "node".

In `@durable-effects/node-runtime.ts`:
- Around line 36-49: The runtime currently destructures timeout in nodeRuntime
but never uses it, so durableExec/durableFetch calls won’t be canceled; update
the *exec generator in node-runtime.ts (and the corresponding fetch
implementation used by durableFetch) to honor the timeout: for the external
process path, either pass timeout into the underlying exec call if that API
supports it or wrap the yielded exec task with a timer that cancels/aborts the
task when timeout elapses; for the HTTP path, create an AbortController, start a
setTimeout that calls controller.abort() after timeout, pass controller.signal
into fetch, and clear the timer on completion; ensure these changes are made
inside nodeRuntime’s *exec and the fetch helper so durableExec/durableFetch
observe the timeout.
- Around line 82-86: Normalize relPath to POSIX-style separators before matching
and returning: convert backslashes to forward slashes (e.g., replace backslashes
with '/') on the relPath variable inside the loop that iterates over yield*
each(stream) so includeRegexes.some((re) => re.test(relPath)) uses the
normalized path and results.push({ path: relPath, isFile: entry.isFile })
returns the normalized path; update both the regex test and the pushed value to
use the same normalizedRelPath.

In `@durable-effects/operations.ts`:
- Around line 95-105: The code records options?.encoding into the local variable
encoding but never uses it when calling runtime.readTextFile(path); update the
logic in the generator passed to createDurableOperation (the function that uses
useScope() and DurableRuntimeCtx) to either pass encoding into
runtime.readTextFile (e.g., runtime.readTextFile(path, encoding)) if the runtime
supports it, or validate/normalize encoding up front and throw an error for
unsupported encodings before yielding the read operation; reference the encoding
local variable, the runtime.readTextFile call, and the
DurableRuntime/DurableRuntimeCtx scope to locate and modify the call site so the
requested encoding is actually applied or explicitly rejected.
- Around line 288-289: The bindings hashing is using JSON.stringify which is
order-sensitive; update the code around computeSHA256 and bindingsHash to
canonicalize the bindings before hashing (e.g., produce a stable JSON encoding
by recursively sorting object keys/Map entries and normalizing arrays/primitive
values) and then pass that canonical string to computeSHA256 instead of
JSON.stringify(bindings); implement or reuse a
stableSerialize/canonicalizeBindings helper and call const bindingsHash = yield*
computeSHA256(canonicalizeBindings(bindings)) so semantically equivalent
bindings yield identical hashes.
- Around line 214-227: The durable fetch currently persists raw headers and
omits the body, risking credential leaks and collapsing different bodies into
the same metadata; update the createDurableOperation metadata (inside
createDurableOperation<Json> call) to store a canonical request fingerprint
instead of raw headers: normalize header names to lower-case, redact or replace
sensitive header values (e.g., Authorization, Cookie, Set-Cookie,
Proxy-Authorization), and compute hashes for any sensitive header values and for
the request body (include a body hash). Perform this canonicalization and
hashing just before calling createDurableOperation (use
useScope()/DurableRuntimeCtx/runtime.fetch flow to access the same
url/method/headers/body), and add a field like requestFingerprint or
canonicalRequest to the metadata so recorded logs contain normalized header
names, hashed/redacted sensitive values, and a body hash rather than raw
secrets.
- Around line 50-71: The durable record is persisting raw env and omitting
throwOnError; update the metadata passed to createDurableOperation so it does
NOT include the verbatim env but instead stores a stable redacted/fingerprint
value (e.g., envFingerprint = hash/redact(env)) and also include the
throwOnError flag. Concretely, in the createDurableOperation call (around
createDurableOperation<Json> in operations.ts) remove or replace ...(env ? {
env: env as Json } : {}) with a fingerprint property (e.g., { envFingerprint })
and add { throwOnError } to the metadata object; keep passing the original env
to runtime.exec({ command, cwd, env, timeout }) so execution behavior
(DurableRuntime, DurableRuntimeCtx, useScope) is unchanged. Ensure the
fingerprinting/redaction logic is implemented in a helper used before the
operation creation and that no raw secrets are serialized into the durable
record.
- Around line 318-346: The operation currently records only kind in descExtras
so changes to bounds aren’t captured and random_int accepts invalid bounds;
update the logic around isKind/descExtras and the resolver.kind handlers: when
isKind and resolver.kind is "random_float" or "random_int", add the validated
min and max into descExtras (e.g., descExtras.min and descExtras.max); before
generating values, validate and normalize bounds in the "random_float" and
"random_int" cases (ensure min and max are numbers, for random_int coerce or
round to integers and reject or throw if min > max after normalization) and use
the validated bounds to compute the random value; keep createDurableOperation
and the existing resolver function path unchanged but ensure the recorded
metadata matches the actual validated bounds used to produce the value.

In `@durable-effects/package.json`:
- Around line 19-24: The dependency "@effectionx/durable-streams" is pinned to a
temporary preview URL (pkg.pr.new for PR `#179`); add a TODO comment in the
package.json near that dependency and/or create a repo issue referencing PR `#179`
to remind maintainers to switch this entry to the appropriate workspace:* or
published npm version once the PR is merged and the package is ready for
release; include the dependency name "@effectionx/durable-streams" and PR `#179`
in the TODO/issue text so it’s easy to find later.

In `@durable-effects/PLAN.md`:
- Line 145: The checklist item "[x] **Step 8:** Verify build, lint, all tests
pass" in PLAN.md is prematurely marked done; change it to an unchecked state by
replacing "[x]" with "[ ]" for the Step 8 line and optionally add a short note
that GitHub Actions is failing with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
so tests/build are still failing; ensure you update the exact checklist line
"[x] **Step 8:** Verify build, lint, all tests pass" to reflect the current
status.

In `@durable-effects/README.md`:
- Around line 14-16: Update the README installation instructions so the npm
install command includes all packages used in the example imports—specifically
add `@effectionx/durable-streams` and effection to the install line (in addition
to `@effectionx/durable-effects`) and update any repeated install blocks later in
the file accordingly so the example imports match the documented install
command.

In `@package.json`:
- Around line 34-36: Add a TODO comment next to the workspace "overrides" entry
that pins "effection" to "4.1.0-alpha.7" describing why the alpha pin exists
(compatibility with `@effectionx/durable-streams`), an expected timeframe or
condition for removing the override, and who to contact or which issue/PR should
track its removal; place this TODO adjacent to the "overrides" -> "effection"
entry so future maintainers can easily find and revisit it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fc6793bb-7649-4a37-9a6b-35602ee95920

📥 Commits

Reviewing files that changed from the base of the PR and between 022418e and 53e8d2b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • durable-effects/PLAN.md
  • durable-effects/README.md
  • durable-effects/guards.test.ts
  • durable-effects/guards.ts
  • durable-effects/hash.test.ts
  • durable-effects/hash.ts
  • durable-effects/mod.ts
  • durable-effects/node-runtime.test.ts
  • durable-effects/node-runtime.ts
  • durable-effects/operations.test.ts
  • durable-effects/operations.ts
  • durable-effects/package.json
  • durable-effects/runtime.ts
  • durable-effects/stub-runtime.ts
  • durable-effects/tsconfig.json
  • package.json
  • pnpm-workspace.yaml
  • tsconfig.json

Comment on lines +50 to +52
durableNow,
durableUUID,
durableEnv,
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

durableEnv makes secret journaling part of the public API.

Durable values are stored for replay, so a convenience wrapper around arbitrary env vars makes it easy to persist tokens and API keys into the stream. Please add guardrails here—e.g. explicit opt-in/redaction—or keep this helper internal until that exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@durable-effects/mod.ts` around lines 50 - 52, The helper durableEnv currently
exposes secret journaling via the public API (durableEnv) and should not persist
sensitive env values; either remove durableEnv from the public exports (so only
durableNow and durableUUID remain) or add explicit guardrails: modify the
durableEnv implementation to require an explicit opt-in flag and/or a redact
callback (e.g., redact: true or redactFn) and update its signature and usage
checks to refuse or redact any keys unless opted-in; also mark the symbol as
internal/private (rename to _durableEnv or add an internal JSDoc) if you choose
to keep it internal until redaction/opt-in is implemented so consumers cannot
accidentally persist secrets.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a prominent JSDoc security warning to durableEnv stating that the env var value is persisted to the journal, and advising against using it for secrets. For secrets, users should read them ephemerally on each run instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Let me verify the JSDoc warning was added as described.


🏁 Script executed:

#!/bin/bash
# Check the durableEnv implementation for the security warning
rg -n -A 20 -B 5 'durableEnv' durable-effects/operations.ts

Length of output: 435


@taras, the security warning doesn't appear to be in the code on the PR branch yet — the current JSDoc on durableEnv in durable-effects/operations.ts (line 373) still reads only:

/** Capture an environment variable value. */

Could you push the commit with the warning? Here's a suggested JSDoc that would address the concern:

/**
 * Capture an environment variable value durably.
 *
 * ⚠️ **Security warning:** The resolved value is persisted to the durable
 * journal for replay. Do **not** use this for secrets, API keys, tokens, or
 * any other sensitive data — those values will be stored in plain text in the
 * stream. For secrets, read them ephemerally inside each run instead (e.g.,
 * `yield* call(() => process.env.MY_SECRET)`) so they are never journaled.
 */
export function* durableEnv(
  varName: string,
  name?: string,
): Workflow<string | null> {

🧠 Learnings used
Learnt from: taras
Repo: thefrontside/effectionx PR: 179
File: durable-streams/each.ts:81-82
Timestamp: 2026-03-07T00:55:23.465Z
Learning: In `durable-streams/each.ts`, the module-level `activeState` variable shared between `durableEach` and `durableEach.next` is an intentional design decision. Durable execution is single-threaded (cooperative generators), so there is no concurrent access risk within a single coroutine chain. Using Effection context is not viable here because both `durableEach` and `durableEach.next` are wrapped in `ephemeral()`, which gives each its own child scope, making context invisible across them. A TODO is in place to revisit if Effection adds context that spans ephemeral scope boundaries.

Learnt from: taras
Repo: thefrontside/effectionx PR: 179
File: durable-streams/structured-concurrency.test.ts:262-279
Timestamp: 2026-03-07T00:55:49.494Z
Learning: In `durable-streams/structured-concurrency.test.ts` (and the durable-streams package generally), race tests using `durableRace` with immediately-resolving promises are deterministic under Effection's cooperative scheduling model. Effection processes spawned tasks in FIFO order, so the first branch (`root.0`) always wins when it completes in fewer effect steps than subsequent branches. The winner is structurally determined by effect count, not by wall-clock timing or scheduler luck.

Learnt from: taras
Repo: thefrontside/effectionx PR: 179
File: durable-streams/effect.ts:97-101
Timestamp: 2026-03-07T00:55:27.549Z
Learning: In `durable-streams/effect.ts` (and the durable-streams protocol spec), divergence detection during replay deliberately compares only `description.type` and `description.name` — not the full EffectDescription. Extra fields (path, URL, marker, version metadata, etc.) are intentionally stored as metadata for ReplayGuard middleware to inspect, not as identity fields. Deep-equality comparison would be incorrect and would break replay guard tests where metadata legitimately differs between the stored and current description. The current `desc` is passed to the Divergence API as `actual`, so guards have access to both sides for comparison.

Learnt from: taras
Repo: thefrontside/effectionx PR: 179
File: durable-streams/README.md:430-449
Timestamp: 2026-03-07T01:02:46.175Z
Learning: In `durable-streams/README.md` (and the durable-streams package generally), `durableCall` only attaches `type` and `name` to the `EffectDescription`. Adding extra metadata fields (e.g., `path`, `resourceId`, `url`) to the description requires using `createDurableEffect` or `createDurableOperation` with a custom description object. The README's `durableReadFile` example illustrates the pattern conceptually; a fully reproducible implementation would need `createDurableOperation` to populate `event.description.path`.

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:31:21.899Z
Learning: Applies to **/*.{ts,tsx} : Use structured concurrency with spawn and scope in Effection code

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:32:37.408Z
Learning: Applies to **/*.{ts,tsx} : Use structured concurrency patterns with spawn and scope in Effection code

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:31:21.899Z
Learning: Applies to **/*.test.{ts,tsx} : Use Node.js test runner for tests (import test utilities from `effectionx/bdd` when needed)

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:32:37.408Z
Learning: Applies to **/*.{ts,tsx} : Prefer `Operation<T>` for async operations in Effection code

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:31:21.899Z
Learning: Applies to **/*.{ts,tsx} : Prefer `Operation<T>` for async operations in Effection code

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:32:37.408Z
Learning: Applies to **/*.test.ts : Import test utilities from `effectionx/bdd` when needed in test files

Learnt from: CR
Repo: thefrontside/effectionx PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T17:31:21.899Z
Learning: Applies to <package>/mod.ts : Create a main entry point at `mod.ts` that exports the public API

Comment on lines +19 to +24
"dependencies": {
"@effectionx/durable-streams": "https://pkg.pr.new/thefrontside/effectionx/@effectionx/durable-streams@179",
"@effectionx/fetch": "workspace:*",
"@effectionx/fs": "workspace:*",
"@effectionx/process": "workspace:*"
},
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if durable-streams exists as a workspace package (it should after PR `#179` merges)
fd -t f "package.json" --exec grep -l '"name": "@effectionx/durable-streams"' {} \; 2>/dev/null || echo "durable-streams not found in workspace"

Repository: thefrontside/effectionx

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Check the current version and if the package appears to be publishable
cat durable-effects/package.json | head -10

# Also check if there's any documentation about handling preview URLs in policies
find .policies -type f -name "*.md" | xargs grep -l "preview\|url\|pkg.pr" 2>/dev/null || echo "No policy docs found for preview URLs"

Repository: thefrontside/effectionx

Length of output: 564


Document the temporary preview URL dependency for future conversion.

The @effectionx/durable-streams dependency currently uses a preview URL from pkg.pr.new (PR #179). This will need to be converted to a workspace or published npm version once PR #179 is merged and this package approaches release. Consider adding a TODO comment or issue reference to track this conversion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@durable-effects/package.json` around lines 19 - 24, The dependency
"@effectionx/durable-streams" is pinned to a temporary preview URL (pkg.pr.new
for PR `#179`); add a TODO comment in the package.json near that dependency and/or
create a repo issue referencing PR `#179` to remind maintainers to switch this
entry to the appropriate workspace:* or published npm version once the PR is
merged and the package is ready for release; include the dependency name
"@effectionx/durable-streams" and PR `#179` in the TODO/issue text so it’s easy to
find later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed — the dependency was updated from the pkg.pr.new preview URL to workspace:* when durable-effects was merged into the durable-streams branch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@taras have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 50 seconds before sending another message.

- [x] **Step 5:** `operations.ts` — All 6 effects (durableResolve, durableReadFile, durableExec, durableFetch, durableGlob, durableEval) + `operations.test.ts`
- [x] **Step 6:** `guards.ts` — All 3 replay guards + `guards.test.ts`
- [x] **Step 7:** `mod.ts` — Complete public API barrel exports
- [x] **Step 8:** Verify build, lint, all tests pass
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Step 8 should not be checked off yet.

GitHub Actions is still failing with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, so “Verify build, lint, all tests pass” is inaccurate right now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@durable-effects/PLAN.md` at line 145, The checklist item "[x] **Step 8:**
Verify build, lint, all tests pass" in PLAN.md is prematurely marked done;
change it to an unchecked state by replacing "[x]" with "[ ]" for the Step 8
line and optionally add a short note that GitHub Actions is failing with
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING so tests/build are still failing;
ensure you update the exact checklist line "[x] **Step 8:** Verify build, lint,
all tests pass" to reflect the current status.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — unchecked Step 8 since CI was still failing (now fixed with cross-platform test changes).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@taras have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 50 seconds before sending another message.

Comment on lines +14 to +16
```bash
npm install @effectionx/durable-effects
```
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Installation instructions don't match the example's imports.

The sample also imports @effectionx/durable-streams and effection, so npm install @effectionx/durable-effects`` is not enough for someone following this README verbatim.

📦 Suggested doc fix
-npm install `@effectionx/durable-effects`
+npm install `@effectionx/durable-effects` `@effectionx/durable-streams` effection

Also applies to: 21-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@durable-effects/README.md` around lines 14 - 16, Update the README
installation instructions so the npm install command includes all packages used
in the example imports—specifically add `@effectionx/durable-streams` and
effection to the install line (in addition to `@effectionx/durable-effects`) and
update any repeated install blocks later in the file accordingly so the example
imports match the documented install command.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — install command now includes all three required packages: @effectionx/durable-effects @effectionx/durable-streams effection.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@taras have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 50 seconds before sending another message.

Comment on lines +34 to 36
"overrides": {
"effection": "4.1.0-alpha.7"
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if effection 4.1.0-alpha.7 exists on npm
curl -s "https://registry.npmjs.org/effection/4.1.0-alpha.7" | jq -r '.version // "NOT_FOUND"'

Repository: thefrontside/effectionx

Length of output: 80


🏁 Script executed:

# Get the full package.json to understand the structure and check for other policy issues
head -50 package.json

Repository: thefrontside/effectionx

Length of output: 1229


🏁 Script executed:

# Check if there's a TODO comment already related to the alpha version override
grep -n "TODO\|FIXME\|alpha" package.json

Repository: thefrontside/effectionx

Length of output: 104


🏁 Script executed:

# Check the coding guidelines to understand all requirements for package.json
cat .policies/package-json-metadata.md 2>/dev/null || echo "Policy file not found - checking index"

Repository: thefrontside/effectionx

Length of output: 5175


🏁 Script executed:

# Check if source files have changed that would require version bump
git diff HEAD~1 --stat -- '*.ts' '*.js' '*.tsx' '*.jsx' 2>/dev/null || echo "Git not available or no diff"

Repository: thefrontside/effectionx

Length of output: 95


Consider adding a TODO comment to document removal of this alpha pin.

Pinning effection to 4.1.0-alpha.7 across the entire workspace via pnpm overrides is intentional for the current development phase with @effectionx/durable-streams, but alpha versions may introduce breaking changes. Adding a TODO comment documenting the expected timeline for removing this override would help future maintainers understand when it can be revisited.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 34 - 36, Add a TODO comment next to the workspace
"overrides" entry that pins "effection" to "4.1.0-alpha.7" describing why the
alpha pin exists (compatibility with `@effectionx/durable-streams`), an expected
timeframe or condition for removing the override, and who to contact or which
issue/PR should track its removal; place this TODO adjacent to the "overrides"
-> "effection" entry so future maintainers can easily find and revisit it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already tracked — issue #181 was created to remove the alpha override once effection@4.1.0 is released as stable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@taras have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 50 seconds before sending another message.

@taras taras force-pushed the feat/durable-effects branch from 00d417b to 87f6d88 Compare March 7, 2026 06:04
@taras taras changed the base branch from main to feat/durable-streams March 7, 2026 06:04
@taras taras force-pushed the feat/durable-effects branch from 089b02b to a1f6f5d Compare March 7, 2026 06:35
@taras taras force-pushed the feat/durable-streams branch from 163028a to 51ac3d6 Compare March 7, 2026 12:20
@taras taras force-pushed the feat/durable-effects branch 2 times, most recently from be33c94 to 22d8ebf Compare March 7, 2026 14:00
@taras taras force-pushed the feat/durable-streams branch from 51ac3d6 to abfe832 Compare March 7, 2026 14:00
@taras taras force-pushed the feat/durable-effects branch from 22d8ebf to e4e8b2c Compare March 7, 2026 19:52
cowboyd and others added 19 commits March 8, 2026 14:15
* ✨ Add @effectionx/middleware package

Composable middleware with min/max priority layering for wrapping
single functions. Pure TypeScript with no dependencies.

* 📝 Add README for @effectionx/middleware

* 📝 Add "When to Use Min vs Max" and "With Effection Operations" README sections

Expand middleware README with two conceptual sections:
- Min vs Max: readFile example showing min as implementation provider
  and max as wrapping behavior, with test fixture swapping
- With Effection Operations: database/transaction/auth pipeline
  demonstrating Effection contexts, try/finally cleanup, and
  structured concurrency guarantees

* 🐛 Address CodeRabbit review feedback

- Add `text` language specifier to fenced code block (MD040)
- Add approved `concurrency` keyword to package.json per policy

* ♻️ Remove mutable createMiddlewareStack, keep only combine()

Address review feedback from cowboyd: the mutable stack API conflicts
with Effection's model where mutation should be limited to the
context layer. The only consumer (context-api) already uses combine()
directly with immutable arrays.

- Remove createMiddlewareStack() and MiddlewareStack interface
- Export only Middleware type and combine() function
- Rewrite README around combine() with Effection generator examples
- Simplify test suite to cover combine() behavior only

* 📝 Prioritize generator examples in README and fix keywords

Move Effection/generator examples to Quick Start section since they
best demonstrate why combining generators into middleware stacks is
valuable. Plain function examples moved to a separate section.

Fix package.json keywords to use only approved values per policy.

* 📝 Update package description per review feedback

Remove inaccurate "min/max priority layering" (that is a context-api
feature) and describe what the package actually provides.
…iority (#189)

* ♻️ Refactor context-api to use @effectionx/middleware with min/max priority

Replace hand-rolled middleware composition in context-api with the shared
@effectionx/middleware package. Internal state changes from a single composed
function per field to immutable {max, min, composed} arrays that support
priority ordering while preserving scope isolation.

- Import Middleware type and combine() from @effectionx/middleware
- Add { at: "min" | "max" } option to around() (defaults to "max")
- Store per-field max/min arrays in context; recompute on each around() call
- Add 5 new tests covering min/max ordering, scope isolation, implementation
  replacement, mixed insertion order, and default behavior
- Rewrite README with Quick Start, Min/Max Priority, Instrumentation,
  Test Mocking, Scope Isolation, and API reference sections
- Bump version to 0.4.0 (new feature, non-breaking)

* ♻️ Address review feedback: clean up noExplicitAny, rename types, remove passthrough wrap

- Disable noExplicitAny for context-api/mod.ts via biome.json override
  instead of 7 scattered inline biome-ignore comments
- Rename FieldState → FieldMiddleware, ContextState → MiddlewareRegistry
  for clarity
- Skip middleware wrapping when no middleware is registered — call
  handlers directly instead of going through an identity passthrough

* ♻️ Simplify type signatures: use keyof A instead of keyof Operations<A>

* ♻️ Address CodeRabbit review feedback

- Fix package.json keywords to approved list (concurrency, interop)
- Export API objects in README examples so later snippets work
- Wrap yield* calls in generator functions in README examples
- Add min middleware to scope-isolation test for full coverage
- Constrain createApi generic to only accept Operation handlers

* 📝 Add externalLogger type annotation and files field to package.json
* 🐛 Fix @effectionx/vitest compatibility with Vitest 4

Vitest 4 moved the suite object from the 1st argument to the 2nd
argument in beforeAll/afterAll hooks, and requires the 1st argument
to use object destructuring syntax (enforced by a static source parser).

Use a createHook() helper that resolves the suite from either argument
position (vitest 3: 1st arg, vitest 4: 2nd arg) and overrides toString()
to satisfy vitest 4's fixture argument parser. Adapters continue to be
stored on vitest's own per-describe suite objects, preserving proper
per-suite scoping without any module-level mutable state.

* 🐛 Replace as-any casts with narrow CompatBeforeAfterAll type bridge and add multi-file isolation test
…178)

* fix(effect-ts): await Effect cleanup during Effection scope teardown

When an Effection scope is halted, the action() teardown calls
controller.abort() synchronously, but Effect's cleanup (finalizers,
acquireRelease) runs asynchronously through promises. This can cause
tests to see incomplete cleanup.

Fix: Track in-flight Effect executions in a Set. During resource
teardown, abort all pending executions and await their settlement
before disposing the ManagedRuntime. Gate reject-path propagation
from action() when the abort signal is already triggered to prevent
spurious FiberFailure errors from reaching Effection.

* 🐛 fix(effect-ts): resolve runExit with Exit.die instead of rejecting

runExit's contract is to always return an Exit, never throw. When
runPromiseExit rejected at the Promise level, the previous code called
reject(error) which propagated as a thrown exception in the parent
Effection scope. Now wraps unexpected errors as Exit.die(error),
preserving Effect's defect semantics while honoring the runExit contract.
* process finally block test

* assertions were missed, fix but hangs

* move proc into task

* fix lint on proc

* task can throw, would fail test

* sure, reject

* comment for ignoring catch
* ✨ Support any handler type in context-api (sync, constants, operations)

Bring context-api to parity with effection v4-1-alpha's API system by
allowing API members to be any kind of value — not just Operations.

- Sync functions are lifted to Operation-returning functions
- Plain constants are lifted to Operations
- Middleware type matches the handler's raw signature (sync stays sync)
- Added isOperation() runtime guard with native iterable exclusion
- Operation check comes before function check in type mapping to prevent
  misclassification of Operation implementations

* 🐛 Fix around() min middleware ordering for nested scopes (#197)
* Strip `development` export condition before publishing to npm

Vitest injects `--conditions=development` into worker/fork execArgv,
causing Node to resolve to raw `.ts` source in node_modules. On Node 24
this fails with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING.

Keep the `development` condition in source for local dev ergonomics,
but strip it from package.json files in the publish workflow before
`pnpm publish` runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Patch bump all packages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rewrite strip-dev-exports to use Effection and readPackages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix lint issues in strip-dev-exports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
🤖 Add no-marketing-material policy and directive
* Fix process tests failing due to Node ExperimentalWarning in stderr

The stderr assertions used exact matching which broke when Node's
type stripping experimental warning appeared in stderr output.
Use toContain instead so tests tolerate runtime diagnostic messages.

* Disable ExperimentalWarning for type stripping

The warning pollutes stderr in child processes, breaking tests that
assert on stderr content. Since type stripping is intentionally
enabled, suppress the warning via --disable-warning=ExperimentalWarning.
Port durable execution for Effection from ctp-durably into the
effectionx monorepo. Crash-safe generator workflows that survive
process restarts by journaling effects to an append-only stream.

- 17 source files ported from Deno to Node (flat structure)
- 11 test files (95 tests, all passing)
- Monorepo-wide effection 4.1.0-alpha.7 override for scope.around()
- Cherry-pick effect-ts teardown fix for alpha compatibility
- README documenting mental model, journal protocol, replay guards,
  divergence detection, and stream backends
Use a shared WorkflowValue alias (Json | void) in durableRun and structured concurrency combinators so Workflow<void> callbacks type-check under strict tsc checks.

Session-ID: ses_33ef1d402ffeBK425121WZG7jG
Add a committable Node-based demo with a README that documents tmux and manual run flows so contributors can reproduce replay and crash-recovery behavior locally.

Session-ID: ses_33ef1d402ffeBK425121WZG7jG
Remove the stale ../fs reference from durable-streams/tsconfig.json so check:tsrefs matches the actual workspace imports and CI passes.

Session-ID: ses_33ef1d402ffeBK425121WZG7jG
taras added 29 commits March 28, 2026 21:13
The previous kill command targeted the entire process group (kill -9 -PGID)
which also killed the pane shell, closing the tmux pane. Now uses pgrep -P
to find the child node process and kills only that, leaving the shell alive
so the presenter can re-run the cook command.
Previous approach using pgrep -P only found direct children of the
shell, missing node subprocesses. Use pkill -9 -f 'demo/cook.ts' to
reliably kill all processes matching the cook script.
…guard

Move DurableRuntime interface + DurableRuntimeCtx context into
durable-streams as the canonical location for the runtime abstraction.

Remove file-guard.ts (useFileContentGuard) — replaced by the
platform-agnostic version in @effectionx/durable-effects that uses
DurableRuntime instead of Node-specific crypto/fs imports.
…d changes

- Add DurableRuntime section explaining context-based runtime pattern
- Replace built-in file content guard section with reference to
  @effectionx/durable-effects pre-built guards
- Update import examples to use @effectionx/durable-effects
- Enhanced comments explaining description comparison design (type+name only)
- Rethrow normalized primary Error instead of raw error in run.ts
- Added JSDoc explaining why closed coroutines are skipped in replay-index
- Expanded comments documenting activeState limitation with TODO in each.ts
- Added comment explaining replay-guard is NOT bypassed by child Close fast-path
- Added upfront minVersion > maxVersion validation for versionCheck()
- Replaced global pkill with pane-scoped kill in demo start.sh
- Updated crash instruction text in demo cook.ts
- Fixed fenced code block language specifiers in README.md and demo/README.md
Add 4 specification documents from the original ctp-durably design repo:
- protocol-specification.md: Two-event durable execution protocol spec
- durable-streams.md: Analysis of Durable Streams as execution log backend
- effection-integration.md: Architecture research mapping protocol to Effection
- DECISIONS.md: 34 architectural decisions (DEC-001 through DEC-034)

References updated for the effectionx monorepo context:
- DEC-001 (Deno) and DEC-004 (@std/assert) marked as superseded
- DEC-002 updated with alpha.7 pin and tracking issue #181
- DEC-003 updated to reflect two-package split
- Project-local lib/ and test/ paths updated to package-root layout
…me interface

Adds a stat(path) method that returns file metadata without reading
contents. Returns { exists: false, isFile: false, isDirectory: false }
for missing paths instead of throwing — existence checks should not
require try/catch. Permission errors still propagate.
Create new package with PLAN.md, package.json, tsconfig.json, README.md,
and placeholder mod.ts. Integrate into monorepo workspace and add
effection alpha override for experimental API compatibility.

Consumes @effectionx/durable-streams via pkg-pr-new preview URL from PR #179.
Define platform-agnostic DurableRuntime interface with Operation-returning
methods for exec, readTextFile, glob, fetch, plus synchronous env/platform.
Uses minimal ResponseHeaders interface to avoid DOM lib dependency.
Web Crypto API based SHA-256 hashing that returns Operation<string>.
Portable across Node 22+, Deno, and browsers. Returns sha256:<hex> format.
All I/O methods throw by default, proving no live execution during replay.
Override per-test to inject specific behavior.
Delegates to @effectionx/process for exec, @effectionx/fetch for HTTP,
and @effectionx/fs for filesystem operations (readTextFile, walk, glob).
All operations integrate with Effection structured concurrency.
…teDurableOperation<Json> pattern

Named interfaces (ExecResult, ReadFileResult, etc.) don't satisfy Json's
index signature constraint. Fix by using createDurableOperation<Json> for
all 6 effects, casting return values as unknown as Json inside callbacks,
and casting the outer yield to the concrete result type.
…nience wrappers

16 tests covering golden run, full replay, and error propagation for
durableExec, durableReadFile, durableGlob, durableFetch, durableEval,
durableResolve, and convenience wrappers (durableNow, durableUUID, durableEnv).
useFileContentGuard — detects file content changes via hash comparison
useGlobContentGuard — detects directory scan changes (add/remove/modify)
useCodeFreshnessGuard — detects source/bindings changes for eval cells

All guards use DurableRuntime for I/O and computeSHA256 for hashing.
Guards compose via Effection's scope.around(ReplayGuard, ...) middleware.
12 tests covering:
- useFileContentGuard: file unchanged/changed/no-path/cache-dedup
- useGlobContentGuard: files unchanged/added/non-glob pass-through
- useCodeFreshnessGuard: source+bindings unchanged/source changed/
  bindings changed/unknown cell/non-eval pass-through
Exports: runtime (DurableRuntimeCtx, DurableRuntime), nodeRuntime,
stubRuntime, computeSHA256, all 6 effects + 3 convenience wrappers,
all 3 replay guards, and all supporting types.
…-metadata policy

Replace custom keywords (durable, effects, workflow) with approved
categories: concurrency, io, process.
… from durable-streams

Replace preview URL dependency with workspace:* for durable-streams.
Replace runtime.ts implementation with re-exports from durable-streams.
Add durable-streams to tsconfig references for type resolution.
Adds the stat(path) implementation using @effectionx/fs, matching the
interface added to DurableRuntime. Re-exports StatResult from
durable-effects/runtime.ts and mod.ts. Adds throwing stub and 3 tests
covering file, directory, and missing path cases.
@taras taras force-pushed the feat/durable-effects branch from 5d599cb to 6b63d3d Compare March 29, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants