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
3 changes: 2 additions & 1 deletion .github/workflows/dts-e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ name: 🧪 DTS Emulator E2E Tests
# Tests are split across parallel jobs with separate task hubs for isolation.

on:
workflow_dispatch:
push:
branches:
- main
Expand All @@ -24,7 +25,7 @@ jobs:
- name: "entity"
pattern: "test/e2e-azuremanaged/entity.spec.ts"
- name: "orchestration"
pattern: "test/e2e-azuremanaged/orchestration.spec.ts"
pattern: "test/e2e-azuremanaged/orchestration.spec.ts test/e2e-azuremanaged/orchestration-id-reuse-policy.spec.ts"
- name: "query-restart"
pattern: "test/e2e-azuremanaged/query-apis.spec.ts test/e2e-azuremanaged/restart.spec.ts"
- name: "retry-history-rewind"
Expand Down
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
### New

- Implement entity support in the in-memory testing backend ([#341](https://github.com/microsoft/durabletask-js/pull/341))
- Add the top-level `StartOrchestrationOptions.dedupeStatuses` option, `ValidDedupeStatuses`, and
`OrchestrationAlreadyExistsError`, aligned with the .NET status-based duplicate rejection and
atomic replacement contract. The gRPC client defers omitted policies to its backend, while the
in-memory test client mirrors the .NET shim by treating omission as all statuses reusable. The
shared protocol does not support atomic no-op/`IGNORE`.
- Add the `CANCELED` member to the public `OrchestrationStatus` enum.

### Fixes


## v0.4.0 (2026-07-31)

### Changes
Expand Down Expand Up @@ -72,7 +77,7 @@
- fix: clear customStatus on continue-as-new in InMemoryOrchestrationBackend ([#155](https://github.com/microsoft/durabletask-js/pull/155))
- fix: propagate parent notification from composite tasks (WhenAllTask/WhenAnyTask) ([#150](https://github.com/microsoft/durabletask-js/pull/150))
- fix: use deterministic time in createTimer instead of Date.now() ([#146](https://github.com/microsoft/durabletask-js/pull/146))
- Fix WhenAllTask constructor resetting _completedTasks counter ([#143](https://github.com/microsoft/durabletask-js/pull/143))
- Fix WhenAllTask constructor resetting \_completedTasks counter ([#143](https://github.com/microsoft/durabletask-js/pull/143))
- Fix retry handler treating undefined/null/NaN/Infinity as retry signal ([#142](https://github.com/microsoft/durabletask-js/pull/142))
- Release v0.3.0 ([#147](https://github.com/microsoft/durabletask-js/pull/147))

Expand Down
48 changes: 32 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ const entityResponseBytes = await worker.processEntityBatchRequest(entityBatchRe

The following npm packages are available for download.

| Name | Latest version | Description |
| - | - | - |
| Core SDK | [![npm version](https://img.shields.io/npm/v/@microsoft/durabletask-js)](https://www.npmjs.com/package/@microsoft/durabletask-js) | Core Durable Task SDK for JavaScript/TypeScript. |
| Name | Latest version | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Core SDK | [![npm version](https://img.shields.io/npm/v/@microsoft/durabletask-js)](https://www.npmjs.com/package/@microsoft/durabletask-js) | Core Durable Task SDK for JavaScript/TypeScript. |
| AzureManaged SDK | [![npm version](https://img.shields.io/npm/v/@microsoft/durabletask-js-azuremanaged)](https://www.npmjs.com/package/@microsoft/durabletask-js-azuremanaged) | Azure-managed [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-task-scheduler) support for the Durable Task JavaScript SDK. |

## Prerequisites
Expand All @@ -50,15 +50,8 @@ npm install @microsoft/durabletask-js @microsoft/durabletask-js-azuremanaged
You can then use the following code to define a simple "Hello, cities" durable orchestration.

```typescript
import {
ActivityContext,
OrchestrationContext,
TOrchestrator,
} from "@microsoft/durabletask-js";
import {
createAzureManagedClient,
createAzureManagedWorkerBuilder,
} from "@microsoft/durabletask-js-azuremanaged";
import { ActivityContext, OrchestrationContext, TOrchestrator } from "@microsoft/durabletask-js";
import { createAzureManagedClient, createAzureManagedWorkerBuilder } from "@microsoft/durabletask-js-azuremanaged";

// Define an activity function
const sayHello = async (_: ActivityContext, name: string): Promise<string> => {
Expand Down Expand Up @@ -90,6 +83,32 @@ console.log(`Result: ${state?.serializedOutput}`);

You can find more samples in the [examples/azure-managed](./examples/azure-managed) directory.

### Reusing orchestration instance IDs

Set the top-level `dedupeStatuses` start option when an instance ID may be reused. The list
contains the existing runtime statuses that must continue to produce an
`OrchestrationAlreadyExistsError`;
instances in every other supported runtime status are atomically replaced:

```typescript
import { OrchestrationStatus } from "@microsoft/durabletask-js";

await client.scheduleNewOrchestration(helloCities, undefined, {
instanceId: "daily-greeting",
dedupeStatuses: [OrchestrationStatus.RUNNING, OrchestrationStatus.PENDING],
});
```

For `TaskHubGrpcClient`, omitting `dedupeStatuses` preserves the backend's default duplicate-ID
behavior; passing `[]` makes every supported runtime status replaceable. The in-memory
`TestOrchestrationClient` mirrors the .NET shim, where omission also makes all statuses reusable.
`ValidDedupeStatuses` exports the seven supported statuses. The transient `CONTINUED_AS_NEW`
status is not replaceable. A list containing `TERMINATED` must also contain `RUNNING`, `PENDING`,
and `SUSPENDED`, because replacing a running instance first terminates it. The production client
forwards this validation to the backend and maps its `INVALID_ARGUMENT` response to `TypeError`;
the in-memory client validates it directly. The current shared protocol does not define a
no-op/`IGNORE` action: a matching dedupe status is an error, while a non-matching status is replaced.

## Supported patterns

The following orchestration patterns are supported.
Expand Down Expand Up @@ -125,10 +144,7 @@ An orchestration can wait for external events, such as a human approval, with op
```typescript
import { whenAny } from "@microsoft/durabletask-js";

const purchaseOrderWorkflow: TOrchestrator = async function* (
ctx: OrchestrationContext,
order: Order,
): any {
const purchaseOrderWorkflow: TOrchestrator = async function* (ctx: OrchestrationContext, order: Order): any {
// Orders under $1000 are auto-approved
if (order.cost < 1000) {
return "Auto-approved";
Expand Down
2 changes: 2 additions & 0 deletions packages/azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
Interactive scenarios (external events, termination, suspend/resume) and entity batches are
covered by driving the `@microsoft/durabletask-js` in-memory test stack with `wrapOrchestrator` /
`wrapEntity`; see the README.
- Forward the top-level `dedupeStatuses` duplicate rejection and atomic replacement option through
`DurableFunctionsClient.startNew()`; the shared protocol does not support atomic no-op/`IGNORE`.

### Fixes

Expand Down
4 changes: 3 additions & 1 deletion packages/azure-functions-durable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ changed:
and throws when the instance is missing. `showInput` suppresses only the top-level input,
`showHistory` populates `history`, and `showHistoryOutput` toggles the per-entry input/result
payloads; `history` entries are core `HistoryEvent`s (v3 types `history` as `Array<unknown>`).
**`client.startNew()` supports the `version` option.**
**`client.startNew()` supports the `version` and top-level `dedupeStatuses` options.** Dedupe
statuses select duplicate errors; other supported statuses are atomically replaced. The current
shared protocol does not expose an atomic no-op/`IGNORE` action.
- **Entity locking / critical sections moved to the core context.** v3's `context.df.lock(...)` /
`context.df.isLocked()` and the `DurableLock` / `LockState` / `LockingRulesViolationError` exports
are removed. Locks live on the core-native `context.entities` surface, which the classic
Expand Down
13 changes: 11 additions & 2 deletions packages/azure-functions-durable/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export interface StartNewOptions {
instanceId?: string;
/** Orchestration version to assign (forwarded to the core scheduler). */
version?: string;
/**
* Existing orchestration statuses that must produce a duplicate-ID error.
* The current shared protocol does not support an atomic no-op/IGNORE action.
*/
dedupeStatuses?: readonly OrchestrationStatus[];
}

/**
Expand Down Expand Up @@ -213,8 +218,12 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
*/
async startNew(orchestratorName: string, options?: StartNewOptions): Promise<string> {
const scheduleOptions =
options?.instanceId !== undefined || options?.version !== undefined
? { instanceId: options?.instanceId, version: options?.version }
options?.instanceId !== undefined || options?.version !== undefined || options?.dedupeStatuses !== undefined
? {
instanceId: options?.instanceId,
version: options?.version,
dedupeStatuses: options?.dedupeStatuses,
}
: undefined;
return this.scheduleNewOrchestration(orchestratorName, options?.input, scheduleOptions);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/azure-functions-durable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
DurableFunctionsClientConfig,
DurableFunctionsClientInput,
DurableOrchestrationClient,
StartNewOptions,
TaskHubOptions,
getGrpcHostAddress,
} from "./client";
Expand All @@ -22,6 +23,11 @@ export { createAzureFunctionsMetadataGenerator } from "./metadata";
export { DurableFunctionsWorker } from "./worker";
export { DurableBindingMetadata, addDurableGrpcMetadata } from "./durable-grpc";
export { RetryOptions } from "./retry-options";
export {
OrchestrationAlreadyExistsError,
StartOrchestrationOptions,
ValidDedupeStatuses,
} from "@microsoft/durabletask-js";
// Re-exported core error so callers can `instanceof`-guard caught orchestration failures, matching
// the classic durable-functions v3 top-level `TaskFailedError` export. (`DurableError` /
// `AggregatedError` were never v3 top-level exports; the core engine surfaces `TaskFailedError` and
Expand Down
5 changes: 4 additions & 1 deletion packages/azure-functions-durable/src/orchestration-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export function toOrchestrationRuntimeStatus(status: OrchestrationStatus): Orche
return OrchestrationRuntimeStatus.ContinuedAsNew;
case OrchestrationStatus.FAILED:
return OrchestrationRuntimeStatus.Failed;
case OrchestrationStatus.CANCELED:
return OrchestrationRuntimeStatus.Canceled;
case OrchestrationStatus.TERMINATED:
return OrchestrationRuntimeStatus.Terminated;
case OrchestrationStatus.PENDING:
Expand All @@ -91,8 +93,9 @@ export function fromOrchestrationRuntimeStatus(status: OrchestrationRuntimeStatu
return OrchestrationStatus.CONTINUED_AS_NEW;
case OrchestrationRuntimeStatus.Failed:
return OrchestrationStatus.FAILED;
case OrchestrationRuntimeStatus.Terminated:
case OrchestrationRuntimeStatus.Canceled:
return OrchestrationStatus.CANCELED;
case OrchestrationRuntimeStatus.Terminated:
return OrchestrationStatus.TERMINATED;
case OrchestrationRuntimeStatus.Suspended:
return OrchestrationStatus.SUSPENDED;
Expand Down
30 changes: 28 additions & 2 deletions packages/azure-functions-durable/test/unit/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,27 @@ describe("DurableFunctionsClient", () => {
await client.stop();
}
});

it("startNew forwards dedupe statuses to the core scheduler", async () => {
const client = new DurableFunctionsClient(CLIENT_CONFIG);
try {
const schedule = jest.spyOn(client, "scheduleNewOrchestration").mockResolvedValue("inst-9");
const dedupeStatuses = [OrchestrationStatus.RUNNING];

await client.startNew("MyOrch", {
instanceId: "inst-9",
dedupeStatuses,
});

expect(schedule).toHaveBeenCalledWith("MyOrch", undefined, {
instanceId: "inst-9",
version: undefined,
dedupeStatuses,
});
} finally {
await client.stop();
}
});
});

describe("control-plane error mapping (v3 parity)", () => {
Expand All @@ -403,7 +424,10 @@ describe("DurableFunctionsClient", () => {
jest
.spyOn(client, "terminateOrchestration")
.mockRejectedValue(
grpcError(grpcStatus.NOT_FOUND, "5 NOT_FOUND: No instance with ID 'inst-1' was found. (Parameter 'instanceId')"),
grpcError(
grpcStatus.NOT_FOUND,
"5 NOT_FOUND: No instance with ID 'inst-1' was found. (Parameter 'instanceId')",
),
);
// terminate surfaces NOT_FOUND(5) for a genuinely missing instance (terminal instances get
// FAILED_PRECONDITION instead), so the mapper consults getStatus: no state -> not-found message.
Expand Down Expand Up @@ -543,7 +567,9 @@ describe("DurableFunctionsClient", () => {
.spyOn(client, "resumeOrchestration")
.mockRejectedValue(grpcError(grpcStatus.UNKNOWN, "2 UNKNOWN: Exception was thrown by handler"));
jest.spyOn(client, "getOrchestrationState").mockResolvedValue(makeStateWith(OrchestrationStatus.RUNNING));
await expect(client.resume("inst-1")).rejects.toThrow("Cannot resume orchestration instance in the Running state.");
await expect(client.resume("inst-1")).rejects.toThrow(
"Cannot resume orchestration instance in the Running state.",
);
} finally {
await client.stop();
}
Expand Down
15 changes: 15 additions & 0 deletions packages/azure-functions-durable/test/unit/query-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EntityStateResponse } from "../../src/entity-state-response";
import {
DurableOrchestrationStatus,
OrchestrationRuntimeStatus,
fromOrchestrationRuntimeStatus,
toDurableOrchestrationStatus,
toOrchestrationRuntimeStatus,
} from "../../src/orchestration-status";
Expand All @@ -25,6 +26,9 @@ describe("toOrchestrationRuntimeStatus", () => {
expect(toOrchestrationRuntimeStatus(OrchestrationStatus.FAILED)).toBe(
OrchestrationRuntimeStatus.Failed,
);
expect(toOrchestrationRuntimeStatus(OrchestrationStatus.CANCELED)).toBe(
OrchestrationRuntimeStatus.Canceled,
);
expect(toOrchestrationRuntimeStatus(OrchestrationStatus.TERMINATED)).toBe(
OrchestrationRuntimeStatus.Terminated,
);
Expand All @@ -37,6 +41,17 @@ describe("toOrchestrationRuntimeStatus", () => {
});
});

describe("fromOrchestrationRuntimeStatus", () => {
it("preserves the distinction between canceled and terminated", () => {
expect(fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.Canceled)).toBe(
OrchestrationStatus.CANCELED,
);
expect(fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.Terminated)).toBe(
OrchestrationStatus.TERMINATED,
);
});
});

describe("toDurableOrchestrationStatus", () => {
it("maps a core OrchestrationState and deserializes JSON payloads", () => {
const created = new Date("2026-01-01T00:00:00.000Z");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export function createExportInstanceHistoryActivity(
OrchestrationStatus.COMPLETED,
OrchestrationStatus.FAILED,
OrchestrationStatus.TERMINATED,
OrchestrationStatus.CANCELED,
];
if (!terminalStatuses.includes(metadata.runtimeStatus)) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,7 @@ export function createExportJobCreationOptions(
);
}
if (!options.completedTimeTo) {
throw new ExportJobClientValidationError(
"CompletedTimeTo is required for Batch export mode.",
"completedTimeTo",
);
throw new ExportJobClientValidationError("CompletedTimeTo is required for Batch export mode.", "completedTimeTo");
}
if (options.completedTimeTo <= options.completedTimeFrom) {
throw new ExportJobClientValidationError(
Expand Down Expand Up @@ -129,31 +126,27 @@ export function createExportJobCreationOptions(
OrchestrationStatus.COMPLETED,
OrchestrationStatus.FAILED,
OrchestrationStatus.TERMINATED,
OrchestrationStatus.CANCELED,
];
if (
options.runtimeStatus &&
options.runtimeStatus.length > 0 &&
options.runtimeStatus.some((s) => !terminalStatuses.includes(s))
) {
throw new ExportJobClientValidationError(
"Export supports terminal orchestration statuses only. Valid statuses are: Completed, Failed, and Terminated.",
"Export supports terminal orchestration statuses only. Valid statuses are: Completed, Failed, Terminated, and Canceled.",
"runtimeStatus",
);
}

// Default runtimeStatus to all terminal statuses if not provided
const runtimeStatus =
options.runtimeStatus && options.runtimeStatus.length > 0
? options.runtimeStatus
: terminalStatuses;
options.runtimeStatus && options.runtimeStatus.length > 0 ? options.runtimeStatus : terminalStatuses;

// Validate maxParallelExports range
const validatedMaxParallelExports = options.maxParallelExports ?? DEFAULT_MAX_PARALLEL_EXPORTS;
if (validatedMaxParallelExports <= 0) {
throw new ExportJobClientValidationError(
"MaxParallelExports must be greater than 0.",
"maxParallelExports",
);
throw new ExportJobClientValidationError("MaxParallelExports must be greater than 0.", "maxParallelExports");
}

return {
Expand Down
1 change: 1 addition & 0 deletions packages/durabletask-js-export-history/test/models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe("Models", () => {
OrchestrationStatus.COMPLETED,
OrchestrationStatus.FAILED,
OrchestrationStatus.TERMINATED,
OrchestrationStatus.CANCELED,
]);
});

Expand Down
Loading
Loading