From 0138656979919ab97896686aae8b37f6c0d9d3b0 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 28 Jun 2026 00:24:40 +0200 Subject: [PATCH 1/2] feat!: migrate to unthrown 1.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit unthrown 1.0.0 renames the result constructors to PascalCase: `ok` → `Ok`, `err` → `Err`, `defect` → `Defect`. Update every import and call site, bump the `unthrown` / `@unthrown/vitest` catalog entries to 1.0.0, and move the published packages' `unthrown` peer range to `^1`. The `.match({ ok, err, defect })` handler keys stay lowercase (object keys, not constructors); `matchTags` / `TaggedError` / `fromPromise` / `fromSafePromise` / `.toAsync()` and `result.isOk()` / `isErr()` / `isDefect()` narrowing are unchanged. Docs, agent rules, and the migration guide updated for the new constructor names. Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/rules/handlers.md | 10 +-- .agents/rules/workflow-determinism.md | 2 +- .changeset/unthrown-v1.md | 14 +++++ AGENTS.md | 2 +- docs/guide/activity-handlers.md | 10 +-- docs/guide/client-usage.md | 2 +- docs/guide/entry-points.md | 12 ++-- docs/guide/migrating-to-unthrown.md | 57 +++++++++-------- docs/guide/result-pattern.md | 4 +- docs/guide/troubleshooting.md | 6 +- docs/guide/worker-usage.md | 8 +-- .../src/application/activities.ts | 6 +- packages/client/package.json | 2 +- packages/client/src/client.ts | 62 +++++++++---------- packages/client/src/internal.ts | 14 ++--- packages/client/src/schedule.ts | 12 ++-- packages/contract/package.json | 2 +- packages/contract/src/result-async.spec.ts | 14 ++--- packages/contract/src/result-async.ts | 6 +- packages/worker/package.json | 2 +- packages/worker/src/__tests__/worker.spec.ts | 6 +- packages/worker/src/activity.spec.ts | 12 ++-- packages/worker/src/activity.ts | 10 +-- packages/worker/src/cancellation.spec.ts | 4 +- packages/worker/src/cancellation.ts | 20 +++--- packages/worker/src/child-workflow.ts | 28 ++++----- packages/worker/src/errors.ts | 4 +- packages/worker/src/workflow.ts | 4 +- pnpm-lock.yaml | 40 ++++++------ pnpm-workspace.yaml | 4 +- 30 files changed, 196 insertions(+), 183 deletions(-) create mode 100644 .changeset/unthrown-v1.md diff --git a/.agents/rules/handlers.md b/.agents/rules/handlers.md index 7d3e8dc8..b0975a2c 100644 --- a/.agents/rules/handlers.md +++ b/.agents/rules/handlers.md @@ -25,7 +25,7 @@ export const activities = declareActivitiesHandler({ `fromPromise(promise, qualify)` forces every rejection through `qualify`, which returns the modeled error `E` (here an `ApplicationFailure`). For a value you -already have, lift a sync result with `ok(value).toAsync()` / `err(failure).toAsync()` +already have, lift a sync result with `Ok(value).toAsync()` / `Err(failure).toAsync()` — unthrown has no `okAsync`/`errAsync`. Canonical example: `examples/order-processing-worker/src/application/activities.ts`. @@ -73,7 +73,7 @@ await worker.run(); ## Cancellation -Workflows opt into cancellation control via `context.cancellableScope` / `context.nonCancellableScope`. They fold cancellation into the project's `AsyncResult` shape — callers branch on `err(WorkflowCancelledError)` instead of catching `CancelledFailure`. +Workflows opt into cancellation control via `context.cancellableScope` / `context.nonCancellableScope`. They fold cancellation into the project's `AsyncResult` shape — callers branch on `Err(WorkflowCancelledError)` instead of catching `CancelledFailure`. ```typescript import { isErr } from "unthrown"; @@ -97,14 +97,14 @@ implementation: async (context, args) => { ``` - `cancellableScope(fn)` — returns `AsyncResult`. Cancels propagate from outside. -- `nonCancellableScope(fn)` — same shape; _outside_ cancels are ignored. Cancels raised _inside_ still surface as `err(...)`. Use for graceful-shutdown cleanup. +- `nonCancellableScope(fn)` — same shape; _outside_ cancels are ignored. Cancels raised _inside_ still surface as `Err(...)`. Use for graceful-shutdown cleanup. - Non-cancellation errors thrown by `fn` are _unmodeled_ failures: they ride unthrown's **`defect`** channel (inspectable via `isDefect(result)` / `result.cause`, re-thrown at the edge), not the modeled `err` channel. Canonical implementation: `packages/worker/src/cancellation.ts:38` (`cancellableScope`), `:75` (`nonCancellableScope`). Error class: `packages/worker/src/errors.ts:193`. ## ApplicationFailure semantics -`ApplicationFailure` (re-exported from `@temporal-contract/worker/activity`) is Temporal's first-class failure type. The wrapper at `packages/worker/src/activity.ts:8-15` rethrows the `err(...)` payload at the activity boundary, where Temporal recognizes it natively and applies the configured retry policy. +`ApplicationFailure` (re-exported from `@temporal-contract/worker/activity`) is Temporal's first-class failure type. The wrapper at `packages/worker/src/activity.ts:8-15` rethrows the `Err(...)` payload at the activity boundary, where Temporal recognizes it natively and applies the configured retry policy. Fields that matter: @@ -129,7 +129,7 @@ ApplicationFailure.create({ ## Anti-patterns -- **Never throw** from activities — Temporal sees thrown errors as `ApplicationFailure(type: "Error", retryable: true)` by default, which masks the real failure type and triggers unwanted retries. Use `err(ApplicationFailure.create({ type, message, nonRetryable })).toAsync()` (or a `fromPromise(promise, qualify)` chain whose `qualify` returns the `ApplicationFailure`) instead. +- **Never throw** from activities — Temporal sees thrown errors as `ApplicationFailure(type: "Error", retryable: true)` by default, which masks the real failure type and triggers unwanted retries. Use `Err(ApplicationFailure.create({ type, message, nonRetryable })).toAsync()` (or a `fromPromise(promise, qualify)` chain whose `qualify` returns the `ApplicationFailure`) instead. - **Never use `any`** — use `unknown` and validate with schemas. Enforced by oxlint. - **Always use `.js` extensions** in imports (even for TypeScript files) — required by ESM module resolution. - **Don't `try/catch` `CancelledFailure` in workflows** — use `cancellableScope` so cancellation flows through the same `AsyncResult` discipline as everything else. diff --git a/.agents/rules/workflow-determinism.md b/.agents/rules/workflow-determinism.md index 2fad25aa..7077848c 100644 --- a/.agents/rules/workflow-determinism.md +++ b/.agents/rules/workflow-determinism.md @@ -26,7 +26,7 @@ That's also why activity inputs/outputs must be serializable (validated through ## Cancellation primitives are deterministic -Use `context.cancellableScope` / `context.nonCancellableScope` (`packages/worker/src/cancellation.ts:38`, `:75`) — they wrap Temporal's `CancellationScope` and surface cancellation as `err(WorkflowCancelledError)` in an `AsyncResult`. Don't `try/catch` `CancelledFailure` directly; that bypasses the project's `Result` discipline. +Use `context.cancellableScope` / `context.nonCancellableScope` (`packages/worker/src/cancellation.ts:38`, `:75`) — they wrap Temporal's `CancellationScope` and surface cancellation as `Err(WorkflowCancelledError)` in an `AsyncResult`. Don't `try/catch` `CancelledFailure` directly; that bypasses the project's `Result` discipline. ## Side-effect escape hatch diff --git a/.changeset/unthrown-v1.md b/.changeset/unthrown-v1.md new file mode 100644 index 00000000..06ef9647 --- /dev/null +++ b/.changeset/unthrown-v1.md @@ -0,0 +1,14 @@ +--- +"@temporal-contract/contract": major +"@temporal-contract/worker": major +"@temporal-contract/client": major +"@temporal-contract/testing": major +--- + +Upgrade to [`unthrown`](https://github.com/btravstack/unthrown) 1.0.0. + +unthrown 1.0.0 renames the result constructors to PascalCase: `ok` → `Ok`, `err` → `Err`, `defect` → `Defect`. All packages are updated, and the `unthrown` peer-dependency range moves to `^1`. + +**Breaking for consumers** who construct results directly (e.g. in activity implementations): replace `ok(value)` / `err(failure)` with `Ok(value)` / `Err(failure)` (and `ok(value).toAsync()` / `err(failure).toAsync()` at promise boundaries), and bump `unthrown` to `^1`. The `result.match({ ok, err, defect })` handler keys are unchanged (they are object keys, not constructors), and `matchTags` / `TaggedError` / `fromPromise` / `fromSafePromise` / `.toAsync()` and the `result.isOk()` / `isErr()` / `isDefect()` narrowing are all unchanged. + +See the [Migrating from neverthrow](https://btravstack.github.io/temporal-contract/guide/migrating-to-unthrown) guide. diff --git a/AGENTS.md b/AGENTS.md index 01df1e31..b6a2785f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ This file is the source of truth for agent guidance in this repo. `CLAUDE.md` an ## The 6 rules that prevent broken PRs 1. **Workflow code is deterministic.** No `Date.now()`, `Math.random()`, `setTimeout`, `crypto.randomUUID()`, native I/O, or `process.env` reads inside `declareWorkflow`'s `implementation`. Use `@temporalio/workflow` primitives (`sleep`, `uuid4`, the patched `Date`) or push the side effect into an activity. See [.agents/rules/workflow-determinism.md](.agents/rules/workflow-determinism.md). This is the #1 cause of broken Temporal workflows — read that file before touching workflow code. -2. **Activities and the typed client return `AsyncResult` from `unthrown`.** Never throw — wrap technical errors in `ApplicationFailure` and surface them via `err(...).toAsync()` (or `fromPromise(promise, qualify)`, where `qualify` returns the modeled error `E`). unthrown has no `okAsync`/`errAsync`: lift a sync `Result` with `.toAsync()`. The client uses unthrown's `Result` for sync returns. unthrown adds a third **`defect`** channel for _unanticipated_ failures — a thrown exception the code didn't model surfaces as a defect (inspectable via `result.isDefect()` / `result.cause`, re-thrown at the edge), not a typed `err`. Narrow before reaching `.value`/`.error`/`.cause` — both the `r.isOk()` method and the `isOk(r)` free function are type guards (same for `isErr`/`isDefect`); the codebase uses the methods. Error classes are built with `TaggedError("@temporal-contract/Name", { name: "Name" })<{ ...payload }>` — the `_tag` is package-namespaced to avoid collisions, while `options.name` keeps `Error.name` the bare class name for readable logs. The worker's `ValidationError` subclasses are the exception — they must stay `ApplicationFailure` for Temporal's terminal-failure semantics. There is no `neverthrow`, no `@swan-io/boxed`, and no `@temporal-contract/boxed` package — those were removed. +2. **Activities and the typed client return `AsyncResult` from `unthrown`.** Never throw — wrap technical errors in `ApplicationFailure` and surface them via `Err(...).toAsync()` (or `fromPromise(promise, qualify)`, where `qualify` returns the modeled error `E`). unthrown has no `okAsync`/`errAsync`: lift a sync `Result` with `.toAsync()`. The client uses unthrown's `Result` for sync returns. unthrown adds a third **`defect`** channel for _unanticipated_ failures — a thrown exception the code didn't model surfaces as a defect (inspectable via `result.isDefect()` / `result.cause`, re-thrown at the edge), not a typed `err`. Narrow before reaching `.value`/`.error`/`.cause` — both the `r.isOk()` method and the `isOk(r)` free function are type guards (same for `isErr`/`isDefect`); the codebase uses the methods. Error classes are built with `TaggedError("@temporal-contract/Name", { name: "Name" })<{ ...payload }>` — the `_tag` is package-namespaced to avoid collisions, while `options.name` keeps `Error.name` the bare class name for readable logs. The worker's `ValidationError` subclasses are the exception — they must stay `ApplicationFailure` for Temporal's terminal-failure semantics. There is no `neverthrow`, no `@swan-io/boxed`, and no `@temporal-contract/boxed` package — those were removed. 3. **No `any`.** Use `unknown` and narrow. Enforced by oxlint. 4. **`.js` extensions in every import.** TypeScript files import each other as `./foo.js`, never `./foo` or `./foo.ts`. Required by ESM module resolution. 5. **ESM only.** All packages are `"type": "module"`. No CommonJS in source. diff --git a/docs/guide/activity-handlers.md b/docs/guide/activity-handlers.md index ce8f4b31..252905f9 100644 --- a/docs/guide/activity-handlers.md +++ b/docs/guide/activity-handlers.md @@ -74,7 +74,7 @@ type ProcessPaymentHandler = ActivitiesHandler["processPayment" const sendEmail: SendEmailHandler = ({ to, body }) => { // Implementation — must return AsyncResult - return ok({ sent: true }).toAsync(); + return Ok({ sent: true }).toAsync(); }; ``` @@ -199,8 +199,8 @@ type Handlers = ActivitiesHandler; // Create mock activities for testing const mockActivities: Handlers = { - sendEmail: ({ to, body }) => ok({ sent: true }).toAsync(), - processPayment: ({ amount }) => ok({ transactionId: "TEST-TXN" }).toAsync(), + sendEmail: ({ to, body }) => Ok({ sent: true }).toAsync(), + processPayment: ({ amount }) => Ok({ transactionId: "TEST-TXN" }).toAsync(), }; // Use in tests @@ -316,10 +316,10 @@ Always extract types for better maintainability: ```typescript // ✅ Good type Handlers = ActivitiesHandler; -const sendEmail: Handlers["sendEmail"] = ({ to, body }) => ok({ sent: true }).toAsync(); +const sendEmail: Handlers["sendEmail"] = ({ to, body }) => Ok({ sent: true }).toAsync(); // ❌ Avoid inline typing -const sendEmail = ({ to, body }: { to: string; body: string }) => ok({ sent: true }).toAsync(); +const sendEmail = ({ to, body }: { to: string; body: string }) => Ok({ sent: true }).toAsync(); ``` ### 2. Organize by Domain diff --git a/docs/guide/client-usage.md b/docs/guide/client-usage.md index b3430abf..f98b475c 100644 --- a/docs/guide/client-usage.md +++ b/docs/guide/client-usage.md @@ -347,7 +347,7 @@ describe("OrderService", () => { const mockClient = { executeWorkflow: vi .fn() - .mockResolvedValue(ok({ status: "success", transactionId: "tx-123" })), + .mockResolvedValue(Ok({ status: "success", transactionId: "tx-123" })), }; const service = new OrderService(mockClient); diff --git a/docs/guide/entry-points.md b/docs/guide/entry-points.md index 50d9ecf3..f3a5816a 100644 --- a/docs/guide/entry-points.md +++ b/docs/guide/entry-points.md @@ -281,7 +281,7 @@ declareActivitiesHandler({ activities: { processPayment: ({ amount }) => { // ✅ amount is number - return ok({ transactionId: "TXN-123" }).toAsync(); + return Ok({ transactionId: "TXN-123" }).toAsync(); // ✅ Must return AsyncResult<{ transactionId: string }, ApplicationFailure> }, }, @@ -373,8 +373,8 @@ Clear separation of concerns: import { ok } from "unthrown"; export const sharedActivities = { - sendEmail: ({ to, body }) => ok({ sent: true }).toAsync(), - logEvent: ({ event }) => ok({ logged: true }).toAsync(), + sendEmail: ({ to, body }) => Ok({ sent: true }).toAsync(), + logEvent: ({ event }) => Ok({ logged: true }).toAsync(), }; // activities/order.ts @@ -385,7 +385,7 @@ export const orderActivities = declareActivitiesHandler({ contract: orderContract, activities: { ...sharedActivities, - processPayment: ({ amount }) => ok({ transactionId: "TXN" }).toAsync(), + processPayment: ({ amount }) => Ok({ transactionId: "TXN" }).toAsync(), }, }); ``` @@ -397,11 +397,11 @@ export const orderActivities = declareActivitiesHandler({ import { ok } from "unthrown"; const baseActivities = { - validateInput: ({ data }) => ok({ valid: true }).toAsync(), + validateInput: ({ data }) => Ok({ valid: true }).toAsync(), }; const paymentActivities = { - processPayment: ({ amount }) => ok({ transactionId: "TXN" }).toAsync(), + processPayment: ({ amount }) => Ok({ transactionId: "TXN" }).toAsync(), }; export const activities = declareActivitiesHandler({ diff --git a/docs/guide/migrating-to-unthrown.md b/docs/guide/migrating-to-unthrown.md index 12543930..76648653 100644 --- a/docs/guide/migrating-to-unthrown.md +++ b/docs/guide/migrating-to-unthrown.md @@ -20,8 +20,9 @@ This page is an end-to-end mapping for upgrading existing code. on `await`/unwrap instead of being silently swallowed. - **Tagged errors**: error classes built with `TaggedError(...)` carry a `_tag` discriminant, enabling exhaustive `matchTags(...)` folds. -- **Sound narrowing**: free functions `isOk` / `isErr` / `isDefect` narrow - the type correctly, instead of relying on method-based type guards. +- **Active maintenance**: a first-party, actively-maintained library + (neverthrow's releases have stalled, see + [supermacro/neverthrow#670](https://github.com/supermacro/neverthrow/issues/670)). ## Drop the dep, add the new one @@ -29,13 +30,13 @@ This page is an end-to-end mapping for upgrading existing code. // package.json "dependencies": { - "neverthrow": "^8" -+ "unthrown": "^0.1.0" ++ "unthrown": "^1" } ``` ```diff - import { ResultAsync, ok, err, okAsync, errAsync } from "neverthrow"; -+ import { fromPromise, ok, err, isOk, isErr, isDefect } from "unthrown"; ++ import { fromPromise, Ok, Err } from "unthrown"; ``` ## Type signatures @@ -50,21 +51,21 @@ the same name but is now imported from `"unthrown"`. ## API mapping -| neverthrow | unthrown | -| -------------------------------------------- | ---------------------------------------------------------------------- | -| `import { ResultAsync } from "neverthrow"` | `import { fromPromise } from "unthrown"` | -| type `ResultAsync` | type `AsyncResult` | -| type `Result` | `Result` (now from `"unthrown"`) | -| `ok(v)` / `err(e)` | `ok(v)` / `err(e)` (from `"unthrown"`) | -| `okAsync(v)` | `ok(v).toAsync()` (no `okAsync`) | -| `errAsync(e)` | `err(e).toAsync()` (no `errAsync`) | -| `ResultAsync.fromPromise(promise, errFn)` | `fromPromise(promise, errFn)` | -| `ResultAsync.fromSafePromise(promise)` | `fromSafePromise(promise)` | -| `.andThen(fn)` | `.flatMap(fn)` | -| `.map(fn)` / `.mapErr(fn)` / `.orElse(fn)` | `.map(fn)` / `.mapErr(fn)` / `.orElse(fn)` | -| `Result.combine([...])` | `all([...])` | -| `result.match(okFn, errFn)` (positional) | `result.match({ ok, err, defect })` (object, 3 channels) | -| `result.isOk()` / `result.isErr()` to narrow | `isOk(result)` / `isErr(result)` / `isDefect(result)` (free functions) | +| neverthrow | unthrown | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `import { ResultAsync } from "neverthrow"` | `import { fromPromise } from "unthrown"` | +| type `ResultAsync` | type `AsyncResult` | +| type `Result` | `Result` (now from `"unthrown"`) | +| `ok(v)` / `err(e)` | `Ok(v)` / `Err(e)` (from `"unthrown"`) | +| `okAsync(v)` | `Ok(v).toAsync()` (no `okAsync`) | +| `errAsync(e)` | `Err(e).toAsync()` (no `errAsync`) | +| `ResultAsync.fromPromise(promise, errFn)` | `fromPromise(promise, errFn)` | +| `ResultAsync.fromSafePromise(promise)` | `fromSafePromise(promise)` | +| `.andThen(fn)` | `.flatMap(fn)` | +| `.map(fn)` / `.mapErr(fn)` / `.orElse(fn)` | `.map(fn)` / `.mapErr(fn)` / `.orElse(fn)` | +| `Result.combine([...])` | `all([...])` | +| `result.match(okFn, errFn)` (positional) | `result.match({ ok, err, defect })` (object, 3 channels) | +| `result.isOk()` / `result.isErr()` to narrow | `result.isOk()` / `result.isErr()` / `result.isDefect()` (methods narrow; `isOk(result)` free functions also exist) | ## `okAsync` / `errAsync` are gone @@ -73,13 +74,13 @@ it to an `AsyncResult` with `.toAsync()`: ```diff - import { okAsync, errAsync } from "neverthrow"; -+ import { ok, err } from "unthrown"; ++ import { Ok, Err } from "unthrown"; - return okAsync({ sent: true }); -+ return ok({ sent: true }).toAsync(); ++ return Ok({ sent: true }).toAsync(); - return errAsync(new MyError()); -+ return err(new MyError()).toAsync(); ++ return Err(new MyError()).toAsync(); ``` ## Narrowing: methods or free functions @@ -108,7 +109,7 @@ unthrown models **three** outcomes, not two: - **`ok`** — success. - **`err`** — a deliberate, anticipated failure that is part of your type - signature (returned with `err(...)` / `err(...).toAsync()`, or produced by + signature (returned with `Err(...)` / `Err(...).toAsync()`, or produced by mapping a rejection through `fromPromise(promise, errFn)`). - **`defect`** — an _unanticipated_ failure (a bug): an unexpected throw that was never modeled. It carries the raw failure on `result.cause` and @@ -120,15 +121,13 @@ becomes a `defect` instead, distinct from your modeled `err` values. Inspect it with `isDefect(result)` / `result.cause`, or handle all three at once: ```ts -import { isOk, isErr, isDefect } from "unthrown"; - const result = await client.executeWorkflow("processOrder", { workflowId, args }); -if (isOk(result)) { +if (result.isOk()) { console.log(result.value); -} else if (isErr(result)) { +} else if (result.isErr()) { console.error("Modeled failure:", result.error); -} else if (isDefect(result)) { +} else if (result.isDefect()) { console.error("Unexpected failure (bug):", result.cause); } ``` @@ -281,7 +280,7 @@ result.match({ `context.cancellableScope` and `context.nonCancellableScope` previously returned `ResultAsync`. They now return `AsyncResult` — narrow the resolved `Result` with -`isErr(result)` (free function) instead of `result.isErr()`. +`result.isErr()` (or the `isErr(result)` free function). ## See Also diff --git a/docs/guide/result-pattern.md b/docs/guide/result-pattern.md index dc50d4a0..6c92a6ae 100644 --- a/docs/guide/result-pattern.md +++ b/docs/guide/result-pattern.md @@ -538,8 +538,8 @@ unthrown models **three** outcomes, not two. Besides `ok` (success) and `defect` — for **unanticipated** failures: bugs, programmer errors, or any exception you never modeled. -- An `err` is a value you returned on purpose (`err(...)` / - `errAsync` → `err(...).toAsync()`, or a rejection mapped through +- An `err` is a value you returned on purpose (`Err(...)` / + `errAsync` → `Err(...).toAsync()`, or a rejection mapped through `fromPromise(promise, errFn)`). It is part of your type signature. - A `defect` is captured when an unexpected throw escapes — for example a `fromSafePromise(...)` thunk that throws, or an unhandled exception inside diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index bbffb1b0..b3c2f21c 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -174,7 +174,7 @@ Property 'transactionId' does not exist on type 'never'. processOrder: { processPayment: ({ customerId, amount }) => { console.log(customerId); // Type-safe! - return ok({ transactionId: "tx-123" }).toAsync(); + return Ok({ transactionId: "tx-123" }).toAsync(); }, }, } @@ -482,8 +482,8 @@ lift a sync `Result` with `.toAsync()`): // ✅ For activities, workflows, and clients import { fromPromise, ok, err, isOk, isErr, isDefect, type AsyncResult } from "unthrown"; -// okAsync(value) -> ok(value).toAsync() -// errAsync(error) -> err(error).toAsync() +// okAsync(value) -> Ok(value).toAsync() +// errAsync(error) -> Err(error).toAsync() ``` See [Migrating from neverthrow](/guide/migrating-to-unthrown) for the full diff --git a/docs/guide/worker-usage.md b/docs/guide/worker-usage.md index 2ac90e12..8cd97351 100644 --- a/docs/guide/worker-usage.md +++ b/docs/guide/worker-usage.md @@ -27,7 +27,7 @@ export const activities = declareActivitiesHandler({ // Global activities log: ({ level, message }) => { console.log(`[${level}] ${message}`); - return ok(undefined).toAsync(); + return Ok(undefined).toAsync(); }, // Workflow-specific activities @@ -293,9 +293,9 @@ processPayment: ({ amount }) => (async () => { try { const tx = await paymentService.charge(amount); - return ok({ transactionId: tx.id }); + return Ok({ transactionId: tx.id }); } catch (err) { - return err(/* ... */); + return Err(/* ... */); } })(), (e) => e, @@ -338,7 +338,7 @@ implementation: async (context, args) => { // ❌ Avoid - returning Result (will lose instance over network) implementation: async (context, args) => { const payment = await context.activities.processPayment({ amount: 100 }); - return ok({ transactionId: payment.transactionId }); // Won't work! + return Ok({ transactionId: payment.transactionId }); // Won't work! }; ``` diff --git a/examples/order-processing-worker/src/application/activities.ts b/examples/order-processing-worker/src/application/activities.ts index e1d671d3..d9568b1d 100644 --- a/examples/order-processing-worker/src/application/activities.ts +++ b/examples/order-processing-worker/src/application/activities.ts @@ -13,7 +13,7 @@ import { /** * Translate an arbitrary thrown value into a Temporal `ApplicationFailure`. * Used by every activity below to wrap use-case rejections in the - * `err(...)` slot without each site repeating the boilerplate. + * `Err(...)` slot without each site repeating the boilerplate. */ const toApplicationFailure = (type: string, fallback: string, error: unknown): ApplicationFailure => ApplicationFailure.create({ @@ -26,8 +26,8 @@ const toApplicationFailure = (type: string, fallback: string, error: unknown): A * Activity implementations using unthrown's `AsyncResult` pattern. * * Instead of throwing exceptions, activities return: - * - ok(value).toAsync() for success - * - err(ApplicationFailure).toAsync() for failures (or a `fromPromise` + * - Ok(value).toAsync() for success + * - Err(ApplicationFailure).toAsync() for failures (or a `fromPromise` * chain that qualifies a rejection into an `ApplicationFailure`). * * All technical exceptions MUST be caught and wrapped in `ApplicationFailure` diff --git a/packages/client/package.json b/packages/client/package.json index 3ee45db6..dfcfea46 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -76,7 +76,7 @@ "peerDependencies": { "@temporalio/client": "^1", "@temporalio/common": "^1", - "unthrown": "^0.2" + "unthrown": "^1" }, "engines": { "node": ">=22.19.0" diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 9df214ab..8ab8a602 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -17,7 +17,7 @@ import type { ClientInferWorkflowSignals, ClientInferWorkflowUpdates, } from "./types.js"; -import { type AsyncResult, type Result, ok, err, fromPromise } from "unthrown"; +import { type AsyncResult, type Result, Ok, Err, fromPromise } from "unthrown"; import { type TemporalFailure, WorkflowAlreadyStartedError, @@ -314,12 +314,12 @@ async function resolveDefinitionAndValidateInput< > { const definition = contract.workflows[workflowName]; if (!definition) { - return err(createWorkflowNotFoundError(workflowName, contract)); + return Err(createWorkflowNotFoundError(workflowName, contract)); } const inputResult = await definition.input["~standard"].validate(args); if (inputResult.issues) { - return err(createWorkflowValidationError(workflowName, "input", inputResult.issues)); + return Err(createWorkflowValidationError(workflowName, "input", inputResult.issues)); } const searchAttributesResult = toTypedSearchAttributes( @@ -330,10 +330,10 @@ async function resolveDefinitionAndValidateInput< // `toTypedSearchAttributes` only ever builds ok/err; assert away the // impossible defect so `.error` / `.value` narrow cleanly. assertNoDefect(searchAttributesResult); - if (searchAttributesResult.isErr()) return err(searchAttributesResult.error); + if (searchAttributesResult.isErr()) return Err(searchAttributesResult.error); const typedSearchAttributes = searchAttributesResult.value; - return ok({ + return Ok({ definition: definition as TContract["workflows"][TWorkflowName], validatedInput: inputResult.value, typedSearchAttributes, @@ -473,7 +473,7 @@ export class TypedClient { ); // The resolver only ever builds ok/err; assert away the impossible defect. assertNoDefect(resolved); - if (resolved.isErr()) return err(resolved.error); + if (resolved.isErr()) return Err(resolved.error); const { definition, validatedInput, typedSearchAttributes } = resolved.value; try { @@ -483,9 +483,9 @@ export class TypedClient { args: [validatedInput], ...(typedSearchAttributes ? { typedSearchAttributes } : {}), }); - return ok(this.createTypedHandle(handle, definition) as Ok); + return Ok(this.createTypedHandle(handle, definition) as Ok); } catch (error) { - return err(classifyStartError("startWorkflow", error)); + return Err(classifyStartError("startWorkflow", error)); } }; return makeAsyncResult(work); @@ -555,7 +555,7 @@ export class TypedClient { ); // The resolver only ever builds ok/err; assert away the impossible defect. assertNoDefect(resolved); - if (resolved.isErr()) return err(resolved.error); + if (resolved.isErr()) return Err(resolved.error); const { definition, validatedInput, typedSearchAttributes } = resolved.value; // Validate signal input — call-site-specific, kept inline. @@ -565,7 +565,7 @@ export class TypedClient { if (!signalDef) { // Type-level constraint should already prevent this; defensive for // raw-call / union-typed-name corner cases. - return err( + return Err( new SignalValidationError(signalName, [ { message: `Signal "${signalName}" is not declared on workflow "${workflowName}".`, @@ -575,7 +575,7 @@ export class TypedClient { } const signalInputResult = await signalDef.input["~standard"].validate(signalArgs); if (signalInputResult.issues) { - return err(new SignalValidationError(signalName, signalInputResult.issues)); + return Err(new SignalValidationError(signalName, signalInputResult.issues)); } try { @@ -590,9 +590,9 @@ export class TypedClient { const typed = this.createTypedHandle(handle, definition) as TypedWorkflowHandle< TContract["workflows"][TWorkflowName] >; - return ok({ ...typed, signaledRunId: handle.signaledRunId } as Ok); + return Ok({ ...typed, signaledRunId: handle.signaledRunId } as Ok); } catch (error) { - return err(classifyStartError("signalWithStart", error)); + return Err(classifyStartError("signalWithStart", error)); } }; return makeAsyncResult(work); @@ -650,7 +650,7 @@ export class TypedClient { ); // The resolver only ever builds ok/err; assert away the impossible defect. assertNoDefect(resolved); - if (resolved.isErr()) return err(resolved.error); + if (resolved.isErr()) return Err(resolved.error); const { definition, validatedInput, typedSearchAttributes } = resolved.value; try { @@ -666,17 +666,17 @@ export class TypedClient { // shape; the helper only handles pre-call concerns. const outputResult = await definition.output["~standard"].validate(result); if (outputResult.issues) { - return err(createWorkflowValidationError(workflowName, "output", outputResult.issues)); + return Err(createWorkflowValidationError(workflowName, "output", outputResult.issues)); } - return ok(outputResult.value as Ok); + return Ok(outputResult.value as Ok); } catch (error) { // executeWorkflow combines start + result, so it can surface any of // the discriminated kinds. Inline the three checks rather than // routing through a dedicated helper — this is the only call site // that needs the full union. if (error instanceof WorkflowExecutionAlreadyStartedError) { - return err(new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error)); + return Err(new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error)); } if (error instanceof TemporalWorkflowFailedError) { // Forward Temporal's nested cause directly — see @@ -687,7 +687,7 @@ export class TypedClient { // ever populates it with a `TemporalFailure` subclass here; narrow // with the public union so the typed `cause` lines up with the // surfaced `WorkflowFailedError`. - return err( + return Err( new WorkflowFailedError( temporalOptions.workflowId, error.cause as TemporalFailure | undefined, @@ -695,7 +695,7 @@ export class TypedClient { ); } if (error instanceof TemporalWorkflowNotFoundError) { - return err( + return Err( new WorkflowExecutionNotFoundError( error.workflowId || temporalOptions.workflowId, error.runId, @@ -703,7 +703,7 @@ export class TypedClient { ), ); } - return err(createRuntimeClientError("executeWorkflow", error)); + return Err(createRuntimeClientError("executeWorkflow", error)); } }; return makeAsyncResult(work); @@ -736,14 +736,14 @@ export class TypedClient { const work = async (): Promise> => { const definition = this.contract.workflows[workflowName]; if (!definition) { - return err(createWorkflowNotFoundError(workflowName, this.contract)); + return Err(createWorkflowNotFoundError(workflowName, this.contract)); } try { const handle = this.client.workflow.getHandle(workflowId); - return ok(this.createTypedHandle(handle, definition) as Ok); + return Ok(this.createTypedHandle(handle, definition) as Ok); } catch (error) { - return err(createRuntimeClientError("getHandle", error)); + return Err(createRuntimeClientError("getHandle", error)); } }; return makeAsyncResult(work); @@ -808,7 +808,7 @@ export class TypedClient { const result = await workflowHandle.result(); const outputResult = await definition.output["~standard"].validate(result); if (outputResult.issues) { - return err( + return Err( new WorkflowValidationError( workflowHandle.workflowId, "output", @@ -816,9 +816,9 @@ export class TypedClient { ), ); } - return ok(outputResult.value as Ok); + return Ok(outputResult.value as Ok); } catch (error) { - return err(classifyResultError("result", error, workflowHandle.workflowId)); + return Err(classifyResultError("result", error, workflowHandle.workflowId)); } }; return makeAsyncResult(work); @@ -933,22 +933,22 @@ function buildValidatedProxy => { const inputResult = await def.input["~standard"].validate(args); if (inputResult.issues) { - return err(makeValidationError(name, "input", inputResult.issues)); + return Err(makeValidationError(name, "input", inputResult.issues)); } try { const result = await invoke(name, inputResult.value); const outputSchema = validateOutput(def); if (!outputSchema) { - return ok(result); + return Ok(result); } const outputResult = await outputSchema["~standard"].validate(result); if (outputResult.issues) { - return err(makeValidationError(name, "output", outputResult.issues)); + return Err(makeValidationError(name, "output", outputResult.issues)); } - return ok(outputResult.value); + return Ok(outputResult.value); } catch (error) { - return err(classifyHandleError(operation, error, workflowId)); + return Err(classifyHandleError(operation, error, workflowId)); } }; return makeAsyncResult(work); diff --git a/packages/client/src/internal.ts b/packages/client/src/internal.ts index e7cc6a24..149139d3 100644 --- a/packages/client/src/internal.ts +++ b/packages/client/src/internal.ts @@ -15,7 +15,7 @@ import { } from "@temporalio/common"; import type { AnyWorkflowDefinition, SearchAttributeDefinition } from "@temporal-contract/contract"; import { _internal_makeAsyncResult } from "@temporal-contract/contract/result-async"; -import { ok, err, type AsyncResult, type Result } from "unthrown"; +import { Ok, Err, type AsyncResult, type Result } from "unthrown"; // `assertNoDefect` narrows an internally-built `Result` (known to carry only // ok/err) to `Ok | Err`, re-throwing a stray defect's cause — so call sites @@ -35,10 +35,10 @@ import { * Temporal client honours indexing when starting the workflow. * * Workflows without a `searchAttributes` block (or callers passing no - * values) resolve to `ok(undefined)`, matching the Temporal SDK's + * values) resolve to `Ok(undefined)`, matching the Temporal SDK's * "absent ≠ empty" semantics. * - * Returns `err(RuntimeClientError)` on unknown keys. The TypeScript + * Returns `Err(RuntimeClientError)` on unknown keys. The TypeScript * surface already gates the happy path; the runtime check catches typed * escape hatches (`as never`, `as any`, raw-call interop) where a typo * would otherwise silently drop the attribute, leaving the workflow @@ -49,7 +49,7 @@ export function toTypedSearchAttributes( workflowName: string, values: Record | undefined, ): Result { - if (!values) return ok(undefined); + if (!values) return Ok(undefined); // Workflows that omit the `searchAttributes` block declare none. Treat // that as an empty declared map so a caller passing values still hits // the per-key "undeclared" check below — silently dropping them would @@ -63,7 +63,7 @@ export function toTypedSearchAttributes( if (value === undefined) continue; const def = declared[name]; if (!def) { - return err( + return Err( new RuntimeClientError( "searchAttributes", new Error( @@ -76,7 +76,7 @@ export function toTypedSearchAttributes( const key = defineSearchAttributeKey(name, def.kind); pairs.push({ key, value } as SearchAttributePair); } - return ok(pairs.length > 0 ? new TypedSearchAttributes(pairs) : undefined); + return Ok(pairs.length > 0 ? new TypedSearchAttributes(pairs) : undefined); } /** @@ -84,7 +84,7 @@ export function toTypedSearchAttributes( * unanticipated rejection through unthrown's `defect` channel. * * The work function is expected to handle its own domain errors and return - * an `err(...)` for them; a thrown exception the work didn't anticipate is an + * an `Err(...)` for them; a thrown exception the work didn't anticipate is an * *unmodeled* failure and surfaces as a defect (inspectable via * `result.isDefect()` / `result.cause`, re-thrown at the edge) rather than a * manufactured `RuntimeClientError`. diff --git a/packages/client/src/schedule.ts b/packages/client/src/schedule.ts index 1d616fba..db8995c1 100644 --- a/packages/client/src/schedule.ts +++ b/packages/client/src/schedule.ts @@ -8,7 +8,7 @@ import type { ScheduleSpec, } from "@temporalio/client"; import type { ContractDefinition } from "@temporal-contract/contract"; -import { type AsyncResult, type Result, ok, err, fromPromise } from "unthrown"; +import { type AsyncResult, type Result, Ok, Err, fromPromise } from "unthrown"; import type { TypedSearchAttributeMap } from "./client.js"; import type { ClientInferInput } from "./types.js"; import { RuntimeClientError, WorkflowNotFoundError, WorkflowValidationError } from "./errors.js"; @@ -133,12 +133,12 @@ export class TypedScheduleClient { const work = async (): Promise> => { const definition = this.contract.workflows[workflowName]; if (!definition) { - return err(new WorkflowNotFoundError(workflowName, Object.keys(this.contract.workflows))); + return Err(new WorkflowNotFoundError(workflowName, Object.keys(this.contract.workflows))); } const inputResult = await definition.input["~standard"].validate(options.args); if (inputResult.issues) { - return err(new WorkflowValidationError(workflowName, "input", inputResult.issues)); + return Err(new WorkflowValidationError(workflowName, "input", inputResult.issues)); } // Translate typed search attributes for the spawned workflow runs. @@ -154,7 +154,7 @@ export class TypedScheduleClient { // `toTypedSearchAttributes` only ever builds ok/err; assert away the // impossible defect so `.error` / `.value` narrow cleanly. assertNoDefect(searchAttributesResult); - if (searchAttributesResult.isErr()) return err(searchAttributesResult.error); + if (searchAttributesResult.isErr()) return Err(searchAttributesResult.error); const typedSearchAttributes = searchAttributesResult.value; try { @@ -193,9 +193,9 @@ export class TypedScheduleClient { ...(options.state !== undefined ? { state: options.state } : {}), ...(options.memo !== undefined ? { memo: options.memo } : {}), }); - return ok(wrapScheduleHandle(handle)); + return Ok(wrapScheduleHandle(handle)); } catch (error) { - return err(new RuntimeClientError("schedule.create", error)); + return Err(new RuntimeClientError("schedule.create", error)); } }; return makeAsyncResult(work); diff --git a/packages/contract/package.json b/packages/contract/package.json index 5a084888..89b5f96d 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -75,7 +75,7 @@ "vitest": "catalog:" }, "peerDependencies": { - "unthrown": "^0.2" + "unthrown": "^1" }, "peerDependenciesMeta": { "unthrown": { diff --git a/packages/contract/src/result-async.spec.ts b/packages/contract/src/result-async.spec.ts index f7f713a9..91a7fe5c 100644 --- a/packages/contract/src/result-async.spec.ts +++ b/packages/contract/src/result-async.spec.ts @@ -4,13 +4,13 @@ * The helper routes a synchronous throw or a rejected promise from `work()` * through unthrown's `defect` channel — an *unanticipated* failure becomes a * defect (a bug, re-thrown at the edge) rather than an unhandled rejection, - * while the work function's own domain `err(...)` flows through untouched. + * while the work function's own domain `Err(...)` flows through untouched. * Both consuming packages (`@temporal-contract/client` and * `@temporal-contract/worker`) rely on this — see `client/src/internal.ts` * and `worker/src/internal.ts`. */ import { describe, expect, it } from "vitest"; -import { ok, err } from "unthrown"; +import { Ok, Err } from "unthrown"; import { _internal_makeAsyncResult } from "./result-async.js"; class TestError extends Error { @@ -21,17 +21,17 @@ class TestError extends Error { } describe("_internal_makeAsyncResult", () => { - it("returns ok(...) when the work function resolves with ok(...)", async () => { - const result = await _internal_makeAsyncResult(async () => ok(42)); + it("returns Ok(...) when the work function resolves with Ok(...)", async () => { + const result = await _internal_makeAsyncResult(async () => Ok(42)); expect(result).toBeOkWith(42); }); - it("returns err(...) unchanged when the work function resolves with err(...)", async () => { + it("returns Err(...) unchanged when the work function resolves with Err(...)", async () => { const domainError = new TestError("domain"); - const result = await _internal_makeAsyncResult(async () => err(domainError)); + const result = await _internal_makeAsyncResult(async () => Err(domainError)); expect(result).toBeErr(); if (result.isErr()) { - // Identity preserved — the domain `err(...)` flows through untouched. + // Identity preserved — the domain `Err(...)` flows through untouched. expect(result.error).toBe(domainError); } }); diff --git a/packages/contract/src/result-async.ts b/packages/contract/src/result-async.ts index 8ad861c0..63a8b216 100644 --- a/packages/contract/src/result-async.ts +++ b/packages/contract/src/result-async.ts @@ -23,13 +23,13 @@ import { * `AsyncResult`, catching synchronous throws and rejected promises and * routing them through unthrown's `defect` channel — so an *unanticipated* * failure surfaces as a defect (a bug, re-thrown at the edge) rather than an - * unhandled rejection, while the work function's own domain `err(...)` flows + * unhandled rejection, while the work function's own domain `Err(...)` flows * through untouched. * * `fromSafePromise(thunk)` invokes the thunk, capturing both a synchronous * throw before the promise is produced and an eventual rejection as a `defect` * (its error channel is `never`) — the work function is expected to model its - * own domain errors as `err(...)`, so any *thrown* failure is by definition + * own domain errors as `Err(...)`, so any *thrown* failure is by definition * unmodeled. The `.flatMap((inner) => inner)` flattens the nested * `Result` the thunk resolves with, surfacing its modeled error channel. * @@ -48,7 +48,7 @@ export function _internal_makeAsyncResult( * unthrown's `Result` type always includes the out-of-band `Defect` * variant, so `if (r.isErr()) … else r.value` does not type-check — the `else` * branch is still `Ok | Defect`. For an internally-produced result that is - * *known* to be built only from `ok(...)` / `err(...)`, this collapses the + * *known* to be built only from `Ok(...)` / `Err(...)`, this collapses the * "impossible defect" case in one call: it re-throws a present defect's cause * (so a genuine bug still rides the defect channel at the boundary) and * narrows the result to `Ok | Err` for the caller, which can then branch on diff --git a/packages/worker/package.json b/packages/worker/package.json index 695939ff..308376dd 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -95,7 +95,7 @@ "@temporalio/common": "^1", "@temporalio/worker": "^1", "@temporalio/workflow": "^1", - "unthrown": "^0.2" + "unthrown": "^1" }, "engines": { "node": ">=22.19.0" diff --git a/packages/worker/src/__tests__/worker.spec.ts b/packages/worker/src/__tests__/worker.spec.ts index 1a220fe6..6abe7df7 100644 --- a/packages/worker/src/__tests__/worker.spec.ts +++ b/packages/worker/src/__tests__/worker.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, vi, beforeEach } from "vitest"; import { Worker } from "@temporalio/worker"; import { TypedClient, WorkflowValidationError } from "@temporal-contract/client"; import { it as baseIt } from "@temporal-contract/testing/extension"; -import { ok, err, type AsyncResult } from "unthrown"; +import { Ok, Err, type AsyncResult } from "unthrown"; import { extname } from "node:path"; import { fileURLToPath } from "node:url"; import { testContract } from "./test.contract.js"; @@ -11,8 +11,8 @@ import { ApplicationFailure, declareActivitiesHandler } from "../activity.js"; import { createWorker } from "../worker.js"; // unthrown has no `okAsync`/`errAsync`; lift a sync `Result` with `.toAsync()`. -const okAsync = (value: T): AsyncResult => ok(value).toAsync(); -const errAsync = (error: E): AsyncResult => err(error).toAsync(); +const okAsync = (value: T): AsyncResult => Ok(value).toAsync(); +const errAsync = (error: E): AsyncResult => Err(error).toAsync(); // ============================================================================ // Test Setup diff --git a/packages/worker/src/activity.spec.ts b/packages/worker/src/activity.spec.ts index 301a3b9c..69aa33df 100644 --- a/packages/worker/src/activity.spec.ts +++ b/packages/worker/src/activity.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ok, err, fromSafePromise, type AsyncResult } from "unthrown"; +import { Ok, Err, fromSafePromise, type AsyncResult } from "unthrown"; import { z } from "zod"; import { ActivityDefinitionNotFoundError, @@ -10,8 +10,8 @@ import type { ContractDefinition } from "@temporal-contract/contract"; import { ApplicationFailure, declareActivitiesHandler } from "./activity.js"; // unthrown has no `okAsync`/`errAsync`; lift a sync `Result` with `.toAsync()`. -const okAsync = (value: T): AsyncResult => ok(value).toAsync(); -const errAsync = (error: E): AsyncResult => err(error).toAsync(); +const okAsync = (value: T): AsyncResult => Ok(value).toAsync(); +const errAsync = (error: E): AsyncResult => Err(error).toAsync(); describe("Worker unthrown Package", () => { describe("declareActivitiesHandler", () => { @@ -126,7 +126,7 @@ describe("Worker unthrown Package", () => { await expect(badActivities["fetchData"]({ id: "abc" })).rejects.toThrow(); }); - it("should handle ok() by returning value", async () => { + it("should handle Ok() by returning value", async () => { // GIVEN const contract = { taskQueue: "test-queue", @@ -153,7 +153,7 @@ describe("Worker unthrown Package", () => { expect(result).toEqual(expect.objectContaining({ result: "success-test" })); }); - it("should handle err() by throwing exception", async () => { + it("should handle Err() by throwing exception", async () => { // GIVEN const contract = { taskQueue: "test-queue", @@ -194,7 +194,7 @@ describe("Worker unthrown Package", () => { expect((rejected as ApplicationFailure).details).toEqual([{ info: "additional details" }]); }); - it("preserves `nonRetryable: true` when unwrapping err() and rethrowing the ApplicationFailure", async () => { + it("preserves `nonRetryable: true` when unwrapping Err() and rethrowing the ApplicationFailure", async () => { const contract = { taskQueue: "test-queue", workflows: {}, diff --git a/packages/worker/src/activity.ts b/packages/worker/src/activity.ts index f17b439c..f984ae59 100644 --- a/packages/worker/src/activity.ts +++ b/packages/worker/src/activity.ts @@ -36,7 +36,7 @@ export { ApplicationFailure } from "@temporalio/common"; * Activity implementation using unthrown's `AsyncResult`. * * Returns `AsyncResult` for explicit error - * handling instead of throwing. The wrapper rethrows `err()` payloads at + * handling instead of throwing. The wrapper rethrows `Err()` payloads at * the activity boundary; Temporal recognizes `ApplicationFailure` natively * and applies the configured retry policy (with `nonRetryable: true` * opting an instance out per-call). An unexpected throw surfaces as a @@ -187,8 +187,8 @@ export type ActivitiesHandler = * @remarks * The wrapper accepts implementations in the * `AsyncResult` shape and produces ordinary - * Promise-returning Temporal handlers (`err(...)` → thrown - * `ApplicationFailure`; `ok(...)` → output validated against the + * Promise-returning Temporal handlers (`Err(...)` → thrown + * `ApplicationFailure`; `Ok(...)` → output validated against the * contract and resolved; `defect` → original cause re-thrown). It does * **not** hide Temporal's * `@temporalio/activity` runtime: inside the body you can still call @@ -236,7 +236,7 @@ export function declareActivitiesHandler( } return outputResult.value; }, - // Convert err(...) payload to thrown ApplicationFailure for Temporal. + // Convert Err(...) payload to thrown ApplicationFailure for Temporal. // Temporal recognizes this class natively and applies the configured // retry policy (honoring `nonRetryable: true`). err: (error) => { @@ -250,7 +250,7 @@ export function declareActivitiesHandler( // coerce it to `nonRetryable`: not every unexpected throw is permanent // (a transient I/O fault is also "unmodeled"), and forcing fail-fast // here would silently change retry semantics. An activity that wants a - // permanent failure should return `err(ApplicationFailure.create({ + // permanent failure should return `Err(ApplicationFailure.create({ // nonRetryable: true }))` explicitly. defect: (cause) => { throw cause; diff --git a/packages/worker/src/cancellation.spec.ts b/packages/worker/src/cancellation.spec.ts index 29c743b4..491a9204 100644 --- a/packages/worker/src/cancellation.spec.ts +++ b/packages/worker/src/cancellation.spec.ts @@ -5,7 +5,7 @@ * Mocks `@temporalio/workflow` so the helpers can be exercised outside a * real workflow context. Asserts that: * - successful resolution surfaces as `ok`, - * - cancellation surfaces as `err(WorkflowCancelledError)` + * - cancellation surfaces as `Err(WorkflowCancelledError)` * (matched via the mocked `isCancellation` predicate), * - non-cancellation errors are *unmodeled* failures and surface on the * `defect` channel with the original error on `cause` — they no longer ride @@ -71,7 +71,7 @@ describe("cancellableScope", () => { it("routes a non-cancellation error to the defect channel", async () => { // A thrown non-cancellation error is an *unmodeled* failure: the helper // re-throws it so the `makeAsyncResult` boundary captures it as a defect, - // with the original error on `cause`, rather than a typed err(...). + // with the original error on `cause`, rather than a typed Err(...). const original = new Error("activity exploded"); const result = await cancellableScope(async () => { throw original; diff --git a/packages/worker/src/cancellation.ts b/packages/worker/src/cancellation.ts index 8d8d7f22..6812d483 100644 --- a/packages/worker/src/cancellation.ts +++ b/packages/worker/src/cancellation.ts @@ -3,29 +3,29 @@ * opt into cancellation control without reaching for * `@temporalio/workflow` directly. The wrappers fold cancellation into * the same `AsyncResult<...>` shape used elsewhere in the worker - * context — callers branch on `err(WorkflowCancelledError)` instead of + * context — callers branch on `Err(WorkflowCancelledError)` instead of * catching `CancelledFailure`. * * Non-cancellation errors thrown inside the scope are *unmodeled* failures: * they ride unthrown's `defect` channel (re-thrown at the edge / inspectable - * via `result.isDefect()` and `result.cause`) rather than a typed `err(...)`, + * via `result.isDefect()` and `result.cause`) rather than a typed `Err(...)`, * keeping the modeled error channel to the single anticipated outcome — * cancellation. */ import { CancellationScope, isCancellation } from "@temporalio/workflow"; -import { type AsyncResult, type Result, ok, err } from "unthrown"; +import { type AsyncResult, type Result, Ok, Err } from "unthrown"; import { WorkflowCancelledError } from "./errors.js"; import { makeAsyncResult } from "./internal.js"; /** * Run `fn` inside a cancellable Temporal scope. If the workflow (or an * ancestor scope) is cancelled while the function is in flight, the - * resulting AsyncResult resolves to `err(WorkflowCancelledError)`, + * resulting AsyncResult resolves to `Err(WorkflowCancelledError)`, * letting callers handle cancellation explicitly — typically to perform * a graceful exit from the current step. * * Non-cancellation errors thrown by `fn` are unmodeled failures: they surface - * on the `defect` channel rather than as a typed `err(...)`, so a genuine bug + * on the `defect` channel rather than as a typed `Err(...)`, so a genuine bug * is not silently treated as an anticipated domain outcome. * * @example @@ -54,10 +54,10 @@ export function cancellableScope( // `() => Promise` signature without forcing every caller to write // `async () => ...` for purely synchronous bodies. const value = await CancellationScope.cancellable(async () => fn()); - return ok(value); + return Ok(value); } catch (error) { if (isCancellation(error)) { - return err(new WorkflowCancelledError(error)); + return Err(new WorkflowCancelledError(error)); } // Non-cancellation throw → re-throw so `makeAsyncResult`'s boundary // routes it through the `defect` channel as an unmodeled failure. @@ -74,7 +74,7 @@ export function cancellableScope( * resource after a graceful shutdown). * * Mirrors `cancellableScope`'s `AsyncResult<...>` shape for symmetry; the - * `err(WorkflowCancelledError)` branch only triggers when cancellation is + * `Err(WorkflowCancelledError)` branch only triggers when cancellation is * raised from inside the scope (rare). Non-cancellation errors surface on the * `defect` channel. * @@ -91,10 +91,10 @@ export function nonCancellableScope( const work = async (): Promise> => { try { const value = await CancellationScope.nonCancellable(async () => fn()); - return ok(value); + return Ok(value); } catch (error) { if (isCancellation(error)) { - return err(new WorkflowCancelledError(error)); + return Err(new WorkflowCancelledError(error)); } throw error; } diff --git a/packages/worker/src/child-workflow.ts b/packages/worker/src/child-workflow.ts index a20a9af5..1e0f96cd 100644 --- a/packages/worker/src/child-workflow.ts +++ b/packages/worker/src/child-workflow.ts @@ -10,7 +10,7 @@ import { executeChild, startChild, } from "@temporalio/workflow"; -import { type AsyncResult, type Result, ok, err } from "unthrown"; +import { type AsyncResult, type Result, Ok, Err } from "unthrown"; import { ChildWorkflowCancelledError, ChildWorkflowError, @@ -61,13 +61,13 @@ async function validateChildWorkflowOutput, ChildWorkflowError>> { const outputResult = await childDefinition.output["~standard"].validate(result); if (outputResult.issues) { - return err( + return Err( new ChildWorkflowError( formatChildWorkflowValidationMessage(childWorkflowName, "output", outputResult.issues), ), ); } - return ok(outputResult.value as ClientInferOutput); + return Ok(outputResult.value as ClientInferOutput); } async function getAndValidateChildWorkflow< @@ -90,7 +90,7 @@ async function getAndValidateChildWorkflow< const childDefinition = childContract.workflows[childWorkflowName]; if (!childDefinition) { - return err( + return Err( new ChildWorkflowNotFoundError( childWorkflowName, Object.keys(childContract.workflows) as string[], @@ -100,7 +100,7 @@ async function getAndValidateChildWorkflow< const inputResult = await childDefinition.input["~standard"].validate(args); if (inputResult.issues) { - return err( + return Err( new ChildWorkflowError( formatChildWorkflowValidationMessage(childWorkflowName, "input", inputResult.issues), ), @@ -111,7 +111,7 @@ async function getAndValidateChildWorkflow< TChildContract["workflows"][TChildWorkflowName] >; - return ok({ + return Ok({ definition: childDefinition as TChildContract["workflows"][TChildWorkflowName], validatedInput, taskQueue: childContract.taskQueue, @@ -137,7 +137,7 @@ function createTypedChildHandle( const result = await handle.result(); return validateChildWorkflowOutput(childDefinition, result, childWorkflowName); } catch (error) { - return err(classifyChildWorkflowError("result", error, childWorkflowName)); + return Err(classifyChildWorkflowError("result", error, childWorkflowName)); } }; return makeAsyncResult(work); @@ -170,7 +170,7 @@ export function createStartChildWorkflow< // impossible defect so `.error` / `.value` narrow cleanly below. assertNoDefect(validationResult); if (validationResult.isErr()) { - return err(validationResult.error); + return Err(validationResult.error); } const { definition: childDefinition, validatedInput, taskQueue } = validationResult.value; @@ -185,9 +185,9 @@ export function createStartChildWorkflow< const typedHandle = createTypedChildHandle(handle, childDefinition, childWorkflowName) as Ok; - return ok(typedHandle); + return Ok(typedHandle); } catch (error) { - return err(classifyChildWorkflowError("startChild", error, String(childWorkflowName))); + return Err(classifyChildWorkflowError("startChild", error, String(childWorkflowName))); } }; return makeAsyncResult(work); @@ -216,7 +216,7 @@ export function createExecuteChildWorkflow< assertNoDefect(validationResult); if (validationResult.isErr()) { - return err(validationResult.error); + return Err(validationResult.error); } const { definition: childDefinition, validatedInput, taskQueue } = validationResult.value; @@ -237,12 +237,12 @@ export function createExecuteChildWorkflow< assertNoDefect(outputValidationResult); if (outputValidationResult.isErr()) { - return err(outputValidationResult.error); + return Err(outputValidationResult.error); } - return ok(outputValidationResult.value as Ok); + return Ok(outputValidationResult.value as Ok); } catch (error) { - return err(classifyChildWorkflowError("executeChild", error, String(childWorkflowName))); + return Err(classifyChildWorkflowError("executeChild", error, String(childWorkflowName))); } }; return makeAsyncResult(work); diff --git a/packages/worker/src/errors.ts b/packages/worker/src/errors.ts index 48f9a3c6..93221d8a 100644 --- a/packages/worker/src/errors.ts +++ b/packages/worker/src/errors.ts @@ -302,7 +302,7 @@ export class ChildWorkflowCancelledError extends TaggedError( } /** - * Error surfaced in the `err(...)` branch of an `AsyncResult` when a typed + * Error surfaced in the `Err(...)` branch of an `AsyncResult` when a typed * cancellation scope is cancelled via Temporal's cancellation propagation. * Returned by both `context.cancellableScope` (when the workflow or an * ancestor scope cancels) and `context.nonCancellableScope` (when @@ -311,7 +311,7 @@ export class ChildWorkflowCancelledError extends TaggedError( * * Non-cancellation errors thrown inside a scope are *unmodeled* failures: they * surface on the scope's `defect` channel (re-thrown at the edge / inspectable - * via `result.isDefect()` and `result.cause`), not as a typed `err(...)`. + * via `result.isDefect()` and `result.cause`), not as a typed `Err(...)`. */ export class WorkflowCancelledError extends TaggedError( "@temporal-contract/WorkflowCancelledError", diff --git a/packages/worker/src/workflow.ts b/packages/worker/src/workflow.ts index 77a944ec..cfba0e0a 100644 --- a/packages/worker/src/workflow.ts +++ b/packages/worker/src/workflow.ts @@ -601,7 +601,7 @@ type WorkflowContext< /** * Run `fn` inside a cancellable Temporal scope. If the workflow (or an * ancestor scope) is cancelled while `fn` is in flight, the resulting - * AsyncResult resolves to `err(WorkflowCancelledError)` instead of + * AsyncResult resolves to `Err(WorkflowCancelledError)` instead of * rejecting — letting callers handle cancellation explicitly, typically * to perform a graceful exit from the current step. * @@ -639,7 +639,7 @@ type WorkflowContext< * * Returns the same `AsyncResult<...>` shape as * {@link WorkflowContext.cancellableScope} for symmetry; the - * `err(WorkflowCancelledError)` branch only triggers when cancellation is + * `Err(WorkflowCancelledError)` branch only triggers when cancellation is * raised from *inside* the scope, which is rare. Non-cancellation errors * surface on the `defect` channel. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70b2ade0..c8186b1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,8 +34,8 @@ catalogs: specifier: 24.13.2 version: 24.13.2 '@unthrown/vitest': - specifier: 0.2.0 - version: 0.2.0 + specifier: 1.0.0 + version: 1.0.0 '@vitest/coverage-v8': specifier: 4.1.8 version: 4.1.8 @@ -82,8 +82,8 @@ catalogs: specifier: 6.0.3 version: 6.0.3 unthrown: - specifier: 0.2.0 - version: 0.2.0 + specifier: 1.0.0 + version: 1.0.0 valibot: specifier: 1.4.1 version: 1.4.1 @@ -181,7 +181,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 0.2.0 + version: 1.0.0 zod: specifier: 'catalog:' version: 4.4.3 @@ -243,7 +243,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 0.2.0 + version: 1.0.0 zod: specifier: 'catalog:' version: 4.4.3 @@ -265,7 +265,7 @@ importers: version: 24.13.2 '@unthrown/vitest': specifier: 'catalog:' - version: 0.2.0(vitest@4.1.8) + version: 1.0.0(vitest@4.1.8) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.8(vitest@4.1.8) @@ -314,7 +314,7 @@ importers: version: 24.13.2 '@unthrown/vitest': specifier: 'catalog:' - version: 0.2.0(vitest@4.1.8) + version: 1.0.0(vitest@4.1.8) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.8(vitest@4.1.8) @@ -332,7 +332,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 0.2.0 + version: 1.0.0 vitest: specifier: 'catalog:' version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -357,7 +357,7 @@ importers: version: link:../../tools/typedoc '@unthrown/vitest': specifier: 'catalog:' - version: 0.2.0(vitest@4.1.8) + version: 1.0.0(vitest@4.1.8) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.8(vitest@4.1.8) @@ -378,7 +378,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 0.2.0 + version: 1.0.0 valibot: specifier: 'catalog:' version: 1.4.1(typescript@6.0.3) @@ -461,7 +461,7 @@ importers: version: 24.13.2 '@unthrown/vitest': specifier: 'catalog:' - version: 0.2.0(vitest@4.1.8) + version: 1.0.0(vitest@4.1.8) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.8(vitest@4.1.8) @@ -479,7 +479,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 0.2.0 + version: 1.0.0 vitest: specifier: 'catalog:' version: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) @@ -2553,8 +2553,8 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@unthrown/vitest@0.2.0': - resolution: {integrity: sha512-0IavcCbccDw5l6NI9Y8EkF+7tDZkz7mYfYalzsYdPMW5z7f/mTRiaUJkZ6qgd4bVPuD1TiQmKTyK3CFZXGi76Q==} + '@unthrown/vitest@1.0.0': + resolution: {integrity: sha512-MEqpq2m6AOae3YGOsPzEmEJ6S5K9+pFFgRFG7BjSUoS4Tu49laPQPsbrCKfYAa//RjS2AfxfwAlPhbms8jEFsg==} engines: {node: '>=22.19'} peerDependencies: vitest: ^4 @@ -4826,8 +4826,8 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unthrown@0.2.0: - resolution: {integrity: sha512-scackbGqniNj2OhQvsr+3x1HaVqcG7NWDrEViTZgtXcalayTl40DDB/0Ck3xkZay/Z8+oGWnFhqZD3RmpqHLUQ==} + unthrown@1.0.0: + resolution: {integrity: sha512-96rhOxlnYSyCmFiHp4HqVMawvXA0H8Poks+ph4p+Uxax1OmS/9aekHZBaiCHEm/d0jApGpbSiuVvxW2NkUGTeA==} engines: {node: '>=22.19'} update-browserslist-db@1.2.3: @@ -6870,9 +6870,9 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@unthrown/vitest@0.2.0(vitest@4.1.8)': + '@unthrown/vitest@1.0.0(vitest@4.1.8)': dependencies: - unthrown: 0.2.0 + unthrown: 1.0.0 vitest: 4.1.8(@types/node@24.13.2)(@vitest/coverage-v8@4.1.8)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) '@upsetjs/venn.js@2.0.0': @@ -9300,7 +9300,7 @@ snapshots: universalify@0.1.2: {} - unthrown@0.2.0: {} + unthrown@1.0.0: {} update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dedfc9aa..518e6f14 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,7 +54,7 @@ catalog: "@temporalio/worker": 1.18.1 "@temporalio/workflow": 1.18.1 "@types/node": 24.13.2 - "@unthrown/vitest": 0.2.0 + "@unthrown/vitest": 1.0.0 "@vitest/coverage-v8": 4.1.8 arktype: 2.2.1 knip: 6.16.1 @@ -70,7 +70,7 @@ catalog: typedoc: 0.28.19 typedoc-plugin-markdown: 4.12.0 typescript: 6.0.3 - unthrown: 0.2.0 + unthrown: 1.0.0 valibot: 1.4.1 vitest: 4.1.8 zod: 4.4.3 From 03f589ed1ec0980e4943cd6369b70bc543fb89f6 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 28 Jun 2026 00:33:11 +0200 Subject: [PATCH 2/2] docs: fix unthrown import names in guide snippets (Ok/Err) The v1.0.0 rename updated the `Ok(...)` / `Err(...)` call sites in the guide code blocks but left their `import { ok, err } from "unthrown"` lines lowercase, so the snippets were internally inconsistent. Rename the imports to the PascalCase constructors (free functions `isOk` / `isErr` / `isDefect` are unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guide/activity-handlers.md | 2 +- docs/guide/client-usage.md | 2 +- docs/guide/entry-points.md | 6 +++--- docs/guide/troubleshooting.md | 2 +- docs/guide/worker-usage.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/guide/activity-handlers.md b/docs/guide/activity-handlers.md index 252905f9..752867f7 100644 --- a/docs/guide/activity-handlers.md +++ b/docs/guide/activity-handlers.md @@ -193,7 +193,7 @@ Mock activities with correct types: ```typescript import type { ActivitiesHandler } from "@temporal-contract/worker/activity"; -import { ok } from "unthrown"; +import { Ok } from "unthrown"; type Handlers = ActivitiesHandler; diff --git a/docs/guide/client-usage.md b/docs/guide/client-usage.md index f98b475c..af71d1b8 100644 --- a/docs/guide/client-usage.md +++ b/docs/guide/client-usage.md @@ -340,7 +340,7 @@ Mock the client for testing: ```typescript import { describe, it, expect, vi } from "vitest"; -import { ok } from "unthrown"; +import { Ok } from "unthrown"; describe("OrderService", () => { it("should process order", async () => { diff --git a/docs/guide/entry-points.md b/docs/guide/entry-points.md index f3a5816a..8e107b49 100644 --- a/docs/guide/entry-points.md +++ b/docs/guide/entry-points.md @@ -370,7 +370,7 @@ Clear separation of concerns: ```typescript // activities/shared.ts -import { ok } from "unthrown"; +import { Ok } from "unthrown"; export const sharedActivities = { sendEmail: ({ to, body }) => Ok({ sent: true }).toAsync(), @@ -378,7 +378,7 @@ export const sharedActivities = { }; // activities/order.ts -import { ok } from "unthrown"; +import { Ok } from "unthrown"; import { sharedActivities } from "./shared"; export const orderActivities = declareActivitiesHandler({ @@ -394,7 +394,7 @@ export const orderActivities = declareActivitiesHandler({ ```typescript // activities/index.ts -import { ok } from "unthrown"; +import { Ok } from "unthrown"; const baseActivities = { validateInput: ({ data }) => Ok({ valid: true }).toAsync(), diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index b3c2f21c..12b69ca9 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -480,7 +480,7 @@ lift a sync `Result` with `.toAsync()`): ```typescript // ✅ For activities, workflows, and clients -import { fromPromise, ok, err, isOk, isErr, isDefect, type AsyncResult } from "unthrown"; +import { fromPromise, Ok, Err, isOk, isErr, isDefect, type AsyncResult } from "unthrown"; // okAsync(value) -> Ok(value).toAsync() // errAsync(error) -> Err(error).toAsync() diff --git a/docs/guide/worker-usage.md b/docs/guide/worker-usage.md index 8cd97351..dec0cdba 100644 --- a/docs/guide/worker-usage.md +++ b/docs/guide/worker-usage.md @@ -18,7 +18,7 @@ Activities use `unthrown` for explicit error handling: ```typescript import { declareActivitiesHandler, ApplicationFailure } from "@temporal-contract/worker/activity"; -import { fromPromise, ok } from "unthrown"; +import { fromPromise, Ok } from "unthrown"; import { myContract } from "./contract"; export const activities = declareActivitiesHandler({