Repository: Azure/azure-functions-durable-js
Pull request #88 (2019-06-05 23:28:47)

durable-functions 1.2.2 payload
Bugfixes:

- #29 Support showInput parameter on getStatus
- #30 Support purgeInstanceHistory
- #45 currentUtcDateTime in DurableOrchestrationClass has the wrong type
- #50 Orchestration function that returns false is converted to null
- #61 Orchestrator output exception when orchestrator returns undefined
- #66 context.df.Task.all doesn't throw error

Dependency Updates
- axios: 0.18.0 ➡️ 0.19.0


Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..7520e9e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,58 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is. Please make an effort to fill in all the sections below; the information will help us investigate your issue.
+
+**Investigative information**
+
+- Durable Functions extension version:
+- Function App version (1.0 or 2.0):
+- Language (JavaScript/TypeScript) and version:
+- Node.js version:
+- durable-functions npm module version:
+
+***If deployed to Azure App Service***
+
+- Timeframe issue observed:
+- Function App name:
+- Function name(s):
+- Region:
+- Orchestration instance ID(s):
+
+> If you don't want to share your Function App name or Functions names on GitHub, please be sure to provide your Invocation ID, Timestamp, and Region - we can use this to look up your Function App/Function. Provide an invocation id per Function. See the [Functions Host wiki](https://github.com/Azure/azure-webjobs-sdk-script/wiki/Sharing-Your-Function-App-name-privately) for more details.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+> While not required, providing your orchestrator's source code in anonymized form is often very helpful when investigating unexpected orchestrator behavior.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Actual behavior**
+A clear and concise description of what actually happened.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Known workarounds**
+Provide a description of any known workarounds you used.
+
+**Additional context**
+
+- Development environment (ex. Visual Studio)
+- Links to source
+- Additional bindings used
+- Function invocation IDs
diff --git a/package-lock.json b/package-lock.json
index a81ae66..012b295 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -147,12 +147,12 @@
       "dev": true
     },
     "axios": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
-      "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
+      "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
       "requires": {
-        "follow-redirects": "^1.3.0",
-        "is-buffer": "^1.1.5"
+        "follow-redirects": "1.5.10",
+        "is-buffer": "^2.0.2"
       }
     },
     "babel-code-frame": {
@@ -318,9 +318,9 @@
       "dev": true
     },
     "follow-redirects": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz",
-      "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==",
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+      "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
       "requires": {
         "debug": "=3.1.0"
       },
