From 136bd09e10ebc14b0f337b46001b6b7036d2cb3c Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 26 Jul 2026 17:40:30 +0200 Subject: [PATCH 1/5] build: bump unthrown to 5.0.0-beta.1 and enter changesets beta pre mode --- .changeset/pre.json | 11 ++++++++ packages/client/package.json | 2 +- packages/contract/package.json | 2 +- packages/worker/package.json | 2 +- pnpm-lock.yaml | 49 +++++++++++++++++++--------------- pnpm-workspace.yaml | 4 +-- 6 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..f3e78500 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,11 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@temporal-contract/client": "7.0.0", + "@temporal-contract/contract": "7.0.0", + "@temporal-contract/testing": "7.0.0", + "@temporal-contract/worker": "7.0.0" + }, + "changesets": [] +} diff --git a/packages/client/package.json b/packages/client/package.json index afa577f7..c7dfa104 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -77,7 +77,7 @@ "peerDependencies": { "@temporalio/client": "^1", "@temporalio/common": "^1", - "unthrown": "^4.1.0" + "unthrown": "^5.0.0-beta.1" }, "engines": { "node": ">=22.19.0" diff --git a/packages/contract/package.json b/packages/contract/package.json index 8b2d2efa..cbed4638 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -87,7 +87,7 @@ "vitest": "catalog:" }, "peerDependencies": { - "unthrown": "^4.1.0" + "unthrown": "^5.0.0-beta.1" }, "peerDependenciesMeta": { "unthrown": { diff --git a/packages/worker/package.json b/packages/worker/package.json index 24a7cab9..b8869a06 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -96,7 +96,7 @@ "@temporalio/common": "^1", "@temporalio/worker": "^1", "@temporalio/workflow": "^1", - "unthrown": "^4.1.0" + "unthrown": "^5.0.0-beta.1" }, "engines": { "node": ">=22.19.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1eb0ecb9..3665812d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ catalogs: specifier: 26.1.1 version: 26.1.1 '@unthrown/vitest': - specifier: 4.1.0 - version: 4.1.0 + specifier: 5.0.0-beta.1 + version: 5.0.0-beta.1 '@vitest/coverage-v8': specifier: 4.1.10 version: 4.1.10 @@ -100,8 +100,8 @@ catalogs: specifier: 6.0.3 version: 6.0.3 unthrown: - specifier: 4.1.0 - version: 4.1.0 + specifier: 5.0.0-beta.1 + version: 5.0.0-beta.1 valibot: specifier: 1.4.2 version: 1.4.2 @@ -211,7 +211,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 4.1.0 + version: 5.0.0-beta.1 zod: specifier: 'catalog:' version: 4.4.3 @@ -273,7 +273,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 4.1.0 + version: 5.0.0-beta.1 zod: specifier: 'catalog:' version: 4.4.3 @@ -295,7 +295,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 4.1.0(unthrown@4.1.0)(vitest@4.1.10) + version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -344,7 +344,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 4.1.0(unthrown@4.1.0)(vitest@4.1.10) + version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -362,7 +362,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 4.1.0 + version: 5.0.0-beta.1 vitest: specifier: 'catalog:' version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) @@ -390,7 +390,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 4.1.0(unthrown@4.1.0)(vitest@4.1.10) + version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -411,7 +411,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 4.1.0 + version: 5.0.0-beta.1 valibot: specifier: 'catalog:' version: 1.4.2(typescript@6.0.3) @@ -500,7 +500,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 4.1.0(unthrown@4.1.0)(vitest@4.1.10) + version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -518,7 +518,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 4.1.0 + version: 5.0.0-beta.1 vitest: specifier: 'catalog:' version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) @@ -2514,11 +2514,11 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@unthrown/vitest@4.1.0': - resolution: {integrity: sha512-48ZKOblwH3jNF7gGvt6piGQzJR6jqxsXqnOiaaAXmhsOoTwfGQYfdNNqhGkMaAeRDuNvOQjb5sr1G0oaBFuZZQ==} + '@unthrown/vitest@5.0.0-beta.1': + resolution: {integrity: sha512-NkHXDyJDVUf5u9XLWccXj3GrWzcC7Oapg9VnnJaqQqWnchXIMyZ257GA7ByesmYDZUWFTabSWSkyBCp/AP0N+g==} engines: {node: '>=20'} peerDependencies: - unthrown: ^4.1.0 + unthrown: ^5.0.0-beta.1 vitest: ^4 '@upsetjs/venn.js@2.0.0': @@ -4680,6 +4680,9 @@ packages: resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} engines: {node: '>=6.10'} + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + tsdown@0.22.7: resolution: {integrity: sha512-4egbOzc9dxVv/QS+gDV75FIxDIjQQeOnXBlUuikyjmn0ozuc6FW11djJjEEo3vqkuJRygpnKHurnj+Iwftk4VA==} engines: {node: ^22.18.0 || >=24.11.0} @@ -4789,8 +4792,8 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unthrown@4.1.0: - resolution: {integrity: sha512-qzbbyiA7CPQ+DwKDIZe+yW5/o7HQjW9/lfiHO0XCqtDiBCaq/ONPFTJoNGLqu19DmPCf+aWXKwZAvbquySXhkA==} + unthrown@5.0.0-beta.1: + resolution: {integrity: sha512-kRlaZhK0BXwoZV2W7YnxnA1JxNWpYtxSPy15jFG1LijTRz2BKbkw3OSLUNzUpAic26o2g0W+Xr6smHV2Yhiazw==} engines: {node: '>=20'} update-browserslist-db@1.2.3: @@ -6818,9 +6821,9 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@unthrown/vitest@4.1.0(unthrown@4.1.0)(vitest@4.1.10)': + '@unthrown/vitest@5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10)': dependencies: - unthrown: 4.1.0 + unthrown: 5.0.0-beta.1 vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) '@upsetjs/venn.js@2.0.0': @@ -9128,6 +9131,8 @@ snapshots: ts-dedent@2.3.0: {} + ts-pattern@5.9.0: {} + tsdown@0.22.7(oxc-resolver@11.21.3)(tsx@4.23.1)(typescript@6.0.3): dependencies: ansis: 4.3.1 @@ -9232,7 +9237,9 @@ snapshots: universalify@0.1.2: {} - unthrown@4.1.0: {} + unthrown@5.0.0-beta.1: + dependencies: + ts-pattern: 5.9.0 update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fab9081d..dd03f895 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,7 +25,7 @@ catalog: "@temporalio/worker": 1.20.2 "@temporalio/workflow": 1.20.2 "@types/node": 26.1.1 - "@unthrown/vitest": 4.1.0 + "@unthrown/vitest": 5.0.0-beta.1 "@vitest/coverage-v8": 4.1.10 arktype: 2.2.3 knip: 6.26.0 @@ -41,7 +41,7 @@ catalog: typedoc: 0.28.20 typedoc-plugin-markdown: 4.12.0 typescript: 6.0.3 - unthrown: 4.1.0 + unthrown: 5.0.0-beta.1 valibot: 1.4.2 vitest: 4.1.10 zod: 4.4.3 From 757e44b453f0767cf4eeb16d3dc339629954a199 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 26 Jul 2026 17:41:12 +0200 Subject: [PATCH 2/5] feat!: adopt unthrown v5 ts-pattern matcher API Migrate the error combinators (flatMapErr / tapErr) and match's err handler to unthrown v5's ts-pattern matcher callbacks, and replace the removed matchTags with result.match({ err: (m) => m.with(tag(...), ...) }). Also pin the modeled error union on the client's widening Err(...) returns (Err(...)) to work around a v5-beta covariance regression where a generic-conditional error union defeats the out-E variance fast path. --- .changeset/adopt-unthrown-v5-beta.md | 7 ++ .../order-processing-client/src/client.ts | 106 ++++++++++-------- packages/client/src/__tests__/client.spec.ts | 8 +- packages/client/src/client.spec.ts | 18 +-- packages/client/src/client.ts | 30 ++--- packages/client/src/interceptors.ts | 6 +- packages/contract/src/errors.spec.ts | 2 +- packages/contract/src/errors.ts | 4 +- .../src/activity-contract-errors.spec.ts | 10 +- packages/worker/src/activity.ts | 31 ++--- packages/worker/src/cancellation.ts | 7 +- packages/worker/src/errors.ts | 6 +- packages/worker/src/worker.ts | 9 +- packages/worker/src/workflow.ts | 4 +- 14 files changed, 144 insertions(+), 104 deletions(-) create mode 100644 .changeset/adopt-unthrown-v5-beta.md diff --git a/.changeset/adopt-unthrown-v5-beta.md b/.changeset/adopt-unthrown-v5-beta.md new file mode 100644 index 00000000..11542ccc --- /dev/null +++ b/.changeset/adopt-unthrown-v5-beta.md @@ -0,0 +1,7 @@ +--- +"@temporal-contract/contract": major +"@temporal-contract/worker": major +"@temporal-contract/client": major +--- + +Adopt unthrown v5 (beta): the error combinators and `match`'s `err` handler now take a ts-pattern matcher callback; peer bumped to `^5.0.0-beta.1`. diff --git a/examples/order-processing-client/src/client.ts b/examples/order-processing-client/src/client.ts index be7d8d2b..bc75abb2 100644 --- a/examples/order-processing-client/src/client.ts +++ b/examples/order-processing-client/src/client.ts @@ -4,7 +4,7 @@ import { type OrderSchema, } from "@temporal-contract/sample-order-processing-contract"; import { Client, Connection } from "@temporalio/client"; -import { matchTags } from "unthrown"; +import { tag } from "unthrown"; import type { z } from "zod"; import { logger } from "./logger.js"; @@ -91,7 +91,7 @@ async function run() { // Chain start → result on the AsyncResult railway: `tap` logs the started // handle without leaving the railway, `flatMap` sequences the dependent // `handle.result()` call, and its error union widens to cover both phases. - // A single `matchTags` then folds the combined result exhaustively — every + // A single `match` then folds the combined result exhaustively — every // modeled error tag (package-namespaced `@temporal-contract/...`) plus `Ok` // and `Defect` must be handled, or it is a compile error. const result = await contractClient @@ -102,8 +102,8 @@ async function run() { }) .flatMap((handle) => handle.result()); - matchTags(result, { - Ok: (output) => { + result.match({ + ok: (output) => { if (output.status === "completed") { logger.info( { @@ -124,32 +124,40 @@ async function run() { ); } }, - "@temporal-contract/WorkflowNotFoundError": (err) => - logger.error({ error: err, orderId: order.orderId }, "❌ Workflow not found"), - "@temporal-contract/WorkflowValidationError": (err) => - logger.error({ error: err, orderId: order.orderId }, "❌ Workflow validation failed"), - // Idempotent fast-path: a workflow with this ID is already running (or in - // retention). Production callers can re-fetch the existing handle; here we - // just log and move on. - "@temporal-contract/WorkflowAlreadyStartedError": (err) => - logger.warn( - { error: err, orderId: order.orderId }, - "⏭️ Workflow already started — skipping", - ), - "@temporal-contract/WorkflowFailedError": (err) => - logger.error( - { error: err, orderId: order.orderId, cause: err.cause }, - "❌ Workflow completed with failure", - ), - "@temporal-contract/WorkflowExecutionNotFoundError": (err) => - logger.error( - { error: err, orderId: order.orderId }, - "❌ Workflow execution not found in namespace", - ), - "@temporal-contract/RuntimeClientError": (err) => - logger.error({ error: err, orderId: order.orderId }, "❌ Workflow execution failed"), + err: (matcher) => + matcher + .with(tag("@temporal-contract/WorkflowNotFoundError"), (err) => + logger.error({ error: err, orderId: order.orderId }, "❌ Workflow not found"), + ) + .with(tag("@temporal-contract/WorkflowValidationError"), (err) => + logger.error({ error: err, orderId: order.orderId }, "❌ Workflow validation failed"), + ) + // Idempotent fast-path: a workflow with this ID is already running (or in + // retention). Production callers can re-fetch the existing handle; here we + // just log and move on. + .with(tag("@temporal-contract/WorkflowAlreadyStartedError"), (err) => + logger.warn( + { error: err, orderId: order.orderId }, + "⏭️ Workflow already started — skipping", + ), + ) + .with(tag("@temporal-contract/WorkflowFailedError"), (err) => + logger.error( + { error: err, orderId: order.orderId, cause: err.cause }, + "❌ Workflow completed with failure", + ), + ) + .with(tag("@temporal-contract/WorkflowExecutionNotFoundError"), (err) => + logger.error( + { error: err, orderId: order.orderId }, + "❌ Workflow execution not found in namespace", + ), + ) + .with(tag("@temporal-contract/RuntimeClientError"), (err) => + logger.error({ error: err, orderId: order.orderId }, "❌ Workflow execution failed"), + ), // A defect is an unmodeled failure (a bug), not an anticipated outcome. - Defect: (cause) => + defect: (cause) => logger.error({ cause, orderId: order.orderId }, "❌ Unexpected failure processing order"), }); } @@ -178,8 +186,8 @@ async function run() { // Handle the result by tag — `executeWorkflow` can surface both start-phase // and result-phase errors, so its union is the widest. - matchTags(result, { - Ok: (output) => { + result.match({ + ok: (output) => { const summary = { id: output.orderId, success: output.status === "completed", @@ -190,20 +198,28 @@ async function run() { }; logger.info({ data: summary }, `📊 Order summary: ${summary.message}`); }, - "@temporal-contract/WorkflowNotFoundError": (err) => - logger.error({ error: err }, "❌ Workflow not found"), - "@temporal-contract/WorkflowValidationError": (err) => - logger.error({ error: err }, "❌ Validation failed"), - "@temporal-contract/WorkflowAlreadyStartedError": (err) => - logger.warn({ error: err }, "⏭️ Workflow already started"), - "@temporal-contract/WorkflowFailedError": (err) => - logger.error({ error: err, cause: err.cause }, "❌ Workflow completed with failure"), - "@temporal-contract/WorkflowExecutionNotFoundError": (err) => - logger.error({ error: err }, "❌ Workflow execution not found in namespace"), - "@temporal-contract/RuntimeClientError": (err) => - logger.error({ error: err }, "❌ Workflow execution failed"), + err: (matcher) => + matcher + .with(tag("@temporal-contract/WorkflowNotFoundError"), (err) => + logger.error({ error: err }, "❌ Workflow not found"), + ) + .with(tag("@temporal-contract/WorkflowValidationError"), (err) => + logger.error({ error: err }, "❌ Validation failed"), + ) + .with(tag("@temporal-contract/WorkflowAlreadyStartedError"), (err) => + logger.warn({ error: err }, "⏭️ Workflow already started"), + ) + .with(tag("@temporal-contract/WorkflowFailedError"), (err) => + logger.error({ error: err, cause: err.cause }, "❌ Workflow completed with failure"), + ) + .with(tag("@temporal-contract/WorkflowExecutionNotFoundError"), (err) => + logger.error({ error: err }, "❌ Workflow execution not found in namespace"), + ) + .with(tag("@temporal-contract/RuntimeClientError"), (err) => + logger.error({ error: err }, "❌ Workflow execution failed"), + ), // A defect is an unmodeled failure (a bug), not an anticipated outcome. - Defect: (cause) => logger.error({ cause }, "❌ Unexpected failure executing workflow"), + defect: (cause) => logger.error({ cause }, "❌ Unexpected failure executing workflow"), }); logger.info("\n✨ Done!"); @@ -213,7 +229,7 @@ async function run() { logger.info(" - Type-safe error values"); logger.info(" - Functional composition with flatMap, map, mapErr, flatMapErr"); logger.info(" - Railway-oriented programming"); - logger.info(" - Exhaustive error matching with unthrown matchTags"); + logger.info(" - Exhaustive error matching with unthrown match"); process.exit(0); } diff --git a/packages/client/src/__tests__/client.spec.ts b/packages/client/src/__tests__/client.spec.ts index f3db97ac..56830fc0 100644 --- a/packages/client/src/__tests__/client.spec.ts +++ b/packages/client/src/__tests__/client.spec.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { it as baseIt } from "@temporal-contract/testing/extension"; import { Client } from "@temporalio/client"; import { Worker } from "@temporalio/worker"; +import { P } from "unthrown"; import { describe, expect, vi, beforeEach } from "vitest"; import { TypedClient } from "../client.js"; @@ -401,9 +402,10 @@ describe("Client Package - Integration Tests", () => { matched = true; expect(value).toEqual({ result: "Processed: test" }); }, - err: () => { - throw new Error("Should not be called"); - }, + err: (matcher) => + matcher.with(P._, () => { + throw new Error("Should not be called"); + }), defect: () => { throw new Error("Should not be called"); }, diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index d0887acf..1a642c92 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -11,7 +11,7 @@ import { TypedSearchAttributes, WorkflowNotFoundError as TemporalWorkflowNotFoundError, } from "@temporalio/common"; -import { Err } from "unthrown"; +import { Err, P } from "unthrown"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { z } from "zod"; @@ -784,9 +784,10 @@ describe("TypedClient", () => { matched = true; expect(value).toEqual({ result: "success" }); }, - err: () => { - throw new Error("Should not be called"); - }, + err: (matcher) => + matcher.with(P._, () => { + throw new Error("Should not be called"); + }), defect: () => { throw new Error("Should not be called"); }, @@ -1512,9 +1513,12 @@ describe("TypedClient — interceptors", () => { .mockRejectedValueOnce(new Error("transient")) .mockResolvedValueOnce({ result: "ok" }); const retryOnce: ClientInterceptor = (_args, next) => - next().flatMapErr( - (error): ReturnType => - error instanceof RuntimeClientError ? next() : Err(error).toAsync(), + next().flatMapErr((matcher) => + matcher.with( + P._, + (error): ReturnType => + error instanceof RuntimeClientError ? next() : Err(error).toAsync(), + ), ); const result = await clientWith([retryOnce]).executeWorkflow("testWorkflow", { diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index f0d230b6..c0b9fbad 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -415,7 +415,7 @@ export class TypedClient { * * await result.match({ * ok: async (handle) => { await handle.pause("maintenance"); }, - * err: (error) => console.error("schedule create failed", error), + * err: (matcher) => matcher.with(P._, (error) => console.error("schedule create failed", error)), * defect: (cause) => console.error("unexpected failure", cause), * }); * ``` @@ -546,7 +546,7 @@ export class TypedClient { * const result = await handle.result(); * // ... handle result * }, - * err: (error) => console.error('Failed to start:', error), + * err: (matcher) => matcher.with(P._, (error) => console.error('Failed to start:', error)), * defect: (cause) => console.error('Unexpected failure:', cause), * }); * ``` @@ -637,7 +637,7 @@ export class TypedClient { * * await result.match({ * ok: (handle) => console.log('signaled run', handle.signaledRunId), - * err: (error) => console.error('signalWithStart failed', error), + * err: (matcher) => matcher.with(P._, (error) => console.error('signalWithStart failed', error)), * defect: (cause) => console.error('unexpected failure', cause), * }); * ``` @@ -761,7 +761,7 @@ export class TypedClient { * * await result.match({ * ok: (output) => console.log('Order processed:', output.status), - * err: (error) => console.error('Processing failed:', error), + * err: (matcher) => matcher.with(P._, (error) => console.error('Processing failed:', error)), * defect: (cause) => console.error('Unexpected failure:', cause), * }); * ``` @@ -802,7 +802,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 { @@ -818,7 +818,9 @@ 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); @@ -828,7 +830,7 @@ export class TypedClient { // routing through a dedicated helper — this is the only call site // that needs the full union. if (error instanceof WorkflowExecutionAlreadyStartedError) { - return Err( + return Err( new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error), ); } @@ -848,7 +850,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, @@ -856,7 +858,7 @@ export class TypedClient { ); } if (error instanceof TemporalWorkflowNotFoundError) { - return Err( + return Err( new WorkflowExecutionNotFoundError( error.workflowId || temporalOptions.workflowId, error.runId, @@ -864,7 +866,7 @@ export class TypedClient { ), ); } - return Err(createRuntimeClientError("executeWorkflow", error)); + return Err(createRuntimeClientError("executeWorkflow", error)); } }; return makeAsyncResult(work); @@ -894,7 +896,7 @@ export class TypedClient { * const result = await handle.result(); * // ... handle result * }, - * err: (error) => console.error('Failed to get handle:', error), + * err: (matcher) => matcher.with(P._, (error) => console.error('Failed to get handle:', error)), * defect: (cause) => console.error('Unexpected failure:', cause), * }); * ``` @@ -992,7 +994,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", @@ -1011,7 +1013,7 @@ export class TypedClient { return Err(rehydrated as Err); } } - return Err(classifyResultError("result", error, workflowHandle.workflowId)); + return Err(classifyResultError("result", error, workflowHandle.workflowId)); } }; return makeAsyncResult(work); @@ -1138,7 +1140,7 @@ function buildValidatedProxy(classifyHandleError(operation, error, workflowId)); } }; return makeAsyncResult(work); diff --git a/packages/client/src/interceptors.ts b/packages/client/src/interceptors.ts index 514ece07..09ef6051 100644 --- a/packages/client/src/interceptors.ts +++ b/packages/client/src/interceptors.ts @@ -100,8 +100,10 @@ export type ClientInterceptorNext = (patch?: { * @example Retry a transient failure once * ```ts * const retryOnce: ClientInterceptor = (args, next) => - * next().flatMapErr((error) => - * error instanceof RuntimeClientError ? next() : Err(error).toAsync(), + * next().flatMapErr((matcher) => + * matcher.with(P._, (error) => + * error instanceof RuntimeClientError ? next() : Err(error).toAsync(), + * ), * ); * ``` */ diff --git a/packages/contract/src/errors.spec.ts b/packages/contract/src/errors.spec.ts index a22b39f3..c244fb53 100644 --- a/packages/contract/src/errors.spec.ts +++ b/packages/contract/src/errors.spec.ts @@ -59,7 +59,7 @@ describe("_internal_buildErrorConstructors", () => { expect(withoutData.cause).toBe(cause); }); - it("carries the unthrown tag for matchTags-style discrimination", () => { + it("carries the unthrown tag for match/tag-style discrimination", () => { const constructors = _internal_buildErrorConstructors(declaredErrors); expect(constructors["OutOfStock"]!()._tag).toBe("@temporal-contract/ContractError"); diff --git a/packages/contract/src/errors.ts b/packages/contract/src/errors.ts index 1175b33f..a8e11b5c 100644 --- a/packages/contract/src/errors.ts +++ b/packages/contract/src/errors.ts @@ -61,8 +61,8 @@ export class TechnicalError extends TaggedError("@temporal-contract/TechnicalErr * * The unthrown `_tag` ("@temporal-contract/ContractError") discriminates a * `ContractError` from the other tagged errors in a Result's error channel - * (e.g. via `matchTags`); `errorName` then narrows to the concrete declared - * error. + * (e.g. via `result.match({ err: (m) => m.with(tag("@temporal-contract/ContractError"), …) })`); + * `errorName` then narrows to the concrete declared error. */ export class ContractError extends TaggedError( "@temporal-contract/ContractError", diff --git a/packages/worker/src/activity-contract-errors.spec.ts b/packages/worker/src/activity-contract-errors.spec.ts index 178f7cc5..b77de491 100644 --- a/packages/worker/src/activity-contract-errors.spec.ts +++ b/packages/worker/src/activity-contract-errors.spec.ts @@ -1,6 +1,6 @@ import { defineContract } from "@temporal-contract/contract"; import { ContractError } from "@temporal-contract/contract/errors"; -import { Ok, Err, type AsyncResult } from "unthrown"; +import { Ok, Err, P, type AsyncResult } from "unthrown"; /** * Runtime coverage for `declareActivitiesHandler`'s contract-declared typed * errors, middleware chain, and dependency context — the boundary where an @@ -298,9 +298,11 @@ describe("declareActivitiesHandler — middleware", () => { it("observes typed errors on the err channel", async () => { const observed: unknown[] = []; const observing: ActivityMiddleware = (_invocation, next) => - next().tapErr((error) => { - observed.push(error); - }); + next().tapErr((matcher) => + matcher.with(P._, (error) => { + observed.push(error); + }), + ); const activities = declareActivitiesHandler({ contract, diff --git a/packages/worker/src/activity.ts b/packages/worker/src/activity.ts index c4fc168a..845689cc 100644 --- a/packages/worker/src/activity.ts +++ b/packages/worker/src/activity.ts @@ -29,7 +29,7 @@ import { type ContractErrorInputUnion, } from "@temporal-contract/contract/errors"; import { ApplicationFailure } from "@temporalio/common"; -import type { AsyncResult } from "unthrown"; +import { P, type AsyncResult } from "unthrown"; import { contractErrorToApplicationFailure } from "./contract-errors.js"; import { @@ -294,9 +294,11 @@ export type ActivityMiddlewareNext< * @example Log every activity invocation and its outcome (read-only) * ```ts * const logging: ActivityMiddleware = ({ activityName, workflowName }, next) => - * next().tapErr((error) => { - * logger.warn({ activityName, workflowName, error }, "activity failed"); - * }); + * next().tapErr((matcher) => + * matcher.with(P._, (error) => { + * logger.warn({ activityName, workflowName, error }, "activity failed"); + * }), + * ); * ``` * * @example Guard-and-narrow: inject a tenant id for everything downstream @@ -739,16 +741,17 @@ export function declareActivitiesHandler< // retry policy (honoring `nonRetryable: true`). Contract errors are // validated against their declared data schema and serialized as // ApplicationFailure(type = error name, details = [data]). - err: async (error) => { - if (error instanceof ContractError) { - throw await contractErrorToApplicationFailure( - error, - activityDef.errors, - `activity "${label}"`, - ); - } - throw error; - }, + err: (matcher) => + matcher.with(P._, async (error) => { + if (error instanceof ContractError) { + throw await contractErrorToApplicationFailure( + error, + activityDef.errors, + `activity "${label}"`, + ); + } + throw error; + }), // A defect is an *unanticipated* throw inside the activity. Re-throw the // original cause unwrapped: Temporal wraps a non-`ApplicationFailure` // error as `ApplicationFailure(type: "Error")` and applies the default diff --git a/packages/worker/src/cancellation.ts b/packages/worker/src/cancellation.ts index a5a83dc3..e2a8fdb0 100644 --- a/packages/worker/src/cancellation.ts +++ b/packages/worker/src/cancellation.ts @@ -37,9 +37,10 @@ import { makeAsyncResult } from "./internal.js"; * * result.match({ * ok: (output) => { ... }, - * err: (error) => { - * // error instanceof WorkflowCancelledError — graceful exit - * }, + * err: (matcher) => + * matcher.with(P._, (error) => { + * // error instanceof WorkflowCancelledError — graceful exit + * }), * defect: (cause) => { * // a non-cancellation failure thrown inside the scope (a bug) * }, diff --git a/packages/worker/src/errors.ts b/packages/worker/src/errors.ts index 7f81af94..8df19f36 100644 --- a/packages/worker/src/errors.ts +++ b/packages/worker/src/errors.ts @@ -345,9 +345,9 @@ export class ChildWorkflowError extends TaggedError("@temporal-contract/ChildWor * A sibling of {@link ChildWorkflowError} rather than a subclass: both are * distinct {@link TaggedError}s, so call sites discriminate on the `_tag` * (or `instanceof ChildWorkflowCancelledError`) instead of relying on an - * `instanceof ChildWorkflowError` that also matches cancellation. `matchTags` - * folds the `ChildWorkflowError | ChildWorkflowCancelledError` union - * exhaustively. + * `instanceof ChildWorkflowError` that also matches cancellation. A + * `result.match` with the ts-pattern `err` matcher folds the + * `ChildWorkflowError | ChildWorkflowCancelledError` union exhaustively. */ export class ChildWorkflowCancelledError extends TaggedError( "@temporal-contract/ChildWorkflowCancelledError", diff --git a/packages/worker/src/worker.ts b/packages/worker/src/worker.ts index 367e6dce..ffdb8862 100644 --- a/packages/worker/src/worker.ts +++ b/packages/worker/src/worker.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { type ContractDefinition } from "@temporal-contract/contract"; import { TechnicalError } from "@temporal-contract/contract/errors"; import { Worker, type WorkerOptions } from "@temporalio/worker"; -import { fromPromise, type AsyncResult } from "unthrown"; +import { fromPromise, P, type AsyncResult } from "unthrown"; import type { ActivitiesHandler } from "./activity.js"; @@ -103,9 +103,10 @@ export async function createWorkerOrThrow( const result = await createWorker(options); return result.match({ ok: (worker) => worker, - err: (error) => { - throw error.cause ?? error; - }, + err: (matcher) => + matcher.with(P._, (error) => { + throw error.cause ?? error; + }), defect: (cause) => { throw cause; }, diff --git a/packages/worker/src/workflow.ts b/packages/worker/src/workflow.ts index 16e3ea0d..9ffee567 100644 --- a/packages/worker/src/workflow.ts +++ b/packages/worker/src/workflow.ts @@ -627,7 +627,7 @@ type WorkflowContext< * const result = await handle.result(); * // ... handle result * }, - * err: (error) => console.error('Failed to start:', error), + * err: (matcher) => matcher.with(P._, (error) => console.error('Failed to start:', error)), * defect: (cause) => console.error('Unexpected failure:', cause), * }); * ``` @@ -667,7 +667,7 @@ type WorkflowContext< * * await result.match({ * ok: (output) => console.log('Payment processed:', output), - * err: (error) => console.error('Processing failed:', error), + * err: (matcher) => matcher.with(P._, (error) => console.error('Processing failed:', error)), * defect: (cause) => console.error('Unexpected failure:', cause), * }); * ``` From 203ed7f00247382a29a2bf4c6478debbc665cc67 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 26 Jul 2026 18:47:46 +0200 Subject: [PATCH 3/5] chore(deps): bump unthrown to 5.0.0-beta.2 --- packages/client/package.json | 2 +- packages/contract/package.json | 2 +- packages/worker/package.json | 2 +- pnpm-lock.yaml | 42 +++++++++++++++++----------------- pnpm-workspace.yaml | 4 ++-- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index c7dfa104..bb7cf915 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -77,7 +77,7 @@ "peerDependencies": { "@temporalio/client": "^1", "@temporalio/common": "^1", - "unthrown": "^5.0.0-beta.1" + "unthrown": "^5.0.0-beta.2" }, "engines": { "node": ">=22.19.0" diff --git a/packages/contract/package.json b/packages/contract/package.json index cbed4638..d85fc120 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -87,7 +87,7 @@ "vitest": "catalog:" }, "peerDependencies": { - "unthrown": "^5.0.0-beta.1" + "unthrown": "^5.0.0-beta.2" }, "peerDependenciesMeta": { "unthrown": { diff --git a/packages/worker/package.json b/packages/worker/package.json index b8869a06..f50c8c5a 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -96,7 +96,7 @@ "@temporalio/common": "^1", "@temporalio/worker": "^1", "@temporalio/workflow": "^1", - "unthrown": "^5.0.0-beta.1" + "unthrown": "^5.0.0-beta.2" }, "engines": { "node": ">=22.19.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3665812d..6a274310 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ catalogs: specifier: 26.1.1 version: 26.1.1 '@unthrown/vitest': - specifier: 5.0.0-beta.1 - version: 5.0.0-beta.1 + specifier: 5.0.0-beta.2 + version: 5.0.0-beta.2 '@vitest/coverage-v8': specifier: 4.1.10 version: 4.1.10 @@ -100,8 +100,8 @@ catalogs: specifier: 6.0.3 version: 6.0.3 unthrown: - specifier: 5.0.0-beta.1 - version: 5.0.0-beta.1 + specifier: 5.0.0-beta.2 + version: 5.0.0-beta.2 valibot: specifier: 1.4.2 version: 1.4.2 @@ -211,7 +211,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.1 + version: 5.0.0-beta.2 zod: specifier: 'catalog:' version: 4.4.3 @@ -273,7 +273,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.1 + version: 5.0.0-beta.2 zod: specifier: 'catalog:' version: 4.4.3 @@ -295,7 +295,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) + version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -344,7 +344,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) + version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -362,7 +362,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.1 + version: 5.0.0-beta.2 vitest: specifier: 'catalog:' version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) @@ -390,7 +390,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) + version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -411,7 +411,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.1 + version: 5.0.0-beta.2 valibot: specifier: 'catalog:' version: 1.4.2(typescript@6.0.3) @@ -500,7 +500,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10) + version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -518,7 +518,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.1 + version: 5.0.0-beta.2 vitest: specifier: 'catalog:' version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) @@ -2514,11 +2514,11 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@unthrown/vitest@5.0.0-beta.1': - resolution: {integrity: sha512-NkHXDyJDVUf5u9XLWccXj3GrWzcC7Oapg9VnnJaqQqWnchXIMyZ257GA7ByesmYDZUWFTabSWSkyBCp/AP0N+g==} + '@unthrown/vitest@5.0.0-beta.2': + resolution: {integrity: sha512-5pwafhxAV14/r4otGa9QjIw3y0Pa9aYPr+bOv9NKToYOhMJEooBZxajGljwr4Jt8fXDYeXaDTUnnULhvjGL5Dg==} engines: {node: '>=20'} peerDependencies: - unthrown: ^5.0.0-beta.1 + unthrown: ^5.0.0-beta.2 vitest: ^4 '@upsetjs/venn.js@2.0.0': @@ -4792,8 +4792,8 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unthrown@5.0.0-beta.1: - resolution: {integrity: sha512-kRlaZhK0BXwoZV2W7YnxnA1JxNWpYtxSPy15jFG1LijTRz2BKbkw3OSLUNzUpAic26o2g0W+Xr6smHV2Yhiazw==} + unthrown@5.0.0-beta.2: + resolution: {integrity: sha512-o9dJOEHH56uXcOrKpgTw6jyjJIz2/BaauJCoqSqHBvs+4bV4g9COhYm8czYB1rFHBqXxWaOf0vVEIfiTNQ16qA==} engines: {node: '>=20'} update-browserslist-db@1.2.3: @@ -6821,9 +6821,9 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@unthrown/vitest@5.0.0-beta.1(unthrown@5.0.0-beta.1)(vitest@4.1.10)': + '@unthrown/vitest@5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10)': dependencies: - unthrown: 5.0.0-beta.1 + unthrown: 5.0.0-beta.2 vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) '@upsetjs/venn.js@2.0.0': @@ -9237,7 +9237,7 @@ snapshots: universalify@0.1.2: {} - unthrown@5.0.0-beta.1: + unthrown@5.0.0-beta.2: dependencies: ts-pattern: 5.9.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dd03f895..a0b4ca2f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,7 +25,7 @@ catalog: "@temporalio/worker": 1.20.2 "@temporalio/workflow": 1.20.2 "@types/node": 26.1.1 - "@unthrown/vitest": 5.0.0-beta.1 + "@unthrown/vitest": 5.0.0-beta.2 "@vitest/coverage-v8": 4.1.10 arktype: 2.2.3 knip: 6.26.0 @@ -41,7 +41,7 @@ catalog: typedoc: 0.28.20 typedoc-plugin-markdown: 4.12.0 typescript: 6.0.3 - unthrown: 5.0.0-beta.1 + unthrown: 5.0.0-beta.2 valibot: 1.4.2 vitest: 4.1.10 zod: 4.4.3 From b74f01c6652f38487923754ee99d1d590765479e Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 26 Jul 2026 23:05:51 +0200 Subject: [PATCH 4/5] chore(deps): bump unthrown to 5.0.0-beta.3, drop covariance workarounds --- .changeset/adopt-unthrown-v5-beta.md | 2 +- packages/client/package.json | 2 +- packages/client/src/client.ts | 20 ++++++------- packages/contract/package.json | 2 +- packages/worker/package.json | 2 +- pnpm-lock.yaml | 42 ++++++++++++++-------------- pnpm-workspace.yaml | 4 +-- 7 files changed, 36 insertions(+), 38 deletions(-) diff --git a/.changeset/adopt-unthrown-v5-beta.md b/.changeset/adopt-unthrown-v5-beta.md index 11542ccc..e594cd44 100644 --- a/.changeset/adopt-unthrown-v5-beta.md +++ b/.changeset/adopt-unthrown-v5-beta.md @@ -4,4 +4,4 @@ "@temporal-contract/client": major --- -Adopt unthrown v5 (beta): the error combinators and `match`'s `err` handler now take a ts-pattern matcher callback; peer bumped to `^5.0.0-beta.1`. +Adopt unthrown v5 (beta): the error combinators and `match`'s `err` handler now take a ts-pattern matcher callback; peer bumped to `^5.0.0-beta.3`. diff --git a/packages/client/package.json b/packages/client/package.json index bb7cf915..40c18cec 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -77,7 +77,7 @@ "peerDependencies": { "@temporalio/client": "^1", "@temporalio/common": "^1", - "unthrown": "^5.0.0-beta.2" + "unthrown": "^5.0.0-beta.3" }, "engines": { "node": ">=22.19.0" diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index c0b9fbad..5b5e0825 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -802,7 +802,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 { @@ -818,9 +818,7 @@ 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); @@ -830,7 +828,7 @@ export class TypedClient { // routing through a dedicated helper — this is the only call site // that needs the full union. if (error instanceof WorkflowExecutionAlreadyStartedError) { - return Err( + return Err( new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error), ); } @@ -850,7 +848,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, @@ -858,7 +856,7 @@ export class TypedClient { ); } if (error instanceof TemporalWorkflowNotFoundError) { - return Err( + return Err( new WorkflowExecutionNotFoundError( error.workflowId || temporalOptions.workflowId, error.runId, @@ -866,7 +864,7 @@ export class TypedClient { ), ); } - return Err(createRuntimeClientError("executeWorkflow", error)); + return Err(createRuntimeClientError("executeWorkflow", error)); } }; return makeAsyncResult(work); @@ -994,7 +992,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", @@ -1013,7 +1011,7 @@ export class TypedClient { return Err(rehydrated as Err); } } - return Err(classifyResultError("result", error, workflowHandle.workflowId)); + return Err(classifyResultError("result", error, workflowHandle.workflowId)); } }; return makeAsyncResult(work); @@ -1140,7 +1138,7 @@ function buildValidatedProxy(classifyHandleError(operation, error, workflowId)); + return Err(classifyHandleError(operation, error, workflowId)); } }; return makeAsyncResult(work); diff --git a/packages/contract/package.json b/packages/contract/package.json index d85fc120..653a35a7 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -87,7 +87,7 @@ "vitest": "catalog:" }, "peerDependencies": { - "unthrown": "^5.0.0-beta.2" + "unthrown": "^5.0.0-beta.3" }, "peerDependenciesMeta": { "unthrown": { diff --git a/packages/worker/package.json b/packages/worker/package.json index f50c8c5a..ba1caf2a 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -96,7 +96,7 @@ "@temporalio/common": "^1", "@temporalio/worker": "^1", "@temporalio/workflow": "^1", - "unthrown": "^5.0.0-beta.2" + "unthrown": "^5.0.0-beta.3" }, "engines": { "node": ">=22.19.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a274310..e959a484 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ catalogs: specifier: 26.1.1 version: 26.1.1 '@unthrown/vitest': - specifier: 5.0.0-beta.2 - version: 5.0.0-beta.2 + specifier: 5.0.0-beta.3 + version: 5.0.0-beta.3 '@vitest/coverage-v8': specifier: 4.1.10 version: 4.1.10 @@ -100,8 +100,8 @@ catalogs: specifier: 6.0.3 version: 6.0.3 unthrown: - specifier: 5.0.0-beta.2 - version: 5.0.0-beta.2 + specifier: 5.0.0-beta.3 + version: 5.0.0-beta.3 valibot: specifier: 1.4.2 version: 1.4.2 @@ -211,7 +211,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.2 + version: 5.0.0-beta.3 zod: specifier: 'catalog:' version: 4.4.3 @@ -273,7 +273,7 @@ importers: version: 13.1.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.2 + version: 5.0.0-beta.3 zod: specifier: 'catalog:' version: 4.4.3 @@ -295,7 +295,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) + version: 5.0.0-beta.3(unthrown@5.0.0-beta.3)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -344,7 +344,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) + version: 5.0.0-beta.3(unthrown@5.0.0-beta.3)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -362,7 +362,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.2 + version: 5.0.0-beta.3 vitest: specifier: 'catalog:' version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) @@ -390,7 +390,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) + version: 5.0.0-beta.3(unthrown@5.0.0-beta.3)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -411,7 +411,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.2 + version: 5.0.0-beta.3 valibot: specifier: 'catalog:' version: 1.4.2(typescript@6.0.3) @@ -500,7 +500,7 @@ importers: version: 26.1.1 '@unthrown/vitest': specifier: 'catalog:' - version: 5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10) + version: 5.0.0-beta.3(unthrown@5.0.0-beta.3)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -518,7 +518,7 @@ importers: version: 6.0.3 unthrown: specifier: 'catalog:' - version: 5.0.0-beta.2 + version: 5.0.0-beta.3 vitest: specifier: 'catalog:' version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) @@ -2514,11 +2514,11 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@unthrown/vitest@5.0.0-beta.2': - resolution: {integrity: sha512-5pwafhxAV14/r4otGa9QjIw3y0Pa9aYPr+bOv9NKToYOhMJEooBZxajGljwr4Jt8fXDYeXaDTUnnULhvjGL5Dg==} + '@unthrown/vitest@5.0.0-beta.3': + resolution: {integrity: sha512-N1nWqXzAm/tmgmR+iwldx/IHS8BqJsWYpW2p9lefD4/dOOBxjWw2VJ5oZ9uyfFvQjVjdbCcE1GcpAcmK0CcA+g==} engines: {node: '>=20'} peerDependencies: - unthrown: ^5.0.0-beta.2 + unthrown: ^5.0.0-beta.3 vitest: ^4 '@upsetjs/venn.js@2.0.0': @@ -4792,8 +4792,8 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unthrown@5.0.0-beta.2: - resolution: {integrity: sha512-o9dJOEHH56uXcOrKpgTw6jyjJIz2/BaauJCoqSqHBvs+4bV4g9COhYm8czYB1rFHBqXxWaOf0vVEIfiTNQ16qA==} + unthrown@5.0.0-beta.3: + resolution: {integrity: sha512-z6tOSiq9ymstsL5o6X6lhRfF+TXRn/CC3/MjaVDGVxdQ7aB5k8JX0G8QUNU4cRjwk7Drw5raUtKqHWFBoyiNVw==} engines: {node: '>=20'} update-browserslist-db@1.2.3: @@ -6821,9 +6821,9 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@unthrown/vitest@5.0.0-beta.2(unthrown@5.0.0-beta.2)(vitest@4.1.10)': + '@unthrown/vitest@5.0.0-beta.3(unthrown@5.0.0-beta.3)(vitest@4.1.10)': dependencies: - unthrown: 5.0.0-beta.2 + unthrown: 5.0.0-beta.3 vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.1)(yaml@2.9.0) '@upsetjs/venn.js@2.0.0': @@ -9237,7 +9237,7 @@ snapshots: universalify@0.1.2: {} - unthrown@5.0.0-beta.2: + unthrown@5.0.0-beta.3: dependencies: ts-pattern: 5.9.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a0b4ca2f..c805450b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,7 +25,7 @@ catalog: "@temporalio/worker": 1.20.2 "@temporalio/workflow": 1.20.2 "@types/node": 26.1.1 - "@unthrown/vitest": 5.0.0-beta.2 + "@unthrown/vitest": 5.0.0-beta.3 "@vitest/coverage-v8": 4.1.10 arktype: 2.2.3 knip: 6.26.0 @@ -41,7 +41,7 @@ catalog: typedoc: 0.28.20 typedoc-plugin-markdown: 4.12.0 typescript: 6.0.3 - unthrown: 5.0.0-beta.2 + unthrown: 5.0.0-beta.3 valibot: 1.4.2 vitest: 4.1.10 zod: 4.4.3 From e1ce92f5749a30afa507139e31cbcbf505852cb5 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 26 Jul 2026 23:18:02 +0200 Subject: [PATCH 5/5] chore(security): patch brace-expansion GHSA-mh99, ignore its unpatchable line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fresh GHSA-mh99-v99m-4gvg (DoS via unbounded expansion length) reddened the audit gate. Its only patched release is 5.0.8 on the 5.x line — applied via the override (with a temporary minimumReleaseAgeExclude entry; 5.0.8 is younger than the 7-day cutoff, published 2026-07-23). The 2.x line under testcontainers>archiver has NO patched release and a cross-major force is unsafe, so the advisory is ignored via auditConfig.ignoreGhsas with the accepted-risk rationale (expansion input is library-internal glob patterns, never user input) and removal criteria. --- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 27 +++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e959a484..22ad452f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,7 +115,7 @@ catalogs: overrides: fast-uri@<3.1.4: 3.1.4 brace-expansion@2: 2.1.2 - brace-expansion@5: 5.0.7 + brace-expansion@5: 5.0.8 js-yaml@3: 3.15.0 js-yaml@4: 4.3.0 vite@<6.4.3: 6.4.3 @@ -3011,9 +3011,9 @@ packages: brace-expansion@2.1.2: resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -7318,7 +7318,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.7: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -8333,7 +8333,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.8 minimatch@5.1.9: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c805450b..b4a2a570 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -60,11 +60,15 @@ overrides: # fast-uri is added to minimumReleaseAgeExclude below because 3.1.4 is younger # than the 7-day cutoff. "fast-uri@<3.1.4": "3.1.4" - # GHSA-3jxr-9vmj-r5cp: brace-expansion ReDoS. Two lines are present — 2.x via - # testcontainers>archiver>minimatch and 5.x via typedoc>minimatch@10. Patch each - # major in-place to avoid forcing cross-major upgrades. + # GHSA-3jxr-9vmj-r5cp (ReDoS) + GHSA-mh99-v99m-4gvg (DoS via unbounded + # expansion length): brace-expansion. Two lines are present — 2.x via + # testcontainers>archiver>minimatch and 5.x via typedoc>minimatch@10. The 5.x + # line is pulled to 5.0.8 (the only release patched for GHSA-mh99); the 2.x + # line stays on its ReDoS-patched release — GHSA-mh99 has NO patched release + # for it (vulnerable range <=5.0.7), so that advisory is ignored via + # auditConfig.ignoreGhsas below rather than force-upgrading across majors. "brace-expansion@2": "2.1.2" - "brace-expansion@5": "5.0.7" + "brace-expansion@5": "5.0.8" # GHSA-52cp-r559-cp3m: js-yaml quadratic-CPU merge keys. Two lines present — # 3.x via @changesets/cli>@manypkg/get-packages>read-yaml-file and 4.x via # @changesets/cli>@changesets/parse. Each major line is version-scoped and @@ -105,3 +109,18 @@ minimumReleaseAgeExclude: # Temporary: fast-uri 3.1.4 (the GHSA-v2hh-gcrm-f6hx override above) is younger # than the 7-day cutoff. Remove once 3.1.4 is 7 days old (published 2026-07-19). - fast-uri + # Temporary: brace-expansion 5.0.8 (the GHSA-mh99-v99m-4gvg override above) is + # younger than the 7-day cutoff. Remove once 5.0.8 is 7 days old (published + # 2026-07-23). + - brace-expansion + +# GHSA-mh99-v99m-4gvg (brace-expansion DoS via unbounded expansion length) has a +# patched release ONLY on the 5.x line (>=5.0.8, applied via the override above); +# the 2.x line resolved under testcontainers > archiver has no patched release, +# and forcing it to 5.x across majors is unsafe. Accepted risk: the expansion +# input there is library-internal glob patterns, never user input. Remove this +# ignore as soon as upstream ships a patched 2.x (or archiver moves to +# minimatch@10). +auditConfig: + ignoreGhsas: + - GHSA-mh99-v99m-4gvg