diff --git a/packages/durabletask-js/src/task/when-all-task.ts b/packages/durabletask-js/src/task/when-all-task.ts index a8c96b6..8677c20 100644 --- a/packages/durabletask-js/src/task/when-all-task.ts +++ b/packages/durabletask-js/src/task/when-all-task.ts @@ -13,6 +13,12 @@ export class WhenAllTask extends CompositeTask { this._completedTasks = 0; this._failedTasks = 0; + + // An empty task list should complete immediately with an empty result + if (tasks.length === 0) { + this._result = [] as T[]; + this._isComplete = true; + } } pendingTasks(): number { diff --git a/packages/durabletask-js/src/task/when-any-task.ts b/packages/durabletask-js/src/task/when-any-task.ts index 290af15..e18c1f5 100644 --- a/packages/durabletask-js/src/task/when-any-task.ts +++ b/packages/durabletask-js/src/task/when-any-task.ts @@ -9,6 +9,9 @@ import { Task } from "./task"; */ export class WhenAnyTask extends CompositeTask> { constructor(tasks: Task[]) { + if (tasks.length === 0) { + throw new Error("whenAny requires at least one task"); + } super(tasks); } diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index cd8b327..c01e19d 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -121,6 +121,12 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { // TODO: check if the task is null? this._previousTask = value; + + // If the yielded task is already complete (e.g., whenAll with an empty array), + // resume immediately so the generator can continue. + if (this._previousTask instanceof Task && this._previousTask.isComplete) { + await this.resume(); + } } async resume() { diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index dda9478..3079af4 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -1128,6 +1128,57 @@ describe("Orchestration Executor", () => { expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); }); }); + + it("should complete immediately when whenAll is called with an empty task array", async () => { + const orchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { + const results = yield whenAll([]); + return results; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + + const oldEvents: any[] = []; + const newEvents = [newOrchestratorStartedEvent(), newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID)]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents); + + // The orchestration should complete immediately with an empty array result + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(completeAction?.getResult()?.getValue()).toEqual(JSON.stringify([])); + }); + + it("should complete when whenAll with empty array is followed by more work", async () => { + const hello = (_: any, name: string) => `Hello ${name}!`; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const emptyResults = yield whenAll([]); + const activityResult = yield ctx.callActivity(hello, "World"); + return { emptyResults, activityResult }; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + const activityName = registry.addActivity(hello); + + // First execution: should schedule the activity after completing whenAll([]) + const oldEvents: any[] = []; + const newEvents = [newOrchestratorStartedEvent(), newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID)]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents); + + // The whenAll([]) should complete, then an activity should be scheduled + expect(result.actions.length).toEqual(1); + expect(result.actions[0].hasScheduletask()).toBeTruthy(); + expect(result.actions[0].getScheduletask()?.getName()).toEqual(activityName); + }); + + it("should throw when whenAny is called with an empty task array", () => { + expect(() => whenAny([])).toThrow("whenAny requires at least one task"); + }); }); function getAndValidateSingleCompleteOrchestrationAction(