Repository: Azure/azure-functions-durable-js
Pull request #133 (2019-12-18 00:08:59)

Refactor Task/TaskSet logic
This PR addresses the following:

1. Allow yielding a task more than once without scheduling it each time. (#85).
2. Correctly handle timers in `Task.any()` (#63)
3. Use TypeScript language features to simplify/clarify logic.

cgillum: Hey @ConnorMcMahon would you mind fixing the merge conflict in this PR? Once that's done, I'll approve and merge it.

Patch:
diff --git a/src/classes.ts b/src/classes.ts
index d218feb..89cd2b0 100644
--- a/src/classes.ts
+++ b/src/classes.ts
@@ -42,10 +42,12 @@ export { TaskScheduledEvent } from "./history/taskscheduledevent";
 export { TimerCreatedEvent } from "./history/timercreatedevent";
 export { TimerFiredEvent } from "./history/timerfiredevent";
 
-export { ITaskMethods } from "./itaskmethods";
-export { TaskSet } from "./taskset";
-export { Task } from "./task";
-export { TimerTask } from "./timertask";
+export { ITaskMethods } from "./tasks/itaskmethods";
+export { Task } from "./tasks/task";
+export { TaskFactory } from "./tasks/taskfactory";
+export { TaskFilter } from "./tasks/taskfilter";
+export { TaskSet } from "./tasks/taskset";
+export { TimerTask } from "./tasks/timertask";
 
 export { OrchestratorState } from "./orchestratorstate";
 export { IOrchestratorState } from "./iorchestratorstate";
diff --git a/src/orchestrator.ts b/src/orchestrator.ts
index 1317372..35c4196 100644
--- a/src/orchestrator.ts
+++ b/src/orchestrator.ts
@@ -5,10 +5,11 @@ import { CallActivityAction, CallActivityWithRetryAction, CallEntityAction, Call
     EventRaisedEvent, EventSentEvent, ExternalEventType, GuidManager, HistoryEvent, HistoryEventType,
     IAction, IOrchestrationFunctionContext, LockState, OrchestratorState, RequestMessage, ResponseMessage,
     RetryOptions, SubOrchestrationInstanceCompletedEvent, SubOrchestrationInstanceCreatedEvent,
-    SubOrchestrationInstanceFailedEvent, Task, TaskCompletedEvent, TaskFailedEvent,
+    SubOrchestrationInstanceFailedEvent, Task, TaskCompletedEvent, TaskFactory, TaskFailedEvent, TaskFilter,
     TaskScheduledEvent, TaskSet, TimerCreatedEvent, TimerFiredEvent, TimerTask,
     Utils, WaitForExternalEventAction,
 } from "./classes";
+import { FailedSingleTask } from "./tasks/taskinterfaces";
 import { TokenSource } from "./tokensource";
 
 /** @hidden */
@@ -87,7 +88,7 @@ export class Orchestrator {
 
             while (true) {
 
-                if (!(g.value instanceof Task || g.value instanceof TaskSet)) {
+                if (!(TaskFilter.isTask(g.value))) {
                     if (!g.done) {
                         // The orchestrator must have yielded a non-Task related type,
                         // so just return execution flow with what they yielded back.
@@ -110,14 +111,21 @@ export class Orchestrator {
                 }
 
                 partialResult = g.value as Task | TaskSet;
-                if (partialResult instanceof Task && partialResult.action) {
-                    actions.push([ partialResult.action ]);
-                } else if (partialResult instanceof TaskSet && partialResult.actions) {
-                    actions.push(partialResult.actions);
+                if (TaskFilter.isSingleTask(partialResult)) {
+                    if (!partialResult.wasYielded) {
+                        actions.push([ partialResult.action ]);
+                        partialResult.wasYielded = true;
+                    }
+                } else if (TaskFilter.isTaskSet(partialResult)) {
+                    const unyieldedTasks = partialResult.tasks.filter((task) => !task.wasYielded);
+                    actions.push(unyieldedTasks.map((task) => task.action));
+                    unyieldedTasks.forEach((task) => {
+                        task.wasYielded = true;
+                    });
                 }
 
                 // Return continue as new events as completed, as the execution itself is now completed.
-                if (partialResult instanceof Task && partialResult.action instanceof ContinueAsNewAction) {
+                if (TaskFilter.isSingleTask(partialResult) && partialResult.action instanceof ContinueAsNewAction) {
                     context.done(
                         null,
                         new OrchestratorState({
@@ -130,7 +138,7 @@ export class Orchestrator {
                     return;
                 }
 
-                if (!partialResult.isCompleted) {
+                if (!TaskFilter.isCompletedTask(partialResult)) {
                     context.done(
                         null,
                         new OrchestratorState({
@@ -143,7 +151,7 @@ export class Orchestrator {
                     return;
                 }
 
-                if (partialResult.isFaulted) {
+                if (TaskFilter.isFailedTask(partialResult)) {
                     if (!gen.throw) {
                         throw new Error("Cannot properly throw the execption returned by customer code");
                     }
@@ -203,21 +211,24 @@ export class Orchestrator {
         if (taskCompleted) {
             const result = this.parseHistoryEvent(taskCompleted);
 
-            return new Task(true, false, newAction, result, taskCompleted.Timestamp, taskCompleted.TaskScheduledId);
+            return TaskFactory.SuccessfulTask(
+                newAction,
+                result,
+                taskCompleted.Timestamp,
+                taskCompleted.TaskScheduledId,
+                state.indexOf(taskCompleted),
+            );
         } else if (taskFailed) {
-            return new Task(
-                true,
-                true,
+            return TaskFactory.FailedTask(
                 newAction,
                 taskFailed.Reason,
                 taskFailed.Timestamp,
                 taskFailed.TaskScheduledId,
+                state.indexOf(taskFailed),
                 new Error(taskFailed.Reason),
             );
         } else {
-            return new Task(
-                false,
-                false,
+            return TaskFactory.UncompletedTask(
                 newAction,
             );
         }
@@ -244,25 +255,20 @@ export class Orchestrator {
             if (taskCompleted) {
                 const result = this.parseHistoryEvent(taskCompleted);
 
-                return new Task(true, false, newAction, result, taskCompleted.Timestamp, taskCompleted.TaskScheduledId);
+                return TaskFactory.SuccessfulTask(newAction, result, taskCompleted.Timestamp, taskCompleted.TaskScheduledId, state.indexOf(taskCompleted));
             } else if (taskFailed && taskRetryTimer && attempt >= retryOptions.maxNumberOfAttempts) {
-                return new Task(
-                    true,
-                    true,
+                return TaskFactory.FailedTask(
                     newAction,
                     taskFailed.Reason,
                     taskFailed.Timestamp,
                     taskFailed.TaskScheduledId,
+                    state.indexOf(taskFailed),
                     new Error(taskFailed.Reason),
                 );
             }
         }
 
-        return new Task(
-            false,
-            false,
-            newAction,
-        );
+        return TaskFactory.UncompletedTask(newAction);
     }
 
     private callEntity(state: HistoryEvent[], entityId: EntityId, operationName: string, operationInput: unknown): Task {
@@ -280,14 +286,12 @@ export class Orchestrator {
         if (eventRaised) {
             const parsedResult = this.parseHistoryEvent(eventRaised) as ResponseMessage;
 
-            return new Task(true, false, newAction, JSON.parse(parsedResult.result), eventRaised.Timestamp, eventSent.EventId);
+            return TaskFactory.SuccessfulTask(newAction, JSON.parse(parsedResult.result), eventRaised.Timestamp, eventSent.EventId, state.indexOf(eventRaised));
         }
 
         // TODO: error handling
 
-        return new Task(
-            false,
-            false,
+        return TaskFactory.UncompletedTask(
             newAction,
         );
     }
@@ -307,28 +311,24 @@ export class Orchestrator {
         if (subOrchestratorCompleted) {
             const result = this.parseHistoryEvent(subOrchestratorCompleted);
 
-            return new Task(
-                true,
-                false,
+            return TaskFactory.SuccessfulTask(
                 newAction,
                 result,
                 subOrchestratorCompleted.Timestamp,
                 subOrchestratorCompleted.TaskScheduledId,
+                state.indexOf(subOrchestratorCompleted),
             );
         } else if (subOrchestratorFailed) {
-            return new Task(
-                true,
-                true,
+            return TaskFactory.FailedTask(
                 newAction,
                 subOrchestratorFailed.Reason,
                 subOrchestratorFailed.Timestamp,
                 subOrchestratorFailed.TaskScheduledId,
+                state.indexOf(subOrchestratorFailed),
                 new Error(subOrchestratorFailed.Reason),
             );
         } else {
-            return new Task(
-                false,
-                false,
+            return TaskFactory.UncompletedTask(
                 newAction,
             );
         }
@@ -366,30 +366,26 @@ export class Orchestrator {
             if (subOrchestratorCompleted) {
                 const result = this.parseHistoryEvent(subOrchestratorCompleted);
 
-                return new Task(
-                    true,
-                    false,
+                return TaskFactory.SuccessfulTask(
                     newAction,
                     result,
                     subOrchestratorCompleted.Timestamp,
                     subOrchestratorCompleted.TaskScheduledId,
+                    state.indexOf(subOrchestratorCompleted),
                 );
             } else if (subOrchestratorFailed && retryTimer && attempt >= retryOptions.maxNumberOfAttempts) {
-                return new Task(
-                    true,
-                    true,
+                return TaskFactory.FailedTask(
                     newAction,
                     subOrchestratorFailed.Reason,
                     subOrchestratorFailed.Timestamp,
                     subOrchestratorFailed.TaskScheduledId,
+                    state.indexOf(subOrchestratorFailed),
                     new Error(subOrchestratorFailed.Reason),
                 );
             }
         }
 
-        return new Task(
-            false,
-            false,
+        return TaskFactory.UncompletedTask(
             newAction,
         );
     }
@@ -417,21 +413,24 @@ export class Orchestrator {
         if (httpCompleted) {
             const result = this.parseHistoryEvent(httpCompleted);
 
-            return new Task(true, false, newAction, result, httpCompleted.Timestamp, httpCompleted.TaskScheduledId);
+            return TaskFactory.SuccessfulTask(
+                newAction,
+                result,
+                httpCompleted.Timestamp,
+                httpCompleted.TaskScheduledId,
+                state.indexOf(httpCompleted),
+            );
         } else if (httpFailed) {
-            return new Task(
-                true,
-                true,
+            return TaskFactory.FailedTask(
                 newAction,
                 httpFailed.Reason,
                 httpFailed.Timestamp,
                 httpFailed.TaskScheduledId,
+                state.indexOf(httpFailed),
                 new Error(httpFailed.Reason),
             );
         } else {
-            return new Task(
-                false,
-                false,
+            return TaskFactory.UncompletedTask(
                 newAction,
             );
         }
@@ -440,9 +439,7 @@ export class Orchestrator {
     private continueAsNew(state: HistoryEvent[], input: unknown): Task {
         const newAction = new ContinueAsNewAction(input);
 
-        return new Task(
-            false,
-            false,
+        return TaskFactory.UncompletedTask(
             newAction,
         );
     }
@@ -455,9 +452,9 @@ export class Orchestrator {
         this.setProcessed([ timerCreated, timerFired ]);
 
         if (timerFired) {
-            return new TimerTask(true, false, newAction, undefined, timerFired.Timestamp, timerFired.TimerId);
+            return TaskFactory.CompletedTimerTask(newAction, timerFired.Timestamp, timerFired.TimerId, state.indexOf(timerFired));
         } else {
-            return new TimerTask(false, false, newAction);
+            return TaskFactory.UncompletedTimerTask(newAction);
         }
     }
 
@@ -537,55 +534,50 @@ export class Orchestrator {
         if (eventRaised) {
             const result = this.parseHistoryEvent(eventRaised);
 
-            return new Task(true, false, newAction, result, eventRaised.Timestamp, eventRaised.EventId);
+            return TaskFactory.SuccessfulTask(
+                newAction,
+                result,
+                eventRaised.Timestamp,
+                eventRaised.EventId,
+                state.indexOf(eventRaised));
         } else {
-            return new Task(false, false, newAction);
+            return TaskFactory.UncompletedTask(newAction);
         }
     }
 
     private all(state: HistoryEvent[], tasks: Task[]): TaskSet {
-        const allActions = tasks.reduce((accumulator, currentValue) => {
-            return [...accumulator, currentValue.action];
-        }, []);
-
-        const faulted = tasks.filter((r) => r.isFaulted);
+        const faulted = tasks.filter(TaskFilter.isFailedTask);
         if (faulted.length > 0) {
-            return new TaskSet(true, true, allActions, undefined, faulted[0].exception);
+            return TaskFactory.FailedTaskSet(tasks, (faulted[0] as FailedSingleTask).exception);
         }
 
-        const isCompleted = tasks.every((r) => r.isCompleted);
+        const succesfulTasks = tasks.filter(TaskFilter.isSuccessfulSingleTask);
+        const isCompleted = succesfulTasks.length === tasks.length;
         if (isCompleted) {
-            const results = tasks.reduce((acc, t) => {
+            const results = succesfulTasks.reduce((acc, t) => {
                 return [...acc, t.result];
             }, []);
 
-            return new TaskSet(isCompleted, false, allActions, results);
+            return TaskFactory.SuccessfulTaskSet(tasks, results);
         } else {
-            return new TaskSet(isCompleted, false, allActions);
+            return TaskFactory.UncompletedTaskSet(tasks);
         }
     }
 
     private any(state: HistoryEvent[], tasks: Task[]): TaskSet {
-        const allActions = tasks.reduce((accumulator, currentValue) => {
-            return [...accumulator, currentValue.action];
-        }, []);
-
         const completedTasks = tasks
-            .filter((t) => t && t.isCompleted)
+            .filter(TaskFilter.isSuccessfulSingleTask)
             .sort((a, b) => {
-                // Because we have filtered by completed tasks, all of them should have timestamps
-                if (a.timestamp && b.timestamp) {
-                    if (a.timestamp > b.timestamp) { return 1; }
-                    if (a.timestamp < b.timestamp) { return -1; }
-                }
+                if (a.completedHistoryEventIndex > b.completedHistoryEventIndex) { return 1; }
+                if (a.completedHistoryEventIndex < b.completedHistoryEventIndex) { return -1; }
                 return 0;
             });
 
         const firstCompleted = completedTasks[0];
         if (firstCompleted) {
-            return new TaskSet(true, false, allActions, firstCompleted);
+            return TaskFactory.SuccessfulTaskSet(tasks, firstCompleted);
         } else {
-            return new TaskSet(false, false, allActions);
+            return TaskFactory.UncompletedTaskSet(tasks);
         }
     }
 
diff --git a/src/itaskmethods.ts b/src/tasks/itaskmethods.ts
similarity index 90%
rename from src/itaskmethods.ts
rename to src/tasks/itaskmethods.ts
index f10ae4c..bd5cdf4 100644
--- a/src/itaskmethods.ts
+++ b/src/tasks/itaskmethods.ts
@@ -1,4 +1,5 @@
-import { Task, TaskSet } from "./classes";
+import { Task } from "./task";
+import { TaskSet } from "./taskset";
 
 /**
  * Methods to handle collections of pending actions represented by [[Task]]
diff --git a/src/task.ts b/src/tasks/task.ts
similarity index 79%
rename from src/task.ts
rename to src/tasks/task.ts
index f23df20..6c7edfb 100644
--- a/src/task.ts
+++ b/src/tasks/task.ts
@@ -1,4 +1,5 @@
-import { IAction } from "./classes";
+import { IAction } from "../classes";
+import { SingleTask } from "./taskinterfaces";
 
 /**
  * Represents some pending action. Similar to a native JavaScript promise in
@@ -32,7 +33,13 @@ import { IAction } from "./classes";
  * return firstDone.result;
  * ```
  */
-export class Task {
+export class Task implements SingleTask {
+    /**
+     * Used to keep track of how many times the task has been yielded to avoid
+     * scheduling the internal action multiple times _Internal use only._
+     */
+    public wasYielded = false;
+
     /** @hidden */
     constructor(
         /**
@@ -65,6 +72,11 @@ export class Task {
          * the Task has not yet completed or has completed successfully,
          * `undefined`.
          */
-        public readonly exception?: unknown,
+        public readonly exception?: Error | undefined,
+
+        /**
+         * The index in the history state where the task was marked completed. _Internal use only._
+         */
+        public readonly completionIndex?: number,
     ) { }
 }
diff --git a/src/tasks/taskfactory.ts b/src/tasks/taskfactory.ts
new file mode 100644
index 0000000..b78fcc1
--- /dev/null
+++ b/src/tasks/taskfactory.ts
@@ -0,0 +1,74 @@
+import { CreateTimerAction, IAction } from "../classes";
+import { Task} from "./task";
+import { TaskSet } from "./taskset";
+import { TimerTask } from "./timertask";
+
+export class TaskFactory {
+    public static UncompletedTask(action: IAction): Task {
+        return new Task(false, false, action);
+    }
+
+    public static SuccessfulTask(action: IAction, result: unknown, timestamp: Date, id: number, completedHistoryEventIndex: number): Task {
+        return new Task(
+            true,
+            false,
+            action,
+            result,
+            timestamp,
+            id,
+            undefined,
+            completedHistoryEventIndex,
+        );
+    }
+
+    public static FailedTask(action: IAction, reason: string | undefined, timestamp: Date, id: number, completedHistoryEventIndex: number, exception: Error): Task {
+        return new Task(
+            true,
+            true,
+            action,
+            reason,
+            timestamp,
+            id,
+            exception,
+            completedHistoryEventIndex,
+        );
+    }
+
+    public static CompletedTimerTask(action: CreateTimerAction, timestamp: Date, id: number, completedHistoryEventIndex: number): TimerTask {
+        return new TimerTask(true, action, timestamp, id, completedHistoryEventIndex);
+    }
+
+    public static UncompletedTimerTask(action: CreateTimerAction): TimerTask {
+        return new TimerTask(false, action);
+    }
+
+    public static SuccessfulTaskSet(tasks: Task[], result: unknown): TaskSet {
+        return new TaskSet(
+            true,
+            false,
+            tasks,
+            result,
+            undefined,
+        );
+    }
+
+    public static FailedTaskSet(tasks: Task[], exception: unknown): TaskSet {
+        return new TaskSet(
+            true,
+            true,
+            tasks,
+            undefined,
+            exception,
+        );
+    }
+
+    public static UncompletedTaskSet(tasks: Task[]): TaskSet {
+        return new TaskSet(
+            false,
+            false,
+            tasks,
+            undefined,
+            undefined,
+        );
+    }
+}
diff --git a/src/tasks/taskfilter.ts b/src/tasks/taskfilter.ts
new file mode 100644
index 0000000..306cec3
--- /dev/null
+++ b/src/tasks/taskfilter.ts
@@ -0,0 +1,42 @@
+import { Task } from "./task";
+import { CompletedTask, FailedTask, SuccessfulSingleTask, SuccessfulTask, UncompletedTask } from "./taskinterfaces";
+import { TaskSet } from "./taskset";
+
+export class TaskFilter {
+    public static isTask(value: unknown): value is (Task | TaskSet) {
+        if (!value) {
+            return false;
+        }
+        const task = value as (Task | TaskSet);
+        return task.isCompleted !== undefined && task.isFaulted !== undefined;
+    }
+
+    public static isSingleTask(task: Task | TaskSet): task is Task {
+        return (task instanceof Task);
+    }
+
+    public static isTaskSet(task: Task | TaskSet): task is TaskSet {
+        return (task instanceof TaskSet);
+    }
+
+    public static isCompletedTask(task: Task | TaskSet): task is CompletedTask {
+        return task.isCompleted;
+    }
+
+    public static isUncompletedTask(task: Task | TaskSet): task is UncompletedTask  {
+        return task.isCompleted === false;
+    }
+
+    public static isSuccessfulTask(task: Task | TaskSet): task is SuccessfulTask  {
+        const successfulTask = task as SuccessfulTask;
+        return successfulTask.isCompleted === true && successfulTask.isFaulted === false && successfulTask.result !== undefined;
+    }
+
+    public static isSuccessfulSingleTask(task: Task): task is SuccessfulSingleTask  {
+        return TaskFilter.isSingleTask(task) && task.isCompleted === true && task.isFaulted === false && task.result !== undefined;
+    }
+
+    public static isFailedTask(task: Task | TaskSet): task is FailedTask {
+        return task.isCompleted === true && task.isFaulted === true;
+    }
+}
diff --git a/src/tasks/taskinterfaces.ts b/src/tasks/taskinterfaces.ts
new file mode 100644
index 0000000..2760013
--- /dev/null
+++ b/src/tasks/taskinterfaces.ts
@@ -0,0 +1,69 @@
+import { IAction } from "../classes";
+
+ // Base interfaces
+interface TaskBase {
+    readonly isCompleted: boolean;
+    readonly isFaulted: boolean;
+ }
+
+export interface SingleTask extends TaskBase {
+    readonly action: IAction;
+    wasYielded: boolean;
+}
+
+export interface UncompletedSingleTask extends TaskBase {
+    readonly isCompleted: false;
+    readonly isFaulted: false;
+}
+
+export interface CompletedSingleTask extends SingleTask {
+    completedHistoryEventIndex: number;
+    readonly timestamp: Date;
+    readonly id: number;
+    readonly isCompleted: true;
+    readonly result: unknown;
+}
+
+export interface SuccessfulSingleTask extends CompletedSingleTask {
+    readonly isFaulted: false;
+    readonly exception: undefined;
+}
+
+export interface FailedSingleTask extends CompletedSingleTask {
+    readonly isFaulted: true;
+    readonly exception: unknown;
+    readonly result: undefined;
+}
+
+interface TaskCollection extends TaskBase {
+    readonly tasks: SingleTask[];
+}
+
+export interface CompletedTaskSet extends TaskCollection {
+    readonly isCompleted: true;
+    readonly result: unknown;
+}
+
+export interface UncompletedTaskSet extends TaskCollection {
+    readonly isCompleted: false;
+    readonly isFaulted: false;
+}
+
+export interface FailedTaskSet extends CompletedTaskSet {
+    readonly isFaulted: true;
+    readonly exception: unknown;
+    readonly result: undefined;
+}
+
+export interface SuccessfulTaskSet extends CompletedTaskSet {
+    readonly isFaulted: false;
+    readonly exception: undefined;
+}
+
+export type CompletedTask = CompletedSingleTask | CompletedTaskSet;
+
+export type UncompletedTask = UncompletedTaskSet & UncompletedSingleTask;
+
+export type SuccessfulTask =  SuccessfulTaskSet & SuccessfulSingleTask;
+
+export type FailedTask = FailedTaskSet & FailedSingleTask;
diff --git a/src/tasks/taskset.ts b/src/tasks/taskset.ts
new file mode 100644
index 0000000..daed0a1
--- /dev/null
+++ b/src/tasks/taskset.ts
@@ -0,0 +1,12 @@
+import { SingleTask } from "./taskinterfaces";
+
+/** @hidden */
+export class TaskSet {
+    constructor(
+        public readonly isCompleted: boolean,
+        public readonly isFaulted: boolean,
+        public readonly tasks: SingleTask[],
+        public result?: unknown,
+        public exception?: unknown,
+    ) { }
+}
diff --git a/src/timertask.ts b/src/tasks/timertask.ts
similarity index 87%
rename from src/timertask.ts
rename to src/tasks/timertask.ts
index 99d6651..89016db 100644
--- a/src/timertask.ts
+++ b/src/tasks/timertask.ts
@@ -1,4 +1,5 @@
-import { Constants, CreateTimerAction, Task } from "./classes";
+import { CreateTimerAction } from "../classes";
+import { Task } from "./task";
 
 /**
  * Returned from [[DurableOrchestrationClient]].[[createTimer]] if the call is
@@ -43,21 +44,22 @@ export class TimerTask extends Task {
     /** @hidden */
     constructor(
         isCompleted: boolean,
-        isFaulted: boolean,
         /**
          * The scheduled action represented by the task. _Internal use only._
          */
         public readonly action: CreateTimerAction,
-        result?: unknown,
         timestamp?: Date,
         id?: number,
-    ) { super(isCompleted, isFaulted, action, result, timestamp, id); }
+        completionIndex?: number,
+    ) {
+        super(isCompleted, false, action, undefined, timestamp, id, undefined, completionIndex);
+    }
 
     /**
      * @returns Whether or not the timer has been canceled.
      */
     get isCanceled(): boolean {
-        return this.action && this.action.isCanceled;
+        return this.action.isCanceled;
     }
 
     /**
diff --git a/src/taskset.ts b/src/taskset.ts
deleted file mode 100644
index 09f30ab..0000000
--- a/src/taskset.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { IAction } from "./classes";
-
-/** @hidden */
-export class TaskSet {
-    constructor(
-        public isCompleted: boolean,
-        public isFaulted: boolean,
-        public actions: IAction[],
-        public result?: unknown,
-        public exception?: unknown,
-    ) { }
-}


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index 82e9f81..297f38a 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -411,6 +411,33 @@ describe("Orchestrator", () => {
             );
         });
 
+        it("is yielded twice, only scheduled once", async () => {
+            const orchestrator = TestOrchestrations.SayHelloWithActivityYieldTwice;
+            const name = "World";
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetSayHelloWithActivityReplayOne(
+                        "CallActivityYieldTwice",
+                        moment.utc().toDate(),
+                        name),
+                    name,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    output: `Hello, ${name}!`,
+                    actions:
+                    [
+                        [ new CallActivityAction("Hello", name) ],
+                    ],
+                }),
+            );
+        });
+
         it("handles a completed activity function by returning instead of yielding", async () => {
             const orchestrator = TestOrchestrations.SayHelloWithActivityDirectReturn;
             const name = "World";
@@ -1725,7 +1752,7 @@ describe("Orchestrator", () => {
                     isDone: true,
                     actions:
                     [
-                        [ new CallActivityAction("TaskA", false), new CallActivityAction("TaskB", true) ],
+                        [ new CallActivityAction("TaskA", true), new CallActivityAction("TaskB", true) ],
                     ],
                     output: "A",
                 }),
@@ -1752,12 +1779,167 @@ describe("Orchestrator", () => {
                     isDone: true,
                     actions:
                     [
-                        [ new CallActivityAction("TaskA", true), new CallActivityAction("TaskB", false) ],
+                        [ new CallActivityAction("TaskA", false), new CallActivityAction("TaskB", false) ],
                     ],
                     output: "B",
                 }),
             );
         });
+
+        it("Task.any proceeds if a scheduled parallel task completes in order", async () => {
+            const orchestrator = TestOrchestrations.AnyAOrB;
+            const completeInOrder = true;
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetAnyAOrB(
+                        moment.utc().toDate(),
+                        completeInOrder,
+                    ),
+                    completeInOrder,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    actions:
+                    [
+                        [ new CallActivityAction("TaskA", true), new CallActivityAction("TaskB", true) ],
+                    ],
+                    output: "A",
+                }),
+            );
+        });
+
+        it("Task yielded in both Task.any() and afterwards scheduled only once", async () => {
+            const orchestrator = TestOrchestrations.AnyAOrBYieldATwice;
+            const completeInOrder = true;
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetAnyAOrB(
+                        moment.utc().toDate(),
+                        completeInOrder,
+                    ),
+                    completeInOrder,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    actions:
+                    [
+                        [ new CallActivityAction("TaskA", true), new CallActivityAction("TaskB", true) ],
+                    ],
+                    output: "A",
+                }),
+            );
+        });
+
+        it("Timer in combination with Task.any() executes deterministically", async () => {
+            const orchestrator = TestOrchestrations.TimerActivityRace;
+            const currentTime = moment.utc();
+
+            // first iteration
+            let mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetTimerActivityRaceActivityWinsHistory(
+                        currentTime.toDate(),
+                        1,
+                    ),
+                    null,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [ new CreateTimerAction(currentTime.add(1, "s").toDate()), new CallActivityAction("TaskA") ],
+                    ],
+                    output: undefined,
+                }),
+            );
+
+            // second iteration
+            mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetTimerActivityRaceActivityWinsHistory(
+                        currentTime.toDate(),
+                        2,
+                    ),
+                    null,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [ new CreateTimerAction(currentTime.add(1, "s").toDate()), new CallActivityAction("TaskA") ],  [ new CallActivityAction("TaskB") ],
+                    ],
+                    output: undefined,
+                }),
+            );
+
+            // third iteration
+            mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetTimerActivityRaceActivityWinsHistory(
+                        currentTime.toDate(),
+                        3,
+                    ),
+                    null,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [ new CreateTimerAction(currentTime.add(1, "s").toDate()), new CallActivityAction("TaskA") ],  [ new CallActivityAction("TaskB") ],
+                    ],
+                    output: undefined,
+                }),
+            );
+
+            // final iteration
+            mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetTimerActivityRaceActivityWinsHistory(
+                        currentTime.toDate(),
+                        4,
+                    ),
+                    null,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    actions:
+                    [
+                        [ new CreateTimerAction(currentTime.add(1, "s").toDate()), new CallActivityAction("TaskA") ],  [ new CallActivityAction("TaskB") ],
+                    ],
+                    output: {},
+                }),
+            );
+        });
     });
 
     // rewind
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index 0b80a3e..d6a255f 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -6,13 +6,45 @@ export class TestOrchestrations {
         const completeInOrder = context.df.getInput();
 
         const tasks = [];
-        tasks.push(context.df.callActivity("TaskA", !completeInOrder));
+        tasks.push(context.df.callActivity("TaskA", completeInOrder));
         tasks.push(context.df.callActivity("TaskB", completeInOrder));
 
         const output = yield context.df.Task.any(tasks);
         return output.result;
     });
 
+    public static AnyAOrBYieldATwice: any = df.orchestrator(function*(context: any) {
+        const completeInOrder = context.df.getInput();
+
+        const tasks = [];
+        tasks.push(context.df.callActivity("TaskA", completeInOrder));
+        tasks.push(context.df.callActivity("TaskB", completeInOrder));
+        const output = yield context.df.Task.any(tasks);
+
+        yield tasks[0];
+        return output.result;
+    });
+
+    public static TimerActivityRace: any = df.orchestrator(function*(context: any) {
+        const tasks = [];
+        const now = new Date(context.df.currentUtcDateTime).getTime();
+        const fireAt = new Date(now + 1000);
+        tasks.push(context.df.createTimer(fireAt));
+        tasks.push(context.df.callActivity("TaskA"));
+
+        const output = yield context.df.Task.any(tasks);
+
+        if (output === tasks[1]) {
+            yield context.df.callActivity("TaskB");
+        } else if (output === tasks[0]) {
+            return "Timer finished";
+        } else {
+            throw new Error("context.df.Task.any() didn't return a task");
+        }
+
+        return output.result;
+    });
+
     public static CallActivityNoInput: any = df.orchestrator(function*(context: any) {
        const returnValue = yield context.df.callActivity("ReturnsFour");
        return returnValue;
@@ -105,6 +137,13 @@ export class TestOrchestrations {
         return output;
     });
 
+    public static SayHelloWithActivityYieldTwice: any = df.orchestrator(function*(context: any) {
+        const input = context.df.getInput();
+        const task = context.df.callActivity("Hello", input);
+        const output = yield task;
+        return yield task;
+    });
+
     public static SayHelloWithActivityDirectReturn: any = df.orchestrator(function*(context: any) {
         const input = context.df.getInput();
         return context.df.callActivity("Hello", input);
diff --git a/test/testobjects/testhistories.ts b/test/testobjects/testhistories.ts
index 9967ddf..b78da39 100644
--- a/test/testobjects/testhistories.ts
+++ b/test/testobjects/testhistories.ts
@@ -71,6 +71,115 @@ export class TestHistories {
         ];
     }
 
+    public static GetTimerActivityRaceActivityWinsHistory(firstTimestamp: Date, iteration: number): HistoryEvent[] {
+        const firstIteration = moment(firstTimestamp);
+        const fireAt = firstIteration.add(1, "s").toDate();
+        const secondIteration = firstIteration.add(500, "ms").toDate();
+        const thirdIteration = firstIteration.add(1100, "ms").toDate();
+        const finalIteration = firstIteration.add(2, "s").toDate();
+
+        const history = [];
+
+        if (iteration >= 1) {
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: false,
+            }));
+            history.push(new ExecutionStartedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: true,
+                name: "TimerActivityRace",
+            }));
+            history.push(new TimerCreatedEvent({
+                eventId: 0,
+                timestamp: firstTimestamp,
+                isPlayed: false,
+                fireAt,
+            }));
+            history.push(new TaskScheduledEvent({
+                eventId: 1,
+                timestamp: firstTimestamp,
+                isPlayed: iteration > 1,
+                name: "TaskA",
+            }));
+            history.push(new OrchestratorCompletedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: iteration > 1,
+            }));
+        }
+
+        if (iteration >= 2) {
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: secondIteration,
+                isPlayed: iteration > 2,
+            }));
+            history.push(new TaskCompletedEvent({
+                eventId: -1,
+                timestamp: secondIteration,
+                isPlayed: iteration > 2,
+                taskScheduledId: 1,
+                result: "{}",
+            }));
+            history.push(new TaskScheduledEvent({
+                eventId: 2,
+                timestamp: secondIteration,
+                isPlayed: iteration > 2,
+                name: "TaskB",
+            }));
+            history.push(new OrchestratorCompletedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: iteration > 2,
+            }));
+        }
+
+        if (iteration >= 3) {
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: thirdIteration,
+                isPlayed: iteration > 3,
+            }));
+            history.push(new TimerFiredEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                fireAt,
+                isPlayed: iteration > 3,
+                timerId: 0,
+            }));
+            history.push(new OrchestratorCompletedEvent({
+                eventId: -1,
+                timestamp: thirdIteration,
+                isPlayed: iteration > 3,
+            }));
+        }
+
+        if (iteration === 4) {
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: finalIteration,
+                isPlayed: false,
+            }));
+            history.push(new TaskCompletedEvent({
+                eventId: -1,
+                timestamp: thirdIteration,
+                isPlayed: false,
+                taskScheduledId: 2,
+                result: "{}",
+            }));
+            history.push(new OrchestratorCompletedEvent({
+                eventId: -1,
+                timestamp: finalIteration,
+                isPlayed: false,
+            }));
+        }
+
+        return history;
+    }
+
     public static GetCallEntitySet(firstTimestamp: Date, entityId: EntityId) {
         const firstMoment = moment(firstTimestamp);
         const orchestratorId = uuidv1();
diff --git a/test/unit/timertask-spec.ts b/test/unit/timertask-spec.ts
index 253e439..3cfaf28 100644
--- a/test/unit/timertask-spec.ts
+++ b/test/unit/timertask-spec.ts
@@ -1,36 +1,25 @@
 import { expect } from "chai";
 import "mocha";
-import { Constants, CreateTimerAction, TimerTask } from "../../src/classes";
+import { CreateTimerAction } from "../../src/classes";
+import { TaskFactory } from "../../src/tasks/taskfactory";
 
 describe("TimerTask", () => {
     it ("throws cannot cancel a completed task", async () => {
-        const task = new TimerTask(true, false, new CreateTimerAction(new Date(), false));
+        const task = TaskFactory.CompletedTimerTask(new CreateTimerAction(new Date(), false), new Date(), 0, 5);
         expect(() => {
             task.cancel();
         }).to.throw("Cannot cancel a completed task.");
     });
 
     it ("cancels an incomplete task", async () => {
-        const task = new TimerTask(
-            false,
-            false,
-            new CreateTimerAction(new Date()),
-            undefined,
-            undefined,
-            undefined);
+        const task = TaskFactory.UncompletedTimerTask(new CreateTimerAction(new Date()));
         task.cancel();
         expect(task.action.isCanceled).to.equal(true);
         expect(task.isCanceled).to.equal(true);
     });
 
     it ("is canceled when its action is canceled", async () => {
-        const task = new TimerTask(
-            false,
-            false,
-            new CreateTimerAction(new Date(), true),
-            undefined,
-            undefined,
-            undefined);
+        const task = TaskFactory.UncompletedTimerTask(new CreateTimerAction(new Date(), true));
         expect(task.isCanceled).to.equal(true);
     });
 });