Repository: Azure/azure-functions-durable-js
Pull request #395 (2022-10-27 21:16:57)

Patch two minor bugs
The corresponding NodeJS fixes for https://github.com/Azure/azure-functions-durable-python/pull/391, and https://github.com/Azure/azure-functions-durable-python/pull/390


Patch:
diff --git a/src/orchestrator.ts b/src/orchestrator.ts
index 8a5b982..64ba006 100644
--- a/src/orchestrator.ts
+++ b/src/orchestrator.ts
@@ -86,7 +86,6 @@ export class Orchestrator {
                 this.taskOrchestrationExecutor
             );
         }
-
         await this.taskOrchestrationExecutor.execute(context, state, upperSchemaVersion, this.fn);
         return;
     }
diff --git a/src/orchestratorstate.ts b/src/orchestratorstate.ts
index 24b6516..786b5d4 100644
--- a/src/orchestratorstate.ts
+++ b/src/orchestratorstate.ts
@@ -57,11 +57,11 @@ export class OrchestratorState implements IOrchestratorState {
         this.output = options.output;
         this.schemaVersion = options.schemaVersion;
 
-        if (options.error) {
+        if (options.error != null) {
             this.error = options.error;
         }
 
-        if (options.customStatus) {
+        if (options.customStatus != null) {
             this.customStatus = options.customStatus;
         }
         // Under replay protocol V1, compound actions are treated as lists of actions and
diff --git a/src/task.ts b/src/task.ts
index b261e5d..00c680d 100644
--- a/src/task.ts
+++ b/src/task.ts
@@ -245,6 +245,13 @@ export abstract class CompoundTask extends DFTask {
         if (children.length == 0) {
             this.state = TaskState.Completed;
         }
+
+        // Sub-tasks may have already completed, so we process them
+        children.forEach((child) => {
+            if (child.state != TaskState.Running) {
+                this.handleCompletion(child);
+            }
+        });
     }
 
     /**
diff --git a/src/taskorchestrationexecutor.ts b/src/taskorchestrationexecutor.ts
index 5887140..ca2b2e8 100644
--- a/src/taskorchestrationexecutor.ts
+++ b/src/taskorchestrationexecutor.ts
@@ -185,6 +185,11 @@ export class TaskOrchestrationExecutor {
                 break;
             }
             case HistoryEventType.ExecutionStarted: {
+                // At the start of replay (when currentTask is NoOpTask),
+                // the `isReplaying` flag is determined from the ExecutionStarted event.
+                if (this.currentTask instanceof NoOpTask) {
+                    this.currentTask.isPlayed = event.IsPlayed;
+                }
                 this.tryResumingUserCode();
                 break;
             }


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index 5568b5f..c48d014 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -172,6 +172,44 @@ describe("Orchestrator", () => {
             expect(mockContext.df!.isReplaying).to.be.equal(lastEvent.IsPlayed);
         });
 
+        it("assigns isReplaying at the start of replay", async () => {
+            const orchestrator = TestOrchestrations.SayHelloSequenceTrackIsReplaying;
+            const name = "World";
+
+            const mockHistory = TestHistories.GetSayHelloWithActivityReplayOne(
+                "SayHelloWithActivity",
+                moment.utc().toDate(),
+                name
+            );
+
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(mockHistory, name),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue?.output).to.be.deep.equal([true, false]);
+        });
+
+        it("handles creating a composite task out of an already completed task", async () => {
+            const orchestrator = TestOrchestrations.MultiYieldWhenAll;
+            const name = "World";
+
+            const mockHistory = TestHistories.GetSayHelloWithActivityReplayOne(
+                "SayHelloWithActivity",
+                moment.utc().toDate(),
+                name
+            );
+
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(mockHistory, name),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue?.output, "done!"); // check the orchestration completed
+        });
+
         it("assigns parentInstanceId", async () => {
             const orchestrator = TestOrchestrations.SayHelloSequence;
             const name = "World";
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index 9e00815..a7a25e0 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -305,6 +305,25 @@ export class TestOrchestrations {
         return output;
     });
 
+    public static SayHelloSequenceTrackIsReplaying: any = df.orchestrator(function* (context: any) {
+        const output = [];
+
+        output.push(context.df.isReplaying);
+        yield context.df.callActivity("Hello", "Tokyo");
+        output.push(context.df.isReplaying);
+        return output;
+    });
+
+    public static MultiYieldWhenAll: any = df.orchestrator(function* (context: any) {
+        const output = [];
+
+        output.push(context.isReplaying);
+        const task = context.df.callActivity("Hello", "Tokyo");
+        yield context.df.Task.all([task]);
+        yield context.df.Task.all([task]);
+        return "done!";
+    });
+
     public static SendHttpRequest: any = df.orchestrator(function* (context) {
         const input = context.df.getInput() as df.DurableHttpRequest;
         const output = yield context.df.callHttp(