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
5 changes: 5 additions & 0 deletions .changeset/enable-all-unthrown-oxlint-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@temporal-contract/client": patch
---

The exported `ClientInferSignal` / `ClientInferQuery` / `ClientInferUpdate` aliases now carry the concrete error unions the handle proxies actually produce (`{Signal,Query,Update}ValidationError | WorkflowExecutionNotFoundError`) instead of a bare, unmatchable `Error` channel — surfaced by adopting `@unthrown/oxlint`'s `no-ambiguous-error-type` rule. `TypedWorkflowHandle` behavior is unchanged (it already substituted the concrete unions); only direct consumers of the aliases see the more precise types. The rest of the adoption (all six rules at `error`, reasoned `no-throw` disables at the sanctioned throw sites) is comment-and-config only.
19 changes: 18 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["./node_modules/@btravstack/oxlint/base.json"]
"extends": ["./node_modules/@btravstack/oxlint/base.json"],
"jsPlugins": [{ "name": "unthrown", "specifier": "@unthrown/oxlint" }],
"rules": {
"unthrown/no-ambiguous-error-type": "error",
"unthrown/no-catch-all-pattern": "error",
"unthrown/no-throw": "error",
"unthrown/no-unhandled-result": "error",
"unthrown/prefer-async-result": "error",
"unthrown/prefer-ensure": "error"
},
"overrides": [
{
"files": ["**/*.spec.ts", "**/__tests__/**"],
"rules": {
"unthrown/no-throw": "off"
}
}
]
}
3 changes: 3 additions & 0 deletions examples/order-processing-worker/src/application/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export const processOrder = declareWorkflow({
if (paymentResult.isDefect()) {
// Unmodeled failure (a bug, not an anticipated outcome) — rethrow at
// the edge so Temporal surfaces the Workflow Task failure.
// oxlint-disable-next-line unthrown/no-throw -- defect-cause rethrow at the edge: an unmodeled failure must surface as a Workflow Task failure
throw paymentResult.cause;
}
const failure = paymentResult.error;
Expand All @@ -169,6 +170,7 @@ export const processOrder = declareWorkflow({
// Rethrow as this workflow's own declared contract error: the
// execution fails with `ApplicationFailure(type: "PaymentDeclined")`
// and the typed client rehydrates it into a `ContractError`.
// oxlint-disable-next-line unthrown/no-throw -- sanctioned ApplicationFailure model: `throw context.errors.X(...)` is how a workflow fails terminally with a typed contract error (CLAUDE.md rule 2 exception)
throw context.errors.PaymentDeclined({ reason: failure.data.reason }, { cause: failure });
}
case "@temporal-contract/ActivityError":
Expand Down Expand Up @@ -244,6 +246,7 @@ export const processOrder = declareWorkflow({
// Cancellation must propagate — swallowing it here would leave the
// workflow running after a cancel request.
if (isCancellation(error)) {
// oxlint-disable-next-line unthrown/no-throw -- cancellation rethrow: swallowing a CancelledFailure would leave the workflow running after a cancel request
throw error;
}
// Non-critical: log but continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export class CreateShipmentUseCase {
async execute(orderId: string, customerId: string): Promise<ShippingResult> {
// Business validation
if (!orderId || orderId.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Order ID is required");
}

if (!customerId || customerId.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Customer ID is required");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export class ProcessPaymentUseCase {
async execute(customerId: string, amount: number): Promise<PaymentOutcome> {
// Business validation
if (amount <= 0) {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Payment amount must be positive");
}

if (!customerId || customerId.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Customer ID is required");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class PurgeExpiredOrdersUseCase {
async execute(olderThanDays: number): Promise<number> {
// Business validation
if (!Number.isInteger(olderThanDays) || olderThanDays <= 0) {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("olderThanDays must be a positive integer");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class RefundPaymentUseCase {
async execute(transactionId: string): Promise<void> {
// Business validation
if (!transactionId || transactionId.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Transaction ID is required");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class ReleaseInventoryUseCase {
async execute(reservationId: string): Promise<void> {
// Business validation
if (!reservationId || reservationId.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Reservation ID is required");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export class ReserveInventoryUseCase {
async execute(items: OrderItem[]): Promise<InventoryReservation> {
// Business validation
if (!items || items.length === 0) {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("At least one item is required");
}

for (const item of items) {
if (item.quantity <= 0) {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error(`Invalid quantity for product ${item.productId}`);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ export class SendNotificationUseCase {
async execute(customerId: string, subject: string, message: string): Promise<void> {
// Business validation
if (!customerId || customerId.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Customer ID is required");
}

if (!subject || subject.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Subject is required");
}

if (!message || message.trim() === "") {
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Message is required");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class MockPaymentAdapter implements PaymentPort {
logger.info(`✅ Refund successful`);
} else {
logger.error(`❌ Refund failed`);
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
throw new Error("Payment processor rejected refund request");
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@btravstack/oxlint": "catalog:",
"@changesets/cli": "catalog:",
"@commitlint/cli": "catalog:",
"@unthrown/oxlint": "catalog:",
"knip": "catalog:",
"lefthook": "catalog:",
"oxfmt": "catalog:",
Expand Down
Loading
Loading