Repository: Azure/azure-functions-durable-js
Pull request #396 (2022-10-31 20:04:39)

Promote dev to main for 2.0.3 release
As in title, routine PR


Patch:
diff --git a/.github/fabricbot.json b/.github/fabricbot.json
new file mode 100644
index 0000000..c998bf3
--- /dev/null
+++ b/.github/fabricbot.json
@@ -0,0 +1,629 @@
+{
+  "version": "1.0",
+  "tasks": [
+    {
+      "taskType": "trigger",
+      "capabilityId": "IssueResponder",
+      "subCapability": "IssuesOnlyResponder",
+      "version": "1.0",
+      "config": {
+        "taskName": "Add needs triage label to new issues",
+        "conditions": {
+          "operator": "and",
+          "operands": [
+            {
+              "name": "isAction",
+              "parameters": {
+                "action": "opened"
+              }
+            },
+            {
+              "operator": "not",
+              "operands": [
+                {
+                  "name": "isPartOfProject",
+                  "parameters": {}
+                }
+              ]
+            },
+            {
+              "operator": "not",
+              "operands": [
+                {
+                  "name": "isAssignedToSomeone",
+                  "parameters": {}
+                }
+              ]
+            }
+          ]
+        },
+        "actions": [
+          {
+            "name": "addLabel",
+            "parameters": {
+              "label": "Needs: Triage :mag:"
+            }
+          }
+        ],
+        "eventType": "issue",
+        "eventNames": [
+          "issues",
+          "project_card"
+        ]
+      },
+      "id": "Q2X6kH3i8"
+    },
+    {
+      "taskType": "trigger",
+      "capabilityId": "IssueResponder",
+      "subCapability": "IssueCommentResponder",
+      "version": "1.0",
+      "config": {
+        "taskName": "Replace needs author feedback label with needs attention label when the author comments on an issue",
+        "conditions": {
+          "operator": "and",
+          "operands": [
+            {
+              "name": "isAction",
+              "parameters": {
+                "action": "created"
+              }
+            },
+            {
+              "name": "isActivitySender",
+              "parameters": {
+                "user": {
+                  "type": "author"
+                }
+              }
+            },
+            {
+              "name": "hasLabel",
+              "parameters": {
+                "label": "Needs: Author Feedback"
+              }
+            }
+          ]
+        },
+        "actions": [
+          {
+            "name": "addLabel",
+            "parameters": {
+              "label": "Needs: Attention :wave:"
+            }
+          },
+          {
+            "name": "removeLabel",
+            "parameters": {
+              "label": "Needs: Author Feedback"
+            }
+          }
+        ],
+        "eventType": "issue",
+        "eventNames": [
+          "issue_comment"
+        ]
+      },
+      "id": "HPyPg7vgiE"
+    },
+    {
+      "taskType": "trigger",
+      "capabilityId": "IssueResponder",
+      "subCapability": "IssuesOnlyResponder",
+      "version": "1.0",
+      "config": {
+        "taskName": "Remove no recent activity label from issues",
+        "conditions": {
+          "operator": "and",
+          "operands": [
+            {
+              "operator": "not",
+              "operands": [
+                {
+                  "name": "isAction",
+                  "parameters": {
+                    "action": "closed"
+                  }
+                }
+              ]
+            },
+            {
+              "name": "hasLabel",
+              "parameters": {
+                "label": "no-recent-activity"
+              }
+            }
+          ]
+        },
+        "actions": [
+          {
+            "name": "removeLabel",
+            "parameters": {
+              "label": "no-recent-activity"
+            }
+          }
+        ],
+        "eventType": "issue",
+        "eventNames": [
+          "issues",
+          "project_card"
+        ]
+      },
+      "id": "NehFInt7Vy"
+    },
+    {
+      "taskType": "scheduled",
+      "capabilityId": "ScheduledSearch",
+      "subCapability": "ScheduledSearch",
+      "version": "1.1",
+      "config": {
+        "taskName": "Close stale issues",
+        "frequency": [
+          {
+            "weekDay": 0,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 1,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 2,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 3,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 4,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 5,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 6,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          }
+        ],
+        "searchTerms": [
+          {
+            "name": "isIssue",
+            "parameters": {}
+          },
+          {
+            "name": "isOpen",
+            "parameters": {}
+          },
+          {
+            "name": "hasLabel",
+            "parameters": {
+              "label": "Needs: Author Feedback"
+            }
+          },
+          {
+            "name": "hasLabel",
+            "parameters": {
+              "label": "no-recent-activity"
+            }
+          },
+          {
+            "name": "noActivitySince",
+            "parameters": {
+              "days": 3
+            }
+          }
+        ],
+        "actions": [
+          {
+            "name": "closeIssue",
+            "parameters": {}
+          }
+        ]
+      },
+      "id": "OFiBwmzJFC"
+    },
+    {
+      "taskType": "scheduled",
+      "capabilityId": "ScheduledSearch",
+      "subCapability": "ScheduledSearch",
+      "version": "1.1",
+      "config": {
+        "taskName": "Add no recent activity label to issues",
+        "frequency": [
+          {
+            "weekDay": 0,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 1,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 2,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 3,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 4,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 5,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 6,
+            "hours": [
+              1,
+              4,
+              7,
+              10,
+              13,
+              16,
+              19,
+              22
+            ],
+            "timezoneOffset": -8
+          }
+        ],
+        "searchTerms": [
+          {
+            "name": "isIssue",
+            "parameters": {}
+          },
+          {
+            "name": "isOpen",
+            "parameters": {}
+          },
+          {
+            "name": "hasLabel",
+            "parameters": {
+              "label": "Needs: Author Feedback"
+            }
+          },
+          {
+            "name": "noActivitySince",
+            "parameters": {
+              "days": 4
+            }
+          },
+          {
+            "name": "noLabel",
+            "parameters": {
+              "label": "no-recent-activity"
+            }
+          }
+        ],
+        "actions": [
+          {
+            "name": "addLabel",
+            "parameters": {
+              "label": "no-recent-activity"
+            }
+          },
+          {
+            "name": "addReply",
+            "parameters": {
+              "comment": "This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**."
+            }
+          }
+        ]
+      },
+      "id": "ksFO8A5VjG"
+    },
+    {
+      "taskType": "scheduled",
+      "capabilityId": "ScheduledSearch",
+      "subCapability": "ScheduledSearch",
+      "version": "1.1",
+      "config": {
+        "taskName": "Close duplicate issues",
+        "frequency": [
+          {
+            "weekDay": 0,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 1,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 2,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 3,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 4,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 5,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          },
+          {
+            "weekDay": 6,
+            "hours": [
+              2,
+              5,
+              8,
+              11,
+              14,
+              17,
+              20,
+              23
+            ],
+            "timezoneOffset": -8
+          }
+        ],
+        "searchTerms": [
+          {
+            "name": "isIssue",
+            "parameters": {}
+          },
+          {
+            "name": "isOpen",
+            "parameters": {}
+          },
+          {
+            "name": "hasLabel",
+            "parameters": {
+              "label": "duplicate"
+            }
+          },
+          {
+            "name": "noActivitySince",
+            "parameters": {
+              "days": 1
+            }
+          }
+        ],
+        "actions": [
+          {
+            "name": "addReply",
+            "parameters": {
+              "comment": "This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes."
+            }
+          },
+          {
+            "name": "closeIssue",
+            "parameters": {}
+          }
+        ]
+      },
+      "id": "TBIOIQH7sE"
+    },
+    {
+      "taskType": "trigger",
+      "capabilityId": "IssueResponder",
+      "subCapability": "IssueCommentResponder",
+      "version": "1.0",
+      "config": {
+        "eventType": "issue",
+        "eventNames": [
+          "issue_comment"
+        ],
+        "conditions": {
+          "operator": "and",
+          "operands": [
+            {
+              "name": "hasLabel",
+              "parameters": {
+                "label": "no-recent-activity"
+              }
+            }
+          ]
+        },
+        "taskName": "Remove no recent activity label when an issue is commented on",
+        "actions": [
+          {
+            "name": "removeLabel",
+            "parameters": {
+              "label": "no-recent-activity"
+            }
+          }
+        ]
+      },
+      "id": "wLypfSoaZ"
+    }
+  ],
+  "userGroups": []
+}
diff --git a/package-lock.json b/package-lock.json
index 374a8e4..8e1c180 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,7 +45,7 @@
                 "sinon": "~7.1.1",
                 "ts-node": "^10.0.0",
                 "typedoc": "^0.22.11",
