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
2 changes: 1 addition & 1 deletion packages/core/src/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ function* executeDocument(options: ExecuteOptions): Operation<DocumentExecution>
yield* LiveFailure.set(liveFailure);

// Create per-document eval scope (spec Β§3.1).
// Created in the same scope as durableRun so that DurableCtx
// Created in the same scope as durableRun so that DurableContext
// (set by durableRun) is visible to eval code that calls
// renderChildren β†’ importComponent β†’ createDurableOperation.
const rootEvalScope = yield* useEvalScope();
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/loop.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createContext, useScope } from "effection";
import type { Context, Operation } from "effection";
import { createDurableOperation, DurableCtx, StaleInputError } from "@executablemd/durable-streams";
import type {
import {
createDurableOperation,
DurableContext,
EffectDescription,
Json,
Workflow,
StaleInputError,
} from "@executablemd/durable-streams";
import type { EffectDescription, Json, Workflow } from "@executablemd/durable-streams";

/**
* The loop a `<Break>` exits (spec Β§6.5 `<Loop>`).
Expand Down Expand Up @@ -191,5 +190,5 @@ export function* recordOutcome(identity: LoopIdentity, record: LoopRecord): Oper

function* durableContext(): Operation<DurableContext | undefined> {
const scope = yield* useScope();
return scope.get(DurableCtx);
return scope.get(DurableContext);
}
14 changes: 7 additions & 7 deletions packages/durable-streams/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
useScope,
} from "effection";
import type { Operation, Task } from "effection";
import { type DurableContext, DurableCtx } from "./context.ts";
import { DurableContext } from "./context.ts";
import {
activeDurabilityFailure,
appendDurableEvent,
Expand All @@ -46,7 +46,7 @@ import type { Close, Json, Workflow, WorkflowValue } from "./types.ts";
*
* It:
* 1. Checks if the child already completed (has Close event) β€” short-circuits
* 2. Sets DurableCtx on the child's scope with the child's coroutineId
* 2. Sets DurableContext on the child's scope with the child's coroutineId
* 3. Runs the child workflow (its DurableEffects use the child's coroutineId)
* 4. Appends Close(ok|err) when the child terminates
*
Expand Down Expand Up @@ -102,7 +102,7 @@ function* runDurableChild<T extends WorkflowValue>(
childCounter: 0,
durability: parentCtx.durability,
};
scope.set(DurableCtx, childCtx);
scope.set(DurableContext, childCtx);

let closeEvent: Close | undefined;
let suppressClose = false;
Expand Down Expand Up @@ -144,7 +144,7 @@ function* runDurableChild<T extends WorkflowValue>(

try {
// Run the child workflow. DurableEffects inside the child read
// DurableCtx from the scope, so they'll use childId.
// DurableContext from the scope, so they'll use childId.
const result: T = yield* childWorkflow();

const durabilityFailure = activeDurabilityFailure(childCtx);
Expand Down Expand Up @@ -224,7 +224,7 @@ export function durableSpawn<T extends WorkflowValue>(
return ephemeral(
(function* (): Operation<Task<T>> {
const scope = yield* useScope();
const ctx = scope.expect<DurableContext>(DurableCtx);
const ctx = scope.expect<DurableContext>(DurableContext);

// Assign deterministic child ID
const childIndex = ctx.childCounter++;
Expand Down Expand Up @@ -256,7 +256,7 @@ export function durableAll<T extends WorkflowValue>(
return ephemeral(
(function* (): Operation<T[]> {
const scope = yield* useScope();
const ctx = scope.expect<DurableContext>(DurableCtx);
const ctx = scope.expect<DurableContext>(DurableContext);

// Build child Operations, one per workflow. Each gets its own
// deterministic coroutineId and Close event handling.
Expand Down Expand Up @@ -303,7 +303,7 @@ export function durableRace<T extends WorkflowValue>(
return ephemeral(
(function* (): Operation<T> {
const scope = yield* useScope();
const ctx = scope.expect<DurableContext>(DurableCtx);
const ctx = scope.expect<DurableContext>(DurableContext);

// Build Operations for each child β€” each gets its own coroutineId
// and Close event handling via runDurableChild.
Expand Down
2 changes: 1 addition & 1 deletion packages/durable-streams/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ export interface DurableContext {
* Effection Context for durable execution state.
* Set on the root scope by durableRun(); inherited by child scopes.
*/
export const DurableCtx: Context<DurableContext> =
export const DurableContext: Context<DurableContext> =
createContext<DurableContext>("@effection/durable");
6 changes: 3 additions & 3 deletions packages/durable-streams/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

import type { Operation } from "effection";
import { type DurableContext, DurableCtx } from "./context.ts";
import { DurableContext } from "./context.ts";
import { Divergence } from "./divergence.ts";
import {
activeDurabilityFailure,
Expand Down Expand Up @@ -209,7 +209,7 @@ export function createDurableEffect<T>(
resolve: Resolve<EffectionResult<T>>,
routine,
): (resolve: Resolve<EffectionResult<void>>) => void {
const ctx = routine.scope.expect<DurableContext>(DurableCtx);
const ctx = routine.scope.expect<DurableContext>(DurableContext);
const durabilityFailure = activeDurabilityFailure(ctx);
if (durabilityFailure) {
resolve({ ok: false, error: durabilityFailure });
Expand Down Expand Up @@ -328,7 +328,7 @@ export function createDurableOperation<T extends Json>(
resolve: Resolve<EffectionResult<T>>,
routine,
): (resolve: Resolve<EffectionResult<void>>) => void {
const ctx = routine.scope.expect<DurableContext>(DurableCtx);
const ctx = routine.scope.expect<DurableContext>(DurableContext);
const durabilityFailure = activeDurabilityFailure(ctx);
if (durabilityFailure) {
resolve({ ok: false, error: durabilityFailure });
Expand Down
3 changes: 1 addition & 2 deletions packages/durable-streams/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ export { ReplayGuard } from "./replay-guard.ts";
export type { ReplayOutcome } from "./replay-guard.ts";

// Context
export { DurableCtx } from "./context.ts";
export type { DurableContext } from "./context.ts";
export { DurableContext } from "./context.ts";

// Serialization utilities
export {
Expand Down
4 changes: 2 additions & 2 deletions packages/durable-streams/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import { useScope } from "effection";
import type { Operation, Scope } from "effection";
import { DurableCtx } from "./context.ts";
import { DurableContext } from "./context.ts";
import { activeDurabilityFailure, appendDurableEvent } from "./durability.ts";
import { EarlyReturnDivergenceError, TerminalDivergenceError } from "./errors.ts";
import { ReplayGuard } from "./replay-guard.ts";
Expand Down Expand Up @@ -97,7 +97,7 @@ export function* durableRun<T extends WorkflowValue>(
childCounter: 0,
durability: {},
};
scope.set(DurableCtx, ctx);
scope.set(DurableContext, ctx);

// ── REPLAY GUARD: Check phase ──
// Run before the workflow starts. Middleware can yield* for I/O (hash
Expand Down
4 changes: 2 additions & 2 deletions packages/durable-streams/specs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ Updated before completion of every phase and committed at the end of each phase.
runner/reducer or inside each effect.
- **Decision:** Each `DurableEffect.enter()` handles its own replay/live
dispatch internally, reading `DurableContext` from the scope via
`routine.scope.expect(DurableCtx)`.
`routine.scope.expect(DurableContext)`.
- **Rationale:** Keeps the Effection reducer completely untouched. The reducer
calls `enter()` on every effect β€” whether `enter()` resolves synchronously
(replay) or asynchronously (live + persist) is invisible to it. This is
Expand Down Expand Up @@ -365,7 +365,7 @@ Updated before completion of every phase and committed at the end of each phase.
the parent generator's try/catch).
- **Consequences:** `durableAll` and `durableRace` delegate to Effection's
native combinators. The durable layer wraps each child in an Operation
that (1) checks for replay short-circuit, (2) sets DurableCtx with a
that (1) checks for replay short-circuit, (2) sets DurableContext with a
child coroutineId, and (3) emits Close events in finally. This is a
thin wrapper that preserves Effection's error semantics perfectly.

Expand Down
16 changes: 8 additions & 8 deletions packages/durable-streams/specs/effection-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ interface DurableContext {
childCounter: number;
}

const DurableCtx = createContext<DurableContext>("@effection/durable");
const DurableContext = createContext<DurableContext>("@effection/durable");

type Executor = (resolve: (result: Result) => void, reject: (error: Error) => void) => () => void;

Expand All @@ -364,7 +364,7 @@ function createDurableEffect<T>(desc: EffectDescription, execute: Executor): Dur
description: `${desc.type}(${desc.name})`,
effectDescription: desc,
enter(resolve, routine) {
const ctx = routine.scope.expect<DurableContext>(DurableCtx);
const ctx = routine.scope.expect<DurableContext>(DurableContext);
const entry = ctx.replayIndex.peekYield(ctx.coroutineId);

if (entry) {
Expand Down Expand Up @@ -623,9 +623,9 @@ interface DurableContext {
When `durableSpawn()` creates a child scope:

```typescript
let parentCtx = scope.expect(DurableCtx);
let parentCtx = scope.expect(DurableContext);
let childId = `${parentCtx.coroutineId}.${parentCtx.childCounter++}`;
childScope.set(DurableCtx, {
childScope.set(DurableContext, {
replayIndex: parentCtx.replayIndex, // shared
stream: parentCtx.stream, // shared
coroutineId: childId,
Expand Down Expand Up @@ -734,7 +734,7 @@ export function durableSpawn<T extends Json | void>(
return ephemeral(
(function* (): Operation<Task<T>> {
const scope = yield* useScope();
const ctx = scope.expect<DurableContext>(DurableCtx);
const ctx = scope.expect<DurableContext>(DurableContext);
const childId = `${ctx.coroutineId}.${ctx.childCounter++}`;
return yield* spawn(() => runDurableChild(childWorkflow, childId, ctx));
})(),
Expand All @@ -755,7 +755,7 @@ function* durableAll<T extends Json | void>(
workflows: (() => Workflow<T> | Operation<T>)[],
): Operation<T[]> {
const scope = yield* useScope();
const ctx = scope.expect<DurableContext>(DurableCtx);
const ctx = scope.expect<DurableContext>(DurableContext);

const childOps: Operation<T>[] = workflows.map((workflow) => {
const childId = `${ctx.coroutineId}.${ctx.childCounter++}`;
Expand Down Expand Up @@ -803,7 +803,7 @@ function* durableRace<T extends Json | void>(
workflows: (() => Workflow<T> | Operation<T>)[],
): Operation<T> {
const scope = yield* useScope();
const ctx = scope.expect<DurableContext>(DurableCtx);
const ctx = scope.expect<DurableContext>(DurableContext);

const childOps: Operation<T>[] = workflows.map((workflow) => {
const childId = `${ctx.coroutineId}.${ctx.childCounter++}`;
Expand Down Expand Up @@ -931,7 +931,7 @@ durable context, and runs the workflow:

```typescript
const ctx = { replayIndex, stream, coroutineId, childCounter: 0, durability: {} };
scope.set(DurableCtx, ctx);
scope.set(DurableContext, ctx);
replayIndex.claim(coroutineId);

try {
Expand Down
28 changes: 28 additions & 0 deletions packages/durable-streams/tests/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* The durable execution state is reachable from a workflow through the
* exported DurableContext, which names both the Effection context and the
* shape that context holds.
*/

import { describe, it } from "@executablemd/test-support/bdd";
import { expect } from "@executablemd/test-support/expect";
import { type Operation, useScope } from "effection";
import { DurableContext, durableRun, InMemoryStream } from "../mod.ts";

describe("DurableContext", () => {
it("holds the state durableRun installs on the workflow scope", function* () {
const stream = new InMemoryStream();

function* workflow(): Operation<string> {
const scope = yield* useScope();
const state: DurableContext = scope.expect(DurableContext);
expect(state.stream).toBe(stream);
expect(state.childCounter).toBe(0);
return state.coroutineId;
}

const coroutineId = yield* durableRun(workflow, { stream, coroutineId: "root.7" });

expect(coroutineId).toBe("root.7");
});
});
2 changes: 1 addition & 1 deletion specs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ operations that cannot proceed without a provider (`importComponent`,
`applyModifiers`, `codeBlock`, `content`) throw named missing-provider
errors that identify the missing installation.

Durable-streams' own contexts (e.g. `DurableCtx`) are unchanged: they
Durable-streams' own contexts (e.g. `DurableContext`) are unchanged: they
store durable runtime state, not overridable core operations.

---
Expand Down
2 changes: 1 addition & 1 deletion specs/executable-mdx-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,7 @@ A component invocation creates its eval scope on its own expansion frame and
runs its body inside a task that scope owns:

```
invocation frame expansion providers, error mode, DurableCtx
invocation frame expansion providers, error mode, DurableContext
└─ evalHost
└─ A's loop task the invocation's eval scope
└─ body task the component body, its resources and its middleware
Expand Down
Loading