From 128730243291abcf46db3c1c319bd6f1b30df25e Mon Sep 17 00:00:00 2001 From: Ben Weis Date: Thu, 10 Sep 2026 22:08:21 -0400 Subject: [PATCH] =?UTF-8?q?0.5.0:=20remove=20the=20deprecated=20authoring?= =?UTF-8?q?=20surface=20=E2=80=94=20converge=20on=20definition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete typed-activity and versioning (modules and package exports); drop the mailbox/update/state-cell exports and their make constructors (the modules stay internal wire homes); privatize engine-sandbox's per-primitive machinery (callActivity, takeMailbox, pollMailbox, takeUpdate, setStateCell, sleepUntil, continueAsNew, the UpdateRequest alias) behind the Temporal WorkflowOps runtime. deprecateVersion/deprecatePatch/patched move to bundle. prefer-definition keeps its tables so a stale import gets a pointer, and now also flags workflowBundle imported from engine-sandbox. Replay drill: two histories recorded on 0.4.0 (versioned + executeChild; timer racing a mailbox take) replay alongside the 0.3.0 one. The version-chain fixtures author with versioned; generation 1 stays pre-versioning code. --- CHANGELOG.md | 69 ++++--- EXAMPLES.md | 2 +- docs/guide/declaring-capabilities.md | 4 +- docs/guide/introduction.md | 3 +- docs/guide/lint-rules.md | 6 +- docs/guide/versioning.md | 6 +- package.json | 22 +-- src/__tests__/fixtures/batch-workflows.ts | 3 +- src/__tests__/fixtures/chain-workflows-v2.ts | 14 +- src/__tests__/fixtures/chain-workflows-v3.ts | 12 +- .../definition-dispatch-0.4.0.history.b64 | 1 + .../definition-grace-0.4.0.history.b64 | 1 + src/__tests__/fixtures/loop-workflows.ts | 3 +- src/__tests__/lint.test.ts | 28 +-- src/__tests__/replay-compat.test.ts | 36 +++- src/__tests__/types.test.ts | 53 +++--- src/__tests__/versioning-chain.test.ts | 7 +- src/bundle.ts | 50 ++++- src/definition.ts | 26 +-- src/engine-sandbox.ts | 178 +++++------------- src/lint.js | 18 +- src/mailbox.ts | 21 +-- src/state-cell.ts | 21 +-- src/typed-activity.ts | 109 ----------- src/update.ts | 29 +-- src/versioning.ts | 153 --------------- 26 files changed, 275 insertions(+), 600 deletions(-) create mode 100644 src/__tests__/fixtures/histories/definition-dispatch-0.4.0.history.b64 create mode 100644 src/__tests__/fixtures/histories/definition-grace-0.4.0.history.b64 delete mode 100644 src/typed-activity.ts delete mode 100644 src/versioning.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 22fd770..8fc86c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,35 +9,46 @@ 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. -## 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.5.0 (2026-09-10) + +The deletion release: everything deprecated in 0.4.0 is gone, and the API +converges on `definition` + `bundle` + `activities` + `client` / +`engine-client` + `testing` (+ `wire` and the engine-level corners of +`engine-sandbox`). No wire change — see the replay drill below. + +- BREAKING: REMOVED the `typed-activity` module and its package export. + `TypedActivity.make` → `defineActivity`; the type helpers → `definition`; + `codecsFor` / `ACTIVITY_EXIT_TYPE` / `TypedActivityCodecs` → `wire`. +- BREAKING: REMOVED the `versioning` module and its package export. + `match` → `versioned`, `version` → `version` (both `definition`). The + Temporal-only retirement step moved to `bundle`: `deprecateVersion`, + `deprecatePatch`, `patched`. +- BREAKING: REMOVED from `engine-sandbox`: `callActivity`, `takeMailbox`, + `pollMailbox`, `takeUpdate`, `setStateCell`, `sleepUntil`, + `continueAsNew`, and the `UpdateRequest` alias. Their bodies are + now module-private machinery behind the Temporal `WorkflowOps` runtime. + `workflowBundle` (import it from `bundle`), `callRawActivity`, + `offerMailbox` (workflow → workflow), `callNexusWorkflowOperation`, and + `SandboxRun` remain. +- BREAKING: REMOVED the `mailbox`, `update`, and `state-cell` package exports + and their `make` constructors (→ `defineMailbox`, `defineUpdate`, + `defineState`). The modules stay in the source tree as internal wire homes + (`MAILBOX_SIGNAL`, `WORKFLOW_UPDATE`, `STATE_CELL_QUERY`, codecs) for the + engine halves and `testing`. +- `prefer-definition` keeps its tables (message now "was removed in 0.5.0 — + use …"), so a stale import gets a pointer instead of a bare module-not-found; + it also reports `workflowBundle` imported from `engine-sandbox` (→ `bundle`). + The 0.4.0 plan said to empty the tables; keeping them is strictly more useful. +- Replay drill: `replay-compat.test.ts` now also replays two histories + recorded on 0.4.0 — `defDispatch` (`versioned` marker + awaited + `executeChild`) and `defGrace` (durable timer racing a mailbox take) — + alongside the 0.3.0 `defOrder` history. The version-chain fixtures + (generations 2 and 3) author with `versioned`; generation 1 stays + pre-versioning code, so the drill still proves adoption is replay-safe. +- The repository has no remaining import of a removed symbol; the examples + and every fixture author against the current surface. + +Built and tested against `effect@4.0.0-rc.112` and `@temporalio/*@1.19.0`. ## 0.4.0 (2026-09-10) diff --git a/EXAMPLES.md b/EXAMPLES.md index e910a41..070438d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -50,7 +50,7 @@ workflow-semantics content). | [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) | ✅ | `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). | +| [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 — generation 1 is pre-versioning code, generations 2 and 3 adopt and extend `versioned`), [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 diff --git a/docs/guide/declaring-capabilities.md b/docs/guide/declaring-capabilities.md index 7adc6b2..72940a3 100644 --- a/docs/guide/declaring-capabilities.md +++ b/docs/guide/declaring-capabilities.md @@ -56,8 +56,8 @@ And the operations that need no declaration — all from the same module, all re **The declaration is the only symbol you ever name.** Every client-side surface — the `WorkflowClient` service, the standalone `engine-client` operations, the [in-memory test world](/guide/testing#the-in-memory-runtime), the fake client, the live harness — takes the declaration directly. Each declaration still carries its underlying primitive (`Approval.deferred`, `Priority.mailbox`, `SetAmount.update`, `Status.cell`; a defined activity *is* its `TypedActivity` projection) for engine-level code, and every surface accepts that too. The decoded types are named with `PayloadOf`, `SuccessOf<…>`, `ErrorOf<…>` from this module. -::: warning Deprecated authoring surface -The pre-0.3.0 modules — `/typed-activity`, `/versioning`, the `make` constructors of `/mailbox`, `/update`, `/state-cell`, and the per-primitive calls in `/engine-sandbox` (`callActivity`, `takeMailbox`, `takeUpdate`, `setStateCell`, `sleepUntil`, `continueAsNew`) — are **deprecated in 0.4.0 and removed in 0.5.0**. Each has a replacement here; the [`prefer-definition` lint rule](/guide/lint-rules) names it at every remaining import. +::: info Coming from 0.3.x or earlier +The pre-0.4.0 modules — `/typed-activity`, `/versioning`, the `make` constructors of `/mailbox`, `/update`, `/state-cell`, and the per-primitive calls in `/engine-sandbox` (`callActivity`, `takeMailbox`, `takeUpdate`, `setStateCell`, `sleepUntil`, `continueAsNew`) — were deprecated in 0.4.0 and **removed in 0.5.0**. Each has a replacement in the tables above; the [`prefer-definition` lint rule](/guide/lint-rules) names it at every stale import, so migrating is running the linter. ::: ::: info Schemas must be context-free diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index 3db6fd8..fe19ff3 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -21,13 +21,12 @@ The library is one npm package, `@springbird/effect-temporal`, with tree-shakeab | Module | Runs in | What it is | | --- | --- | --- | | `@springbird/effect-temporal/definition` | everywhere | `define*` capability declarations, timers, `continueAsNew`, `executeChild`, `version`/`versioned`, `evolved`, the `WorkflowOps` seam — engine-free | -| `@springbird/effect-temporal/bundle` | the workflow bundle's entry file | `workflowBundle` — hosts registrations behind the bundle's default export, provides the Temporal `WorkflowOps` | +| `@springbird/effect-temporal/bundle` | the workflow bundle's entry file | `workflowBundle` — hosts registrations behind the bundle's default export, provides the Temporal `WorkflowOps`; `deprecateVersion` for retiring version names | | `@springbird/effect-temporal/engine-sandbox` | the workflow bundle | engine-level escape hatches: raw activity proxies (`callRawActivity`), workflow → workflow offers, Nexus calls | | `@springbird/effect-temporal/engine-client` | ordinary Node | the client-side engine + standalone read/signal operations | | `@springbird/effect-temporal/client` | ordinary Node | `WorkflowClient` — the one client service | | `@springbird/effect-temporal/activities` | worker registration | activity implementation tables (`handle`, `implementActivities`) + the attach bridge | | `@springbird/effect-temporal/wire` | engine-level | the wire contract: codecs (`codecsFor`), signal/query names, failure types | -| `@springbird/effect-temporal/typed-activity`, `/versioning`, and the `make` constructors in `/mailbox`, `/update`, `/state-cell` | — | **deprecated** (removed in 0.5.0): re-exports and aliases of the `definition` surface; the `prefer-definition` lint rule reports them | | `@springbird/effect-temporal/nexus` | worker registration | workflow-backed Nexus operations | | `@springbird/effect-temporal/testing` | tests | the in-memory `WorkflowOps` runtime, a typed fake Temporal client, a live test harness | | `@springbird/effect-temporal/lint` | your lint config | oxlint/ESLint rules for the authoring footguns | diff --git a/docs/guide/lint-rules.md b/docs/guide/lint-rules.md index 1a3e5c0..0cfb5b1 100644 --- a/docs/guide/lint-rules.md +++ b/docs/guide/lint-rules.md @@ -11,7 +11,7 @@ The whole Effect program runs inside the Temporal workflow sandbox. `Activity.ma 3. **No module-level mutable state in workflow code** — under the worker's default `reuseV8Context`, module-level variables are shared across every workflow instance on a thread. Keep run state inside the handler. 4. **Never mix the halves** — a module must not import both the sandbox half (`@temporalio/workflow`, `engine-sandbox`) and the client half (`@temporalio/client`, `engine-client`): they can never share a process. 5. **Evaluate versions on the main fiber** — [version / versioned](/guide/versioning) markers evaluated inside forks or races make marker order nondeterministic. -6. **Author with the definition module** — the pre-0.4.0 surface (`typed-activity`, `versioning`, the primitive `make` constructors, the per-primitive `engine-sandbox` calls) is deprecated and removed in 0.5.0; every remaining import is a regression waiting to break. +6. **Author with the definition module** — the pre-0.4.0 surface (`typed-activity`, `versioning`, the primitive `make` constructors, the per-primitive `engine-sandbox` calls) was removed in 0.5.0; a stale import fails to resolve, and this rule tells you what replaced it. ## Setup @@ -46,7 +46,7 @@ Two presets ship: `recommended` (all six rules, `prefer-call-temporal-activity` A file counts as workflow code when it imports `@temporalio/workflow`, the `bundle` module, or the `engine-sandbox` module — the rules are inert elsewhere, so enabling them repo-wide is safe. `no-mixed-halves` applies everywhere by nature. `versioning-on-main-fiber` has one more trigger: importing `version` or `versioned` from the [definition module](/guide/declaring-capabilities) marks the file for that rule (alias-aware), since definition-authored handler modules deliberately import nothing engine-shaped. The other sandbox rules cannot see such modules — a handler that needs them linted can live next to its bundle entry, which imports `bundle`. -`prefer-definition` applies everywhere: it keys off the import source alone (the package specifier or a relative path to one of this package's modules), and its message names the replacement — `defineActivity` for `TypedActivity.make`, `versioned` for `Versioning.match`, the declaration's `.take` for `takeMailbox`, `codecsFor` from `wire`, and so on. Because it is an error in `recommended`, `oxlint` exits non-zero on any file still importing a deprecated symbol — which is what stops a migrated codebase regressing. +`prefer-definition` applies everywhere: it keys off the import source alone (the package specifier or a relative path to one of this package's modules), and its message names the replacement — `defineActivity` for `TypedActivity.make`, `versioned` for `Versioning.match`, the declaration's `.take` for `takeMailbox`, `codecsFor` from `wire`, `bundle` for a `workflowBundle` import from `engine-sandbox`, and so on. Because it is an error in `recommended`, `oxlint` exits non-zero on any file still importing a removed symbol — a migration guide that runs as a lint. The remaining footguns — drain mailboxes before `continueAsNew`, respond to updates before completion — are runtime-shaped and covered by runtime guards and the guide instead. @@ -57,4 +57,4 @@ The remaining footguns — drain mailboxes before `continueAsNew`, respond to up | `no-mixed-halves` | one module importing both process halves | | `prefer-call-temporal-activity` | raw `Effect.promise` where a cancellable call belongs | | `versioning-on-main-fiber` | `version` / `versioned` / `Versioning.*` inside `fork` / `race` / `all` | -| `prefer-definition` | any import of a deprecated symbol (removed in 0.5.0), with its replacement | +| `prefer-definition` | any import of the pre-0.4.0 surface (removed in 0.5.0), with its replacement | diff --git a/docs/guide/versioning.md b/docs/guide/versioning.md index 4b28746..1fbf78b 100644 --- a/docs/guide/versioning.md +++ b/docs/guide/versioning.md @@ -43,10 +43,10 @@ The **key order is the chain order**: the first key is the original, unguarded b ### The lifecycle of a name 1. **Append** `"v3"`. Deploy. Fresh runs take v3; in-flight runs keep replaying their recorded name. -2. **Retire** an old name only after every history carrying its marker has closed: remove it from the list and deploy `deprecateVersion(site, name)` in its place for one release. Replaying a *removed* version's history fails loudly rather than silently running the wrong code. (`deprecateVersion` lives in the `versioning` module today — the one non-deprecated reason to import it; it moves to `bundle` in 0.5.0.) +2. **Retire** an old name only after every history carrying its marker has closed: remove it from the list and deploy `deprecateVersion(site, name)` (from `@springbird/effect-temporal/bundle` — it is Temporal-only, and belongs in the bundle, never in a handler) in its place for one release. Replaying a *removed* version's history fails loudly rather than silently running the wrong code. The raw primitives, `patched(id)` and `deprecatePatch(id)`, are exported from `bundle` too for one-off guards. -::: warning `Versioning.match` is deprecated -The pre-0.4.0 `versioning` module's `match(site, [{ version, run }])` is the Temporal-only ancestor of `versioned` — same markers, same semantics, but it imports `@temporalio/workflow` and so cannot run in the in-memory runtime. It is deprecated and removed in 0.5.0; `versioned(site, { v1: run1, v2: run2 })` is the drop-in replacement, and the `prefer-definition` lint rule points at it. +::: details Coming from `Versioning.match` +The pre-0.4.0 `versioning` module's `match(site, [{ version, run }])` was the Temporal-only ancestor of `versioned` — same markers, same semantics. It was removed in 0.5.0; `versioned(site, { v1: run1, v2: run2 })` is the drop-in replacement (each `{ version, run }` case becomes a `version: run` key), and histories recorded under `match` replay through `versioned` unchanged — the marker ids are the same. ::: ### Rules diff --git a/package.json b/package.json index 2ff1db2..ef4205f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@springbird/effect-temporal", - "version": "0.4.0", + "version": "0.5.0", "description": "Run `effect/unstable/workflow` programs (Workflow / Activity / DurableClock / DurableDeferred) on a Temporal engine, plus durable mailboxes, updates, queryable state, versioning, schedules, and Nexus operations.", "license": "MIT", "type": "module", @@ -43,35 +43,15 @@ "default": "./dist/engine-sandbox.js" }, "./lint": "./dist/lint.js", - "./mailbox": { - "types": "./dist/mailbox.d.ts", - "default": "./dist/mailbox.js" - }, "./nexus": { "types": "./dist/nexus.d.ts", "default": "./dist/nexus.js" }, "./oxlint-presets/*.json": "./oxlint-presets/*.json", - "./state-cell": { - "types": "./dist/state-cell.d.ts", - "default": "./dist/state-cell.js" - }, "./testing": { "types": "./dist/testing.d.ts", "default": "./dist/testing.js" }, - "./typed-activity": { - "types": "./dist/typed-activity.d.ts", - "default": "./dist/typed-activity.js" - }, - "./update": { - "types": "./dist/update.d.ts", - "default": "./dist/update.js" - }, - "./versioning": { - "types": "./dist/versioning.d.ts", - "default": "./dist/versioning.js" - }, "./wire": { "types": "./dist/wire.d.ts", "default": "./dist/wire.js" diff --git a/src/__tests__/fixtures/batch-workflows.ts b/src/__tests__/fixtures/batch-workflows.ts index 5a262e2..1658292 100644 --- a/src/__tests__/fixtures/batch-workflows.ts +++ b/src/__tests__/fixtures/batch-workflows.ts @@ -9,8 +9,9 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Activity from "effect/unstable/workflow/Activity"; import { proxyActivities } from "@temporalio/workflow"; -import { callRawActivity, continueAsNew, offerMailbox } from "../../engine-sandbox.js"; +import { callRawActivity, offerMailbox } from "../../engine-sandbox.js"; import { workflowBundle } from "../../bundle.js"; +import { continueAsNew } from "../../definition.js"; import { BatchDemo, CompletionReports, RecordDemo } from "./batch-demo.js"; const acts = proxyActivities<{ processRecord(index: string): Promise }>({ diff --git a/src/__tests__/fixtures/chain-workflows-v2.ts b/src/__tests__/fixtures/chain-workflows-v2.ts index 224a712..c332596 100644 --- a/src/__tests__/fixtures/chain-workflows-v2.ts +++ b/src/__tests__/fixtures/chain-workflows-v2.ts @@ -1,6 +1,6 @@ // Version-chain demo, generation 2: the first revision, adopting -// `Versioning.match` — the original behavior becomes the unguarded first -// case, so pre-versioning histories replay through it. +// `versioned` — the original behavior becomes the unguarded first case +// (the first key), so pre-versioning histories replay through it. import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; @@ -8,7 +8,7 @@ import * as Activity from "effect/unstable/workflow/Activity"; import { proxyActivities } from "@temporalio/workflow"; import { callRawActivity } from "../../engine-sandbox.js"; import { workflowBundle } from "../../bundle.js"; -import * as Versioning from "../../versioning.js"; +import { versioned } from "../../definition.js"; import { ChainDemo } from "./chain-demo.js"; const acts = proxyActivities<{ greetV1(): Promise; greetV2(): Promise }>({ @@ -19,10 +19,10 @@ const greet = (name: string, call: () => Promise) => Activity.make({ name, success: Schema.String, execute: callRawActivity(call) }); const ChainDemoLive = ChainDemo.toLayer(() => - Versioning.match("greeting", [ - { version: "v1", run: greet("greet", () => acts.greetV1()) }, - { version: "v2", run: greet("greet-v2", () => acts.greetV2()) }, - ]).pipe(Effect.map((greeting) => `greeted:${greeting}`)), + versioned("greeting", { + v1: greet("greet", () => acts.greetV1()), + v2: greet("greet-v2", () => acts.greetV2()), + }).pipe(Effect.map((greeting) => `greeted:${greeting}`)), ); export default workflowBundle(ChainDemoLive); diff --git a/src/__tests__/fixtures/chain-workflows-v3.ts b/src/__tests__/fixtures/chain-workflows-v3.ts index 2549690..3ecdff2 100644 --- a/src/__tests__/fixtures/chain-workflows-v3.ts +++ b/src/__tests__/fixtures/chain-workflows-v3.ts @@ -6,7 +6,7 @@ import * as Activity from "effect/unstable/workflow/Activity"; import { proxyActivities } from "@temporalio/workflow"; import { callRawActivity } from "../../engine-sandbox.js"; import { workflowBundle } from "../../bundle.js"; -import * as Versioning from "../../versioning.js"; +import { versioned } from "../../definition.js"; import { ChainDemo } from "./chain-demo.js"; const acts = proxyActivities<{ @@ -21,11 +21,11 @@ const greet = (name: string, call: () => Promise) => Activity.make({ name, success: Schema.String, execute: callRawActivity(call) }); const ChainDemoLive = ChainDemo.toLayer(() => - Versioning.match("greeting", [ - { version: "v1", run: greet("greet", () => acts.greetV1()) }, - { version: "v2", run: greet("greet-v2", () => acts.greetV2()) }, - { version: "v3", run: greet("greet-v3", () => acts.greetV3()) }, - ]).pipe(Effect.map((greeting) => `greeted:${greeting}`)), + versioned("greeting", { + v1: greet("greet", () => acts.greetV1()), + v2: greet("greet-v2", () => acts.greetV2()), + v3: greet("greet-v3", () => acts.greetV3()), + }).pipe(Effect.map((greeting) => `greeted:${greeting}`)), ); export default workflowBundle(ChainDemoLive); diff --git a/src/__tests__/fixtures/histories/definition-dispatch-0.4.0.history.b64 b/src/__tests__/fixtures/histories/definition-dispatch-0.4.0.history.b64 new file mode 100644 index 0000000..6200eaa --- /dev/null +++ b/src/__tests__/fixtures/histories/definition-dispatch-0.4.0.history.b64 @@ -0,0 +1 @@ +CqECCAESCwjYwo3VBhDAvPVyGAEyjQIKDQoLZGVmRGlzcGF0Y2gqLworcmVjb3JkLTY0OTIwYjRjLWZlZDAtNDI2YS1iMTFhLTg4MzBkMDRhMTZkNxABMkQKQgoWCghlbmNvZGluZxIKanNvbi9wbGFpbhIoeyJvcmRlcklkIjoiaGlzdC0wLjQuMCIsImRpc2NhcmQiOmZhbHNlfToGCICGsJYBQgYIgIawlgFKAggKciQyZjUxZDI2Yi1kMDE4LTQ4MWQtOGQ3ZC03Yjc0YWZhYzIzOGZ6GzMyNjkwQFVyaXMtTWFjLVN0dWRpby5sb2NhbIIBJDJmNTFkMjZiLWQwMTgtNDgxZC04ZDdkLTdiNzRhZmFjMjM4ZpABAaoBAMoBAApKCAISCwjYwo3VBhDAvPVyGAVSNwovCityZWNvcmQtNjQ5MjBiNGMtZmVkMC00MjZhLWIxMWEtODgzMGQwNGExNmQ3EAESAggKGAEKNQgDEgsI2MKN1QYQwLz1chgGWiIIAhIbMzI2OTBAVXJpcy1NYWMtU3R1ZGlvLmxvY2FsKOsCCrcBCAQSDAjYwo3VBhCAxoaPARgHYqIBCAIaGzMyNjkwQFVyaXMtTWFjLVN0dWRpby5sb2NhbCJaQHRlbXBvcmFsaW8vd29ya2VyQDEuMTkuMCs1YmNhNzc5NzJkNTUxMTQ2YzYzYWVlOGI0MDkyNzEwMjgzNzdhZWY5MzEyZmEyNjQ1ZmU2NjNjM2Q2YjNmNzk3MiMIAggBCAMaE3RlbXBvcmFsLXR5cGVzY3JpcHQiBjEuMTkuMGoACoIBCAUSDAjYwo3VBhCAxoaPARgZygFtCgpjb3JlX3BhdGNoEl0KCnBhdGNoLWRhdGESTwpNChYKCGVuY29kaW5nEgpqc29uL3BsYWluEjN7ImlkIjoiZGVmRGlzcGF0Y2gvbW9kZS1yb3V0ZWQiLCJkZXByZWNhdGVkIjpmYWxzZX0YAwp+CAYSDAjYwo3VBhCAxoaPARgo6gJpCAMSZQpjChVUZW1wb3JhbENoYW5nZVZlcnNpb24SSgoTCgR0eXBlEgtLZXl3b3JkTGlzdAoWCghlbmNvZGluZxIKanNvbi9wbGFpbhIbWyJkZWZEaXNwYXRjaC9tb2RlLXJvdXRlZCJdCsgBCAcSDAjYwo3VBhCAxoaPARgdkgKyAQoHZGVmYXVsdBIgMGYxOTNlNmZkOTZlOTExMjhiNzM1YjE4NzU1MTU1ODkaCwoJZGVmRnVsZmlsIi8KK3JlY29yZC02NDkyMGI0Yy1mZWQwLTQyNmEtYjExYS04ODMwZDA0YTE2ZDcQASo0CjIKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SGHsib3JkZXJJZCI6Imhpc3QtMC40LjAifTIAOgBCAggKSANYA2ADegCCAQAKdwgIEgwI2MKN1QYQgMaGjwEYH6ICYgoHZGVmYXVsdBAHGkgKIDBmMTkzZTZmZDk2ZTkxMTI4YjczNWIxODc1NTE1NTg5EiQ4ODEyOWQ2YS1jNTllLTQxMzAtODhmZi0wN2Y5NGRiMzBmNjQiCwoJZGVmRnVsZmlsCksICRIMCNjCjdUGEIDGho8BGAVSNwovCityZWNvcmQtNjQ5MjBiNGMtZmVkMC00MjZhLWIxMWEtODgzMGQwNGExNmQ3EAESAggKGAEKNggKEgwI2MKN1QYQgMaGjwEYBloiCAkSGzMyNjkwQFVyaXMtTWFjLVN0dWRpby5sb2NhbCjgCQqTAQgLEgwI2MKN1QYQwNy3kQEYB2J/CAkaGzMyNjkwQFVyaXMtTWFjLVN0dWRpby5sb2NhbCJaQHRlbXBvcmFsaW8vd29ya2VyQDEuMTkuMCs1YmNhNzc5NzJkNTUxMTQ2YzYzYWVlOGI0MDkyNzEwMjgzNzdhZWY5MzEyZmEyNjQ1ZmU2NjNjM2Q2YjNmNzk3MgBqAArNAQgMEgwI2MKN1QYQwICglQEYIKoCtwEKUQpPChYKCGVuY29kaW5nEgpqc29uL3BsYWluEjV7Il90YWciOiJTdWNjZXNzIiwidmFsdWUiOiJmdWxmaWxsZWQ6cmVzLWhpc3QtMC40LjAifRIHZGVmYXVsdBpICiAwZjE5M2U2ZmQ5NmU5MTEyOGI3MzViMTg3NTUxNTU4ORIkODgxMjlkNmEtYzU5ZS00MTMwLTg4ZmYtMDdmOTRkYjMwZjY0IgsKCWRlZkZ1bGZpbCgHMAgKSwgNEgwI2MKN1QYQwICglQEYBVI3Ci8KK3JlY29yZC02NDkyMGI0Yy1mZWQwLTQyNmEtYjExYS04ODMwZDA0YTE2ZDcQARICCAoYAQo2CA4SDAjYwo3VBhDAgKCVARgGWiIIDRIbMzI2OTBAVXJpcy1NYWMtU3R1ZGlvLmxvY2FsKMENCpMBCA8SDAjYwo3VBhDAiZqWARgHYn8IDRobMzI2OTBAVXJpcy1NYWMtU3R1ZGlvLmxvY2FsIlpAdGVtcG9yYWxpby93b3JrZXJAMS4xOS4wKzViY2E3Nzk3MmQ1NTExNDZjNjNhZWU4YjQwOTI3MTAyODM3N2FlZjkzMTJmYTI2NDVmZTY2M2MzZDZiM2Y3OTcyAGoACnAIEBIMCNjCjdUGEMCJmpYBGAI6XApYClYKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SPHsiX3RhZyI6IlN1Y2Nlc3MiLCJ2YWx1ZSI6InJvdXRlZHxmdWxmaWxsZWQ6cmVzLWhpc3QtMC40LjAifRAO \ No newline at end of file diff --git a/src/__tests__/fixtures/histories/definition-grace-0.4.0.history.b64 b/src/__tests__/fixtures/histories/definition-grace-0.4.0.history.b64 new file mode 100644 index 0000000..e6cb771 --- /dev/null +++ b/src/__tests__/fixtures/histories/definition-grace-0.4.0.history.b64 @@ -0,0 +1 @@ +CpUCCAESDAjYyL3rBxCAjteWARgBMoACCgoKCGRlZkdyYWNlKi8KK3JlY29yZC02NDkyMGI0Yy1mZWQwLTQyNmEtYjExYS04ODMwZDA0YTE2ZDcQATI6CjgKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SHnsib3JkZXJJZCI6Imhpc3QtZ3JhY2UtMC40LjAifToGCICGsJYBQgYIgIawlgFKAggKciQ4ZTg3ZDA0OS1hZjBiLTQ5MmQtOGQ0NS03ZWQ1M2MyNTUxZmV6GzMyNjkwQFVyaXMtTWFjLVN0dWRpby5sb2NhbIIBJDhlODdkMDQ5LWFmMGItNDkyZC04ZDQ1LTdlZDUzYzI1NTFmZZABAaoBAMoBAApLCAISDAjYyL3rBxCAjteWARgFUjcKLworcmVjb3JkLTY0OTIwYjRjLWZlZDAtNDI2YS1iMTFhLTg4MzBkMDRhMTZkNxABEgIIChgBCjYIAxIMCNjIvesHEICO15YBGAZaIggCEhszMjY5MEBVcmlzLU1hYy1TdHVkaW8ubG9jYWwo4AIKtwEIBBIMCNjIvesHEMCkiJkBGAdiogEIAhobMzI2OTBAVXJpcy1NYWMtU3R1ZGlvLmxvY2FsIlpAdGVtcG9yYWxpby93b3JrZXJAMS4xOS4wKzViY2E3Nzk3MmQ1NTExNDZjNjNhZWU4YjQwOTI3MTAyODM3N2FlZjkzMTJmYTI2NDVmZTY2M2MzZDZiM2Y3OTcyIwgDCAEIAhoTdGVtcG9yYWwtdHlwZXNjcmlwdCIGMS4xOS4wagAKHwgFEgwI2Mi96wcQwKSImQEYEaIBCgoBMRIDCJAcGAMKqwEIBhIMCNjIvesHEMCkiJkBGBrSAZUBChdlZmZlY3Qtd29ya2Zsb3ctbWFpbGJveBJdClsKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SQXsibWFpbGJveE5hbWUiOiJkZWZHcmFjZS9jYW5jZWwiLCJwYXlsb2FkIjp7InJlYXNvbiI6InJlY29yZGVkIn19GhszMjY5MEBVcmlzLU1hYy1TdHVkaW8ubG9jYWwKSwgHEgwI2Mi96wcQwKSImQEYBVI3Ci8KK3JlY29yZC02NDkyMGI0Yy1mZWQwLTQyNmEtYjExYS04ODMwZDA0YTE2ZDcQARICCAoYAgo2CAgSDAjYyL3rBxDApIiZARgGWiIIBxIbMzI2OTBAVXJpcy1NYWMtU3R1ZGlvLmxvY2FsKOEGCpYBCAkSDAjYyL3rBxDArYKaARgHYoEBCAcaGzMyNjkwQFVyaXMtTWFjLVN0dWRpby5sb2NhbCJaQHRlbXBvcmFsaW8vd29ya2VyQDEuMTkuMCs1YmNhNzc5NzJkNTUxMTQ2YzYzYWVlOGI0MDkyNzEwMjgzNzdhZWY5MzEyZmEyNjQ1ZmU2NjNjM2Q2YjNmNzk3MgIQAmoACmMIChIMCNjIvesHEMCtgpoBGAI6TwpLCkkKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SL3siX3RhZyI6IlN1Y2Nlc3MiLCJ2YWx1ZSI6ImNhbmNlbGxlZDpyZWNvcmRlZCJ9EAg= \ No newline at end of file diff --git a/src/__tests__/fixtures/loop-workflows.ts b/src/__tests__/fixtures/loop-workflows.ts index 265ea12..25f9c7d 100644 --- a/src/__tests__/fixtures/loop-workflows.ts +++ b/src/__tests__/fixtures/loop-workflows.ts @@ -5,8 +5,9 @@ import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as Activity from "effect/unstable/workflow/Activity"; import { proxyActivities } from "@temporalio/workflow"; -import { callRawActivity, continueAsNew } from "../../engine-sandbox.js"; +import { callRawActivity } from "../../engine-sandbox.js"; import { workflowBundle } from "../../bundle.js"; +import { continueAsNew } from "../../definition.js"; import { CellLoopDemo, LoopDemo, LoopGate, LoopStage } from "./loop-demo.js"; const acts = proxyActivities<{ record(iteration: string): Promise }>({ diff --git a/src/__tests__/lint.test.ts b/src/__tests__/lint.test.ts index e2782bb..3786b15 100644 --- a/src/__tests__/lint.test.ts +++ b/src/__tests__/lint.test.ts @@ -54,29 +54,30 @@ export const fine = pickVersion("site", ["v1", "v2"]); export const fineTable = versioned("site", { v1: Effect.succeed(1), v2: Effect.succeed(2) }); `; -// The deprecated authoring surface, every shape the rule must catch: named -// imports of deprecated engine-sandbox ops, the typed-activity and -// versioning modules (named and namespace), and the primitive constructors. +// The removed (0.5.0) authoring surface, every shape the rule must catch: +// named imports of the old engine-sandbox ops, the typed-activity and +// versioning modules (named and namespace), the primitive constructors — +// and `workflowBundle` from `engine-sandbox`, whose home is `bundle`. const DEPRECATED_IMPORTS = ` -import { callActivity, takeMailbox, sleepUntil, type UpdateRequest } from "@springbird/effect-temporal/engine-sandbox"; +import { callActivity, takeMailbox, sleepUntil, workflowBundle, type UpdateRequest } from "@springbird/effect-temporal/engine-sandbox"; import * as TypedActivity from "@springbird/effect-temporal/typed-activity"; import { codecsFor, make as makeActivity } from "@springbird/effect-temporal/typed-activity"; import * as Versioning from "@springbird/effect-temporal/versioning"; import { make as makeMailbox, MAILBOX_SIGNAL } from "@springbird/effect-temporal/mailbox"; import { make as makeUpdate } from "@springbird/effect-temporal/update"; import { make as makeCell } from "../state-cell.js"; -export const all = [callActivity, takeMailbox, sleepUntil, TypedActivity, codecsFor, makeActivity, Versioning, makeMailbox, MAILBOX_SIGNAL, makeUpdate, makeCell]; +export const all = [callActivity, takeMailbox, sleepUntil, workflowBundle, TypedActivity, codecsFor, makeActivity, Versioning, makeMailbox, MAILBOX_SIGNAL, makeUpdate, makeCell]; export type R = UpdateRequest; `; -// The 0.4.0 authoring surface — nothing here may be reported. +// The current authoring surface — nothing here may be reported. const MODERN = ` import { workflowBundle } from "@springbird/effect-temporal/bundle"; -import { callRawActivity, workflowBundle as legacyBundleImport } from "@springbird/effect-temporal/engine-sandbox"; +import { callRawActivity, offerMailbox } from "@springbird/effect-temporal/engine-sandbox"; import { defineActivity, sleep, continueAsNew, executeChild, versioned, type PayloadOf } from "@springbird/effect-temporal/definition"; import { codecsFor, ACTIVITY_EXIT_TYPE } from "@springbird/effect-temporal/wire"; import { MAILBOX_SIGNAL } from "some-other-lib/mailbox"; -export const all = [workflowBundle, callRawActivity, legacyBundleImport, defineActivity, sleep, continueAsNew, executeChild, versioned, codecsFor, ACTIVITY_EXIT_TYPE, MAILBOX_SIGNAL]; +export const all = [workflowBundle, callRawActivity, offerMailbox, defineActivity, sleep, continueAsNew, executeChild, versioned, codecsFor, ACTIVITY_EXIT_TYPE, MAILBOX_SIGNAL]; export type P = PayloadOf; `; @@ -122,7 +123,7 @@ describe("lint plugin", { concurrent: false }, () => { expect(definitionOutput.match(/effect-temporal\(/g)).toHaveLength(2); }, 60_000); - it("prefer-definition reports every deprecated import with its replacement, and nothing modern", () => { + it("prefer-definition reports every removed import with its replacement, and nothing modern", () => { const directory = mkdtempSync(join(tmpdir(), "effect-workflow-lint-")); const deprecated = join(directory, "deprecated.ts"); const modern = join(directory, "modern.ts"); @@ -131,10 +132,11 @@ describe("lint plugin", { concurrent: false }, () => { const output = runOxlint(directory, [deprecated]); const findings = output.match(/effect-temporal\(prefer-definition\)/g) ?? []; - // callActivity, takeMailbox, sleepUntil, UpdateRequest, * as - // TypedActivity, codecsFor, make (typed-activity), * as Versioning, - // make (mailbox), MAILBOX_SIGNAL, make (update), make (state-cell) - expect(findings).toHaveLength(12); + // callActivity, takeMailbox, sleepUntil, workflowBundle (→ bundle), + // UpdateRequest, * as TypedActivity, codecsFor, make (typed-activity), + // * as Versioning, make (mailbox), MAILBOX_SIGNAL, make (update), + // make (state-cell) + expect(findings).toHaveLength(13); for (const replacement of [ "call the declared activity directly", "`defineActivity` from `definition`", diff --git a/src/__tests__/replay-compat.test.ts b/src/__tests__/replay-compat.test.ts index 55af640..a596c01 100644 --- a/src/__tests__/replay-compat.test.ts +++ b/src/__tests__/replay-compat.test.ts @@ -3,12 +3,20 @@ // through the current bundle // ========================================================================= // -// `fixtures/histories/definition-order-0.3.0.history.b64` is a real -// `defOrder` run recorded with the 0.3.0 `definition-workflows` bundle -// (update, mailbox signal, patch marker, activities, deferred signal). The -// current bundle must replay it clean: activity types, signal and query -// names, update names, and patch-marker ids are byte-identical, and the -// `OrderFlow` handler still issues the same commands in the same order. +// `fixtures/histories/` holds real runs recorded with earlier bundles: +// +// definition-order-0.3.0 `defOrder` on 0.3.0 — update, mailbox signal, +// patch marker, activities, deferred signal +// definition-dispatch-0.4.0 `defDispatch` on 0.4.0 — `versioned` marker + +// an awaited child (`executeChild`) with its +// own activity +// definition-grace-0.4.0 `defGrace` on 0.4.0 — a durable timer racing +// a mailbox take (the take wins, timer cancelled) +// +// The current bundle must replay every one clean: activity types, signal +// and query names, update names, child workflow types, timer commands, and +// patch-marker ids are byte-identical, and each handler still issues the +// same commands in the same order. // // Record a new fixture only when a release INTENDS a wire change — and then // keep the old one too, so the drill covers every generation still in @@ -41,4 +49,20 @@ describe("replay compatibility", { concurrent: false }, () => { const workflowBundle = await bundleWorkflowCode({ workflowsPath }); await expect(Worker.runReplayHistory({ workflowBundle }, history)).resolves.toBeUndefined(); }, 120_000); + + it("replays the 0.4.0 child/versioned and timer/mailbox histories through the current bundle", async () => { + const dispatch = loadHistory("definition-dispatch-0.4.0"); + const dispatchKinds = new Set(dispatch.events.map((event) => event.eventType)); + expect(dispatchKinds.has(proto.api.enums.v1.EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED)).toBe(true); + expect(dispatchKinds.has(proto.api.enums.v1.EventType.EVENT_TYPE_MARKER_RECORDED)).toBe(true); + + const grace = loadHistory("definition-grace-0.4.0"); + const graceKinds = new Set(grace.events.map((event) => event.eventType)); + expect(graceKinds.has(proto.api.enums.v1.EventType.EVENT_TYPE_TIMER_STARTED)).toBe(true); + expect(graceKinds.has(proto.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED)).toBe(true); + + const workflowBundle = await bundleWorkflowCode({ workflowsPath }); + await expect(Worker.runReplayHistory({ workflowBundle }, dispatch)).resolves.toBeUndefined(); + await expect(Worker.runReplayHistory({ workflowBundle }, grace)).resolves.toBeUndefined(); + }, 120_000); }); diff --git a/src/__tests__/types.test.ts b/src/__tests__/types.test.ts index 113e52e..387dc2f 100644 --- a/src/__tests__/types.test.ts +++ b/src/__tests__/types.test.ts @@ -30,14 +30,10 @@ import { } from "../engine-client.js"; import { callRawActivity, - continueAsNew, offerMailbox as offerMailboxFromWorkflow, - setStateCell, - takeMailbox, - takeUpdate, type SandboxRun, } from "../engine-sandbox.js"; -import * as Versioning from "../versioning.js"; +import { continueAsNew, version, versioned, type WorkflowOps } from "../definition.js"; import { ChildDemo } from "./fixtures/child-demo.js"; import { Demo } from "./fixtures/demo.js"; import { LoopDemo } from "./fixtures/loop-demo.js"; @@ -99,14 +95,16 @@ const _activities = () => { const _mailboxes = () => { const { client } = clientOptions; - const taken = takeMailbox(StateUpdates.mailbox); + const taken = StateUpdates.take; expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); return [ taken, + // The declaration and its primitive are both accepted client-side. + offerMailbox(StateUpdates, { client, workflowId: "id", payload: { op: "finish" } }), offerMailbox(StateUpdates.mailbox, { client, workflowId: "id", payload: { op: "finish" } }), - offerMailboxFromWorkflow(StateUpdates.mailbox, { workflowId: "id", payload: { op: "finish" } }), + offerMailboxFromWorkflow(StateUpdates, { workflowId: "id", payload: { op: "finish" } }), // @ts-expect-error unknown mailbox op offerMailbox(StateUpdates.mailbox, { client, workflowId: "id", payload: { op: "reset" } }), // @ts-expect-error a set requires key and value @@ -125,9 +123,10 @@ const _stateCells = () => { return [ read, - setStateCell(StateSnapshot.cell, { a: 1 }), + readStateCell(StateSnapshot, { client: clientOptions.client, workflowId: "id" }), + StateSnapshot.set({ a: 1 }), // @ts-expect-error cell values are numbers - setStateCell(StateSnapshot.cell, { a: "one" }), + StateSnapshot.set({ a: "one" }), ]; }; @@ -145,8 +144,8 @@ const _updates = () => { // @ts-expect-error payload must match the update's schema executeUpdate(SetLanguage.update, { client, workflowId: "id", payload: { lang: "x" } }); - const taken = takeUpdate(SetLanguage.update); - expectTypeOf>().toEqualTypeOf(); + const taken = SetLanguage.take; + expectTypeOf>().toEqualTypeOf(); const served = Effect.andThen(taken, (request) => { expectTypeOf(request.payload).toEqualTypeOf<{ readonly language: string }>(); expectTypeOf(request.respond).parameter(0).toEqualTypeOf>(); @@ -182,33 +181,31 @@ const _deferreds = () => { }; const _versioning = () => { - const selected = Versioning.version("site", ["v1", "v2", "v3"]); + const selected = version("site", ["v1", "v2", "v3"]); expectTypeOf>().toEqualTypeOf<"v1" | "v2" | "v3">(); - const matched = Versioning.match("site", [ - { version: "v1", run: Effect.succeed(1) }, - { version: "v2", run: Effect.fail("legacy-error") }, - { version: "v3", run: Effect.succeed("three") }, - ]); + const matched = versioned("site", { + v1: Effect.succeed(1), + v2: Effect.fail("legacy-error"), + v3: Effect.succeed("three"), + }); expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); // Heterogeneous cases reduce with full fidelity: distinct result shapes // union, an always-failing case contributes `never` to the success union, - // and a case requiring services carries its requirement into the union. - const heterogeneous = Versioning.match("pricing", [ - { version: "v1", run: Effect.succeed({ kind: "flat" as const, cents: 100 }) }, - { version: "v2", run: Effect.fail({ kind: "quote-required" as const }) }, - { - version: "v3", - run: callRawActivity(() => Promise.resolve({ kind: "dynamic" as const, quote: "q" })), - }, - ]); + // and a case requiring services carries its requirement into the union + // (alongside the seam every case dispatches through). + const heterogeneous = versioned("pricing", { + v1: Effect.succeed({ kind: "flat" as const, cents: 100 }), + v2: Effect.fail({ kind: "quote-required" as const }), + v3: callRawActivity(() => Promise.resolve({ kind: "dynamic" as const, quote: "q" })), + }); expectTypeOf>().toEqualTypeOf< { kind: "flat"; cents: number } | { kind: "dynamic"; quote: string } >(); expectTypeOf>().toEqualTypeOf<{ kind: "quote-required" }>(); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); // The selected literal narrows through ordinary control flow. const narrowed = Effect.map(selected, (name) => { diff --git a/src/__tests__/versioning-chain.test.ts b/src/__tests__/versioning-chain.test.ts index 74da671..1ef1f96 100644 --- a/src/__tests__/versioning-chain.test.ts +++ b/src/__tests__/versioning-chain.test.ts @@ -1,9 +1,10 @@ -// `Versioning.match` across three generations of the same workflow, +// `versioned` across three generations of the same workflow, // mirroring the Temporal `patching-api` sample (see EXAMPLES.md): every // older generation's history must replay clean through every newer bundle // (taking its own version's branch), fresh runs must take the newest // version, and the unguarded control must fail replay — proving the drill -// can see the change at all. +// can see the change at all. Generation 1 is pre-versioning code; adopting +// `versioned` on it (generation 2) must be replay-safe. import { fileURLToPath } from "node:url"; import * as Effect from "effect/Effect"; @@ -61,7 +62,7 @@ describe("version chains", { concurrent: false }, () => { expect(generation1.result).toBe("greeted:hello-v1"); // Its history replays through both versioned bundles (v1 branch) — - // adopting `Versioning.match` on an existing workflow is safe. + // adopting `versioned` on an existing workflow is safe. await replay("v2", generation1.history); await replay("v3", generation1.history); diff --git a/src/bundle.ts b/src/bundle.ts index ed179da..c2509f2 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -17,14 +17,22 @@ * * This is the whole public sandbox-side surface for applications authored * with the `definition` module: handlers import `definition` (and their own - * modules), the bundle entry imports this. Everything else in the sandbox - * half (`engine-sandbox`) is engine machinery — raw activity proxies, the - * Nexus caller, workflow-to-workflow offers — for code that deliberately - * steps below the declaration surface. + * modules), the bundle entry imports this. The one other thing here is the + * Temporal-only retirement step of a version chain (`deprecateVersion`), + * which belongs in a bundle, never in a handler. Everything else in the + * sandbox half (`engine-sandbox`) is engine machinery — raw activity + * proxies, the Nexus caller, workflow-to-workflow offers — for code that + * deliberately steps below the declaration surface. * * @since 0.4.0 */ +import * as Effect from "effect/Effect"; +import { + deprecatePatch as temporalDeprecatePatch, + patched as temporalPatched, +} from "@temporalio/workflow"; + export { /** * Host `Workflow.toLayer` registrations behind one dynamic Temporal @@ -35,3 +43,37 @@ export { */ workflowBundle, } from "./engine-sandbox.js"; + +/** + * `true` on fresh executions (records the patch marker), `false` when + * replaying a history recorded before this patch id existed — Temporal's + * raw patch primitive, for one-off guards outside a `version` chain. + * + * @since 0.5.0 + * @category versioning + */ +export const patched = (patchId: string): Effect.Effect => + Effect.sync(() => temporalPatched(patchId)); + +/** + * Phase two of Temporal's patch lifecycle: keep a marker recognized for + * histories that carry it while no longer branching. Deploy after every + * pre-patch execution has drained. + * + * @since 0.5.0 + * @category versioning + */ +export const deprecatePatch = (patchId: string): Effect.Effect => + Effect.sync(() => temporalDeprecatePatch(patchId)); + +/** + * Retire one name of a `version` / `versioned` site: `deprecatePatch` for + * its marker `${site}-${name}`. Remove the name from the chain and deploy + * this in its place for one release, once every history carrying the marker + * has closed. + * + * @since 0.5.0 + * @category versioning + */ +export const deprecateVersion = (site: string, name: string): Effect.Effect => + deprecatePatch(`${site}-${name}`); diff --git a/src/definition.ts b/src/definition.ts index 19cbfce..61b2fa9 100644 --- a/src/definition.ts +++ b/src/definition.ts @@ -45,9 +45,9 @@ import * as Schema from "effect/Schema"; import * as SchemaGetter from "effect/SchemaGetter"; import * as DurableDeferred from "effect/unstable/workflow/DurableDeferred"; import type * as Workflow from "effect/unstable/workflow/Workflow"; -import * as DurableMailbox from "./mailbox.js"; -import * as DurableUpdate from "./update.js"; -import * as StateCell from "./state-cell.js"; +import type * as DurableMailbox from "./mailbox.js"; +import type * as DurableUpdate from "./update.js"; +import type * as StateCell from "./state-cell.js"; // ─── Activity declarations (the types) ─────────────────────────────────────── @@ -133,13 +133,8 @@ export type SuccessOf = export type ErrorOf = A extends TypedActivity ? E["Type"] : never; -/** - * Build the serializable projection of an activity declaration. Shared by - * `defineActivity` and the deprecated `TypedActivity.make`. - * - * @internal - */ -export const makeTypedActivity = < +/** Build the serializable projection of an activity declaration. */ +const makeTypedActivity = < const Name extends string, Payload extends Schema.Struct.Fields | Schema.Top, Success extends Schema.Top = Schema.Void, @@ -410,7 +405,7 @@ export const defineMailbox = ( name: string, decl: { readonly payload: Payload }, ): DefinedMailbox => { - const mailbox = DurableMailbox.make(name, { payload: decl.payload }); + const mailbox: DurableMailbox.DurableMailbox = { name, payloadSchema: decl.payload }; return { name, mailbox, @@ -465,7 +460,12 @@ export const defineUpdate = < name: string, decl: { readonly payload: Payload; readonly success: Success; readonly error: Error }, ): DefinedUpdate => { - const update = DurableUpdate.make(name, decl); + const update: DurableUpdate.DurableUpdate = { + name, + payloadSchema: decl.payload, + successSchema: decl.success, + errorSchema: decl.error, + }; return { name, update, @@ -502,7 +502,7 @@ export const defineState = ( name: string, decl: { readonly value: Value }, ): DefinedState => { - const cell = StateCell.make(name, { value: decl.value }); + const cell: StateCell.StateCell = { name, valueSchema: decl.value }; return { name, cell, diff --git a/src/engine-sandbox.ts b/src/engine-sandbox.ts index a227fff..656bae3 100644 --- a/src/engine-sandbox.ts +++ b/src/engine-sandbox.ts @@ -1,19 +1,14 @@ /** * Sandbox half — engine machinery. Applications import `workflowBundle` * from the `bundle` module (the one file the Temporal worker points at) and - * author handlers with the `definition` module; nothing else here is part of - * the declaration surface: - * - * - **Engine-level, kept:** `callRawActivity` (invoke a foreign Temporal - * activity proxy cancellably), `offerMailbox` (workflow → workflow), - * `callNexusWorkflowOperation`, `SandboxRun`. These have no - * engine-agnostic counterpart and are for code that deliberately steps - * below the declaration surface. - * - **Deprecated, removed in 0.5.0:** the per-primitive calls - * (`callActivity`, `takeMailbox`, `pollMailbox`, `takeUpdate`, - * `setStateCell`), `sleepUntil`, `continueAsNew`, and the `UpdateRequest` - * alias — each has a `definition` replacement named in its JSDoc. They - * remain the machinery the Temporal `WorkflowOps` runtime dispatches into. + * author handlers with the `definition` module. What this module exports + * beyond that is engine-level, for code that deliberately steps below the + * declaration surface: `callRawActivity` (invoke a foreign Temporal activity + * proxy cancellably), `offerMailbox` (workflow → workflow), + * `callNexusWorkflowOperation`, `SandboxRun`. The per-primitive machinery + * the Temporal `WorkflowOps` runtime dispatches into (activity calls, + * mailbox takes, update takes, state publishes, timers, continue-as-new, + * patch markers) is module-private since 0.5.0. * * `workflowBundle(layer)` hosts plain `Workflow.toLayer` registrations * behind one dynamic Temporal workflow — export it as the workflow bundle's @@ -76,6 +71,7 @@ import { defineQuery, defineSignal, getExternalWorkflowHandle, + patched, proxyActivities, setHandler, sleep, @@ -116,10 +112,9 @@ import { type ErrorOf, type PayloadOf, type SuccessOf, - type UpdateRequest as DefUpdateRequest, + type UpdateRequest, type WorkflowOpsRuntime, } from "./definition.js"; -import * as Versioning from "./versioning.js"; import { updateCodec, WORKFLOW_UPDATE, @@ -270,17 +265,12 @@ const typedActivityProxies = new Map Promise const typedActivityProxyKey = (activity: AnyTypedActivity): string => `${activity.name}|${JSON.stringify(activity.options)}`; -/** - * Call a `TypedActivity` definition: the payload is schema-encoded onto the - * wire, the result decoded, and a typed failure the worker raised (a +/** Call a declared activity: the payload is schema-encoded onto the wire, + * the result decoded, and a typed failure the worker raised (a * non-retryable `ACTIVITY_EXIT_TYPE` failure) lands in the Effect error * channel. Infra failures and exhausted retries stay defects. Runs under the - * same per-call cancellable scope as `callRawActivity`. - * - * @since 0.1.0 - * @category workflow - */ -export const callActivity = ( + * same per-call cancellable scope as `callRawActivity`. */ +const callTypedActivity = ( activity: A, payload: PayloadOf, ): Effect.Effect, ErrorOf, SandboxRun> => { @@ -308,25 +298,10 @@ export const callActivity = ( ); }; -/** - * Sleep durably until an absolute time, no-op when it is already past. The +/** Sleep durably until an absolute time, no-op when already past. The * target is read against the sandbox's deterministic clock, so the delay is - * stable on replay. The timestamp rule is `sleepUntilTarget` (definition): - * zone-less date-time strings and unparseable timestamps die loudly. - * - * @deprecated Use `sleepUntil` from `definition` — same timer, same rule, - * runs on any engine. Removed in 0.5.0. - * @since 0.1.0 - * @category workflow - */ -export const sleepUntil = (options: { - readonly name: string; - /** Epoch milliseconds, or a date-time string CARRYING ITS ZONE (`Z` or an - * explicit offset; date-only forms are UTC per ECMAScript). Zone-less - * date-times are rejected: `Date.parse` reads them in the worker's local - * timezone, which is nondeterministic across workers and replays. */ - readonly timestamp: number | string; -}) => + * stable on replay; the timestamp rule is `sleepUntilTarget`. */ +const sleepUntil = (options: { readonly name: string; readonly timestamp: number | string }) => Effect.gen(function* () { const now = yield* Clock.currentTimeMillis; const target = yield* sleepUntilTarget(options); @@ -345,34 +320,11 @@ const updateBuffer = (run: RunState, name: string): PendingUpdate[] => { return buffer; }; -/** - * A taken update request — the LEGACY spelling with type parameters in - * `` order. - * - * @deprecated Use `UpdateRequest` from - * `definition` (Type-level parameters, in declaration order). Removed in - * 0.5.0. - * @since 0.1.0 - * @category models - */ -export type UpdateRequest = DefUpdateRequest< - P, - S["Type"], - E["Type"] ->; - -/** - * 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 - */ -export const takeUpdate =

