Skip to content

Commit cd2b6ff

Browse files
YunchuWangCopilot
andcommitted
Fix wrapOrchestrator to drive classic v3 orchestrators via async generator
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 073bf25 commit cd2b6ff

2 files changed

Lines changed: 17 additions & 8 deletions

File tree

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,18 +207,22 @@ export type ClassicOrchestrator = (
207207
export function wrapOrchestrator(handler: TOrchestrator | ClassicOrchestrator): TOrchestrator {
208208
if (typeof handler === "function" && handler.length === 1) {
209209
const classic = handler as ClassicOrchestrator;
210-
const wrapped = function* (
210+
// The core engine only drives orchestrators whose invocation returns an ASYNC generator
211+
// (it gates on Symbol.asyncIterator). Classic v3 orchestrators are SYNC generators, so the
212+
// wrapper must itself be an async generator that delegates to the classic one via `yield*`;
213+
// that forwards each core Task to the engine and pipes sent results back into the classic body.
214+
const wrapped = async function* (
211215
ctx: OrchestrationContext,
212216
input: unknown,
213-
): Generator<Task<unknown>, unknown, unknown> {
217+
): AsyncGenerator<Task<unknown>, unknown, unknown> {
214218
const df = new DurableOrchestrationContext(ctx, input);
215219
const result = classic({ df });
216220
if (isGenerator(result)) {
217221
return yield* result;
218222
}
219223
return result;
220224
};
221-
return wrapped as TOrchestrator;
225+
return wrapped as unknown as TOrchestrator;
222226
}
223227
return handler as TOrchestrator;
224228
}

packages/azure-functions-durable/test/unit/orchestration-context.spec.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ describe("wrapOrchestrator", () => {
171171
expect(wrapOrchestrator(native)).toBe(native);
172172
});
173173

174-
it("wraps a single-parameter classic orchestrator and drives it through context.df", () => {
174+
it("wraps a single-parameter classic orchestrator and drives it through context.df", async () => {
175175
const classic = function* (
176176
context: ClassicOrchestrationContext,
177177
): Generator<Task<unknown>, string, unknown> {
@@ -185,14 +185,19 @@ describe("wrapOrchestrator", () => {
185185
expect(wrapped).not.toBe(classic);
186186

187187
const { ctx, raw } = createFakeCoreContext();
188-
const gen = wrapped(ctx, "INPUT") as Generator<unknown, unknown, unknown>;
189-
190-
const firstYield = gen.next();
188+
// The engine gates on Symbol.asyncIterator: the wrapper MUST return an async generator or the
189+
// core executor never drives it (it would complete immediately with the raw return value).
190+
const gen = wrapped(ctx, "INPUT") as AsyncGenerator<unknown, unknown, unknown>;
191+
expect(typeof (gen as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator]).toBe(
192+
"function",
193+
);
194+
195+
const firstYield = await gen.next();
191196
expect(raw.callActivity).toHaveBeenCalledWith("a", "INPUT");
192197
expect(firstYield.value).toBe("callActivity-task");
193198
expect(firstYield.done).toBe(false);
194199

195-
const final = gen.next("ACTIVITY_RESULT");
200+
const final = await gen.next("ACTIVITY_RESULT");
196201
expect(final.done).toBe(true);
197202
expect(final.value).toBe("done:ACTIVITY_RESULT");
198203
});

0 commit comments

Comments
 (0)