-                "typescript": "^3.8.3"
+                "typescript": "~4.5.0"
             },
             "engines": {
                 "node": ">=6.5.0"
@@ -2271,9 +2271,9 @@
             "dev": true
         },
         "node_modules/moment": {
-            "version": "2.29.2",
-            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
-            "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
+            "version": "2.29.4",
+            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+            "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
             "engines": {
                 "node": "*"
             }
@@ -3096,9 +3096,9 @@
             }
         },
         "node_modules/typescript": {
-            "version": "3.8.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
-            "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
+            "version": "4.5.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
+            "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
             "dev": true,
             "bin": {
                 "tsc": "bin/tsc",
@@ -4955,9 +4955,9 @@
             }
         },
         "moment": {
-            "version": "2.29.2",
-            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
-            "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
+            "version": "2.29.4",
+            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+            "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
         },
         "ms": {
             "version": "2.0.0",
@@ -5536,9 +5536,9 @@
             }
         },
         "typescript": {
-            "version": "3.8.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
-            "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
+            "version": "4.5.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
+            "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
             "dev": true
         },
         "uri-js": {
diff --git a/package.json b/package.json
index 5648519..81eadc0 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,7 @@
         "sinon": "~7.1.1",
         "ts-node": "^10.0.0",
         "typedoc": "^0.22.11",
-        "typescript": "^3.8.3"
+        "typescript": "~4.5.0"
     },
     "engines": {
         "node": ">=6.5.0"
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 7b33ede..ca2b2e8 100644
--- a/src/taskorchestrationexecutor.ts
+++ b/src/taskorchestrationexecutor.ts
@@ -113,7 +113,7 @@ export class TaskOrchestrationExecutor {
         }
 
         // Construct current orchestration state
-        const actions: IAction[][] = this.actions.length == 0 ? [] : [this.actions];
+        const actions: IAction[][] = this.actions.length == 0 ? [[]] : [this.actions];
         const orchestratorState = new OrchestratorState({
             isDone: this.hasCompletedSuccessfully(),
             actions: actions,
@@ -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(
diff --git a/test/testobjects/testentities.ts b/test/testobjects/testentities.ts
index 4e6aa0f..b8fb8e0 100644
--- a/test/testobjects/testentities.ts
+++ b/test/testobjects/testentities.ts
@@ -44,7 +44,7 @@ export class TestEntities {
     });
 
     public static AsyncStringStore = df.entity<string>(async (context) => {
-        await new Promise((resolve) => setTimeout(() => resolve(), 0)); // force onto the event loop and result in a no-op delay
+        await new Promise<void>((resolve) => setTimeout(() => resolve(), 0)); // force onto the event loop and result in a no-op delay
         switch (context.df.operationName) {
             case "set":
                 context.df.setState(context.df.getInput() || "");