diff --git a/.changeset/activity-idempotency-key.md b/.changeset/pre/activity-idempotency-key.md similarity index 100% rename from .changeset/activity-idempotency-key.md rename to .changeset/pre/activity-idempotency-key.md diff --git a/.changeset/best-effort-and-propagate-rename.md b/.changeset/pre/best-effort-and-propagate-rename.md similarity index 100% rename from .changeset/best-effort-and-propagate-rename.md rename to .changeset/pre/best-effort-and-propagate-rename.md diff --git a/.changeset/client-error-patterns.md b/.changeset/pre/client-error-patterns.md similarity index 100% rename from .changeset/client-error-patterns.md rename to .changeset/pre/client-error-patterns.md diff --git a/.changeset/derived-workflow-id.md b/.changeset/pre/derived-workflow-id.md similarity index 100% rename from .changeset/derived-workflow-id.md rename to .changeset/pre/derived-workflow-id.md diff --git a/.changeset/time-skipping-contract-test.md b/.changeset/pre/time-skipping-contract-test.md similarity index 100% rename from .changeset/time-skipping-contract-test.md rename to .changeset/pre/time-skipping-contract-test.md diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 0559c4b7..2175a490 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,49 @@ # @temporal-contract/client +## 8.0.0-beta.9 + +### Minor Changes + +- 3ed260c: Ready-made error pattern groups — `WORKFLOW_START_PATTERNS`, + `WORKFLOW_RESULT_PATTERNS`, `WORKFLOW_EXECUTE_PATTERNS`, + `WORKFLOW_STOPPED_PATTERNS`, `SIGNAL_PATTERNS`, `QUERY_PATTERNS`, + `UPDATE_PATTERNS`, `SCHEDULE_CREATE_PATTERNS`. Each mirrors one method's error + union exactly, so `matcher.with(...WORKFLOW_RESULT_PATTERNS, handler)` replaces + six hand-written `P.tag(...)` arguments. + + Exhaustiveness is unchanged: these are ordinary pattern tuples, so a missing + member is still a compile error naming it. A workflow's **declared contract + errors** are deliberately not in these groups — no shipped group can name a + user's own errors — so for a workflow that declares `errors`, a group alone is + not exhaustive: match those first with `{ errorName: "..." }`. + +- 5545236: Workflows can derive their **workflow ID** from their input: + + ```ts + const processOrder = defineWorkflow({ + input: OrderSchema, + output: OrderResultSchema, + workflowId: ({ orderId }) => `order-${orderId}`, + startPolicy: "once-per-id", + }); + ``` + + `startPolicy` only bites when two starts of the same logical request collide on + one ID, and the ID used to be entirely the caller's — passing + `crypto.randomUUID()` made `"once-per-id"` inert with no diagnostic. A workflow + that declares `workflowId` now derives it from the validated payload on + `startWorkflow` / `executeWorkflow` / `signalWithStart`, and supplying one at + the call site is a type error. Workflows that declare none are unchanged. + + `IdempotencyMode` is renamed to `WorkflowStartPolicy` (the old name stays as a + deprecated type alias). + +### Patch Changes + +- Updated dependencies [4e47875] +- Updated dependencies [5545236] + - @temporal-contract/contract@8.0.0-beta.9 + ## 8.0.0-beta.8 ### Patch Changes diff --git a/packages/client/package.json b/packages/client/package.json index 40009805..9aaf8aac 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@temporal-contract/client", - "version": "8.0.0-beta.8", + "version": "8.0.0-beta.9", "description": "Client utilities with unthrown Result/AsyncResult for consuming temporal-contract workflows", "keywords": [ "client", diff --git a/packages/contract/CHANGELOG.md b/packages/contract/CHANGELOG.md index c8961db9..2ddda94b 100644 --- a/packages/contract/CHANGELOG.md +++ b/packages/contract/CHANGELOG.md @@ -1,5 +1,62 @@ # @temporal-contract/contract +## 8.0.0-beta.9 + +### Minor Changes + +- 4e47875: Activities can declare an **idempotency key**, derived from their input: + + ```ts + const chargeCard = defineActivity({ + input: z.object({ orderId: z.string(), customerId: z.string(), amount: z.number() }), + output: PaymentSchema, + // Key on what IDENTIFIES the charge, not on what describes it: one customer + // placing two orders of the same value must not collide on one key. + idempotencyKey: ({ orderId }) => `charge:${orderId}`, + }); + + chargeCard: ({ input, idempotencyKey }) => + fromPromise( + gateway.charge(input, { idempotencyKey }), + qualifyFailure("CHARGE_FAILED", { expected: GatewayError }), + ), + ``` + + Temporal runs activities **at least once**, and nothing in the library helped + with that until now — `idempotency` on a workflow is start deduplication and + says nothing about an activity running twice. Being payload-derived, the key is + stable across activity retries, worker crashes, and a fresh workflow execution + with the same input. + + `helpers.idempotencyKey` is typed `string` for an activity that declares one and + `undefined` for one that does not, so reaching for a key that was never declared + is a compile error. `runActivity` hands over the same value. + + Good key sources: a business identifier already in the input, a dedicated + `idempotencyKey` field the caller mints, or the workflow ID — which is + per-execution and, when the contract derives it, a function of the payload. + +- 5545236: Workflows can derive their **workflow ID** from their input: + + ```ts + const processOrder = defineWorkflow({ + input: OrderSchema, + output: OrderResultSchema, + workflowId: ({ orderId }) => `order-${orderId}`, + startPolicy: "once-per-id", + }); + ``` + + `startPolicy` only bites when two starts of the same logical request collide on + one ID, and the ID used to be entirely the caller's — passing + `crypto.randomUUID()` made `"once-per-id"` inert with no diagnostic. A workflow + that declares `workflowId` now derives it from the validated payload on + `startWorkflow` / `executeWorkflow` / `signalWithStart`, and supplying one at + the call site is a type error. Workflows that declare none are unchanged. + + `IdempotencyMode` is renamed to `WorkflowStartPolicy` (the old name stays as a + deprecated type alias). + ## 8.0.0-beta.8 ## 8.0.0-beta.7 diff --git a/packages/contract/package.json b/packages/contract/package.json index a7c82fe0..00bd2998 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -1,6 +1,6 @@ { "name": "@temporal-contract/contract", - "version": "8.0.0-beta.8", + "version": "8.0.0-beta.9", "description": "Contract builder for temporal-contract", "keywords": [ "contract", diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index ebc989bf..b3b0c6cd 100644 --- a/packages/testing/CHANGELOG.md +++ b/packages/testing/CHANGELOG.md @@ -1,5 +1,62 @@ # @temporal-contract/testing +## 8.0.0-beta.9 + +### Minor Changes + +- 4e47875: Activities can declare an **idempotency key**, derived from their input: + + ```ts + const chargeCard = defineActivity({ + input: z.object({ orderId: z.string(), customerId: z.string(), amount: z.number() }), + output: PaymentSchema, + // Key on what IDENTIFIES the charge, not on what describes it: one customer + // placing two orders of the same value must not collide on one key. + idempotencyKey: ({ orderId }) => `charge:${orderId}`, + }); + + chargeCard: ({ input, idempotencyKey }) => + fromPromise( + gateway.charge(input, { idempotencyKey }), + qualifyFailure("CHARGE_FAILED", { expected: GatewayError }), + ), + ``` + + Temporal runs activities **at least once**, and nothing in the library helped + with that until now — `idempotency` on a workflow is start deduplication and + says nothing about an activity running twice. Being payload-derived, the key is + stable across activity retries, worker crashes, and a fresh workflow execution + with the same input. + + `helpers.idempotencyKey` is typed `string` for an activity that declares one and + `undefined` for one that does not, so reaching for a key that was never declared + is a compile error. `runActivity` hands over the same value. + + Good key sources: a business identifier already in the input, a dedicated + `idempotencyKey` field the caller mints, or the workflow ID — which is + per-execution and, when the contract derives it, a function of the payload. + +- 2cc0053: `createTimeSkippingContractTest({ contract, workflowsPath, activities })` — the + one-call fixture for the **time-skipping** tier, the Docker-free counterpart to + `createContractTest`. It owns the `TestWorkflowEnvironment`, the workflow bundle + (built once per Vitest worker process), the worker, the `TypedClient` binding, + and the replay-on-finish check, and hands the test `{ worker, client }`. + + Previously the tier with the better ergonomics was also the one that needed + Docker: the time-skipping tier only offered `testRig`, which makes the caller + build a bundle and manage the environment. `testRig` stays as the lower-level + seam. + +### Patch Changes + +- Updated dependencies [4e47875] +- Updated dependencies [8d1359a] +- Updated dependencies [3ed260c] +- Updated dependencies [5545236] + - @temporal-contract/contract@8.0.0-beta.9 + - @temporal-contract/worker@8.0.0-beta.9 + - @temporal-contract/client@8.0.0-beta.9 + ## 8.0.0-beta.8 ### Patch Changes diff --git a/packages/testing/package.json b/packages/testing/package.json index 29a50a92..ca34f529 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@temporal-contract/testing", - "version": "8.0.0-beta.8", + "version": "8.0.0-beta.9", "description": "Temporal testing utilities", "keywords": [ "contract", @@ -88,9 +88,9 @@ "zod": "catalog:" }, "peerDependencies": { - "@temporal-contract/client": "^8.0.0-beta.8", - "@temporal-contract/contract": "^8.0.0-beta.8", - "@temporal-contract/worker": "^8.0.0-beta.8", + "@temporal-contract/client": "^8.0.0-beta.9", + "@temporal-contract/contract": "^8.0.0-beta.9", + "@temporal-contract/worker": "^8.0.0-beta.9", "@temporalio/client": "^1.16.0", "@temporalio/testing": "^1.16.0", "@temporalio/worker": "^1.16.0", diff --git a/packages/worker/CHANGELOG.md b/packages/worker/CHANGELOG.md index 085b3121..48217afc 100644 --- a/packages/worker/CHANGELOG.md +++ b/packages/worker/CHANGELOG.md @@ -1,5 +1,61 @@ # @temporal-contract/worker +## 8.0.0-beta.9 + +### Minor Changes + +- 4e47875: Activities can declare an **idempotency key**, derived from their input: + + ```ts + const chargeCard = defineActivity({ + input: z.object({ orderId: z.string(), customerId: z.string(), amount: z.number() }), + output: PaymentSchema, + // Key on what IDENTIFIES the charge, not on what describes it: one customer + // placing two orders of the same value must not collide on one key. + idempotencyKey: ({ orderId }) => `charge:${orderId}`, + }); + + chargeCard: ({ input, idempotencyKey }) => + fromPromise( + gateway.charge(input, { idempotencyKey }), + qualifyFailure("CHARGE_FAILED", { expected: GatewayError }), + ), + ``` + + Temporal runs activities **at least once**, and nothing in the library helped + with that until now — `idempotency` on a workflow is start deduplication and + says nothing about an activity running twice. Being payload-derived, the key is + stable across activity retries, worker crashes, and a fresh workflow execution + with the same input. + + `helpers.idempotencyKey` is typed `string` for an activity that declares one and + `undefined` for one that does not, so reaching for a key that was never declared + is a compile error. `runActivity` hands over the same value. + + Good key sources: a business identifier already in the input, a dedicated + `idempotencyKey` field the caller mints, or the workflow ID — which is + per-execution and, when the contract derives it, a function of the payload. + +- 8d1359a: `bestEffort(result, onFailure)` — the counterpart to `propagateFailure` for a + non-critical call (a notification, a metric, an audit write). It hands the + failure to `onFailure` and resolves `undefined` instead of ending the workflow, + but **re-raises real cancellation** (`ActivityCancelledError`, + `ChildWorkflowCancelledError`, `WorkflowCancelledError`) so a workflow can no + longer absorb its own cancel by accident. That rule used to live in every + hand-written best-effort fold; it is now structural. + + `propagateActivityFailure` is renamed to **`propagateFailure`** — it has always + also handled child-workflow calls and cancellation scopes, and the old name said + otherwise. The old name is **removed**, not aliased: it only ever shipped in 8.0 + betas, and this release already renames `idempotency` to `startPolicy` outright. + Rename the import; behaviour is unchanged. + +### Patch Changes + +- Updated dependencies [4e47875] +- Updated dependencies [5545236] + - @temporal-contract/contract@8.0.0-beta.9 + ## 8.0.0-beta.8 ### Minor Changes diff --git a/packages/worker/package.json b/packages/worker/package.json index e5bf32f0..d85479c9 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,6 +1,6 @@ { "name": "@temporal-contract/worker", - "version": "8.0.0-beta.8", + "version": "8.0.0-beta.9", "description": "Worker utilities with unthrown Result/AsyncResult for implementing temporal-contract workflows and activities", "keywords": [ "contract",