Skip to content

Commit f935bce

Browse files
YunchuWangCopilot
andcommitted
Address PR #282 review: arity fix, parseJson, exports, docs, node bump
Resolves andystaples' review comments plus a ListAll E2E bug on packages/azure-functions-durable: - #1 orchestrator arity: detect generator kind (sync=classic v3, async=core native) instead of an arity heuristic; driver now handles sync+async generators so single-param native orchestrators actually execute. - #2 parseJson tolerates non-JSON serializedOutput (fixes getStatusAll 400). - #3 createCheckStatusResponse accepts undefined request (baseUrl fallback). - #4 createTimer returns TimerTask (cancelable) in its type signature. - #5 re-export TaskFailedError from core + compat; document removed v3-only exports in CHANGELOG/README. - #6/#7 document getStatus + classic-context breaking changes. - #8 bump @types/node to ^22 to match engines.node>=22. - #9 set version 0.4.0 + document release order. - #10 distinct abandon error for the single work-item execution path. - #11 revert proto codegen churn to origin/main. Adds regression tests for items #1/#2/#3/#5/#10. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent e4fd451 commit f935bce

17 files changed

Lines changed: 371 additions & 156 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Changelog
22

3-
## 4.0.0-alpha.0
3+
## 0.4.0
44

5-
- Added the initial gRPC-consolidated Azure Functions Durable provider package.
5+
Initial gRPC-consolidated Azure Functions Durable provider, built on `@microsoft/durabletask-js`.
6+
7+
### Requirements
8+
9+
- Node.js >= 22 (drops the Node 18/20 that `durable-functions` v3 supported).
10+
11+
### Breaking changes vs `durable-functions` v3
12+
13+
- Classic orchestration and entity contexts no longer extend `InvocationContext`; they expose only
14+
`df` plus replay-safe log helpers (no `invocationId` / `functionName` / `extraInputs`).
15+
- Task result shape follows the core SDK: use `isComplete` / `isFailed` / `getResult()` (v3 used
16+
`isCompleted` / `isFaulted` / `result`). `context.df.createTimer(...)` returns a cancelable
17+
`TimerTask`, so the timeout-race pattern (`Task.any` then `timer.cancel()`) keeps working.
18+
- `client.getStatus()` returns `DurableOrchestrationStatus | undefined` (was non-optional) and honors
19+
only `showInput`; `showHistory` / `showHistoryOutput` are ignored and `history` is not populated.
20+
- `client.startNew()` drops the v3 `version` option.
21+
- Removed top-level exports: `DummyOrchestrationContext`, `DummyEntityContext`, `DurableError`,
22+
`AggregatedError`, `ManagedIdentityTokenSource`, `TokenSource`. `TaskFailedError` is re-exported
23+
from the core SDK and aggregate failures surface as JS-native `AggregateError`. For orchestration
24+
unit tests, use the core `TestOrchestrationWorker` / `TestOrchestrationClient` in place of the v3
25+
dummy contexts.
26+
27+
### Release order
28+
29+
- Publish `@microsoft/durabletask-js` (0.3.0) before this package.
30+
- Confirm the target `@azure/functions` (^4.16.1) build includes the Durable extension changes
31+
(`DurableRequiresGrpc`) required by the consolidated gRPC path.

