diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8506cf..22fd770 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,122 @@ interfaces from `effect/unstable/*`, whose API can move between releases. Each r
of this package states the one `effect` version it is built and tested against, and
tracking a new `effect` release is a new release of this package.
-## 0.3.0 (unreleased)
+## Unreleased / planned — 0.5.0
+
+The removal PR is this checklist. Everything below was deprecated in 0.4.0
+with its replacement named in the JSDoc and reported by the
+`prefer-definition` lint rule; nothing else changes.
+
+- REMOVE the `typed-activity` module and its package export
+ (`TypedActivity.make` → `defineActivity`; `PayloadOf`/`SuccessOf`/`ErrorOf`/
+ `AnyTypedActivity`/`TypedActivity`/`TypedActivityOptions`/
+ `DEFAULT_ACTIVITY_OPTIONS` → `definition`; `codecsFor`/`ACTIVITY_EXIT_TYPE`/
+ `TypedActivityCodecs` → `wire`).
+- REMOVE the `versioning` module and its package export (`match` → `versioned`,
+ `version` → `version` from `definition`). Move `deprecateVersion` /
+ `deprecatePatch` / `patched` (the Temporal-only retirement step) to `bundle`.
+- REMOVE from `engine-sandbox`: `callActivity`, `takeMailbox`, `pollMailbox`,
+ `takeUpdate`, `setStateCell`, `sleepUntil`, `continueAsNew`, and the
+ `UpdateRequest` alias. The Temporal `WorkflowOps` runtime keeps
+ their bodies as private functions. `callRawActivity`, `offerMailbox`
+ (workflow → workflow), `callNexusWorkflowOperation`, `SandboxRun`, and
+ `workflowBundle` stay.
+- REMOVE `make` from `mailbox`, `update`, `state-cell` (→ `defineMailbox`,
+ `defineUpdate`, `defineState`) and drop those three package exports — the
+ modules become internal wire homes (`MAILBOX_SIGNAL`, `WORKFLOW_UPDATE`,
+ `STATE_CELL_QUERY`, codecs) consumed by the engine halves and `testing`.
+- DELETE the deprecation entries from `prefer-definition` once the symbols are
+ gone (the rule stays, empty tables are fine, so a future deprecation has a
+ home).
+- KEEP `histories/definition-order-0.3.0` in the replay drill; record a
+ 0.4.0 history alongside it.
+
+## 0.4.0 (2026-09-10)
+
+Closes the gaps between the `definition` module and the legacy authoring
+surface: after this release a consumer imports `definition`, `bundle`,
+`activities`, `client`/`engine-client`, `testing`, and (engine-level) `wire`
+— never `engine-sandbox` except for the engine-level escape hatches,
+never `typed-activity`, `versioning`, or `mailbox`.
+
+- NEW: timers on the `WorkflowOps` seam — `sleep({ name, duration })` and
+ `sleepUntil({ name, timestamp })` from `definition`, requiring only
+ `WorkflowOps`. On Temporal they dispatch to `DurableClock.sleep` / the
+ existing `sleepUntil` (same timestamp rule, now the shared
+ `sleepUntilTarget`: zone-less and unparseable timestamps die). In
+ `makeTestWorkflowOps` they follow Effect's `Clock`, so a `TestClock` can
+ `adjust` past them — deliberately not instant, so a mailbox take racing a
+ grace-period timer is testable in both orders (pinned on both engines).
+- NEW: `continueAsNew(workflow, payload, options?)` on the seam and in
+ `definition`. In memory it interrupts the handler fiber and records the
+ continuation (`world.continuedAsNew`, `world.continuedAsNewOf(W)`), payload
+ round-tripped through the workflow's payload schema (a schema-invalid
+ payload dies, as on the wire).
+- NEW: child workflows on the seam — `executeChild(workflow, payload,
+ { discard? })` from `definition`. On Temporal it is upstream `execute`
+ under the sandbox engine (identical commands: `startChild` with the digest
+ id, `REQUEST_CANCEL`/`ABANDON`, attach-on-taken). `makeTestWorkflowOps`
+ accepts `workflows: [handleWorkflow(Child, handler)]` and runs children
+ in-process with schema round-tripping, discard-forks, and attach on a
+ taken id.
+- NEW: `versioned(site, { v1: run1, v2: run2 })` — the run-table form of
+ `version` (key order is the chain order, oldest first), built on the same
+ `WorkflowOps.version`; the `versioning-on-main-fiber` lint rule recognises
+ it (alias-aware) alongside `version`.
+- NEW: the activity type helpers live on `definition` — `PayloadOf`,
+ `SuccessOf`, `ErrorOf`, `AnyTypedActivity`, `TypedActivity`,
+ `TypedActivityOptions`, `DEFAULT_ACTIVITY_OPTIONS`. `codecsFor`,
+ `TypedActivityCodecs`, and `ACTIVITY_EXIT_TYPE` moved to `wire` (the
+ `typed-activity` module re-exports everything, deprecated).
+- NEW: every client-side operation takes the declaration directly —
+ `offerMailbox(Priority, …)`, `executeUpdate(SetAmount, …)`,
+ `readStateCell(Status, …)`, `deferredState(Approval, …)` in `engine-client`
+ and on `WorkflowClient`; the underlying primitive (`Priority.mailbox`, …) is
+ still accepted (`MailboxLike` / `UpdateLike` / `StateCellLike` /
+ `DeferredLike`). NEW `completeDeferred(Approval, { client, workflowId,
+ exit })` / `wf.completeDeferred(Approval, workflowId, exit)`: the client
+ half of `Approval.await` — same done-signal as `DurableDeferred.done`, no
+ token, no `WorkflowEngine`. The workflow → workflow `offerMailbox`
+ (engine-sandbox) accepts the declaration too.
+- NEW: `makeFakeTemporalClient` gains `offer(Priority, workflowId, payload)`
+ and `offersTo(Priority)` (decoded through the declaration) — no
+ `MAILBOX_SIGNAL` import in consumer tests. The live harness client gains
+ `offer` / `request` / `stateOf` / `resolve`, mirroring the in-memory world.
+- NEW: the `bundle` module — `workflowBundle`'s home, "the one file the
+ Temporal worker points at". `engine-sandbox` still exports it; its other
+ exports are documented as engine-level (`callRawActivity`, workflow →
+ workflow `offerMailbox`, `callNexusWorkflowOperation`, `SandboxRun`) or
+ deprecated (below).
+- NEW: `prefer-definition` lint rule, on in the `recommended` preset as an
+ error: reports any import of a deprecated symbol from this package's
+ modules (published specifier or relative path) with the replacement in the
+ message, so `oxlint` fails on regressions.
+- NEW: `replay-compat.test.ts` — a history recorded on 0.3.0
+ (`fixtures/histories/definition-order-0.3.0`) replays through the current
+ bundle. Wire identity is unchanged: activity types, signal/query/update
+ names, and patch-marker ids are byte-identical.
+- DEPRECATED (removed in 0.5.0; replacement in each JSDoc):
+ `engine-sandbox`'s `callActivity`, `takeMailbox`, `pollMailbox`,
+ `takeUpdate`, `setStateCell`, `sleepUntil`, `continueAsNew`, and the
+ `UpdateRequest` alias (use `definition`'s
+ `UpdateRequest
` — one parameter order); `TypedActivity.make`;
+ `DurableMailbox.make`, `DurableUpdate.make`, `StateCell.make`;
+ `Versioning.match` / `Versioning.version`; the whole `typed-activity` and
+ `versioning` modules.
+- `defineDeferred` / `defineMailbox` / `defineUpdate` / `defineState` return
+ named interfaces (`DefinedDeferred`, `DefinedMailbox`, `DefinedUpdate`,
+ `DefinedState`) — structurally what they returned before.
+- The repository authors with the new surface throughout: fixtures and
+ examples import `workflowBundle` from `bundle`, the examples use `sleep`
+ and `continueAsNew` from `definition` and drive declarations directly;
+ the versioning-chain and loop fixtures deliberately stay on the deprecated
+ calls so the replay drills keep covering them.
+- `effect` peer/dev pin: `4.0.0-rc.112` (was `4.0.0-beta.101`); full suite
+ and both examples green against it. `@temporalio/*` stays `1.19.0`.
+
+Built and tested against `effect@4.0.0-rc.112` and `@temporalio/*@1.19.0`.
+
+## 0.3.0 (2026-09-10)
- NEW: the `definition` module — declare each capability once and use it
directly inside handlers: `defineActivity` (callable: `yield* Charge({ orderId })`),
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 0c827e6..e910a41 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -35,22 +35,22 @@ workflow-semantics content).
| Sample | Status | With this package |
| ------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [timer-progress](https://github.com/temporalio/samples-typescript/tree/main/timer-progress) | ✅ | `DurableClock.sleep`. Test: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) (delay skipped by the time-skipping server). |
+| [timer-progress](https://github.com/temporalio/samples-typescript/tree/main/timer-progress) | ✅ | `sleep` (definition module; `DurableClock.sleep` underneath on Temporal, an Effect `Clock` in memory). Test: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) (delay skipped by the time-skipping server). |
| [sleep-for-days](https://github.com/temporalio/samples-typescript/tree/main/sleep-for-days) | ✅ | Same mechanism; a Temporal timer's duration is unbounded. Test: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) pins a 2-minute durable delay under time skipping — the same mechanism at any duration. |
-| [timer-examples](https://github.com/temporalio/samples-typescript/tree/main/timer-examples) | ✅ | The order-timeout race is `Effect.raceFirst(activity, DurableClock.sleep)` — pinned by the `timeout-activity` fixture mode and the [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) test "cancels the server-side activity when the calling FIBER is interrupted", which asserts the timed-out activity is cancelled server-side, not abandoned. The `UpdatableTimer` half races the mailbox's `.take` against the timer. Test: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |
+| [timer-examples](https://github.com/temporalio/samples-typescript/tree/main/timer-examples) | ✅ | The order-timeout race is `Effect.raceFirst(activity, sleep)` — pinned by the `timeout-activity` fixture mode and the [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) test "cancels the server-side activity when the calling FIBER is interrupted", which asserts the timed-out activity is cancelled server-side, not abandoned. The `UpdatableTimer` half races the mailbox's `.take` against the timer. Tests: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts), and [definition.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/definition.test.ts) runs the same grace-period race on the in-memory runtime under `TestClock` in both orders AND on Temporal. |
## Workflow composition
| Sample | Status | With this package |
| ------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [child-workflows](https://github.com/temporalio/samples-typescript/tree/main/child-workflows) | ✅ | `MyChild.execute` in a workflow body; typed results/failures compose, cancellation cascades, discard = fire-and-forget, taken ids attach. Tests: [child.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/child.test.ts). |
+| [child-workflows](https://github.com/temporalio/samples-typescript/tree/main/child-workflows) | ✅ | `executeChild(Child, payload)` (definition module; `MyChild.execute` in a body is the same engine path); typed results/failures compose, cancellation cascades, `{ discard: true }` = fire-and-forget, taken ids attach. Tests: [child.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/child.test.ts), [definition.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/definition.test.ts) (same parent handler on the in-memory runtime via `handleWorkflow` bindings and on Temporal). |
| [saga](https://github.com/temporalio/samples-typescript/tree/main/saga) | ✅ | `Workflow.withCompensation`, firing on typed failure and on interrupt. Tests: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts), [child.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/child.test.ts). |
-| [continue-as-new](https://github.com/temporalio/samples-typescript/tree/main/continue-as-new) | ✅ | `continueAsNew(workflow, payload)` (engine-sandbox): ends the run and starts a fresh one with the same workflow id. Test: [continue-as-new.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/continue-as-new.test.ts) — asserts the fresh history, not just the looped result. |
+| [continue-as-new](https://github.com/temporalio/samples-typescript/tree/main/continue-as-new) | ✅ | `continueAsNew(workflow, payload)` (definition module): ends the run and starts a fresh one with the same workflow id. Tests: [continue-as-new.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/continue-as-new.test.ts) — asserts the fresh history, not just the looped result; [definition.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/definition.test.ts) — the in-memory runtime records the schema-checked continuation, Temporal rolls the history. |
| [batch-sliding-window](https://github.com/temporalio/samples-typescript/tree/main/batch-sliding-window) | ✅ | Sliding window of discarded children reporting completion via workflow-to-workflow mailbox; the orchestrator continues-as-new mid-batch, draining reports with the mailbox's `.poll` into the carried in-flight set. Test: [batch.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/batch.test.ts). |
| [dsl-interpreter](https://github.com/temporalio/samples-typescript/tree/main/dsl-interpreter) | ✅ | The payload carries a declarative program (sequential steps of single or parallel activity calls) and Effect interprets it in-workflow. Test: [dsl.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/dsl.test.ts). |
| [early-return](https://github.com/temporalio/samples-typescript/tree/main/early-return) | ✅ | A forked fiber serves the confirmation `DurableUpdate` once authorization lands, while the main flow continues to the final result. Test: [early-return.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/early-return.test.ts). |
| [polling](https://github.com/temporalio/samples-typescript/tree/main/polling) | ✅ | The infrequent variant: the poll interval IS the activity retry policy, so the workflow is one `callRawActivity`. Test: [polling.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/polling.test.ts). Frequent polling inside one activity is activity-side; unbounded polling adds `continueAsNew`. |
-| [patching-api](https://github.com/temporalio/samples-typescript/tree/main/patching-api) | ✅ | `Versioning.match`: an ordered version chain per code site with typed version literals and union-typed channels; fresh runs take the newest case, replays their own. Tests: [versioning-chain.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/versioning-chain.test.ts) (three generations, append-safety, negative control). |
+| [patching-api](https://github.com/temporalio/samples-typescript/tree/main/patching-api) | ✅ | `version(site, names)` / `versioned(site, { v1, v2 })` (definition module): an ordered version chain per code site with typed version literals and union-typed channels; fresh runs take the newest case, replays their own. Tests: [versioning-chain.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/versioning-chain.test.ts) (three generations, append-safety, negative control — on the deprecated `Versioning.match`, kept byte-identical for the replay drill), [definition.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/definition.test.ts) (`versioned` on both engines), [replay-compat.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/replay-compat.test.ts) (a history recorded on 0.3.0 replays through the current bundle). |
## Scheduling and client features
@@ -108,10 +108,13 @@ are worth a smoke test once the package is adopted in a real worker.
Every sample with workflow-semantics content is supported (tested) or works
unchanged; the remainder is infrastructure with no package involvement.
Where a sample needed a primitive the upstream Effect API does not define,
-the package provides one: `DurableMailbox` (repeated signals, long-lived
-state), `DurableUpdate` (request/response with typed channels), `StateCell`
-(queryable state), `continueAsNew` (unbounded workflows), schedule and
-Nexus bridges, and patch-marker versioning.
+the package provides one, declared through the `definition` module:
+`defineMailbox` (repeated signals, long-lived state), `defineUpdate`
+(request/response with typed channels), `defineState` (queryable state),
+`continueAsNew` (unbounded workflows), `executeChild`, `sleep`/`sleepUntil`,
+`version`/`versioned` (patch-marker versioning), plus schedule and Nexus
+bridges. Every one of them runs on the in-memory test runtime as well as on
+Temporal.
New capabilities land with a test mirroring the corresponding sample, in
this package's suite.
diff --git a/README.md b/README.md
index e351d97..17d726a 100644
--- a/README.md
+++ b/README.md
@@ -14,11 +14,10 @@ start with
The same pages live in [docs/](docs/) (`pnpm docs:dev` to browse locally).
```ts
-import { Effect, Schema } from "effect";
+import { Effect, Exit, Schema } from "effect";
import * as Workflow from "effect/unstable/workflow/Workflow";
-import * as DurableClock from "effect/unstable/workflow/DurableClock";
-import { defineActivity, defineDeferred } from "@springbird/effect-temporal/definition";
-import { workflowBundle } from "@springbird/effect-temporal/engine-sandbox";
+import { defineActivity, defineDeferred, sleep } from "@springbird/effect-temporal/definition";
+import { workflowBundle } from "@springbird/effect-temporal/bundle";
import { WorkflowClient } from "@springbird/effect-temporal/client";
// Declare once — shared by the workflow bundle, the worker, and every client.
@@ -42,7 +41,7 @@ const ManagerApproval = defineDeferred("manager-approval", {
const OrderFlowLive = OrderFlow.toLayer((payload) =>
Effect.gen(function* () {
const paid = yield* Charge({ orderId: payload.orderId });
- yield* DurableClock.sleep({ name: "cooling-off", duration: "3 days" });
+ yield* sleep({ name: "cooling-off", duration: "3 days" });
const approver = yield* ManagerApproval.await;
return `${paid}:approved-by:${approver}`;
}),
@@ -50,8 +49,10 @@ const OrderFlowLive = OrderFlow.toLayer((payload) =>
export default workflowBundle(OrderFlowLive); // the bundle's dynamic default
// Drive it from ordinary Node — typed success/error, idempotent by digest id.
+// Client-side ops take the declaration itself.
const program = Effect.gen(function* () {
const wf = yield* WorkflowClient;
+ yield* wf.completeDeferred(ManagerApproval, workflowId, Exit.succeed("ben"));
return yield* wf.execute(OrderFlow, { orderId: "ord_123" });
});
```
@@ -67,7 +68,9 @@ pnpm add @springbird/effect-temporal # or npm / yarn / bun
want a second durable-execution system (Effect's own `effect/unstable/cluster`
engine persists to its own SQL tables).
- **One package, tree-shakeable modules** — `@springbird/effect-temporal/definition`
- (declare capabilities once, engine-agnostic), `/engine-sandbox` (workflow
+ (declare capabilities once, engine-agnostic — activities, messages, state,
+ timers, continue-as-new, children, versioning), `/bundle` (the one file the
+ worker points at), `/engine-sandbox` (engine-level
bundle), `/engine-client` + `/client` (ordinary Node), `/activities`
(worker), `/testing`, `/nexus`, `/lint`. Nexus, worker, and testing peers
are optional.
@@ -146,7 +149,7 @@ shipped preset, which resolves the plugin through the package's own
## Versioning policy
Pre-1.0: **minor bumps may break APIs**. The `effect` peer dependency is
-pinned **exactly** (currently `4.0.0-beta.101`) and on purpose — the engine
+pinned **exactly** (currently `4.0.0-rc.112`) and on purpose — the engine
implements interfaces from `effect/unstable/*`, whose API can move between
releases. Each release states the one `effect` version it is built and
tested against; tracking a new `effect` release is a new release of this
diff --git a/docs/guide/activities.md b/docs/guide/activities.md
index a5704e0..fbd28b9 100644
--- a/docs/guide/activities.md
+++ b/docs/guide/activities.md
@@ -81,6 +81,18 @@ Payloads are decoded (validated) before a handler runs — a payload failing its
An `ActivityRunner` with a service requirement `R` lets handlers use your application services; supply the runner from your `ManagedRuntime`.
+### Naming the types
+
+The declaration's decoded types are available as helpers from the definition module — for typing an implementation you write separately, or a service that takes a payload:
+
+```ts
+import type { PayloadOf, SuccessOf, ErrorOf } from "@springbird/effect-temporal/definition";
+
+const reserve = (payload: PayloadOf (
- update: DurableUpdate ,
+ update: UpdateLike ,
workflowId: string,
payload: P["Type"],
) => Effect.Effect {
readonly respond: (exit: Exit.Exit {
* @category models
*/
export interface WorkflowOpsRuntime {
- readonly activity: (
- activity: TypedActivity.AnyTypedActivity,
- payload: unknown,
- ) => Effect.Effect =
+ | DurableUpdate.DurableUpdate
+ | { readonly update: DurableUpdate.DurableUpdate };
+
+/**
+ * A state cell as either its declaration or its underlying primitive.
+ *
+ * @since 0.4.0
+ * @category models
+ */
+export type StateCellLike (
+ update: UpdateLike ,
+): DurableUpdate.DurableUpdate => ("update" in update ? update.update : update);
+
+/**
+ * The primitive behind a state-cell-like value.
+ *
+ * @since 0.4.0
+ * @category utils
+ */
+export const toStateCell = (
- update: DurableUpdate ,
+ updateLike: UpdateLike ,
options: {
readonly client: Client;
readonly workflowId: string;
readonly payload: P["Type"];
},
): Effect.Effect (
- cell: StateCell {
+ readonly workflowId: string;
+ readonly payload: P;
+}
+
/**
* The fake: a `Client`-shaped double plus the typed records of everything
* that was started, signalled, or terminated through it.
@@ -109,6 +134,21 @@ export interface FakeTemporalClient {
readonly starts: ReadonlyArray (
+ update: UpdateLike ,
+ workflowId: string,
+ payload: P["Type"],
+ ): Promise {
* (ordered oldest first). Checked newest-first, so a fresh execution
* records only the newest marker.
*
+ * @deprecated Use `version` from `definition` (engine-agnostic; the
+ * Temporal runtime dispatches here). Removed in 0.5.0.
* @since 0.1.0
* @category combinators
*/
@@ -73,6 +87,8 @@ export const version = (
- mailbox: DurableMailbox,
+ mailbox: MailboxLike,
workflowId: string,
payload: S["Type"],
) => Effect.Effect;
/** Read the latest published snapshot of a state cell — `None` while the
- * execution is unknown or the cell unpublished, including after close. */
+ * execution is unknown or the cell unpublished, including after close.
+ * Takes the declaration (`Status`) or its underlying primitive. */
readonly readStateCell: (
- cell: StateCell,
+ cell: StateCellLike,
workflowId: string,
) => Effect.Effect) => Effect.Effect =
+ | DurableMailbox.DurableMailbox
+ | { readonly mailbox: DurableMailbox.DurableMailbox };
+
+/**
+ * An update as either its declaration or its underlying primitive.
+ *
+ * @since 0.4.0
+ * @category models
+ */
+export type UpdateLike =
+ | StateCell.StateCell
+ | { readonly cell: StateCell.StateCell };
+
+/**
+ * A deferred as either its declaration or its underlying primitive.
+ *
+ * @since 0.4.0
+ * @category models
+ */
+export type DeferredLike(
+ mailbox: MailboxLike,
+): DurableMailbox.DurableMailbox => ("mailbox" in mailbox ? mailbox.mailbox : mailbox);
+
+/**
+ * The primitive behind an update-like value.
+ *
+ * @since 0.4.0
+ * @category utils
+ */
+export const toUpdate = (cell: StateCellLike): StateCell.StateCell =>
+ "cell" in cell ? cell.cell : cell;
+
+/**
+ * The primitive behind a deferred-like value.
+ *
+ * @since 0.4.0
+ * @category utils
+ */
+export const toDeferred = (
- mailbox: DurableMailbox,
+ mailboxLike: MailboxLike,
options: {
readonly client: Client;
readonly workflowId: string;
readonly payload: S["Type"];
},
): Effect.Effect(
* Send an update request to a running workflow and receive its typed
* response: the workflow's `respond` exit lands in this effect's
* success/error channels. Unlike mailbox offers, an update expects an
- * answer, so an unknown execution is a defect rather than a no-op.
+ * answer, so an unknown execution is a defect rather than a no-op. Accepts
+ * the declaration (`SetAmount`) or its underlying primitive.
*
* @since 0.1.0
* @category client
*/
export const executeUpdate = => {
+ const update = toUpdate(updateLike);
const codecs = updateCodec(update);
const wire = codecs.encodePayload(options.payload);
return Effect.flatMap(
@@ -355,19 +363,21 @@ export const executeUpdate = ,
+ cellLike: StateCellLike,
options: {
readonly client: Client;
readonly workflowId: string;
},
-): Effect.Effect(
if (wire === null || wire === undefined) return Option.none();
return Option.some(stateCellCodec(cell).decode(wire));
});
+};
/**
* Read a deferred's current state from outside the workflow: `None` while
* the execution is unknown or the deferred unresolved, `Some(typed exit)`
* once completed. Backed by a Temporal query, so it never perturbs the
- * signal path.
+ * signal path. Accepts the declaration (`Approval`) or its underlying
+ * primitive.
*
* @since 0.1.0
* @category client
*/
export const deferredState = = DefUp
* Durably await the next `executeUpdate` request for `update`, in delivery
* order. The claim is synchronous on the taking fiber, like `takeMailbox`.
*
+ * @deprecated Use the declaration's `SetAmount.take` (`defineUpdate` from
+ * `definition`). Removed in 0.5.0.
* @since 0.1.0
* @category workflow
*/
@@ -413,6 +426,8 @@ const mailboxBuffer = (run: RunState, name: string): unknown[] => {
* The claim happens synchronously on the taking fiber after the wait, so an
* interrupted take never steals a message from a later one.
*
+ * @deprecated Use the declaration's `Priority.take` (`defineMailbox` from
+ * `definition`). Removed in 0.5.0.
* @since 0.1.0
* @category workflow
*/
@@ -451,6 +466,8 @@ export const takeMailbox = (
* canonical use is draining reports into carried state before
* `continueAsNew`, since buffered messages do not survive the run change.
*
+ * @deprecated Use the declaration's `Priority.poll` (`defineMailbox` from
+ * `definition`). Removed in 0.5.0.
* @since 0.1.0
* @category workflow
*/
@@ -479,14 +496,19 @@ export const pollMailbox = (
* Offer a message to ANOTHER workflow's mailbox (workflow → workflow).
* Offering to a closed or unknown execution is a no-op — the receiver
* finishing first is a normal race, matching `DurableDeferred.done`.
+ * Accepts the declaration (`Reports`) or its underlying primitive.
+ *
+ * **Engine-level.** Kept (not deprecated): it has no engine-agnostic
+ * counterpart yet, and is Temporal-only by nature.
*
* @since 0.1.0
* @category workflow
*/
export const offerMailbox = (
- mailbox: DurableMailbox,
+ mailboxLike: MailboxLike,
options: { readonly workflowId: string; readonly payload: S["Type"] },
): Effect.Effect(
* outside via `readStateCell` (engine-client), including after the run
* closes.
*
+ * @deprecated Use the declaration's `Status.set(value)` (`defineState` from
+ * `definition`). Removed in 0.5.0.
* @since 0.1.0
* @category workflow
*/
@@ -532,6 +556,8 @@ export const setStateCell = (
* messages buffered but not yet taken do not carry into the new run — drain
* before continuing.
*
+ * @deprecated Use `continueAsNew` from `definition` — same semantics on
+ * Temporal, observable in the in-memory runtime. Removed in 0.5.0.
* @since 0.1.0
* @category workflow
*/
@@ -544,6 +570,15 @@ export const continueAsNew = <
workflow: Workflow.Workflow {
}
/**
- * Declare a mailbox: a name (unique within the workflows that use it) and
- * the payload schema. Shared by the workflow body and every offering side.
+ * Declare a mailbox primitive (name + payload schema).
*
+ * @deprecated Use `defineMailbox` from `definition` — its `.take`/`.poll`
+ * run on any engine and every client-side offer accepts the declaration
+ * directly. Removed in 0.5.0.
* @since 0.1.0
* @category constructors
*/
diff --git a/src/state-cell.ts b/src/state-cell.ts
index 1e59238..4507430 100644
--- a/src/state-cell.ts
+++ b/src/state-cell.ts
@@ -6,9 +6,10 @@
*
* Temporal query handlers are synchronous and read-only, which is why the
* abstraction is a published snapshot rather than an on-demand computation.
- * This module holds the shared definition and codec; the operations live
- * with their process: `setStateCell` in `engine-sandbox`, `readStateCell`
- * in `engine-client`.
+ * This module holds the shared wire contract (query name, definition shape,
+ * codec) the engine halves consume. Applications declare cells with
+ * `defineState` from the `definition` module and never import this one; the
+ * `make` constructor here is deprecated.
*
* @since 0.1.0
*/
@@ -38,9 +39,11 @@ export interface StateCell {
}
/**
- * Declare a state cell: a name (unique within the workflows that use it)
- * and the value schema. Shared by the workflow body and every reading side.
+ * Declare a state-cell primitive (name + value schema).
*
+ * @deprecated Use `defineState` from `definition` — its `.set` runs on any
+ * engine and every client-side read accepts the declaration directly.
+ * Removed in 0.5.0.
* @since 0.1.0
* @category constructors
*/
diff --git a/src/testing.ts b/src/testing.ts
index 9b9bc6f..16f2435 100644
--- a/src/testing.ts
+++ b/src/testing.ts
@@ -15,7 +15,9 @@
*/
import * as Cause from "effect/Cause";
+import * as Clock from "effect/Clock";
import * as Deferred from "effect/Deferred";
+import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
@@ -26,10 +28,21 @@ import type * as Workflow from "effect/unstable/workflow/Workflow";
import type { ActivityRunner, BoundActivity } from "./activities.js";
import { makeWorkflowClient, type WorkflowStartOptions } from "./client.js";
import { WorkflowExecutionAlreadyStartedError, type Client } from "@temporalio/client";
-import type { PayloadOf, SuccessOf } from "./client.js";
-import { WorkflowOps, type UpdateRequest, type WorkflowOpsRuntime } from "./definition.js";
-import { codecsFor } from "./typed-activity.js";
-import { wireCodecsFor, wireValueCodec } from "./wire.js";
+import type { ErrorOf, PayloadOf, SuccessOf } from "./client.js";
+import {
+ sleepUntilTarget,
+ toMailbox,
+ WorkflowOps,
+ type DeferredLike,
+ type MailboxLike,
+ type StateCellLike,
+ type UpdateLike,
+ type UpdateRequest,
+ type WorkflowOpsRuntime,
+} from "./definition.js";
+import { offerMailbox } from "./engine-client.js";
+import { MAILBOX_SIGNAL, mailboxCodec, type MailboxSignalPayload } from "./mailbox.js";
+import { codecsFor, wireCodecsFor, wireValueCodec } from "./wire.js";
/**
* One recorded `workflow.start` call, as the fake captured it.
@@ -95,6 +108,18 @@ export interface FakeTemporalClientOptions {
readonly result?: (start: RecordedWorkflowStart) => unknown;
}
+/**
+ * One mailbox offer the fake captured, decoded through the mailbox's own
+ * payload schema.
+ *
+ * @since 0.4.0
+ * @category models
+ */
+export interface RecordedMailboxOffer(
+ mailbox: MailboxLike,
+ workflowId: string,
+ payload: S["Type"],
+ ) => Effect.Effect(
+ mailbox: MailboxLike,
+ ) => ReadonlyArray(
+ mailbox: MailboxLike,
+ workflowId: string,
+ payload: S["Type"],
+ ): Promise;
+ /** Read a declared state cell's latest snapshot. @since 0.4.0 */
+ stateOf(
+ cell: StateCellLike,
+ workflowId: string,
+ ): Promise