@@ -406,9 +406,9 @@
       "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
     },
     "is-buffer": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+      "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
     },
     "isarray": {
       "version": "0.0.1",
diff --git a/package.json b/package.json
index 942d069..45d1792 100644
--- a/package.json
+++ b/package.json
@@ -27,15 +27,15 @@
   "dependencies": {
     "@azure/functions": "^1.0.2-beta2",
     "@types/lodash": "^4.14.119",
-    "@types/validator": "^9.4.3",
     "@types/uuid": "~3.4.4",
-    "axios": "~0.18.0",
+    "@types/validator": "^9.4.3",
+    "axios": "^0.19.0",
     "commander": "~2.9.0",
     "debug": "~2.6.9",
     "lodash": "^4.17.11",
     "rimraf": "~2.5.4",
-    "validator": "~10.8.0",
-    "uuid": "~3.3.2"
+    "uuid": "~3.3.2",
+    "validator": "~10.8.0"
   },
   "devDependencies": {
     "@types/chai": "~4.1.6",
diff --git a/src/classes.ts b/src/classes.ts
index 1f9bae1..4a109b7 100644
--- a/src/classes.ts
+++ b/src/classes.ts
@@ -54,5 +54,6 @@ export { IHttpResponse } from "./ihttpresponse";
 
 export { DurableOrchestrationStatus } from "./durableorchestrationstatus";
 export { OrchestrationRuntimeStatus } from "./orchestrationruntimestatus";
+export { PurgeHistoryResult } from "./purgehistoryresult";
 
 export { GuidManager } from "./guidmanager";
diff --git a/src/durableorchestrationclient.ts b/src/durableorchestrationclient.ts
index 9db9009..78e0ba0 100644
--- a/src/durableorchestrationclient.ts
+++ b/src/durableorchestrationclient.ts
@@ -5,9 +5,9 @@ import process = require("process");
 import url = require("url");
 import { isURL } from "validator";
 import { Constants, DurableOrchestrationStatus, HttpCreationPayload, HttpManagementPayload, IFunctionContext,
-    IHttpRequest, IHttpResponse, OrchestrationClientInputData, OrchestrationRuntimeStatus, Utils,
+    IHttpRequest, IHttpResponse, OrchestrationClientInputData, OrchestrationRuntimeStatus,
+    PurgeHistoryResult, Utils,
 } from "./classes";
-
 /**
  * Returns an OrchestrationClient instance.
  * @param context The context object of the Azure function whose body
@@ -100,6 +100,7 @@ export class DurableOrchestrationClient {
     private readonly runtimeStatusQueryKey = "runtimeStatus";
     private readonly showHistoryQueryKey = "showHistory";
     private readonly showHistoryOutputQueryKey = "showHistoryOutput";
+    private readonly showInputQueryKey = "showInput";
 
     private urlValidationOptions: ValidatorJS.IsURLOptions = {
         protocols: ["http", "https"],
@@ -175,6 +176,7 @@ export class DurableOrchestrationClient {
         instanceId: string,
         showHistory?: boolean,
         showHistoryOutput?: boolean,
+        showInput?: boolean,
         ): Promise<DurableOrchestrationStatus> {
         const template = this.clientData.managementUrls.statusQueryGetUri;
         const idPlaceholder = this.clientData.managementUrls.id;
@@ -186,6 +188,9 @@ export class DurableOrchestrationClient {
         if (showHistoryOutput) {
             webhookUrl += `&${this.showHistoryOutputQueryKey}=${showHistoryOutput}`;
         }
+        if (showInput === false) {
+            webhookUrl += `&${this.showInputQueryKey}=${showInput}`;
+        }
 
         try {
             const response = await this.axiosInstance.get(webhookUrl);
@@ -270,6 +275,86 @@ export class DurableOrchestrationClient {
         }
     }
 
+    /**
+     * Purge the history for a concerete instance.
+     * @param instanceId The ID of the orchestration instance to purge.
+     */
+    public async purgeInstanceHistory(instanceId: string): Promise<PurgeHistoryResult> {
+        const template = this.clientData.managementUrls.purgeHistoryDeleteUri;
+        const idPlaceholder = this.clientData.managementUrls.id;
+
+        const webhookUrl = template.replace(idPlaceholder, instanceId);
+
+        try {
+            const response = await this.axiosInstance.delete(webhookUrl);
+            switch (response.status) {
+                case 200: // instance found
+                    return response.data as PurgeHistoryResult;
+                case 404: // instance not found
+                    return new PurgeHistoryResult(0);
+                default:
+                    return Promise.reject(new Error(`Webhook returned unrecognized status code ${response.status}`));
+            }
+        } catch (error) {   // error object is axios-specific, not a JavaScript Error; extract relevant bit
+            throw error.message;
+        }
+    }
+
+    /**
+     * Purge the orchestration history for isntances that match the conditions.
+     * @param createdTimeFrom Start creation time for querying instances for
+     *  purging.
+     * @param createdTimeTo End creation time fo rquerying instanes for
+     *  purging.
+     * @param runtimeStatus List of runtime statuses for querying instances for
+     *  purging. Only Completed, Terminated or Failed will be processed.
+     */
+    public async purgeInstanceHistoryBy(
+        createdTimeFrom: Date,
+        createdTimeTo?: Date,
+        runtimeStatus?: OrchestrationRuntimeStatus[],
+        ): Promise<PurgeHistoryResult> {
+        const idPlaceholder = this.clientData.managementUrls.id;
+        let requestUrl = this.clientData.managementUrls.statusQueryGetUri
+        .replace(idPlaceholder, "");
+
+        if (!(createdTimeFrom instanceof Date)) {
+            throw new Error("createdTimeFrom must be a valid Date");
+        }
+
+        if (createdTimeFrom) {
+            requestUrl += `&${this.createdTimeFromQueryKey}=${createdTimeFrom.toISOString()}`;
+        }
+
+        if (createdTimeTo) {
+            requestUrl += `&${this.createdTimeToQueryKey}=${createdTimeTo.toISOString()}`;
+        }
+
+        if (runtimeStatus && runtimeStatus.length > 0) {
+            const statusesString = runtimeStatus
+                .map((value) => value.toString())
+                .reduce((acc, curr, i, arr) => {
+                    return acc + (i > 0 ? "," : "") + curr;
+            });
+
+            requestUrl += `&${this.runtimeStatusQueryKey}=${statusesString}`;
+        }
+
+        try {
+            const response = await this.axiosInstance.delete(requestUrl);
+            switch (response.status) {
+                case 200: // instance found
+                    return response.data as PurgeHistoryResult;
+                case 404: // instance not found
+                    return new PurgeHistoryResult(0);
+                default:
+                    return Promise.reject(new Error(`Webhook returned unrecognized status code ${response.status}`));
+            }
+        } catch (error) {   // error object is axios-specific, not a JavaScript Error; extract relevant bit
+            throw error.message;
+        }
+    }
+
     /**
      * Sends an event notification message to a waiting orchestration instance.
      * @param instanceId The ID of the orchestration instance that will handle
diff --git a/src/httpmanagementpayload.ts b/src/httpmanagementpayload.ts
index 0014ab4..d064caf 100644
--- a/src/httpmanagementpayload.ts
+++ b/src/httpmanagementpayload.ts
@@ -17,5 +17,7 @@ export class HttpManagementPayload {
         public readonly terminatePostUri: string,
         /** The HTTP POST instance rewind endpoint. */
         public readonly rewindPostUri: string,
+        /** The HTTP DELETE purge endpoint. */
+        public readonly purgeHistoryDeleteUri: string,
     ) { }
 }