packages/azure-functions-durable/README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Write [Azure Durable Functions](https://learn.microsoft.com/azure/azure-function
66

77
`durable-functions` is the Azure Functions Durable provider for JavaScript, built on `@microsoft/durabletask-js`. You author Durable Functions apps with the familiar `app.orchestration` / `app.activity` / `app.entity` model, and the provider talks to the Durable Task backend over the Functions host's gRPC channel.
88

9-
This is a new major version that supersedes the legacy [`durable-functions`](https://github.com/Azure/azure-functions-durable-js) package. New and existing (classic v3) orchestration, activity, and entity code all run on the same gRPC engine.
9+
This package supersedes the legacy [`durable-functions`](https://github.com/Azure/azure-functions-durable-js) package. New and existing (classic v3) orchestration, activity, and entity code all run on the same gRPC engine.
1010

1111
## Why it is needed
1212

@@ -20,6 +20,24 @@ This is a new major version that supersedes the legacy [`durable-functions`](htt
2020
- **Client**`getClient(context)` returns a `DurableFunctionsClient` for scheduling, querying, signaling, and managing instances, plus HTTP management-payload helpers (`createCheckStatusResponse`, `createHttpManagementPayload`) for durable HTTP starters.
2121
- **Classic (v3) compatibility** — orchestrators and entities written in the legacy `context.df.*` style, `RetryOptions`, `EntityId`, and the deprecated client aliases are adapted onto the core engine.
2222

23+
## Migrating from durable-functions v3
24+
25+
This provider keeps classic `context.df.*` orchestrators and entities working, but a few surfaces
26+
changed. See [`CHANGELOG.md`](./CHANGELOG.md) for the full list; the highlights:
27+
28+
- **Node.js >= 22** is required (v3 supported Node 18/20).
29+
- **Classic contexts no longer extend `InvocationContext`** — only `df` plus replay-safe log helpers
30+
are available (no `invocationId` / `functionName` / `extraInputs`).
31+
- **Task result shape follows the core SDK** — use `isComplete` / `isFailed` / `getResult()` instead
32+
of v3's `isCompleted` / `isFaulted` / `result`. `context.df.createTimer(...)` still returns a
33+
cancelable `TimerTask` for the timeout-race pattern.
34+
- **`client.getStatus()` may return `undefined`** and honors only `showInput` (`showHistory` /
35+
`showHistoryOutput` are ignored); **`client.startNew()` drops the `version` option**.
36+
- **Some v3 top-level exports were removed**`DummyOrchestrationContext` / `DummyEntityContext`,
37+
`DurableError` / `AggregatedError`, and `ManagedIdentityTokenSource` / `TokenSource`.
38+
`TaskFailedError` is re-exported from the core SDK; use the core `TestOrchestrationWorker` /
39+
`TestOrchestrationClient` for orchestration unit tests.
40+
2341
## Getting started
2442

2543
```typescript
@@ -47,4 +65,4 @@ app.http("startHello", {
4765

4866
## Status
4967

50-
This package is an early preview (`4.0.0-alpha.0`); APIs may change before the stable release.
68+
This package is an early preview (`0.4.0`); APIs may change before the stable release.

packages/azure-functions-durable/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "durable-functions",
3-
"version": "4.0.0-alpha.0",
3+
"version": "0.4.0",
44
"description": "Azure Functions Durable provider for the Durable Task JavaScript SDK",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -55,7 +55,7 @@
5555
},
5656
"devDependencies": {
5757
"@types/jest": "^29.5.1",
58-
"@types/node": "^18.16.1",
58+
"@types/node": "^22.0.0",
5959
"jest": "^29.5.0",
6060
"ts-jest": "^29.1.0",
6161
"typescript": "^5.0.4"

packages/azure-functions-durable/src/client.ts

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ import {
99
PurgeInstanceCriteria,
1010
TaskHubGrpcClient,
1111
} from "@microsoft/durabletask-js";
12-
import {
13-
HttpManagementPayload,
14-
createHttpManagementPayload as createPayload,
15-
} from "./http-management-payload";
12+
import { HttpManagementPayload, createHttpManagementPayload as createPayload } from "./http-management-payload";
1613
import { EntityStateResponse } from "./entity-state-response";
1714
import { createAzureFunctionsMetadataGenerator } from "./metadata";
1815
import {
@@ -84,8 +81,19 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
8481
this.grpcHttpClientTimeout = config.grpcHttpClientTimeout;
8582
}
8683

87-
createCheckStatusResponse(request: HttpRequest, instanceId: string): HttpResponse {
88-
const payload = this.createHttpManagementPayload(request, instanceId);
84+
/**
85+
* Builds a 202 HTTP response whose body and `Location` header expose the orchestration's
86+
* management URLs (classic Durable Functions v3 `createCheckStatusResponse`).
87+
*
88+
* @param request - The incoming HTTP request, or `undefined` to build the URLs from the client
89+
* binding's `baseUrl` (v3 accepted an undefined request and fell back to the binding).
90+
* @param instanceId - The orchestration instance to build management URLs for.
91+
*/
92+
createCheckStatusResponse(request: HttpRequest | undefined, instanceId: string): HttpResponse {
93+
const payload =
94+
request === undefined
95+
? this.createHttpManagementPayload(instanceId)
96+
: this.createHttpManagementPayload(request, instanceId);
8997

9098
return new HttpResponse({
9199
status: 202,
@@ -109,10 +117,7 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
109117
*/
110118
createHttpManagementPayload(instanceId: string): HttpManagementPayload;
111119
createHttpManagementPayload(request: HttpRequest, instanceId: string): HttpManagementPayload;
112-
createHttpManagementPayload(
113-
requestOrInstanceId: HttpRequest | string,
114-
instanceId?: string,
115-
): HttpManagementPayload {
120+
createHttpManagementPayload(requestOrInstanceId: HttpRequest | string, instanceId?: string): HttpManagementPayload {
116121
// Classic Durable Functions (v3) accepted a single positional `instanceId`. Detect that call
117122
// style (a lone string argument) and fall back to the client binding's `baseUrl` when building
118123
// the payload URLs.
@@ -137,10 +142,7 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
137142
* binding's `baseUrl`.
138143
* @param instanceId - The orchestration instance to build management URLs for.
139144
*/
140-
getClientResponseLinks(
141-
request: HttpRequest | undefined,
142-
instanceId: string,
143-
): HttpManagementPayload {
145+
getClientResponseLinks(request: HttpRequest | undefined, instanceId: string): HttpManagementPayload {
144146
return request === undefined
145147
? this.createHttpManagementPayload(instanceId)
146148
: this.createHttpManagementPayload(request, instanceId);
@@ -153,11 +155,11 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
153155
* @param orchestratorName - The name of the orchestrator to start.
154156
* @param options - Optional input and instance ID.
155157
* @returns The instance ID of the started orchestration.
158+
*
159+
* @remarks
160+
* Breaking change from v3: the v3 `version` option is not supported and is silently dropped.
156161
*/
157-
async startNew(
158-
orchestratorName: string,
159-
options?: { input?: unknown; instanceId?: string },
160-
): Promise<string> {
162+
async startNew(orchestratorName: string, options?: { input?: unknown; instanceId?: string }): Promise<string> {
161163
return this.scheduleNewOrchestration(
162164
orchestratorName,
163165
options?.input,
@@ -172,6 +174,13 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
172174
* @param instanceId - The ID of the orchestration instance to query.
173175
* @param options - When `showInput` is `false`, input/output payloads are not fetched.
174176
* @returns The instance status, or `undefined` if the instance does not exist.
177+
*
178+
* @remarks
179+
* Breaking changes from v3:
180+
* - The return type is `DurableOrchestrationStatus | undefined` (v3 returned a non-optional value),
181+
* so `(await getStatus(id)).runtimeStatus` must guard against `undefined`.
182+
* - Only `showInput` is honored. The v3 `showHistory` / `showHistoryOutput` options are not
183+
* supported and `history` is never populated.
175184
*/
176185
async getStatus(
177186
instanceId: string,
@@ -221,10 +230,7 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
221230
instanceId: string,
222231
waitOptions?: { timeoutInMilliseconds?: number },
223232
): Promise<HttpResponse> {
224-
const timeoutSeconds = Math.max(
225-
1,
226-
Math.ceil((waitOptions?.timeoutInMilliseconds ?? 10000) / 1000),
227-
);
233+
const timeoutSeconds = Math.max(1, Math.ceil((waitOptions?.timeoutInMilliseconds ?? 10000) / 1000));
228234
try {
229235
const state = await this.waitForOrchestrationCompletion(instanceId, true, timeoutSeconds);
230236
if (state) {
@@ -261,10 +267,7 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
261267
* @param entityId - The target entity instance ID.
262268
* @param includeState - Whether to include the entity state in the response (default `true`).
263269
*/
264-
async readEntityState<T = unknown>(
265-
entityId: EntityInstanceId,
266-
includeState = true,
267-
): Promise<EntityStateResponse<T>> {
270+
async readEntityState<T = unknown>(entityId: EntityInstanceId, includeState = true): Promise<EntityStateResponse<T>> {
268271
const metadata = await this.getEntity<T>(entityId, includeState);
269272
if (!metadata) {
270273
return new EntityStateResponse<T>(false);
@@ -399,11 +402,7 @@ export function getGrpcHostAddress(rpcBaseUrl: string): string {
399402
}
400403
}
401404

402-
function getInstanceStatusUrl(
403-
request: HttpRequest | undefined,
404-
instanceId: string,
405-
baseUrl: string,
406-
): string {
405+
function getInstanceStatusUrl(request: HttpRequest | undefined, instanceId: string, baseUrl: string): string {
407406
const encodedInstanceId = encodeURIComponent(instanceId);
408407
if (request !== undefined) {
409408
const requestUrl = new URL(request.url);
@@ -473,10 +472,7 @@ function optionalNumber(record: Record<string, unknown>, name: string): number |
473472
return value;
474473
}
475474

476-
function optionalStringRecord(
477-
record: Record<string, unknown>,
478-
name: string,
479-
): Record<string, string> | undefined {
475+
function optionalStringRecord(record: Record<string, unknown>, name: string): Record<string, string> | undefined {
480476
const value = record[name];
481477
if (value === undefined || value === null) {
482478
return undefined;
@@ -486,9 +482,7 @@ function optionalStringRecord(
486482
const result: Record<string, string> = {};
487483
for (const [key, entry] of Object.entries(valueRecord)) {
488484
if (typeof entry !== "string") {
489-
throw new TypeError(
490-
`Durable Functions client configuration field ${name}.${key} must be a string.`,
491-
);
485+
throw new TypeError(`Durable Functions client configuration field ${name}.${key} must be a string.`);
492486
}
493487
result[key] = entry;
494488
}

packages/azure-functions-durable/src/entity-context.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import {
5-
EntityFactory,
6-
EntityInstanceId,
7-
ITaskEntity,
8-
TaskEntityOperation,
9-
} from "@microsoft/durabletask-js";
4+
import { EntityFactory, EntityInstanceId, ITaskEntity, TaskEntityOperation } from "@microsoft/durabletask-js";
105

116
/**
127
* Classic Durable Functions (v3) entity context, exposed to migrating entity functions as
@@ -94,11 +89,7 @@ export class DurableEntityContext {
9489
* @param operationName - The name of the operation to invoke.
9590
* @param operationInput - Optional input for the operation.
9691
*/
97-
signalEntity(
98-
entityId: EntityInstanceId,
99-
operationName: string,
100-
operationInput?: unknown,
101-
): void {
92+
signalEntity(entityId: EntityInstanceId, operationName: string, operationInput?: unknown): void {
10293
this._operation.context.signalEntity(entityId, operationName, operationInput);
10394
}
10495

@@ -108,7 +99,13 @@ export class DurableEntityContext {
10899
}
109100
}
110101

111-
/** The object passed to a classic (v3) entity function; its `df` is the durable entity context. */
102+
/**
103+
* The object passed to a classic (v3) entity function; its `df` is the durable entity context.
104+
*
105+
* @remarks
106+
* Breaking change from `durable-functions` v3, where the entity context extended `InvocationContext`.
107+
* This context exposes only `df`; entity code that read `InvocationContext` members must be updated.
108+
*/
112109
export interface ClassicEntityContext {
113110
df: DurableEntityContext;
114111
}

packages/azure-functions-durable/src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@ export { createAzureFunctionsMetadataGenerator } from "./metadata";
2121
export { DurableFunctionsWorker } from "./worker";
2222
export { DurableBindingMetadata, addDurableGrpcMetadata } from "./durable-grpc";
2323
export { RetryOptions } from "./retry-options";
24+
// Re-exported core error so callers can `instanceof`-guard caught orchestration failures, matching
25+
// the classic durable-functions v3 top-level `TaskFailedError` export. Note: v3's `DurableError` /
26+
// `AggregatedError` are intentionally not provided — the core engine surfaces `TaskFailedError` and
27+
// JS-native `AggregateError`. See the package README/CHANGELOG migration notes.
28+
export { TaskFailedError } from "@microsoft/durabletask-js";
2429
export {
2530
DurableOrchestrationContext,
2631
ClassicOrchestrationContext,
2732
ClassicOrchestrator,
2833
wrapOrchestrator,
2934
} from "./orchestration-context";
30-
export {
31-
DurableEntityContext,
32-
ClassicEntityContext,
33-
ClassicEntity,
34-
wrapEntity,
35-
} from "./entity-context";
35+
export { DurableEntityContext, ClassicEntityContext, ClassicEntity, wrapEntity } from "./entity-context";
3636
export {
3737
DurableOrchestrationStatus,
3838
DurableOrchestrationStatusInit,

0 commit comments

Comments
 (0)