Skip to content

Commit 132dea1

Browse files
YunchuWangCopilot
andcommitted
feat(durable-functions): add app.client.* durable client function helpers for v3 parity
Restores the 10 client-starter registration helpers (http, timer, storageBlob, storageQueue, serviceBusQueue, serviceBusTopic, eventHub, eventGrid, cosmosDB, generic). Each adds the durableClient input binding and injects a DurableFunctionsClient as the handler's 2nd arg, so (trigger, client, context) handlers work without manual df.input.durableClient() + df.getClient(context). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent cc420b8 commit 132dea1

6 files changed

Lines changed: 414 additions & 1 deletion

File tree

packages/azure-functions-durable/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
Initial gRPC-consolidated Azure Functions Durable provider, built on `@microsoft/durabletask-js`.
66

7+
### Added
8+
9+
- `app.client.*` durable client function registration helpers (`http`, `timer`, `storageBlob`,
10+
`storageQueue`, `serviceBusQueue`, `serviceBusTopic`, `eventHub`, `eventGrid`, `cosmosDB`,
11+
`generic`), restoring v3 parity. Each registers a normal trigger and injects a
12+
`DurableFunctionsClient` as the handler's second argument, so the client-starter signature
13+
`(trigger, client, context)` works without manually wiring
14+
`extraInputs: [df.input.durableClient()]` + `df.getClient(context)`.
15+
716
### Requirements
817

918
- Node.js >= 22 (drops the Node 18/20 that `durable-functions` v3 supported).

