Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/bump-unthrown-v5-stable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@temporal-contract/contract": patch
"@temporal-contract/client": patch
"@temporal-contract/worker": patch
---

Bump `unthrown` to the stable `5.0.0` and raise the peer range to `^5.0.0`.

The only API change since `5.0.0-beta.7` is that the standalone `tag` export is
gone — it now lives on the pattern namespace as `P.tag(t)`, alongside every
other pattern constructor (`P._` / `P.any` / `P.instanceOf` / `P.when` /
`P.union`). The type and runtime behaviour are unchanged: it still produces the
`{ _tag: t }` pattern, still narrows to the matching variant with its payload,
and still works in grouped patterns.

Migration is mechanical — drop `tag` from the import (keeping or adding `P`) and
prefix the call sites:

```diff
- import { tag } from "unthrown";
+ import { P } from "unthrown";

result.mapErrCases((matcher) =>
- matcher.with(tag("@temporal-contract/WorkflowFailedError"), (error) => handle(error)),
+ matcher.with(P.tag("@temporal-contract/WorkflowFailedError"), (error) => handle(error)),
);
```

Every example in the docs, READMEs and TSDoc has been updated to the `P.tag`
spelling, and the upgrade guide gained a note for anyone already tracking an
8.0 beta.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export const activities = declareActivitiesHandler({
Call it — names, arguments, and results all typed, and validated at runtime:

```typescript
import { P } from "unthrown";

const result = await client.executeWorkflow("processOrder", {
workflowId: "order-123",
args: { orderId: "ORD-1", customerId: "CUST-1", amount: 99.99 },
Expand All @@ -93,8 +95,8 @@ result.match({
ok: (output) => console.log(output.transactionId),
errCases: (matcher) =>
matcher.with(
tag("@temporal-contract/WorkflowValidationError"),
tag("@temporal-contract/WorkflowFailedError"),
P.tag("@temporal-contract/WorkflowValidationError"),
P.tag("@temporal-contract/WorkflowFailedError"),
Comment thread
btravers marked this conversation as resolved.
// ...exhaustive — a missing tag is a compile error
(error) => console.error(error.message),
),
Expand Down
8 changes: 4 additions & 4 deletions docs/explanation/the-result-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,16 @@ The `errCases` handler and the `*ErrCases` combinators receive a matcher rather
than the bare error:

```typescript
import { tag } from "unthrown";
import { P } from "unthrown";

result.match({
ok: (output) => output.transactionId,
errCases: (matcher) =>
matcher
.with(tag("@temporal-contract/ContractError"), (e) => handleDomain(e))
.with(P.tag("@temporal-contract/ContractError"), (e) => handleDomain(e))
.with(
tag("@temporal-contract/WorkflowFailedError"),
tag("@temporal-contract/WorkflowExecutionNotFoundError"),
P.tag("@temporal-contract/WorkflowFailedError"),
P.tag("@temporal-contract/WorkflowExecutionNotFoundError"),
(e) => handleInfra(e),
),
defect: (cause) => report(cause),
Expand Down
4 changes: 2 additions & 2 deletions docs/how-to/add-activity-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ import {
declareActivitiesHandler,
type ActivityMiddleware,
} from "@temporal-contract/worker/activity";
import { P, tag } from "unthrown";
import { P } from "unthrown";

const logging: ActivityMiddleware = ({ activityName, workflowName }, next) =>
next().tapErrCases((matcher) =>
matcher.with(
P.instanceOf(ApplicationFailure),
tag("@temporal-contract/ContractError"),
P.tag("@temporal-contract/ContractError"),
(error) => {
logger.warn({ activityName, workflowName, error }, "activity failed");
},
Expand Down
18 changes: 9 additions & 9 deletions docs/how-to/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ describes a compatible set. Do not mix versions.
These are peers, not dependencies, because they appear in the packages' public
types. Your code and the library must resolve to the _same_ copy.

| Peer | Required by | Range |
| ---------------------- | ------------------------ | --------------- |
| `unthrown` | contract, worker, client | `^5.0.0-beta.7` |
| `@temporalio/common` | worker, client | `^1` |
| `@temporalio/worker` | worker, testing | `^1` |
| `@temporalio/workflow` | worker | `^1` |
| `@temporalio/client` | client, testing | `^1` |
| `@temporalio/testing` | testing | `^1` |
| `vitest` | testing | `^4` |
| Peer | Required by | Range |
| ---------------------- | ------------------------ | -------- |
| `unthrown` | contract, worker, client | `^5.0.0` |
| `@temporalio/common` | worker, client | `^1` |
| `@temporalio/worker` | worker, testing | `^1` |
| `@temporalio/workflow` | worker | `^1` |
| `@temporalio/client` | client, testing | `^1` |
| `@temporalio/testing` | testing | `^1` |
| `vitest` | testing | `^4` |

Plus a [Standard Schema](https://standardschema.dev/) library to write your
schemas with — Zod, Valibot, or ArkType.
Expand Down
6 changes: 3 additions & 3 deletions docs/how-to/intercept-client-calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ Modeled domain errors (`WorkflowNotFoundError`, a `ContractError`, a validation
failure) stay on the `err` channel. Branch on those with `flatMapErrCases`:

```typescript
import { Err, Ok, P, tag } from "unthrown";
import { Err, Ok, P } from "unthrown";

const fallback: ClientInterceptor = (args, next) =>
next().flatMapErrCases((matcher) =>
matcher
// Idempotent start: treat "already running" as success.
.with(tag("@temporal-contract/WorkflowAlreadyStartedError"), () => Ok(undefined).toAsync())
.with(P.tag("@temporal-contract/WorkflowAlreadyStartedError"), () => Ok(undefined).toAsync())
// The matcher must cover the whole union — `P._` passes the rest through.
.with(P._, (error) => Err(error).toAsync()),
);
Expand All @@ -110,7 +110,7 @@ const fallback: ClientInterceptor = (args, next) =>
::: warning The matcher is exhaustive
`flatMapErrCases`, `mapErrCases`, `tapErrCases`, and `recoverErrCases` all
require a match that covers every member of the error union — here the nine
members of `ClientCallError`. A single `.with(tag(...))` arm will not compile.
members of `ClientCallError`. A single `.with(P.tag(...))` arm will not compile.
Handle the cases you care about, then close with `P._`.
:::

Expand Down
14 changes: 7 additions & 7 deletions docs/how-to/migrate-from-neverthrow.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ exhaustive matcher instead of the bare error:
result.mapErr((error) => new WrappedError(error));

// after — one arm per tag in the union (abbreviated here; see the note below)
import { tag } from "unthrown";
import { P } from "unthrown";

result.mapErrCases((matcher) =>
matcher.with(tag("@temporal-contract/WorkflowFailedError"), (error) => new WrappedError(error)),
matcher.with(P.tag("@temporal-contract/WorkflowFailedError"), (error) => new WrappedError(error)),
);
```

Expand All @@ -125,9 +125,9 @@ into one branch is compact:

```typescript
matcher.with(
tag("@temporal-contract/WorkflowNotFoundError"),
tag("@temporal-contract/WorkflowValidationError"),
tag("@temporal-contract/WorkflowFailedError"),
P.tag("@temporal-contract/WorkflowNotFoundError"),
P.tag("@temporal-contract/WorkflowValidationError"),
P.tag("@temporal-contract/WorkflowFailedError"),
(error) => report(error),
);
```
Expand All @@ -146,8 +146,8 @@ const message = result.match({
ok: (value) => `charged ${value.transactionId}`,
errCases: (matcher) =>
matcher.with(
tag("@temporal-contract/WorkflowFailedError"),
tag("@temporal-contract/WorkflowValidationError"),
P.tag("@temporal-contract/WorkflowFailedError"),
P.tag("@temporal-contract/WorkflowValidationError"),
(error) => `failed: ${error.message}`,
),
defect: (cause) => `unexpected: ${String(cause)}`,
Expand Down
22 changes: 11 additions & 11 deletions docs/how-to/model-domain-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Declaring errors on an activity **changes its workflow-side call signature.**
So an errors-declaring activity is awaited as a result, not a plain value:

```typescript
import { tag } from "unthrown";
import { P } from "unthrown";

implementation: async (context, order) => {
const charged = await context.activities.chargeCard({
Expand All @@ -141,16 +141,16 @@ implementation: async (context, order) => {
ok: (payment) => ({ status: "completed" as const, transactionId: payment.transactionId }),
errCases: (matcher) =>
matcher
.with(tag("@temporal-contract/ContractError"), (error) => {
.with(P.tag("@temporal-contract/ContractError"), (error) => {
// error.errorName narrows; error.data is typed from the schema
if (error.errorName === "CardDeclined") {
return { status: "failed" as const, reason: error.data.reason };
}
return { status: "failed" as const, reason: error.errorName };
})
.with(
tag("@temporal-contract/ActivityError"),
tag("@temporal-contract/ActivityCancelledError"),
P.tag("@temporal-contract/ActivityError"),
P.tag("@temporal-contract/ActivityCancelledError"),
(error) => ({ status: "failed" as const, reason: error.message }),
),
defect: (cause) => ({
Expand Down Expand Up @@ -179,7 +179,7 @@ A workflow whose declared error caused the failure surfaces it as a
`WorkflowFailedError`:

```typescript
import { tag } from "unthrown";
import { P } from "unthrown";

const result = await client.executeWorkflow("processOrder", {
workflowId: "order-1",
Expand All @@ -190,7 +190,7 @@ result.match({
ok: (output) => console.log("done:", output),
errCases: (matcher) =>
matcher
.with(tag("@temporal-contract/ContractError"), (error) => {
.with(P.tag("@temporal-contract/ContractError"), (error) => {
switch (error.errorName) {
case "EmptyOrder":
return console.error("no items on order", error.data.orderId);
Expand All @@ -199,11 +199,11 @@ result.match({
}
})
.with(
tag("@temporal-contract/WorkflowNotFoundError"),
tag("@temporal-contract/WorkflowValidationError"),
tag("@temporal-contract/WorkflowAlreadyStartedError"),
tag("@temporal-contract/WorkflowFailedError"),
tag("@temporal-contract/WorkflowExecutionNotFoundError"),
P.tag("@temporal-contract/WorkflowNotFoundError"),
P.tag("@temporal-contract/WorkflowValidationError"),
P.tag("@temporal-contract/WorkflowAlreadyStartedError"),
P.tag("@temporal-contract/WorkflowFailedError"),
P.tag("@temporal-contract/WorkflowExecutionNotFoundError"),
(error) => console.error("failed:", error.message),
),
defect: (cause) => console.error("unexpected:", cause),
Expand Down
8 changes: 4 additions & 4 deletions docs/how-to/run-child-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ same-contract and cross-contract calls look identical.

```typescript
import { declareWorkflow } from "@temporal-contract/worker/workflow";
import { tag } from "unthrown";
import { P } from "unthrown";

import { orderContract } from "./contract.js";

Expand All @@ -31,9 +31,9 @@ export const processOrder = declareWorkflow({
ok: (output) => ({ status: "completed" as const, transactionId: output.transactionId }),
errCases: (matcher) =>
matcher.with(
tag("@temporal-contract/ChildWorkflowError"),
tag("@temporal-contract/ChildWorkflowCancelledError"),
tag("@temporal-contract/ChildWorkflowNotFoundError"),
P.tag("@temporal-contract/ChildWorkflowError"),
P.tag("@temporal-contract/ChildWorkflowCancelledError"),
P.tag("@temporal-contract/ChildWorkflowNotFoundError"),
(error) => ({ status: "failed" as const, reason: error.message }),
),
defect: (cause) => ({
Expand Down
4 changes: 3 additions & 1 deletion docs/how-to/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,11 @@ Since 8.0, `TechnicalError` and `RuntimeClientError` ride the defect channel.
Handle them in `defect`, `recoverDefect`, or `tapDefect`:

```typescript
import { P } from "unthrown";

result.match({
Comment thread
Copilot marked this conversation as resolved.
ok: (v) => v,
errCases: (m) => m.with(tag("@temporal-contract/WorkflowFailedError"), handle),
errCases: (m) => m.with(P.tag("@temporal-contract/WorkflowFailedError"), handle),
defect: (cause) => {
if (cause instanceof RuntimeClientError) return report(cause);
throw cause;
Expand Down
40 changes: 29 additions & 11 deletions docs/how-to/upgrade-to-v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ explicitly:

```bash
pnpm add @temporal-contract/contract@beta @temporal-contract/worker@beta \
@temporal-contract/client@beta unthrown@beta
@temporal-contract/client@beta unthrown@^5
```

`unthrown` itself is stable — only the `@temporal-contract/*` packages are on
the `beta` tag.

The [stable docs](https://btravstack.github.io/temporal-contract/) document
7.x; you are reading the beta docs.
:::
Expand All @@ -32,7 +35,7 @@ pnpm add @temporal-contract/contract@beta \
@temporal-contract/worker@beta \
@temporal-contract/client@beta
pnpm add -D @temporal-contract/testing@beta
pnpm add unthrown@^5.0.0-beta.7
pnpm add unthrown@^5.0.0
```

If an intermediate beta had you install `ts-pattern` as a peer, remove it —
Expand All @@ -43,6 +46,20 @@ dependencies:
pnpm remove ts-pattern
```

If you already tracked an 8.0 beta, the standalone `tag` export is gone in
unthrown 5.0.0 — it is `P.tag` now. Drop `tag` from the import (keeping or
adding `P`) and prefix the call sites:

```diff
- import { tag } from "unthrown";
+ import { P } from "unthrown";

result.mapErrCases((matcher) =>
- matcher.with(tag("@temporal-contract/WorkflowFailedError"), (error) => handle(error)),
+ matcher.with(P.tag("@temporal-contract/WorkflowFailedError"), (error) => handle(error)),
);
```

## 2. Rename the error combinators

The bare combinators gained a `Cases` suffix, and their callback now receives a
Expand All @@ -61,7 +78,7 @@ result.mapErr((error) => new WrappedError(error));

// 8.0 — one arm per tag in the union (abbreviated here; see the note below)
result.mapErrCases((matcher) =>
matcher.with(tag("@temporal-contract/WorkflowFailedError"), (error) => new WrappedError(error)),
matcher.with(P.tag("@temporal-contract/WorkflowFailedError"), (error) => new WrappedError(error)),
);
```

Expand Down Expand Up @@ -92,8 +109,8 @@ result.match({
ok: (value) => value,
errCases: (matcher) =>
matcher.with(
tag("@temporal-contract/WorkflowFailedError"),
tag("@temporal-contract/WorkflowValidationError"),
P.tag("@temporal-contract/WorkflowFailedError"),
P.tag("@temporal-contract/WorkflowValidationError"),
(error) => handle(error),
),
defect: (cause) => report(cause),
Expand Down Expand Up @@ -162,16 +179,16 @@ result.match({
ok: (value) => value,
errCases: (matcher) =>
matcher
.with(tag("@temporal-contract/RuntimeClientError"), (e) => report(e)) // ❌ remove
.with(tag("@temporal-contract/WorkflowFailedError"), (e) => handle(e)),
.with(P.tag("@temporal-contract/RuntimeClientError"), (e) => report(e)) // ❌ remove
.with(P.tag("@temporal-contract/WorkflowFailedError"), (e) => handle(e)),
defect: (cause) => report(cause),
});

// 8.0
result.match({
ok: (value) => value,
errCases: (matcher) =>
matcher.with(tag("@temporal-contract/WorkflowFailedError"), (e) => handle(e)),
matcher.with(P.tag("@temporal-contract/WorkflowFailedError"), (e) => handle(e)),
defect: (cause) => {
if (cause instanceof RuntimeClientError) {
return report(cause); // handle it here instead
Expand Down Expand Up @@ -223,13 +240,14 @@ const retryOnce: ClientInterceptor = (args, next) =>
## Checklist

- [ ] All four `@temporal-contract/*` packages on the same 8.0 version
- [ ] `unthrown` resolves to `^5.0.0-beta.7`
- [ ] `unthrown` resolves to `^5.0.0`
- [ ] `ts-pattern` removed if it was added for beta.5
- [ ] `tag(...)` → `P.tag(...)` if you tracked an earlier 8.0 beta
- [ ] `mapErr` / `flatMapErr` / `tapErr` / `recoverErr` → `*Cases`
- [ ] `match({ err })` → `match({ errCases })`
- [ ] `TypedClient.create` / `createWorker` use `isDefect()` or `.get()`
- [ ] No `tag("@temporal-contract/RuntimeClientError")` or
`tag("@temporal-contract/TechnicalError")` arms remain
- [ ] No `P.tag("@temporal-contract/RuntimeClientError")` or
`P.tag("@temporal-contract/TechnicalError")` arms remain
- [ ] `pnpm typecheck` clean

The exhaustive matcher does most of the work: once it compiles, the migration is
Expand Down
Loading
Loading