diff --git a/src/iorchestratorstate.ts b/src/iorchestratorstate.ts
index 51b7dea..a2e084f 100644
--- a/src/iorchestratorstate.ts
+++ b/src/iorchestratorstate.ts
@@ -4,7 +4,7 @@ import { IAction } from "./classes";
 export interface IOrchestratorState {
     isDone: boolean;
     actions: IAction[][];
-    output?: unknown;
+    output: unknown;
     error?: string;
     customStatus?: unknown;
 }
diff --git a/src/orchestrator.ts b/src/orchestrator.ts
index 0fcbb80..b4ce85a 100644
--- a/src/orchestrator.ts
+++ b/src/orchestrator.ts
@@ -40,7 +40,7 @@ export class Orchestrator {
         let decisionStartedEvent: HistoryEvent = state.find((e) =>
             (e.EventType === HistoryEventType.OrchestratorStarted));
         this.currentUtcDateTime = decisionStartedEvent
-            ? decisionStartedEvent.Timestamp
+            ? new Date(decisionStartedEvent.Timestamp)
             : undefined;
 
         // Reset newGuidCounter
@@ -89,15 +89,20 @@ export class Orchestrator {
                         null,
                         new OrchestratorState({
                             isDone: false,
+                            output: undefined,
                             actions,
                             customStatus: this.customStatus,
                         }),
                     );
                     return;
-                } else if (partialResult instanceof Task && partialResult.isFaulted) {
+                }
+
+                if ((partialResult instanceof Task || partialResult instanceof TaskSet) && partialResult.isFaulted) {
                     g = gen.throw(partialResult.exception);
                     continue;
-                } else if (g.done) {
+                }
+
+                if (g.done) {
                     log("Iterator is done");
                     context.done(null,
                         new OrchestratorState({
@@ -114,7 +119,7 @@ export class Orchestrator {
                     e.EventType === HistoryEventType.OrchestratorStarted &&
                     e.Timestamp > decisionStartedEvent.Timestamp);
                 context.df.currentUtcDateTime = this.currentUtcDateTime = decisionStartedEvent
-                    ? decisionStartedEvent.Timestamp
+                    ? new Date(decisionStartedEvent.Timestamp)
                     : undefined;
 
                 g = gen.next(partialResult ? partialResult.result : undefined);
@@ -125,6 +130,7 @@ export class Orchestrator {
                 error,
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions,
                     error: error.stack,
                     customStatus: this.customStatus,
@@ -359,15 +365,20 @@ export class Orchestrator {
             return [...accumulator, currentValue.action];
         }, []);
 
+        const faulted = tasks.filter((r) => r.isFaulted);
+        if (faulted.length > 0) {
+            return new TaskSet(true, true, allActions, undefined, faulted[0].exception);
+        }
+
         const isCompleted = tasks.every((r) => r.isCompleted);
         if (isCompleted) {
             const results = tasks.reduce((acc, t) => {
                 return [...acc, t.result];
             }, []);
 
-            return new TaskSet(isCompleted, allActions, results);
+            return new TaskSet(isCompleted, false, allActions, results);
         } else {
-            return new TaskSet(isCompleted, allActions);
+            return new TaskSet(isCompleted, false, allActions);
         }
     }
 
@@ -386,9 +397,9 @@ export class Orchestrator {
 
         const firstCompleted = completedTasks[0];
         if (firstCompleted) {
-            return new TaskSet(true, allActions, firstCompleted);
+            return new TaskSet(true, false, allActions, firstCompleted);
         } else {
-            return new TaskSet(false, allActions);
+            return new TaskSet(false, false, allActions);
         }
     }
 
@@ -544,7 +555,7 @@ export class Orchestrator {
     }
 
     private shouldFinish(result: unknown): boolean {
-        return Object.prototype.hasOwnProperty.call(result, "isCompleted") && !(result as Task).isCompleted
+        return result && Object.prototype.hasOwnProperty.call(result, "isCompleted") && !(result as Task).isCompleted
             || result instanceof Task && result.action instanceof ContinueAsNewAction;
     }
 }
diff --git a/src/orchestratorstate.ts b/src/orchestratorstate.ts
index dd210a8..a7e599d 100644
--- a/src/orchestratorstate.ts
+++ b/src/orchestratorstate.ts
@@ -4,17 +4,14 @@ import { IAction, IOrchestratorState } from "./classes";
 export class OrchestratorState implements IOrchestratorState {
     public readonly isDone: boolean;
     public readonly actions: IAction[][];
-    public readonly output?: unknown;
+    public readonly output: unknown;
     public readonly error?: string;
     public readonly customStatus?: unknown;
 
     constructor(options: IOrchestratorState) {
         this.isDone = options.isDone;
         this.actions = options.actions;
-
-        if (options.output) {
-            this.output = options.output;
-        }
+        this.output = options.output;
 
         if (options.error) {
             this.error = options.error;
diff --git a/src/purgehistoryresult.ts b/src/purgehistoryresult.ts
new file mode 100644
index 0000000..6c5dc3b
--- /dev/null
+++ b/src/purgehistoryresult.ts
@@ -0,0 +1,11 @@
+/**
+ * Class to hold statistics about this execution of purge history.
+ */
+export class PurgeHistoryResult {
+    constructor(
+        /**
+         * The number of deleted instances.
+         */
+        public readonly instancesDeleted: number,
+    ) { }
+}
diff --git a/src/taskset.ts b/src/taskset.ts
index 207a57f..09f30ab 100644
--- a/src/taskset.ts
+++ b/src/taskset.ts
@@ -4,7 +4,9 @@ import { IAction } from "./classes";
 export class TaskSet {
     constructor(
         public isCompleted: boolean,
+        public isFaulted: boolean,
         public actions: IAction[],
         public result?: unknown,
+        public exception?: unknown,
     ) { }
 }
diff --git a/src/utils.ts b/src/utils.ts
index 69976e1..e5d6aac 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,5 +1,3 @@
-import { Constants } from "./classes";
-
 /** @hidden */
 export class Utils {
     public static getInstancesOf<T>(collection: { [index: string]: unknown }, typeInstance: T): T[] {


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index ea17516..cfb78be 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -12,6 +12,8 @@ import { TestHistories } from "../testobjects/testhistories";
 import { TestOrchestrations } from "../testobjects/TestOrchestrations";
 
 describe("Orchestrator", () => {
+    const falsyValues = [ false, 0, "", null, undefined, NaN ];  
+  
     it("handles a simple orchestration function (no activity functions)", async () => {
         const orchestrator = TestOrchestrations.SayHelloInline;
         const name = "World";
@@ -35,6 +37,37 @@ describe("Orchestrator", () => {
         );
     });
 
+    falsyValues.forEach((falsyValue) => {
+        it(`handles an orchestration function that returns ${falsyValue === "" ? "empty string" : falsyValue}`, async () => {
+            const orchestrator = TestOrchestrations.PassThrough;
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetOrchestratorStart(
+                        "PassThrough",
+                        moment.utc().toDate(),
+                        falsyValue
+                    ),
+                    falsyValue,
+                ),
+            });
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    actions: [],
+                    output: falsyValue,
+                })
+            );
+            if (isNaN(falsyValue as number)) {
+                expect(isNaN(mockContext.doneValue.output as number)).to.equal(true);
+            } else {
+                expect(mockContext.doneValue.output).to.equal(falsyValue);
+            }
+            expect(mockContext.err).to.equal(null);
+        });
+    });
+
     describe("Properties", () => {
         it("assigns instanceId", async () => {
             const orchestrator = TestOrchestrations.SayHelloInline;
@@ -188,6 +221,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallActivityAction("ThrowsErrorActivity")],
@@ -217,6 +251,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallActivityAction("Hello", name) ],
@@ -240,6 +275,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallActivityAction("ReturnsFour") ],
@@ -249,9 +285,8 @@ describe("Orchestrator", () => {
         });
 
         describe("Falsy Input", () => {
-            const falsyValues = [ false, 0, "", null, undefined, NaN ];
             falsyValues.forEach((falsyValue) => {
-                it(`schedules an activity function with falsy input ${falsyValue}`, async () => {
+                it(`schedules an activity function with input ${falsyValue}`, async () => {
                     const orchestrator = TestOrchestrations.SayHelloWithActivity;
                     const mockContext = new MockContext({
                         context: new DurableOrchestrationBindingInfo(
@@ -268,6 +303,7 @@ describe("Orchestrator", () => {
                     expect(mockContext.doneValue).to.be.deep.equal(
                         new OrchestratorState({
                             isDone: false,
+                            output: undefined,
                             actions:
                             [
                                 [ new CallActivityAction("Hello", falsyValue) ],
@@ -381,6 +417,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.an("object").that.deep.include(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions: [],
                 }),
             );
@@ -408,6 +445,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [
@@ -440,6 +478,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [
@@ -470,6 +509,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions: [
                         [
                             new CallActivityWithRetryAction(
@@ -572,6 +612,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallSubOrchestratorAction("SayHelloWithActivity", childId, name) ],
@@ -600,6 +641,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallSubOrchestratorAction("SayHelloWithActivity", undefined, name) ],
@@ -725,6 +767,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [
@@ -763,6 +806,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [
@@ -800,6 +844,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [
@@ -914,6 +959,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new ContinueAsNewAction({ value: 6 }) ],
@@ -944,6 +990,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CreateTimerAction(fireAt) ],
@@ -1037,6 +1084,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.deep.eq(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallActivityAction("Hello", "Tokyo") ],
@@ -1066,6 +1114,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new WaitForExternalEventAction("start") ],
@@ -1093,6 +1142,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new WaitForExternalEventAction("start") ],
@@ -1121,6 +1171,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new WaitForExternalEventAction("start") ],
@@ -1149,6 +1200,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallActivityAction("GetFileList", "C:\\Dev")],
@@ -1176,6 +1228,7 @@ describe("Orchestrator", () => {
             expect(mockContext.doneValue).to.be.deep.equal(
                 new OrchestratorState({
                     isDone: false,
+                    output: undefined,
                     actions:
                     [
                         [ new CallActivityAction("GetFileList", "C:\\Dev")],
@@ -1213,6 +1266,38 @@ describe("Orchestrator", () => {
             );
         });
 
+        it("Task.all throws if a parallel task has faulted", async () => {
+            const orchestrator = TestOrchestrations.FanOutFanInDiskUsage;
+            const filePaths = ["file1.txt", "file2.png", "file3.csx"];
+            const mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo (
+                    TestHistories.GetFanOutFanInDiskUsageFaulted(
+                        moment.utc().toDate(),
+                        filePaths,
+                    ),
+                    "C:\\Dev",
+                ),
+            });
+
+            const expectedErr = "Activity function 'GetFileSize' failed: Could not find file.";
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.include(
+                new OrchestratorState({
+                    isDone: false,
+                    output: undefined,
+                    actions:
+                    [
+                        [ new CallActivityAction("GetFileList", "C:\\Dev")],
+                        filePaths.map((file) => new CallActivityAction("GetFileSize", file)),
+                    ],
+                }),
+            );
+            expect(mockContext.doneValue.error).to.include(expectedErr);
+            expect(mockContext.err.toString()).to.include(expectedErr);
+        });
+
         it("Task.any proceeds if a scheduled parallel task completes in order", async () => {
             const orchestrator = TestOrchestrations.AnyAOrB;
             const completeInOrder = true;
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index 3a402e0..9dcc306 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -56,6 +56,11 @@ export class TestOrchestrations {
         return outputs;
     });
 
+    public static PassThrough: any = df.orchestrator(function*(context: any) {
+        const input = context.df.getInput();
+        return input;
+    });
+
     public static SayHelloInline: any = df.orchestrator(function*(context: any) {
         const input = context.df.getInput();
         return `Hello, ${input}!`;
diff --git a/test/testobjects/testconstants.ts b/test/testobjects/testconstants.ts
index 519e404..088a828 100644
--- a/test/testobjects/testconstants.ts
+++ b/test/testobjects/testconstants.ts
@@ -19,6 +19,7 @@ export class TestConstants {
     public static readonly sendEventPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/raiseEvent/{eventName}?${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length
     public static readonly terminatePostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/terminate?reason=${TestConstants.reasonPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length
     public static readonly rewindPostUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}/rewind?reason=${TestConstants.reasonPlaceholder}&${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length
+    public static readonly purgeDeleteUriTemplate: string = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}instances/${TestConstants.idPlaceholder}?${TestConstants.uriSuffix}`; // tslint:disable-line max-line-length
 
     public static readonly createPostUriTemplate = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}orchestrators/${TestConstants.functionPlaceholder}${TestConstants.idPlaceholder}?${TestConstants.testCode}`; // tslint:disable-line max-line-length
     public static readonly waitOnPostUriTemplate = `${TestConstants.hostPlaceholder}${TestConstants.webhookPath}orchestrators/${TestConstants.functionPlaceholder}${TestConstants.idPlaceholder}?timeout=${TestConstants.timeoutPlaceholder}&pollingInterval=${TestConstants.intervalPlaceholder}&${TestConstants.testCode}`; // tslint:disable-line max-line-length
diff --git a/test/testobjects/testhistories.ts b/test/testobjects/testhistories.ts
index f61070d..a111c4c 100644
--- a/test/testobjects/testhistories.ts
+++ b/test/testobjects/testhistories.ts
@@ -177,10 +177,24 @@ export class TestHistories {
                     taskScheduledId: 2,
                 },
             ),
+            new OrchestratorCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                },
+            ),
+            new OrchestratorStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(4, "s").toDate(),
+                    isPlayed: false,
+                },
+            ),
             new TaskCompletedEvent(
                 {
                     eventId: -1,
-                    timestamp: firstMoment.add(3, "s").toDate(),
+                    timestamp: firstMoment.add(4, "s").toDate(),
                     isPlayed: false,
                     result: JSON.stringify(3),
                     taskScheduledId: 3,
@@ -189,6 +203,141 @@ export class TestHistories {
         ]);
     }
 
+    public static GetFanOutFanInDiskUsageFaulted(firstTimestamp: Date, files: string[]): HistoryEvent[] {
+        const firstMoment = moment(firstTimestamp);
+
+        return [
+            new OrchestratorStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                },
+            ),
+            new ExecutionStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: true,
+                    name: "FanOutFanInDiskUsage",
+                    input: undefined,
+                },
+            ),
+            new TaskScheduledEvent(
+                {
+                    eventId: 0,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                    name: "GetFileList",
+                    input: "C:\\Dev",
+                },
+            ),
+            new OrchestratorCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                },
+            ),
+            new OrchestratorStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(1, "s").toDate(),
+                    isPlayed: false,
+                },
+            ),
+            new TaskCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(1, "s").toDate(),
+                    isPlayed: false,
+                    result: JSON.stringify(files),
+                    taskScheduledId: 0,
+                },
+            ),
+            new OrchestratorCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                },
+            ),
+            new OrchestratorStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(2, "s").toDate(),
+                    isPlayed: false,
+                },
+            ),
+        ].concat(files.map((file, index) => new TaskScheduledEvent(
+            {
+                eventId: index + 1,
+                timestamp: firstMoment.add(2, "s").toDate(),
+                isPlayed: false,
+                name: "GetFileSize",
+                input: file,
+            },
+        ))).concat(
+        [
+            new OrchestratorCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                },
+            ),
+            new OrchestratorStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(3, "s").toDate(),
+                    isPlayed: false,
+                },
+            ),
+            new TaskCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(3, "s").toDate(),
+                    isPlayed: false,
+                    result: JSON.stringify(1),
+                    taskScheduledId: 1,
+                },
+            ),
+            new TaskCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(3, "s").toDate(),
+                    isPlayed: false,
+                    result: JSON.stringify(2),
+                    taskScheduledId: 2,
+                },
+            ),
+            new OrchestratorCompletedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    isPlayed: false,
+                },
+            ),
+            new OrchestratorStartedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(4, "s").toDate(),
+                    isPlayed: false,
+                },
+            ),
+            new TaskFailedEvent(
+                {
+                    eventId: -1,
+                    timestamp: firstMoment.add(4, "s").toDate(),
+                    isPlayed: false,
+                    taskScheduledId: 3,
+                    reason: "Activity function 'GetFileSize' failed: Could not find file.",
+                    details: "Serialized System.Exception here",
+                },
+            ),
+        ]);
+    }
+
     public static GetFanOutFanInDiskUsagePartComplete(firstTimestamp: Date, files: string[]): HistoryEvent[] {
         const firstMoment = moment(firstTimestamp);
 
diff --git a/test/testobjects/testutils.ts b/test/testobjects/testutils.ts
index 85e187e..cbc4e31 100644
--- a/test/testobjects/testutils.ts
+++ b/test/testobjects/testutils.ts
@@ -50,6 +50,11 @@ export class TestUtils {
                 .replace(TestConstants.idPlaceholder, id)
                 .replace(TestConstants.taskHubPlaceholder, taskHub)
                 .replace(TestConstants.connectionPlaceholder, connection),
+            TestConstants.purgeDeleteUriTemplate
+                .replace(TestConstants.hostPlaceholder, host)
+                .replace(TestConstants.idPlaceholder, id)
+                .replace(TestConstants.taskHubPlaceholder, taskHub)
+                .replace(TestConstants.connectionPlaceholder, connection),
         );
     }
 }