( +/** Durably await the next `executeUpdate` request for `update`, in delivery + * order. The claim is synchronous on the taking fiber, like `takeMailbox`. */ +const takeUpdate =

( update: DurableUpdate, -): Effect.Effect, never, SandboxRun> => +): Effect.Effect, never, SandboxRun> => Effect.gen(function* () { const run = yield* SandboxRunTag; const buffer = updateBuffer(run, update.name); @@ -421,17 +373,10 @@ const mailboxBuffer = (run: RunState, name: string): unknown[] => { return buffer; }; -/** - * Durably await the next message offered to `mailbox`, in delivery order. +/** Durably await the next message offered to `mailbox`, in delivery order. * 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 - */ -export const takeMailbox = ( + * interrupted take never steals a message from a later one. */ +const takeMailbox = ( mailbox: DurableMailbox, ): Effect.Effect => Effect.gen(function* () { @@ -460,18 +405,9 @@ export const takeMailbox = ( } }); -/** - * Take the next message if one is buffered, without waiting — `None` when - * the mailbox is empty. The non-blocking counterpart to `takeMailbox`; its - * 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 - */ -export const pollMailbox = ( +/** Take the next message if one is buffered, without waiting — `None` when + * the mailbox is empty. */ +const pollMailbox = ( mailbox: DurableMailbox, ): Effect.Effect, never, SandboxRun> => Effect.gen(function* () { @@ -526,17 +462,10 @@ export const offerMailbox = ( }); }; -/** - * Publish a snapshot to `cell`, replacing the previous one. Readable from +/** Publish a snapshot to `cell`, replacing the previous one. Readable from * 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 - */ -export const setStateCell = ( + * closes. */ +const setStateCell = ( cell: StateCell, value: S["Type"], ): Effect.Effect => @@ -545,33 +474,6 @@ export const setStateCell = ( run.stateCells.set(cell.name, stateCellCodec(cell).encode(value)); }); -/** - * End this run and atomically start a fresh one of the SAME workflow with - * `payload`, keeping the workflow id (and so the execution id) while - * resetting history — Temporal's continue-as-new, for unbounded workflows. - * - * Like the native API, this unwinds the current run as a throw: Effect - * finalizers and `Workflow.withCompensation` steps run on the way out, so - * call it at iteration boundaries, outside compensation regions. Mailbox - * 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 - */ -export const continueAsNew = < - Tag extends string, - Payload extends Workflow.AnyStructSchema, - Success extends Schema.Top, - Error extends Schema.Top, ->( - workflow: Workflow.Workflow, - payload: Payload["Type"], - options?: { readonly memo?: Record }, -): Effect.Effect => continueAsNewDecoded(workflow, payload, options); - /** The engine-level continue-as-new: encode the DECODED payload through the * workflow's own codec (a schema-invalid payload dies here, as it would for * a client start) and hand the wire to Temporal. */ @@ -1112,6 +1014,22 @@ const buildRegistry = ( return registry; }); +/** Which version of `site` this execution runs: names ordered oldest first, + * checked newest-first so a fresh execution records only the newest marker + * `${site}-${name}`; the first name is the unguarded original. The marker + * ids are the wire — `versioned` / `version` in `definition` share them + * with every history recorded since 0.1.0. */ +const patchedVersion = ( + site: string, + names: Names, +): Effect.Effect => + Effect.sync(() => { + for (let i = names.length - 1; i >= 1; i--) { + if (patched(`${site}-${names[i]!}`)) return names[i]!; + } + return names[0]; + }); + /** Forget ONLY the requirements of a sandbox op; success and error survive. */ const eraseR = (effect: Effect.Effect): Effect.Effect => effect as Effect.Effect; @@ -1132,13 +1050,13 @@ const temporalWorkflowOps: WorkflowOpsRuntime = { // (and the engine services) at the type level, and the per-run wrapper // provides them, same discipline as SandboxHandler. Success and error // shapes stay compile-checked against the seam. - activity: (activity, payload) => eraseR(callActivity(activity, payload)), + activity: (activity, payload) => eraseR(callTypedActivity(activity, payload)), deferredAwait: (deferred) => eraseR(DurableDeferred.await(deferred)), mailboxTake: (mailbox) => eraseR(takeMailbox(mailbox)), mailboxPoll: (mailbox) => eraseR(pollMailbox(mailbox)), updateTake: (update) => eraseR(takeUpdate(update)), stateSet: (cell, value) => eraseR(setStateCell(cell, value)), - version: (site, names) => Versioning.version(site, names), + version: (site, names) => patchedVersion(site, names), sleep: (options) => eraseR(DurableClock.sleep(options)), sleepUntil: (options) => eraseR(sleepUntil(options)), continueAsNew: (workflow, payload, options) => continueAsNewDecoded(workflow, payload, options), diff --git a/src/lint.js b/src/lint.js index 71024e1..b1dba5d 100644 --- a/src/lint.js +++ b/src/lint.js @@ -31,11 +31,13 @@ const moduleOf = (source) => { return match?.[1]; }; -/** The deprecated authoring surface (removed in 0.5.0) and what replaces - * each symbol. `*` covers the module's namespace import and any named - * import not listed individually. */ +/** The pre-0.4.0 authoring surface — deprecated in 0.4.0, REMOVED in 0.5.0 + * — and what replaces each symbol, so a stale import gets a pointer instead + * of a bare "module not found". `*` covers the module's namespace import + * and any named import not listed individually. */ const DEPRECATED = { "engine-sandbox": { + workflowBundle: "`workflowBundle` from `bundle`", callActivity: "call the declared activity directly (`yield* Charge(payload)`, from `defineActivity` in `definition`)", takeMailbox: "the declaration's `.take` (`defineMailbox` in `definition`)", pollMailbox: "the declaration's `.poll` (`defineMailbox` in `definition`)", @@ -283,13 +285,13 @@ const rules = { type: "problem", docs: { description: - "Report imports of the deprecated authoring surface (engine-sandbox per-primitive " + + "Report imports of the pre-0.4.0 authoring surface (engine-sandbox per-primitive " + "calls, typed-activity, versioning, the mailbox/update/state-cell constructors) — " + - "each has a `definition`, `bundle`, or `wire` replacement, and the deprecated " + - "symbols are removed in 0.5.0.", + "deprecated in 0.4.0 and removed in 0.5.0; each has a `definition`, `bundle`, or " + + "`wire` replacement named in the message.", }, messages: { - deprecated: "`{{name}}` from `{{module}}` is deprecated (removed in 0.5.0) — use {{replacement}}.", + deprecated: "`{{name}}` from `{{module}}` was removed in 0.5.0 — use {{replacement}}.", }, schema: [], }, @@ -327,6 +329,6 @@ const rules = { }; export default { - meta: { name: "effect-temporal", version: "0.4.0" }, + meta: { name: "effect-temporal", version: "0.5.0" }, rules, }; diff --git a/src/mailbox.ts b/src/mailbox.ts index 6089b9f..2554336 100644 --- a/src/mailbox.ts +++ b/src/mailbox.ts @@ -4,10 +4,10 @@ * signal (recorded in history, so consumption is deterministic on replay) * and buffer until the workflow takes them. * - * This module holds the shared wire contract (signal name, definition - * shape, codec) the engine halves consume. Applications declare mailboxes - * with `defineMailbox` from the `definition` module and never import this - * one; the `make` constructor here is deprecated. + * **Internal.** This module holds the shared wire contract (signal name, + * definition shape, codec) the engine halves and `testing` consume. It has + * no package export: applications declare mailboxes with `defineMailbox` + * from the `definition` module. * * @since 0.1.0 */ @@ -47,19 +47,6 @@ export interface DurableMailbox { readonly payloadSchema: S; } -/** - * 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 - */ -export const make = ( - name: string, - options: { readonly payload: S }, -): DurableMailbox => ({ name, payloadSchema: options.payload }); /** * The wire codec for a mailbox's payload — how messages are encoded by diff --git a/src/state-cell.ts b/src/state-cell.ts index 4507430..92dfc02 100644 --- a/src/state-cell.ts +++ b/src/state-cell.ts @@ -6,10 +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 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. + * **Internal.** This module holds the shared wire contract (query name, + * definition shape, codec) the engine halves consume. It has no package + * export: applications declare cells with `defineState` from the + * `definition` module. * * @since 0.1.0 */ @@ -38,19 +38,6 @@ export interface StateCell { readonly valueSchema: S; } -/** - * 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 - */ -export const make = ( - name: string, - options: { readonly value: S }, -): StateCell => ({ name, valueSchema: options.value }); /** * The wire codec for a cell's value — how snapshots are encoded when diff --git a/src/typed-activity.ts b/src/typed-activity.ts deleted file mode 100644 index 07d2a04..0000000 --- a/src/typed-activity.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * **Deprecated module** — superseded by the `definition` module in 0.3.0 and - * scheduled for removal in 0.5.0. Everything here is a re-export or a thin - * alias so existing imports keep compiling while you migrate: - * - * - `TypedActivity.make(name, decl)` → `defineActivity(name, decl)` from - * `@springbird/effect-temporal/definition` (a declared activity IS its - * `TypedActivity` projection, and is callable inside handlers). - * - the type helpers `PayloadOf`, `SuccessOf`, `ErrorOf`, `AnyTypedActivity`, - * `TypedActivity`, `TypedActivityOptions` → the same names from - * `@springbird/effect-temporal/definition`. - * - `codecsFor`, `ACTIVITY_EXIT_TYPE` → the same names from - * `@springbird/effect-temporal/wire`. - * - * The `prefer-definition` lint rule reports every import from this module. - * - * @deprecated Import from `definition` (types, `defineActivity`) or `wire` - * (`codecsFor`, `ACTIVITY_EXIT_TYPE`) instead. Removed in 0.5.0. - * @since 0.1.0 - */ - -import { defineActivity, makeTypedActivity } from "./definition.js"; - -export { - /** - * @deprecated Import `ACTIVITY_EXIT_TYPE` from `wire`. Removed in 0.5.0. - * @since 0.1.0 - * @category wire - */ - ACTIVITY_EXIT_TYPE, - /** - * @deprecated Import `codecsFor` from `wire`. Removed in 0.5.0. - * @since 0.1.0 - * @category codecs - */ - codecsFor, - /** - * @deprecated Import `TypedActivityCodecs` from `wire`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type TypedActivityCodecs, -} from "./wire.js"; - -export { - /** - * @deprecated Import `DEFAULT_ACTIVITY_OPTIONS` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - DEFAULT_ACTIVITY_OPTIONS, - /** - * @deprecated Import `AnyTypedActivity` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type AnyTypedActivity, - /** - * @deprecated Import `ErrorOf` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type ErrorOf, - /** - * @deprecated Import `PayloadOf` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type PayloadOf, - /** - * @deprecated Import `SuccessOf` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type SuccessOf, - /** - * @deprecated Import `TypedActivity` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type TypedActivity, - /** - * @deprecated Import `TypedActivityOptions` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category models - */ - type TypedActivityOptions, -} from "./definition.js"; - -/** - * Declare a typed activity — the pre-0.3.0 spelling of `defineActivity`. - * Returns exactly what `defineActivity` returns (the projection plus the - * in-handler callable), so migrating is renaming the import. - * - * @deprecated Use `defineActivity` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - * @category constructors - */ -export const make: typeof defineActivity = defineActivity; - -export { - /** - * The projection-only constructor, for engine-level code that needs a - * `TypedActivity` without the callable. - * - * @internal - */ - makeTypedActivity, -}; diff --git a/src/update.ts b/src/update.ts index 2bb8cd1..57537aa 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,12 +1,13 @@ /** * Request/response into a running workflow — Temporal updates with typed * channels: the caller gets the handler's typed success or typed failure - * back, unlike a fire-and-forget mailbox message. This module holds the - * shared wire contract (update name, definition shape, codec) the engine - * halves consume. Applications declare updates with `defineUpdate` from the + * back, unlike a fire-and-forget mailbox message. + * + * **Internal.** This module holds the shared wire contract (update name, + * definition shape, codec) the engine halves consume. It has no package + * export: applications declare updates with `defineUpdate` from the * `definition` module (`.take` in the handler, `wf.executeUpdate` from - * clients) and never import this one; the `make` constructor here is - * deprecated. + * clients). * * The response always travels as a wire-encoded Exit in the update RESULT * (the update itself never fails), so typed failures round-trip without a @@ -53,24 +54,6 @@ export interface DurableUpdate

( - name: string, - options: { readonly payload: P; readonly success: S; readonly error: E }, -): DurableUpdate => ({ - name, - payloadSchema: options.payload, - successSchema: options.success, - errorSchema: options.error, -}); /** * The encode/decode pairs for an update's two crossings: the request diff --git a/src/versioning.ts b/src/versioning.ts deleted file mode 100644 index d8cd743..0000000 --- a/src/versioning.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * **Deprecated module** — the Temporal-only patch-marker primitives behind - * the `definition` module's engine-agnostic `version` / `versioned`, which - * are what applications author with. Scheduled for removal in 0.5.0: - * - * - `Versioning.match(site, cases)` → `versioned(site, { v1: run1, v2: run2 })` - * - `Versioning.version(site, names)` → `version(site, names)` - * - `deprecateVersion` / `deprecatePatch` / `patched` → the retirement step - * stays a Temporal concern; it moves to the `bundle` module in 0.5.0. - * - * The `prefer-definition` lint rule reports every import from this module. - * - * Workflow versioning over Temporal patch markers, for changing workflow - * code while executions are in flight. - * - * The composable form is a version CHAIN per code site: the first case is - * the original behavior (guarded by no marker, so histories from before the - * site adopted versioning replay through it), and each later case is - * guarded by its own patch marker `${site}-${name}`. Fresh executions take - * the newest case and record only its marker; replays take the case whose - * marker their history carries. - * - * ```ts - * const result = yield* Versioning.match("pricing", [ - * { version: "v1", run: originalPricing }, - * { version: "v2", run: revisedPricing }, - * ]); - * ``` - * - * Evolving a site is appending a case. Retiring one is removing it AFTER - * every history carrying its marker has closed, then `deprecateVersion` — - * replaying a removed version's history fails loudly rather than silently - * running the wrong code. - * - * Evaluate a site's version at a deterministic point on the main workflow - * fiber, never from racing fibers — marker order is part of history. As - * with Temporal's native API, a site re-evaluated in a LATER workflow task - * of the same run may advance to a newer version; evaluate once and reuse - * the result where that matters. - * - * Sandbox-only: this module imports `@temporalio/workflow`. - * - * @deprecated Author with `version` / `versioned` from `definition`. Removed in 0.5.0. - * @since 0.1.0 - */ - -import * as Effect from "effect/Effect"; -import { - deprecatePatch as temporalDeprecatePatch, - patched as temporalPatched, -} from "@temporalio/workflow"; - -/** - * One case of a version chain: the version's name and its behavior. - * - * @since 0.1.0 - * @category models - */ -export interface Version { - readonly version: Name; - readonly run: Effect.Effect; -} - -/** - * Which version of `site` this execution runs, as a literal of `names` - * (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 - */ -export const version = ( - site: string, - names: Names, -): Effect.Effect => - Effect.sync(() => { - for (let i = names.length - 1; i >= 1; i--) { - if (temporalPatched(`${site}-${names[i]!}`)) return names[i]!; - } - return names[0]; - }); - -/** - * Run the case `version(site, ...)` selects — the effectful form of the - * patch branch, with the result, error, and service channels unioned across - * cases. - * - * @deprecated Use `versioned(site, { v1: run1, v2: run2 })` from - * `definition` — same markers, engine-agnostic. Removed in 0.5.0. - * @since 0.1.0 - * @category combinators - */ -export const match = < - const Cases extends readonly [ - Version, - ...Version[], - ], ->( - site: string, - cases: Cases, -): Effect.Effect< - Effect.Success, - Effect.Error, - Effect.Services -> => { - // SAFETY: `cases` is a non-empty tuple, so mapping it yields a non-empty - // array of version names. - const names = cases.map((entry) => entry.version) as unknown as readonly [string, ...string[]]; - // SAFETY: `version` returns one of the cases' names, so `find` always - // matches, and the matched case's run is covered by the channels unioned - // over `Cases[number]`. - return Effect.flatMap( - version(site, names), - (name) => cases.find((entry) => entry.version === name)!.run, - ) as Effect.Effect< - Effect.Success, - Effect.Error, - Effect.Services - >; -}; - -/** - * `true` on fresh executions (records the patch marker), `false` when - * replaying a history recorded before this patch id existed. The boolean - * primitive `match` is built on; useful for one-off guards. - * - * @since 0.1.0 - * @category combinators - */ -export const patched = (patchId: string): Effect.Effect => - Effect.sync(() => temporalPatched(patchId)); - -/** - * Phase two of Temporal's patch lifecycle: keep a marker recognized for - * histories that carry it while no longer branching. Deploy after every - * pre-patch execution has drained. - * - * @since 0.1.0 - * @category combinators - */ -export const deprecatePatch = (patchId: string): Effect.Effect => - Effect.sync(() => temporalDeprecatePatch(patchId)); - -/** - * `deprecatePatch` for a version-chain case's marker. - * - * @since 0.1.0 - * @category combinators - */ -export const deprecateVersion = (site: string, name: string): Effect.Effect => - deprecatePatch(`${site}-${name}`);