packages/azure-functions-durable/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This package supersedes the legacy [`durable-functions`](https://github.com/Azur
1717
## What it supports
1818

1919
- **Authoring**`app.orchestration`, `app.activity`, and `app.entity` register durable functions (each trigger opts into the host's gRPC protocol automatically).
20-
- **Client**`getClient(context)` returns a `DurableFunctionsClient` for scheduling, querying, signaling, and managing instances, plus HTTP management-payload helpers (`createCheckStatusResponse`, `createHttpManagementPayload`) for durable HTTP starters.
20+
- **Client**`getClient(context)` returns a `DurableFunctionsClient` for scheduling, querying, signaling, and managing instances, plus HTTP management-payload helpers (`createCheckStatusResponse`, `createHttpManagementPayload`) for durable HTTP starters. The `app.client.*` starter helpers (`http`, `timer`, `storageBlob`, `storageQueue`, `serviceBusQueue`, `serviceBusTopic`, `eventHub`, `eventGrid`, `cosmosDB`, `generic`) register a normal trigger and inject the client as the handler's second argument, so `(trigger, client, context)` works without manually wiring `df.input.durableClient()` + `df.getClient(context)`.
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

2323
## Migrating from durable-functions v3
@@ -68,6 +68,23 @@ app.http("startHello", {
6868
});
6969
```
7070

71+
### Client (starter) functions
72+
73+
`app.client.*` is sugar for the client-starter above — it adds the `durableClient` input binding and
74+
injects a `DurableFunctionsClient` as the handler's second argument, so you don't wire
75+
`extraInputs: [df.input.durableClient()]` + `df.getClient(context)` yourself:
76+
77+
```typescript
78+
// Sugar equivalent of app.http + df.input.durableClient() + df.getClient(context):
79+
df.app.client.http("startHello", {
80+
route: "orchestrators/helloOrchestrator",
81+
handler: async (request, client, context) => {
82+
const instanceId = await client.scheduleNewOrchestration("helloOrchestrator", "Durable");
83+
return client.createCheckStatusResponse(request, instanceId);
84+
},
85+
});
86+
```
87+
7188
## Status
7289

7390
This package is an early preview (`0.4.0`); APIs may change before the stable release.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
CosmosDBv3FunctionOptions,
6+
CosmosDBv4FunctionOptions,
7+
EventGridEvent,
8+
EventGridFunctionOptions,
9+
EventHubFunctionOptions,
10+
FunctionHandler,
11+
FunctionOptions,
12+
FunctionResult,
13+
HttpFunctionOptions,
14+
HttpRequest,
15+
HttpResponse,
16+
HttpResponseInit,
17+
InvocationContext,
18+
ServiceBusQueueFunctionOptions,
19+
ServiceBusTopicFunctionOptions,
20+
StorageBlobFunctionOptions,
21+
StorageQueueFunctionOptions,
22+
Timer,
23+
TimerFunctionOptions,
24+
app as azFuncApp,
25+
} from "@azure/functions";
26+
import { DurableFunctionsClient } from "./client";
27+
import { getClient, isDurableClientInput } from "./get-client";
28+
import * as input from "./input";
29+
30+
// The `app.client.*` helpers register an ordinary Azure Functions trigger and inject a
31+
// DurableFunctionsClient as the handler's second argument, restoring the classic durable-functions
32+
// v3 client-starter signature `(trigger, client, context)`. Each helper is a thin wrapper over the
33+
// matching `@azure/functions` `app.*` method: it adds the `durableClient` input binding (so the
34+
// host populates the client configuration) and wraps the handler to build the client via
35+
// `getClient(context)` before delegating. `getClient` is synchronous, so no async is introduced.
36+
37+
/**
38+
* A handler triggered by some Azure Functions trigger that additionally receives a
39+
* {@link DurableFunctionsClient} as its second argument.
40+
*/
41+
export type DurableClientHandler = (
42+
triggerInput: any,
43+
client: DurableFunctionsClient,
44+
context: InvocationContext,
45+
) => FunctionResult<any>;
46+
47+
/** Configures the inputs, outputs, and handler for a generic Durable Client function. */
48+
export interface DurableClientOptions extends Omit<FunctionOptions, "handler"> {
49+
handler: DurableClientHandler;
50+
}
51+
52+
export type HttpDurableClientHandler = (
53+
request: HttpRequest,
54+
client: DurableFunctionsClient,
55+
context: InvocationContext,
56+
) => FunctionResult<HttpResponseInit | HttpResponse>;
57+
58+
/** Configures options for an HTTP-triggered Durable Client function. */
59+
export interface HttpDurableClientOptions extends Omit<HttpFunctionOptions, "handler"> {
60+
handler: HttpDurableClientHandler;
61+
}
62+
63+
export type TimerDurableClientHandler = (
64+
myTimer: Timer,
65+
client: DurableFunctionsClient,
66+
context: InvocationContext,
67+
) => FunctionResult;
68+
69+
/** Configures options for a timer-triggered Durable Client function. */
70+
export interface TimerDurableClientOptions extends Omit<TimerFunctionOptions, "handler"> {
71+
handler: TimerDurableClientHandler;
72+
}
73+
74+
export type StorageBlobDurableClientHandler = (
75+
blob: unknown,
76+
client: DurableFunctionsClient,
77+
context: InvocationContext,
78+
) => FunctionResult;
79+
80+
/** Configures options for a storage-blob-triggered Durable Client function. */
81+
export interface StorageBlobDurableClientOptions extends Omit<StorageBlobFunctionOptions, "handler"> {
82+
handler: StorageBlobDurableClientHandler;
83+
}
84+
85+
export type StorageQueueDurableClientHandler = (
86+
queueEntry: unknown,
87+
client: DurableFunctionsClient,
88+
context: InvocationContext,
89+
) => FunctionResult;
90+
91+
/** Configures options for a storage-queue-triggered Durable Client function. */
92+
export interface StorageQueueDurableClientOptions extends Omit<StorageQueueFunctionOptions, "handler"> {
93+
handler: StorageQueueDurableClientHandler;
94+
}
95+
96+
export type ServiceBusQueueDurableClientHandler = (
97+
message: unknown,
98+
client: DurableFunctionsClient,
99+
context: InvocationContext,
100+
) => FunctionResult;
101+
102+
/** Configures options for a service-bus-queue-triggered Durable Client function. */
103+
export interface ServiceBusQueueDurableClientOptions extends Omit<ServiceBusQueueFunctionOptions, "handler"> {
104+
handler: ServiceBusQueueDurableClientHandler;
105+
}
106+
107+
export type ServiceBusTopicDurableClientHandler = (
108+
message: unknown,
109+
client: DurableFunctionsClient,
110+
context: InvocationContext,
111+
) => FunctionResult;
112+
113+
/** Configures options for a service-bus-topic-triggered Durable Client function. */
114+
export interface ServiceBusTopicDurableClientOptions extends Omit<ServiceBusTopicFunctionOptions, "handler"> {
115+
handler: ServiceBusTopicDurableClientHandler;
116+
}
117+
118+
export type EventHubDurableClientHandler = (
119+
messages: unknown,
120+
client: DurableFunctionsClient,
121+
context: InvocationContext,
122+
) => FunctionResult;
123+
124+
/** Configures options for an Event Hub-triggered Durable Client function. */
125+
export interface EventHubDurableClientOptions extends Omit<EventHubFunctionOptions, "handler"> {
126+
handler: EventHubDurableClientHandler;
127+
}
128+
129+
export type EventGridDurableClientHandler = (
130+
event: EventGridEvent,
131+
client: DurableFunctionsClient,
132+
context: InvocationContext,
133+
) => FunctionResult;
134+
135+
/** Configures options for an Event Grid-triggered Durable Client function. */
136+
export interface EventGridDurableClientOptions extends Omit<EventGridFunctionOptions, "handler"> {
137+
handler: EventGridDurableClientHandler;
138+
}
139+
140+
export type CosmosDBDurableClientHandler = (
141+
documents: unknown[],
142+
client: DurableFunctionsClient,
143+
context: InvocationContext,
144+
) => FunctionResult;
145+
146+
/** Configures options for a CosmosDB-triggered Durable Client function (extension v3). */
147+
export interface CosmosDBv3DurableClientOptions extends Omit<CosmosDBv3FunctionOptions, "handler"> {
148+
handler: CosmosDBDurableClientHandler;
149+
}
150+
151+
/** Configures options for a CosmosDB-triggered Durable Client function (extension v4). */
152+
export interface CosmosDBv4DurableClientOptions extends Omit<CosmosDBv4FunctionOptions, "handler"> {
153+
handler: CosmosDBDurableClientHandler;
154+
}
155+
156+
export type CosmosDBDurableClientOptions = CosmosDBv3DurableClientOptions | CosmosDBv4DurableClientOptions;
157+
158+
/**
159+
* Registers an HTTP-triggered Durable Client function. Triggered like a normal HTTP function, but
160+
* the handler receives a {@link DurableFunctionsClient} as its second argument.
161+
*/
162+
export function http(functionName: string, options: HttpDurableClientOptions): void {
163+
addClientInput(options);
164+
azFuncApp.http(functionName, {
165+
...options,
166+
handler: convertToFunctionHandler(options.handler),
167+
});
168+
}
169+
170+
/** Registers a timer-triggered Durable Client function. */
171+
export function timer(functionName: string, options: TimerDurableClientOptions): void {
172+
addClientInput(options);
173+
azFuncApp.timer(functionName, {
174+
...options,
175+
handler: convertToFunctionHandler(options.handler),
176+
});
177+
}
178+
179+
/** Registers a storage-blob-triggered Durable Client function. */
180+
export function storageBlob(functionName: string, options: StorageBlobDurableClientOptions): void {
181+
addClientInput(options);
182+
azFuncApp.storageBlob(functionName, {
183+
...options,
184+
handler: convertToFunctionHandler(options.handler),
185+
});
186+
}
187+
188+
/** Registers a storage-queue-triggered Durable Client function. */
189+
export function storageQueue(functionName: string, options: StorageQueueDurableClientOptions): void {
190+
addClientInput(options);
191+
azFuncApp.storageQueue(functionName, {
192+
...options,
193+
handler: convertToFunctionHandler(options.handler),
194+
});
195+
}
196+
197+
/** Registers a service-bus-queue-triggered Durable Client function. */
198+
export function serviceBusQueue(functionName: string, options: ServiceBusQueueDurableClientOptions): void {
199+
addClientInput(options);
200+
azFuncApp.serviceBusQueue(functionName, {
201+
...options,
202+
handler: convertToFunctionHandler(options.handler),
203+
});
204+
}
205+
206+
/** Registers a service-bus-topic-triggered Durable Client function. */
207+
export function serviceBusTopic(functionName: string, options: ServiceBusTopicDurableClientOptions): void {
208+
addClientInput(options);
209+
azFuncApp.serviceBusTopic(functionName, {
210+
...options,
211+
handler: convertToFunctionHandler(options.handler),
212+
});
213+
}
214+
215+
/** Registers an Event Hub-triggered Durable Client function. */
216+
export function eventHub(functionName: string, options: EventHubDurableClientOptions): void {
217+
addClientInput(options);
218+
azFuncApp.eventHub(functionName, {
219+
...options,
220+
handler: convertToFunctionHandler(options.handler),
221+
});
222+
}
223+
224+
/** Registers an Event Grid-triggered Durable Client function. */
225+
export function eventGrid(functionName: string, options: EventGridDurableClientOptions): void {
226+
addClientInput(options);
227+
azFuncApp.eventGrid(functionName, {
228+
...options,
229+
handler: convertToFunctionHandler(options.handler),
230+
});
231+
}
232+
233+
/** Registers a CosmosDB-triggered Durable Client function. */
234+
export function cosmosDB(functionName: string, options: CosmosDBDurableClientOptions): void {
235+
addClientInput(options);
236+
azFuncApp.cosmosDB(functionName, {
237+
...options,
238+
handler: convertToFunctionHandler(options.handler),
239+
});
240+
}
241+
242+
/** Registers a generic-triggered Durable Client function. */
243+
export function generic(functionName: string, options: DurableClientOptions): void {
244+
addClientInput(options);
245+
azFuncApp.generic(functionName, {
246+
...options,
247+
handler: convertToFunctionHandler(options.handler),
248+
});
249+
}
250+
251+
/** @hidden Adds the `durableClient` input binding unless one is already present. */
252+
function addClientInput(options: Partial<DurableClientOptions>): void {
253+
options.extraInputs = options.extraInputs ?? [];
254+
if (!options.extraInputs.find(isDurableClientInput)) {
255+
options.extraInputs.push(input.durableClient());
256+
}
257+
}
258+
259+
/** @hidden Wraps a client handler so the DurableFunctionsClient is injected as the 2nd argument. */
260+
function convertToFunctionHandler(clientHandler: DurableClientHandler): FunctionHandler {
261+
return (triggerInput: unknown, context: InvocationContext): FunctionResult<any> => {
262+
const client = getClient(context);
263+
return clientHandler(triggerInput, client, context);
264+
};
265+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import { ClassicEntity, wrapEntity } from "./entity-context";
1414
import { ClassicOrchestrator, wrapOrchestrator } from "./orchestration-context";
1515
import { DurableFunctionsWorker } from "./worker";
1616

17+
// The `client` namespace groups the durable-client starter registration helpers so that
18+
// `df.app.client.http(...)` (and siblings) restore the v3 client-starter sugar. `index.ts`
19+
// re-exports this whole module as `app`, so `app.client.*` resolves for consumers.
20+
export * as client from "./app-client";
21+
1722
// A single worker owns the orchestrator/entity registry for the whole function app. It is never
1823
// started (no gRPC channel, no work-item loop); it only replays one work item per invocation via
1924
// the byte-processor methods. Orchestrators and entities are pre-registered at decoration time so

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,32 @@ export { OrchestrationFilter } from "./orchestration-filter";
4949
// Legacy durable-functions v3 API compatibility aliases (types only). These let orchestrator/
5050
// activity code written against the classic `durable-functions` v3 API type-check unchanged.
5151
export type { ActivityHandler } from "./app";
52+
// Durable-client starter option/handler types, surfaced at the top level like v3 so consumers can
53+
// annotate their `app.client.*` handlers. The registration functions live under `app.client.*`.
54+
export type {
55+
DurableClientHandler,
56+
DurableClientOptions,
57+
HttpDurableClientHandler,
58+
HttpDurableClientOptions,
59+
TimerDurableClientHandler,
60+
TimerDurableClientOptions,
61+
StorageBlobDurableClientHandler,
62+
StorageBlobDurableClientOptions,
63+
StorageQueueDurableClientHandler,
64+
StorageQueueDurableClientOptions,
65+
ServiceBusQueueDurableClientHandler,
66+
ServiceBusQueueDurableClientOptions,
67+
ServiceBusTopicDurableClientHandler,
68+
ServiceBusTopicDurableClientOptions,
69+
EventHubDurableClientHandler,
70+
EventHubDurableClientOptions,
71+
EventGridDurableClientHandler,
72+
EventGridDurableClientOptions,
73+
CosmosDBDurableClientHandler,
74+
CosmosDBDurableClientOptions,
75+
CosmosDBv3DurableClientOptions,
76+
CosmosDBv4DurableClientOptions,
77+
} from "./app-client";
5278
export type {
5379
ClassicOrchestrator as OrchestrationHandler,
5480
ClassicOrchestrationContext as OrchestrationContext,

0 commit comments

Comments
 (0)