diff --git a/test/unit/orchestrationclient-spec.ts b/test/unit/orchestrationclient-spec.ts
index 0018789..c9a8477 100644
--- a/test/unit/orchestrationclient-spec.ts
+++ b/test/unit/orchestrationclient-spec.ts
@@ -6,7 +6,7 @@ import nock = require("nock");
 import url = require("url");
 import uuidv1 = require("uuid/v1");
 import { Constants, DurableOrchestrationClient, DurableOrchestrationStatus,
-    HttpManagementPayload, OrchestrationRuntimeStatus } from "../../src/classes";
+    HttpManagementPayload, OrchestrationRuntimeStatus, PurgeHistoryResult } from "../../src/classes";
 import { TestConstants } from "../testobjects/testconstants";
 import { TestUtils } from "../testobjects/testutils";
 
@@ -186,6 +186,96 @@ describe("Orchestration Client", () => {
             expect(result).to.be.deep.equal(expectedStatus);
         });
 
+        it("calls expected webhook when showHistory = true", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const expectedStatus = new DurableOrchestrationStatus(
+                defaultOrchestrationName,
+                defaultInstanceId,
+                null,
+                null,
+                null,
+                null,
+                OrchestrationRuntimeStatus.Pending,
+                null,
+                null);
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.statusQueryGetUri
+                .replace(TestConstants.idPlaceholder, defaultInstanceId)
+                + "&showHistory=true");
+
+            const scope = nock(expectedWebhookUrl.origin)
+                .get(expectedWebhookUrl.pathname)
+                .query((actualQueryObj: { [ key: string]: string }) => {
+                    const queryObj = getQueryObjectFromSearchParams(expectedWebhookUrl);
+                    return haveSameQuery(queryObj, actualQueryObj);
+                })
+                .reply(202, expectedStatus);
+
+            const result = await client.getStatus(defaultInstanceId, true);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedStatus);
+        });
+
+        it("calls expected webhook when showHistoryOutput = true", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const expectedStatus = new DurableOrchestrationStatus(
+                defaultOrchestrationName,
+                defaultInstanceId,
+                null,
+                null,
+                null,
+                null,
+                OrchestrationRuntimeStatus.Pending,
+                null,
+                null);
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.statusQueryGetUri
+                .replace(TestConstants.idPlaceholder, defaultInstanceId)
+                + "&showHistoryOutput=true");
+
+            const scope = nock(expectedWebhookUrl.origin)
+                .get(expectedWebhookUrl.pathname)
+                .query((actualQueryObj: { [ key: string]: string }) => {
+                    const queryObj = getQueryObjectFromSearchParams(expectedWebhookUrl);
+                    return haveSameQuery(queryObj, actualQueryObj);
+                })
+                .reply(202, expectedStatus);
+
+            const result = await client.getStatus(defaultInstanceId, false, true);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedStatus);
+        });
+
+        it("calls expected webhook when showInput = false", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const expectedStatus = new DurableOrchestrationStatus(
+                defaultOrchestrationName,
+                defaultInstanceId,
+                null,
+                null,
+                null,
+                null,
+                OrchestrationRuntimeStatus.Pending,
+                null,
+                null);
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.statusQueryGetUri
+                .replace(TestConstants.idPlaceholder, defaultInstanceId)
+                + "&showInput=false");
+
+            const scope = nock(expectedWebhookUrl.origin)
+                .get(expectedWebhookUrl.pathname)
+                .query((actualQueryObj: { [ key: string]: string }) => {
+                    const queryObj = getQueryObjectFromSearchParams(expectedWebhookUrl);
+                    return haveSameQuery(queryObj, actualQueryObj);
+                })
+                .reply(202, expectedStatus);
+
+            const result = await client.getStatus(defaultInstanceId, false, false, false);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedStatus);
+        });
+
         // TODO: test status codes individually
     });
 
