Skip to content

feat(durable): add @effectionx/durable durable-native runtime#174

Draft
taras wants to merge 12 commits intomainfrom
feat/durable-native-package-spec
Draft

feat(durable): add @effectionx/durable durable-native runtime#174
taras wants to merge 12 commits intomainfrom
feat/durable-native-package-spec

Conversation

@taras
Copy link
Member

@taras taras commented Feb 26, 2026

Motivation

Introduce @effectionx/durable, a durable-native structured concurrency runtime for Effection. Unlike @effectionx/durably (which wraps plain operations with compatibility prefixes), this package makes durability the default — every primitive (spawn, all, race, resource, scoped) is durable by design, with a branded DurableOperation<T> type that enforces durable boundaries at compile time.

The package uses a simplified 4-event stream protocol (yield, next, close, spawn) replacing durably's 8-event schema, and a clean-room reducer implementation built on effection@4.1.0-alpha.5 experimental APIs (api.Scope, ReducerContext).

Approach

Type Model

  • DurableOperation<T>: Branded Operation<T> via private unique symbol. Structurally compatible with yield* but prevents plain operations from entering durable APIs. Only the durable runtime can mint branded operations.

4-Event Stream Protocol

Event Purpose
yield Coroutine yielded an effect (outbound)
next Outside world responded (inbound, status: ok/err)
close Coroutine terminal state (ok/err/cancelled)
spawn Coroutine spawned a child (structural)

Eliminates scope:set, scope:delete, workflow:return (informational events never consumed during replay). Renames scopeIdcoroutineId to align with coroutine protocol terminology.

DurableReducer (clean-room)

Written from scratch using the same api.Scope.around() middleware pattern as durably but targeting the new 4-event schema. Key capabilities:

  • ReplayIndex: Per-coroutine event cursors for deterministic replay
  • Scope middleware: Emits spawn before child execution, close with 3-way status on teardown
  • Effect interception: Records yield/next on live path, feeds stored values during replay
  • Divergence detection: Throws DivergenceError on replay mismatch

Runtime Invariants (7)

  1. Spawn registration before child execution
  2. Halt persistence (close(cancelled) before teardown)
  3. Resource determinism (no re-acquire on replay)
  4. All completeness (branch closes before join)
  5. Race cancellation (winner ok, losers cancelled)
  6. Replay suppression (no live effect.enter() for recorded events)
  7. Single-stream transactionality (one stream, one checkpoint)

Test Coverage (24 tests)

  • Stream recording: close events, yield/next pairs, spawn ordering
  • Replay: basic/multi-step/error replay, partial replay from cutoff, divergence detection
  • Concurrency: spawn/close lifecycle, all branches, race winner/losers, nested scopes
  • Resources: acquire/release lifecycle, value recording, error handling

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Important

Review skipped

Draft detected.

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.

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/durable-native-package-spec

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.

Add protocol decisions from coroutine-transport-protocol exploration:
- Single flat stream per workflow with correlation IDs
- 4 event types (yield/next/close/spawn) replacing 8-type schema
- DurableOperation.id as coroutineId in stream events
- Three-way terminal state for cancellation (ok/err/cancelled)
- Updated runtime invariants to reference new event types
- Add @effectionx/durable package scaffold (package.json, tsconfig.json)
- Wire into monorepo (pnpm-workspace.yaml, root tsconfig references)
- Implement DurableOperation<T> branded type with private symbol
- Define 4-event schema (Yield, Next, Close, Spawn)
- Define DurableStream interface and StreamEntry type
- Add DivergenceError and serialization helpers
- Implement InMemoryDurableStream with static from() factory
Clean-room implementation of the durable reducer engine using the new
4-event schema (yield, next, close, spawn). Uses api.Scope.around()
middleware from effection/experimental to intercept scope lifecycle.

Key components:
- ReplayIndex: indexes events per-coroutine for deterministic replay
- Scope middleware: emits spawn events before child execution,
  close events with 3-way status (ok/err/cancelled) on teardown
- Effect interception: records yield/next pairs on live path,
  feeds stored values during replay without calling effect.enter()
- Divergence detection: throws DivergenceError on replay mismatch
- Serialization: toJson with cycle detection and LiveOnly sentinels
Wires DurableReducer into an Effection scope via createScope/global
and ReducerContext injection. Accepts a DurableOperation and optional
DurableStream for persistence. Falls back to InMemoryDurableStream
for ephemeral execution.
…rce, scoped)

Branded wrappers around Effection primitives that enforce
DurableOperation<T> at the type level. The actual durable semantics
(event recording/replay) come from the reducer middleware — these
are thin type-safe entry points.
Export all public types, runtime, stream, and primitives from mod.ts.
Add test helpers for filtering events (allEvents, lifecycleEvents,
userFacingEvents, userEffectPairs).
- stream-recording: close events, yield/next pairs, spawn events
- replay: basic replay, multi-step replay, error replay, partial
  replay from cutoff, divergence detection
- concurrency: spawn/close lifecycle, all branches, race winner/losers,
  nested scope hierarchy
- resource: acquire/release lifecycle, value recording, error handling

Fix: capture parent ID before unregistering in destroy middleware
(close events for root were being lost because parent mapping was
deleted before being read).
@taras taras changed the title spec(durable): propose @effectionx/durable durable-native runtime feat(durable): add @effectionx/durable durable-native runtime Feb 26, 2026
CI resolves effection differently than local development.
Add a scoped pnpm override for @effectionx/durable to ensure
it always uses the preview build with experimental API exports
(ReducerContext, InstructionQueue, DelimiterContext).
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.

1 participant