Skip to content

Commit 900064f

Browse files
YunchuWangCopilot
andcommitted
Refine getStatus/startNew v3 parity (review item 5)
- getStatus throws on not-found with a v3-style 'DurableClient error:' message (verified v3 DurableClient.getStatus throws on the extension's HTTP 404; returns non-optional DurableOrchestrationStatus). - showInput now gates only the top-level input (payloads always fetched); output/customStatus are always returned, matching v3. - showHistoryOutput now strips input/result payloads from history entries when falsy and keeps them when true. history stays core HistoryEvent[] (v3 types history as Array<unknown>). - startNew forwards the v3 version option (already wired to core scheduleNewOrchestration). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 8f62a35 commit 900064f

5 files changed

Lines changed: 76 additions & 38 deletions

File tree

packages/azure-functions-durable/CHANGELOG.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ Initial gRPC-consolidated Azure Functions Durable provider, built on `@microsoft
1919
`isCompleted` / `isFaulted` / `result`). `context.df.createTimer(...)` returns a cancelable
2020
`TimerTask`, so the timeout-race pattern (`Task.any` then `timer.cancel()`) keeps working.
2121
- `client.getStatus()` matches the v3 signature: it returns a non-optional `DurableOrchestrationStatus`
22-
and throws when the instance does not exist. `showInput`, `showHistory`, and `showHistoryOutput` are
23-
accepted, with two consolidated-path caveats: `showInput` maps to the core `fetchPayloads` flag (so
24-
`showInput: false` also omits output/custom status, where v3 omitted only the input), and
25-
`showHistory` surfaces core `HistoryEvent`s rather than the classic .NET extension's history shape
26-
(`showHistoryOutput` is accepted but does not strip payloads on the gRPC path).
22+
and throws when the instance does not exist. `showInput` suppresses only the top-level input (output
23+
and custom status are always returned, as in v3); `showHistory` populates `history`, and
24+
`showHistoryOutput` toggles whether those entries keep their input/result payloads. One
25+
consolidated-path note: `history` entries are core `HistoryEvent`s (v3 types `history` as
26+
`Array<unknown>`), not the classic .NET extension's history serialization.
2727
- `client.startNew()` supports the v3 `version` option (forwarded to the core scheduler).
2828
- Removed top-level exports: `DummyOrchestrationContext`, `DummyEntityContext`, `DurableError`,
2929
`AggregatedError`, `ManagedIdentityTokenSource`, `TokenSource`. `TaskFailedError` is re-exported

packages/azure-functions-durable/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ changed. See [`CHANGELOG.md`](./CHANGELOG.md) for the full list; the highlights:
3434
of v3's `isCompleted` / `isFaulted` / `result`. `context.df.createTimer(...)` still returns a
3535
cancelable `TimerTask` for the timeout-race pattern.
3636
- **`client.getStatus()` keeps the v3 shape** — it returns a non-optional `DurableOrchestrationStatus`
37-
and throws when the instance is missing. `showInput` / `showHistory` / `showHistoryOutput` are
38-
accepted, but `showInput` also gates output/custom status and `showHistory` returns core
39-
`HistoryEvent`s (not the v3 extension history shape). **`client.startNew()` supports the `version`
40-
option.**
37+
and throws when the instance is missing. `showInput` suppresses only the top-level input,
38+
`showHistory` populates `history`, and `showHistoryOutput` toggles the per-entry input/result
39+
payloads; `history` entries are core `HistoryEvent`s (v3 types `history` as `Array<unknown>`).
40+
**`client.startNew()` supports the `version` option.**
4141
- **Some v3 top-level exports were removed**`DummyOrchestrationContext` / `DummyEntityContext`,
4242
`DurableError` / `AggregatedError`, and `ManagedIdentityTokenSource` / `TokenSource`.
4343
`TaskFailedError` is re-exported from the core SDK; use the core `TestOrchestrationWorker` /

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

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ export interface GetStatusOptions {
4242
/** Include the execution history in {@link DurableOrchestrationStatus.history}. */
4343
showHistory?: boolean;
4444
/**
45-
* Include input/output payloads in the history entries. Accepted for v3 compatibility; on the
46-
* consolidated gRPC path the core history events always carry their payloads (see
47-
* {@link DurableFunctionsClient.getStatus}).
45+
* Include the input/result payloads on the history entries. When omitted or `false`, those
46+
* payloads are stripped from each entry (see {@link DurableFunctionsClient.getStatus}).
4847
*/
4948
showHistoryOutput?: boolean;
5049
/** Include the orchestration input/output/custom-status payloads (maps to core `fetchPayloads`). */
@@ -61,6 +60,21 @@ export interface StartNewOptions {
6160
version?: string;
6261
}
6362

63+
/**
64+
* @hidden
65+
* Removes the input/result payload fields from a core history event, honoring v3's
66+
* `showHistoryOutput: false`. Returns a shallow copy so the source event is left untouched.
67+
*/
68+
function stripHistoryEventOutput(event: unknown): unknown {
69+
if (event !== null && typeof event === "object") {
70+
const clone: Record<string, unknown> = { ...(event as Record<string, unknown>) };
71+
delete clone.input;
72+
delete clone.result;
73+
return clone;
74+
}
75+
return event;
76+
}
77+
6478
export class DurableFunctionsClient extends TaskHubGrpcClient {
6579
public readonly taskHubName: string;
6680
public readonly connectionName: string;
@@ -197,29 +211,36 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
197211
* @param instanceId - The ID of the orchestration instance to query.
198212
* @param options - v3 {@link GetStatusOptions}: `showInput`, `showHistory`, `showHistoryOutput`.
199213
* @returns The instance status.
200-
* @throws If no orchestration instance with the given ID exists (matches v3, which threw on the
201-
* extension's HTTP 404 response).
214+
* @throws If no orchestration instance with the given ID exists. This matches v3's
215+
* `DurableClient.getStatus`, which throws on the extension's HTTP 404 not-found response.
202216
*
203217
* @remarks
204-
* v3 parity and its boundaries on the consolidated gRPC path:
218+
* v3 parity, verified against `azure-functions-durable-js`'s `DurableClient.getStatus`:
205219
* - Returns a non-optional {@link DurableOrchestrationStatus} and throws when the instance is not
206-
* found, exactly like v3's `DurableClient.getStatus`.
220+
* found, exactly like v3 (its `case 404` branch throws).
221+
* - `showInput` gates only the top-level `input`: v3 keeps output/custom status regardless, so the
222+
* payloads are always fetched and only `input` is suppressed when `showInput === false`.
207223
* - `showHistory` populates {@link DurableOrchestrationStatus.history} from the core execution
208-
* history (`getOrchestrationHistory`). The entries are core `HistoryEvent`s, not the classic
209-
* .NET extension's history shape (the gRPC path bypasses the extension's HTTP history formatter).
210-
* - `showHistoryOutput` is accepted for API compatibility but does not strip payloads from the
211-
* history events: core history events always carry their intrinsic input/output.
212-
* - `showInput` maps to the core `fetchPayloads` flag. Because that call fetches input, output, and
213-
* custom status together, `showInput: false` also omits output/custom status (v3 omitted only the
214-
* input).
224+
* history (`getOrchestrationHistory`). v3 types `history` as `Array<unknown>`, so the entries are
225+
* core `HistoryEvent`s. On the consolidated gRPC path these are the core event shape, not the
226+
* classic .NET extension's history serialization (the gRPC path bypasses that HTTP formatter).
227+
* - `showHistoryOutput` controls whether the history entries carry their input/result payloads:
228+
* when falsy, those fields are stripped from each event; when `true`, they are kept.
215229
*/
216230
async getStatus(instanceId: string, options?: GetStatusOptions): Promise<DurableOrchestrationStatus> {
217-
const state = await this.getOrchestrationState(instanceId, options?.showInput ?? true);
231+
const state = await this.getOrchestrationState(instanceId, true);
218232
if (state === undefined) {
219-
throw new Error(`No orchestration instance with ID '${instanceId}' was found.`);
233+
throw new Error(
234+
`DurableClient error: No orchestration instance with ID '${instanceId}' was found. ` +
235+
`This usually means no data is associated with the provided instanceId.`,
236+
);
237+
}
238+
let history: unknown[] | undefined;
239+
if (options?.showHistory) {
240+
const events = await this.getOrchestrationHistory(instanceId);
241+
history = options.showHistoryOutput ? events : events.map(stripHistoryEventOutput);
220242
}
221-
const history = options?.showHistory ? await this.getOrchestrationHistory(instanceId) : undefined;
222-
return toDurableOrchestrationStatus(state, history);
243+
return toDurableOrchestrationStatus(state, history, options?.showInput ?? true);
223244
}
224245

225246
/**

packages/azure-functions-durable/src/orchestration-status.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,22 @@ export function fromOrchestrationRuntimeStatus(status: OrchestrationRuntimeStatu
110110
*
111111
* @param state - The core orchestration state to map.
112112
* @param history - Optional execution history to attach (see {@link DurableFunctionsClient.getStatus}
113-
* `showHistory`). The entries are core {@link OrchestrationState} history events; the consolidated
114-
* gRPC path surfaces core `HistoryEvent`s rather than the classic .NET extension's history shape.
113+
* `showHistory`). The entries are core `HistoryEvent`s; the consolidated gRPC path surfaces those
114+
* rather than the classic .NET extension's history shape (v3 types `history` as `Array<unknown>`).
115+
* @param includeInput - When `false`, the top-level `input` is omitted (maps to v3's `showInput`,
116+
* which suppresses only the input while still returning output/custom status). Defaults to `true`.
115117
*/
116118
export function toDurableOrchestrationStatus(
117119
state: OrchestrationState,
118120
history?: unknown[],
121+
includeInput = true,
119122
): DurableOrchestrationStatus {
120123
return new DurableOrchestrationStatus({
121124
name: state.name,
122125
instanceId: state.instanceId,
123126
createdTime: state.createdAt,
124127
lastUpdatedTime: state.lastUpdatedAt,
125-
input: parseJson(state.serializedInput),
128+
input: includeInput ? parseJson(state.serializedInput) : undefined,
126129
output: parseJson(state.serializedOutput),
127130
runtimeStatus: toOrchestrationRuntimeStatus(state.runtimeStatus),
128131
customStatus: parseJson(state.serializedCustomStatus),

packages/azure-functions-durable/test/unit/client.spec.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,13 @@ describe("DurableFunctionsClient", () => {
290290
undefined,
291291
);
292292

293-
it("getStatus returns a non-optional DurableOrchestrationStatus and honors showInput", async () => {
293+
it("getStatus returns a non-optional DurableOrchestrationStatus and gates only input via showInput", async () => {
294294
const client = new DurableFunctionsClient(CLIENT_CONFIG);
295295
try {
296296
const getState = jest.spyOn(client, "getOrchestrationState").mockResolvedValue(makeState());
297297

298298
const status = await client.getStatus("inst-1");
299+
// Payloads are always fetched; showInput only gates the top-level input (v3 keeps output).
299300
expect(getState).toHaveBeenCalledWith("inst-1", true);
300301
expect(status.instanceId).toBe("inst-1");
301302
expect(status.name).toBe("MyOrch");
@@ -304,9 +305,11 @@ describe("DurableFunctionsClient", () => {
304305
expect(status.output).toBe("the-output");
305306
expect(status.history).toBeUndefined();
306307

307-
// showInput maps to the core fetchPayloads flag.
308-
await client.getStatus("inst-1", { showInput: false });
309-
expect(getState).toHaveBeenLastCalledWith("inst-1", false);
308+
// showInput: false suppresses only input; output/custom status still come back.
309+
const suppressed = await client.getStatus("inst-1", { showInput: false });
310+
expect(getState).toHaveBeenLastCalledWith("inst-1", true);
311+
expect(suppressed.input).toBeUndefined();
312+
expect(suppressed.output).toBe("the-output");
310313
} finally {
311314
await client.stop();
312315
}
@@ -324,16 +327,27 @@ describe("DurableFunctionsClient", () => {
324327
}
325328
});
326329

327-
it("getStatus populates history from the core execution history when showHistory is set", async () => {
330+
it("getStatus populates history and honors showHistoryOutput", async () => {
328331
const client = new DurableFunctionsClient(CLIENT_CONFIG);
329332
try {
330333
jest.spyOn(client, "getOrchestrationState").mockResolvedValue(makeState());
331-
const events = [{ eventId: 1 }, { eventId: 2 }];
334+
const events = [
335+
{ eventId: 1, type: "TaskCompleted", input: "in", result: "out" },
336+
{ eventId: 2, type: "TimerFired" },
337+
];
332338
const getHistory = jest.spyOn(client, "getOrchestrationHistory").mockResolvedValue(events as never);
333339

334-
const status = await client.getStatus("inst-1", { showHistory: true });
340+
// showHistoryOutput: true keeps the input/result payloads on each entry.
341+
const withOutput = await client.getStatus("inst-1", { showHistory: true, showHistoryOutput: true });
335342
expect(getHistory).toHaveBeenCalledWith("inst-1");
336-
expect(status.history).toEqual(events);
343+
expect(withOutput.history).toEqual(events);
344+
345+
// showHistory without showHistoryOutput strips input/result from each entry.
346+
const stripped = await client.getStatus("inst-1", { showHistory: true });
347+
expect(stripped.history).toEqual([
348+
{ eventId: 1, type: "TaskCompleted" },
349+
{ eventId: 2, type: "TimerFired" },
350+
]);
337351

338352
// Without showHistory the core history call is skipped and history stays undefined.
339353
getHistory.mockClear();

0 commit comments

Comments
 (0)