@@ -298,6 +388,122 @@ describe("Orchestration Client", () => {
         });
     });
 
+    describe("purgeInstanceHistory()", () => {
+        beforeEach(async () => {
+            nock.cleanAll();
+        });
+
+        afterEach(async () => {
+            nock.cleanAll();
+        });
+
+        it("calls expected webhook and returns expected result when instance exists", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const expectedResult = new PurgeHistoryResult(1);
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.statusQueryGetUri
+                .replace(TestConstants.idPlaceholder, defaultInstanceId));
+
+            const scope = nock(expectedWebhookUrl.origin)
+                .delete(expectedWebhookUrl.pathname)
+                .query(() => {
+                    return getQueryObjectFromSearchParams(expectedWebhookUrl);
+                })
+                .reply(200, expectedResult);
+
+            const result = await client.purgeInstanceHistory(defaultInstanceId);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedResult);
+        });
+
+        it("calls expected webhook and returns expected result when instance does not exist", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const expectedResult = new PurgeHistoryResult(0);
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.statusQueryGetUri
+                .replace(TestConstants.idPlaceholder, defaultInstanceId));
+
+            const scope = nock(expectedWebhookUrl.origin)
+                .delete(expectedWebhookUrl.pathname)
+                .query(() => {
+                    return getQueryObjectFromSearchParams(expectedWebhookUrl);
+                })
+                .reply(404);
+
+            const result = await client.purgeInstanceHistory(defaultInstanceId);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedResult);
+        });
+    });
+
+    describe("purgeInstanceHistoryBy()", () => {
+        beforeEach(async () => {
+            nock.cleanAll();
+        });
+
+        afterEach(async () => {
+            nock.cleanAll();
+        });
+
+        it("throws if createdTimeFrom is not a valid date", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            await expect(client.purgeInstanceHistoryBy(undefined, undefined, undefined))
+                .to.be.rejectedWith("createdTimeFrom must be a valid Date");
+        });
+
+        it("calls expected webhook with all filters and returns expected result when instance(s) found", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const createdTimeTo = new Date();
+            const createdTimeFrom = new Date(createdTimeTo.getTime() - 1000 * 60 * 60 * 24 * 3);    // last three days
+            const runtimeStatuses = [ OrchestrationRuntimeStatus.Failed, OrchestrationRuntimeStatus.Terminated ];
+
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.purgeHistoryDeleteUri
+                .replace(TestConstants.idPlaceholder, "")
+                .concat(`&createdTimeFrom=${createdTimeFrom.toISOString()}`)
+                .concat(`&createdTimeTo=${createdTimeTo.toISOString()}`)
+                .concat("&runtimeStatus=Failed,Terminated"));
+
+            const expectedResult = new PurgeHistoryResult(3);
+            const scope = nock(expectedWebhookUrl.origin)
+                .delete(expectedWebhookUrl.pathname)
+                .query(() => {
+                    return getQueryObjectFromSearchParams(expectedWebhookUrl);
+                })
+                .reply(200, expectedResult);
+
+            const result = await client.purgeInstanceHistoryBy(createdTimeFrom, createdTimeTo, runtimeStatuses);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedResult);
+        });
+
+        it("calls expected webhook with some filters and returns expected result when no instance(s) found", async () => {
+            const client = new DurableOrchestrationClient(defaultClientInputData);
+
+            const createdTimeTo = new Date();
+            const createdTimeFrom = new Date(createdTimeTo.getTime() - 1000 * 60 * 60 * 24 * 3);    // last three days
+            const runtimeStatuses = [ OrchestrationRuntimeStatus.Failed, OrchestrationRuntimeStatus.Terminated ];
+
+            const expectedWebhookUrl = new url.URL(defaultClientInputData.managementUrls.statusQueryGetUri
+                .replace(TestConstants.idPlaceholder, "")
+                .concat(`&createdTimeFrom=${createdTimeFrom.toISOString()}`)
+                .concat("&runtimeStatus=Failed,Terminated"));
+
+            const expectedResult = new PurgeHistoryResult(0);
+            const scope = nock(expectedWebhookUrl.origin)
+                .delete(expectedWebhookUrl.pathname)
+                .query(() => {
+                    return getQueryObjectFromSearchParams(expectedWebhookUrl);
+                })
+                .reply(404);
+
+            const result = await client.purgeInstanceHistoryBy(createdTimeFrom, undefined, runtimeStatuses);
+            expect(scope.isDone()).to.be.equal(true);
+            expect(result).to.be.deep.equal(expectedResult);
+        });
+    });
+
     describe("raiseEvent()", () => {
         const defaultTestEvent = "test";
         const defaultTestData = 42;
@@ -502,7 +708,7 @@ describe("Orchestration Client", () => {
                 .query(() => {
                     return getQueryObjectFromSearchParams(expectedWebhookUrl);
                 })
-                .reply(202, new HttpManagementPayload(defaultInstanceId, "", "", "", ""));
+                .reply(202, new HttpManagementPayload(defaultInstanceId, "", "", "", "", ""));
 
             const result = await client.startNew(functionName);
             expect(scope.isDone()).to.be.equal(true);
@@ -524,7 +730,7 @@ describe("Orchestration Client", () => {
                 .query(() => {
                     return getQueryObjectFromSearchParams(expectedWebhookUrl);
                 })
-                .reply(202, new HttpManagementPayload(defaultInstanceId, "", "", "", ""));
+                .reply(202, new HttpManagementPayload(defaultInstanceId, "", "", "", "", ""));
 
             const result = await client.startNew(functionName, defaultInstanceId, testData);
             expect(scope.isDone()).to.be.equal(true);
@@ -914,3 +1120,14 @@ function getQueryObjectFromSearchParams(requestUrl: url.URL) {
     });
     return queryObject;
 }
+
+function haveSameQuery(
+    expectedQueryObj: { [key: string]: string },
+    actualQueryObj: { [key: string]: string }): boolean {
+    for (const prop in expectedQueryObj) {
+        if (actualQueryObj[prop] !== expectedQueryObj[prop]) {
+            return false;
+        }
+    }
+    return true;
+}