Repository: Azure/azure-functions-durable-js
Pull request #527 (2023-07-22 00:54:10)

Refactor and rename files into meaningful directories
No functional changes here, just moving code around. This is mostly just grouping files together in logical groupings and moving them from the `src` directory to their own subdirectories. This helps clean up `src` a little bit, as right now it's mostly flat. I also took the opportunity to fix the casing on the naming of the files I moved.


Patch:
diff --git a/src/classes.ts b/src/classes.ts
index c273707..eb5efdc 100644
--- a/src/classes.ts
+++ b/src/classes.ts
@@ -1,11 +1,10 @@
 /** @hidden */
 export { Constants } from "./constants";
-export { Utils } from "./utils";
+export { Utils } from "./util/Utils";
 
 export { Orchestrator } from "./orchestrator";
-export { Entity } from "./entity";
+export { Entity } from "./entities/Entity";
 
-export { DurableEntityBindingInfo } from "./durableentitybindinginfo";
 export { DurableOrchestrationBindingInfo } from "./durableorchestrationbindinginfo";
 export { DurableOrchestrationContext } from "./durableorchestrationcontext";
 
@@ -46,8 +45,8 @@ export { RetryOptions } from "./retryoptions";
 
 export { DurableOrchestrationClient } from "./durableorchestrationclient";
 export { OrchestrationClientInputData } from "./orchestrationclientinputdata";
-export { HttpCreationPayload } from "./httpcreationpayload";
-export { HttpManagementPayload } from "./httpmanagementpayload";
+export { HttpCreationPayload } from "./http/HttpCreationPayload";
+export { HttpManagementPayload } from "./http/HttpManagementPayload";
 
 export { DurableOrchestrationStatus } from "./durableorchestrationstatus";
 export { OrchestrationRuntimeStatus } from "./orchestrationruntimestatus";
@@ -55,8 +54,8 @@ export { PurgeHistoryResult } from "./purgehistoryresult";
 
 export { GuidManager } from "./guidmanager";
 
-export { DurableHttpRequest } from "./durablehttprequest";
-export { DurableHttpResponse } from "./durablehttpresponse";
+export { DurableHttpRequest } from "./http/DurableHttpRequest";
+export { DurableHttpResponse } from "./http/DurableHttpResponse";
 export { DurableLock } from "./entities/durablelock";
 export { EntityId } from "./entities/entityid";
 export { EntityState } from "./entities/entitystate";
@@ -66,4 +65,6 @@ export { OperationResult } from "./entities/operationresult";
 export { RequestMessage } from "./entities/requestmessage";
 export { ResponseMessage } from "./entities/responsemessage";
 export { Signal } from "./entities/signal";
-export { DummyOrchestrationContext } from "./testingUtils";
+export { DurableEntityBindingInfo } from "./entities/DurableEntityBindingInfo";
+
+export { DummyOrchestrationContext } from "./util/testingUtils";
diff --git a/src/durableorchestrationclient.ts b/src/durableorchestrationclient.ts
index e97aafb..f9eb386 100644
--- a/src/durableorchestrationclient.ts
+++ b/src/durableorchestrationclient.ts
@@ -30,7 +30,7 @@ import {
     TaskHubOptions,
     WaitForCompletionOptions,
 } from "durable-functions";
-import { WebhookUtils } from "./webhookutils";
+import { WebhookUtils } from "./util/WebhookUtils";
 
 /** @hidden */
 const URL = url.URL;
diff --git a/src/durableentitybindinginfo.ts b/src/entities/DurableEntityBindingInfo.ts
similarity index 88%
rename from src/durableentitybindinginfo.ts
rename to src/entities/DurableEntityBindingInfo.ts
index 17f66bc..15f6445 100644
--- a/src/durableentitybindinginfo.ts
+++ b/src/entities/DurableEntityBindingInfo.ts
@@ -1,22 +1,22 @@
-import { EntityId, RequestMessage } from "./classes";
-
-/** @hidden */
-export class DurableEntityBindingInfoReqFields {
-    constructor(
-        public readonly self: EntityId,
-        public readonly exists: boolean,
-        public readonly batch: RequestMessage[]
-    ) {}
-}
-
-/** @hidden */
-export class DurableEntityBindingInfo extends DurableEntityBindingInfoReqFields {
-    constructor(
-        public readonly self: EntityId,
-        public readonly exists: boolean,
-        public readonly state: string | undefined,
-        public readonly batch: RequestMessage[]
-    ) {
-        super(self, exists, batch);
-    }
-}
+import { EntityId, RequestMessage } from "../classes";
+
+/** @hidden */
+export class DurableEntityBindingInfoReqFields {
+    constructor(
+        public readonly self: EntityId,
+        public readonly exists: boolean,
+        public readonly batch: RequestMessage[]
+    ) {}
+}
+
+/** @hidden */
+export class DurableEntityBindingInfo extends DurableEntityBindingInfoReqFields {
+    constructor(
+        public readonly self: EntityId,
+        public readonly exists: boolean,
+        public readonly state: string | undefined,
+        public readonly batch: RequestMessage[]
+    ) {
+        super(self, exists, batch);
+    }
+}
diff --git a/src/entity.ts b/src/entities/Entity.ts
similarity index 93%
rename from src/entity.ts
rename to src/entities/Entity.ts
index d12ff13..826fc3a 100644
--- a/src/entity.ts
+++ b/src/entities/Entity.ts
@@ -1,154 +1,149 @@
-import * as debug from "debug";
-import {
-    DurableEntityBindingInfo,
-    EntityId,
-    EntityState,
-    OperationResult,
-    RequestMessage,
-    Signal,
-    Utils,
-} from "./classes";
-import { DurableEntityBindingInfoReqFields } from "./durableentitybindinginfo";
-import { DurableEntityContext, EntityContext } from "durable-functions";
-
-/** @hidden */
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const log = debug("orchestrator");
-
-/** @hidden */
-export class Entity<T> {
-    constructor(public fn: (context: EntityContext<T>) => void) {}
-
-    public listen(): (
-        entityTrigger: DurableEntityBindingInfo,
-        context: EntityContext<T>
-    ) => Promise<EntityState> {
-        return this.handle.bind(this);
-    }
-
-    private async handle(
-        entityTrigger: DurableEntityBindingInfo,
-        context: EntityContext<T>
-    ): Promise<EntityState> {
-        const entityBinding = Utils.getInstancesOf<DurableEntityBindingInfo>(
-            { trigger: entityTrigger },
-            new DurableEntityBindingInfoReqFields(
-                new EntityId("samplename", "samplekey"),
-                true,
-                []
-            ) as DurableEntityBindingInfo
-        )[0];
-
-        if (entityBinding === undefined) {
-            throw new Error("Could not find an entityTrigger binding on context.");
-        }
-
-        // Setup
-        const returnState: EntityState = new EntityState([], []);
-        returnState.entityExists = entityBinding.exists;
-        returnState.entityState = entityBinding.state;
-        for (let i = 0; i < entityBinding.batch.length; i++) {
-            const startTime = new Date();
-            context.df = this.getCurrentDurableEntityContext(
-                entityBinding,
-                returnState,
-                i,
-                startTime
-            );
-
-            try {
-                await Promise.resolve(this.fn(context));
-                if (!returnState.results[i]) {
-                    const elapsedMs = this.computeElapsedMilliseconds(startTime);
-                    returnState.results[i] = new OperationResult(false, elapsedMs);
-                }
-            } catch (error) {
-                const elapsedMs = this.computeElapsedMilliseconds(startTime);
-                returnState.results[i] = new OperationResult(
-                    true,
-                    elapsedMs,
-                    JSON.stringify(error)
-                );
-            }
-        }
-
-        return returnState;
-    }
-
-    private getCurrentDurableEntityContext(
-        bindingInfo: DurableEntityBindingInfo,
-        batchState: EntityState,
-        requestIndex: number,
-        startTime: Date
-    ): DurableEntityContext<T> {
-        const currentRequest = bindingInfo.batch[requestIndex];
-        return {
-            entityName: bindingInfo.self.name,
-            entityKey: bindingInfo.self.key,
-            entityId: bindingInfo.self,
-            operationName: currentRequest.name,
-            isNewlyConstructed: !batchState.entityExists,
-            getState: this.getState.bind(this, batchState),
-            setState: this.setState.bind(this, batchState),
-            getInput: this.getInput.bind(this, currentRequest) as <TInput>() => TInput | undefined,
-            return: this.return.bind(this, batchState, startTime) as <TResult = T>(
-                value: TResult
-            ) => void,
-            destructOnExit: this.destructOnExit.bind(this, batchState),
-            signalEntity: this.signalEntity.bind(this, batchState),
-        };
-    }
-
-    private destructOnExit(batchState: EntityState): void {
-        batchState.entityExists = false;
-        batchState.entityState = undefined;
-    }
-
-    private getInput<TInput>(currentRequest: RequestMessage): TInput | undefined {
-        if (currentRequest.input) {
-            return JSON.parse(currentRequest.input) as TInput;
-        }
-        return undefined;
-    }
-
-    private getState(returnState: EntityState, initializer?: () => T): T | undefined {
-        if (returnState.entityState) {
-            return JSON.parse(returnState.entityState) as T;
-        } else if (initializer) {
-            return initializer();
-        }
-        return undefined;
-    }
-
-    private return(returnState: EntityState, startTime: Date, result: T): void {
-        returnState.entityExists = true;
-        returnState.results.push(
-            new OperationResult(
-                false,
-                this.computeElapsedMilliseconds(startTime),
-                JSON.stringify(result)
-            )
-        );
-    }
-
-    private setState(returnState: EntityState, state: T): void {
-        returnState.entityExists = true;
-        returnState.entityState = JSON.stringify(state);
-    }
-
-    private signalEntity(
-        returnState: EntityState,
-        entity: EntityId,
-        operationName: string,
-        operationInput?: unknown
-    ): void {
-        returnState.signals.push(
-            new Signal(entity, operationName, operationInput ? JSON.stringify(operationInput) : "")
-        );
-    }
-
-    private computeElapsedMilliseconds(startTime: Date): number {
-        const endTime = new Date();
-        return endTime.getTime() - startTime.getTime();
-    }
-}
+import * as debug from "debug";
+import { EntityId, EntityState, OperationResult, RequestMessage, Signal, Utils } from "../classes";
+import {
+    DurableEntityBindingInfoReqFields,
+    DurableEntityBindingInfo,
+} from "./DurableEntityBindingInfo";
+import { DurableEntityContext, EntityContext } from "durable-functions";
+
+/** @hidden */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const log = debug("orchestrator");
+
+/** @hidden */
+export class Entity<T> {
+    constructor(public fn: (context: EntityContext<T>) => void) {}
+
+    public listen(): (
+        entityTrigger: DurableEntityBindingInfo,
+        context: EntityContext<T>
+    ) => Promise<EntityState> {
+        return this.handle.bind(this);
+    }
+
+    private async handle(
+        entityTrigger: DurableEntityBindingInfo,
+        context: EntityContext<T>
+    ): Promise<EntityState> {
+        const entityBinding = Utils.getInstancesOf<DurableEntityBindingInfo>(
+            { trigger: entityTrigger },
+            new DurableEntityBindingInfoReqFields(
+                new EntityId("samplename", "samplekey"),
+                true,
+                []
+            ) as DurableEntityBindingInfo
+        )[0];
+
+        if (entityBinding === undefined) {
+            throw new Error("Could not find an entityTrigger binding on context.");
+        }
+
+        // Setup
+        const returnState: EntityState = new EntityState([], []);
+        returnState.entityExists = entityBinding.exists;
+        returnState.entityState = entityBinding.state;
+        for (let i = 0; i < entityBinding.batch.length; i++) {
+            const startTime = new Date();
+            context.df = this.getCurrentDurableEntityContext(
+                entityBinding,
+                returnState,
+                i,
+                startTime
+            );
+
+            try {
+                await Promise.resolve(this.fn(context));
+                if (!returnState.results[i]) {
+                    const elapsedMs = this.computeElapsedMilliseconds(startTime);
+                    returnState.results[i] = new OperationResult(false, elapsedMs);
+                }
+            } catch (error) {
+                const elapsedMs = this.computeElapsedMilliseconds(startTime);
+                returnState.results[i] = new OperationResult(
+                    true,
+                    elapsedMs,
+                    JSON.stringify(error)
+                );
+            }
+        }
+
+        return returnState;
+    }
+
+    private getCurrentDurableEntityContext(
+        bindingInfo: DurableEntityBindingInfo,
+        batchState: EntityState,
+        requestIndex: number,
+        startTime: Date
+    ): DurableEntityContext<T> {
+        const currentRequest = bindingInfo.batch[requestIndex];
+        return {
+            entityName: bindingInfo.self.name,
+            entityKey: bindingInfo.self.key,
+            entityId: bindingInfo.self,
+            operationName: currentRequest.name,
+            isNewlyConstructed: !batchState.entityExists,
+            getState: this.getState.bind(this, batchState),
+            setState: this.setState.bind(this, batchState),
+            getInput: this.getInput.bind(this, currentRequest) as <TInput>() => TInput | undefined,
+            return: this.return.bind(this, batchState, startTime) as <TResult = T>(
+                value: TResult
+            ) => void,
+            destructOnExit: this.destructOnExit.bind(this, batchState),
+            signalEntity: this.signalEntity.bind(this, batchState),
+        };
+    }
+
+    private destructOnExit(batchState: EntityState): void {
+        batchState.entityExists = false;
+        batchState.entityState = undefined;
+    }
+
+    private getInput<TInput>(currentRequest: RequestMessage): TInput | undefined {
+        if (currentRequest.input) {
+            return JSON.parse(currentRequest.input) as TInput;
+        }
+        return undefined;
+    }
+
+    private getState(returnState: EntityState, initializer?: () => T): T | undefined {
+        if (returnState.entityState) {
+            return JSON.parse(returnState.entityState) as T;
+        } else if (initializer) {
+            return initializer();
+        }
+        return undefined;
+    }
+
+    private return(returnState: EntityState, startTime: Date, result: T): void {
+        returnState.entityExists = true;
+        returnState.results.push(
+            new OperationResult(
+                false,
+                this.computeElapsedMilliseconds(startTime),
+                JSON.stringify(result)
+            )
+        );
+    }
+
+    private setState(returnState: EntityState, state: T): void {
+        returnState.entityExists = true;
+        returnState.entityState = JSON.stringify(state);
+    }
+
+    private signalEntity(
+        returnState: EntityState,
+        entity: EntityId,
+        operationName: string,
+        operationInput?: unknown
+    ): void {
+        returnState.signals.push(
+            new Signal(entity, operationName, operationInput ? JSON.stringify(operationInput) : "")
+        );
+    }
+
+    private computeElapsedMilliseconds(startTime: Date): number {
+        const endTime = new Date();
+        return endTime.getTime() - startTime.getTime();
+    }
+}
diff --git a/src/entities/entityid.ts b/src/entities/entityid.ts
index 806dca8..f1c8d87 100644
--- a/src/entities/entityid.ts
+++ b/src/entities/entityid.ts
@@ -1,6 +1,6 @@
 // tslint:disable:member-access
 
-import { Utils } from "../utils";
+import { Utils } from "../util/Utils";
 import * as types from "durable-functions";
 
 export class EntityId implements types.EntityId {
diff --git a/src/entities/responsemessage.ts b/src/entities/responsemessage.ts
index 9b3f73c..af03566 100644
--- a/src/entities/responsemessage.ts
+++ b/src/entities/responsemessage.ts
@@ -1,4 +1,4 @@
-import { Utils } from "./../utils";
+import { Utils } from "../util/Utils";
 
 /** @hidden */
 export class ResponseMessage {
diff --git a/src/aggregatederror.ts b/src/error/AggregatedError.ts
similarity index 97%
rename from src/aggregatederror.ts
rename to src/error/AggregatedError.ts
index f3b1a05..b8d5075 100644
--- a/src/aggregatederror.ts
+++ b/src/error/AggregatedError.ts
@@ -1,19 +1,19 @@
-import * as types from "durable-functions";
-
-/** @hidden */
-const separator = "-----------------------------------";
-
-export class AggregatedError extends Error implements types.AggregatedError {
-    public errors: Error[];
-
-    constructor(errors: Error[]) {
-        const errorStrings = errors.map(
-            (error) => `Name: ${error.name}\nMessage: ${error.message}\nStackTrace: ${error.stack}`
-        );
-        const message = `context.df.Task.all() encountered the below error messages:\n\n${errorStrings.join(
-            `\n${separator}\n`
-        )}`;
-        super(message);
-        this.errors = errors;
-    }
-}
+import * as types from "durable-functions";
+
+/** @hidden */
+const separator = "-----------------------------------";
+
+export class AggregatedError extends Error implements types.AggregatedError {
+    public errors: Error[];
+
+    constructor(errors: Error[]) {
+        const errorStrings = errors.map(
+            (error) => `Name: ${error.name}\nMessage: ${error.message}\nStackTrace: ${error.stack}`
+        );
+        const message = `context.df.Task.all() encountered the below error messages:\n\n${errorStrings.join(
+            `\n${separator}\n`
+        )}`;
+        super(message);
+        this.errors = errors;
+    }
+}
diff --git a/src/durableerror.ts b/src/error/DurableError.ts
similarity index 96%
rename from src/durableerror.ts
rename to src/error/DurableError.ts
index d3081bb..22e86cb 100644
--- a/src/durableerror.ts
+++ b/src/error/DurableError.ts
@@ -1,7 +1,7 @@
-import * as types from "durable-functions";
-
-export class DurableError extends Error implements types.DurableError {
-    constructor(message: string | undefined) {
-        super(message);
-    }
-}
+import * as types from "durable-functions";
+
+export class DurableError extends Error implements types.DurableError {
+    constructor(message: string | undefined) {
+        super(message);
+    }
+}
diff --git a/src/orchestrationfailureerror.ts b/src/error/OrchestrationFailureError.ts
similarity index 93%
rename from src/orchestrationfailureerror.ts
rename to src/error/OrchestrationFailureError.ts
index f5911c6..c495461 100644
--- a/src/orchestrationfailureerror.ts
+++ b/src/error/OrchestrationFailureError.ts
@@ -1,30 +1,30 @@
-import { OrchestratorState } from "./classes";
-/** @hidden */
-const outOfProcDataLabel = "\n\n$OutOfProcData$:";
-
-/**
- * @hidden
- * A wrapper for all errors thrown within an orchestrator function. This exception will embed
- * the orchestrator state in a way that the C# extension knows how to read so that it can replay the
- * actions scheduled before throwing an exception. This prevents non-determinism errors in Durable Task.
- *
- * Note that making any changes to the following schema to OrchestrationFailureError.message could be considered a breaking change:
- *
- * "<error message as a string>\n\n$OutOfProcData$<json representation of state>"
- */
-export class OrchestrationFailureError extends Error {
-    constructor(error: any, state: OrchestratorState) {
-        let errorMessage: string;
-        if (error instanceof Error) {
-            errorMessage = error.message;
-        } else if (typeof error === "string") {
-            errorMessage = error;
-        } else {
-            errorMessage = JSON.stringify(error);
-        }
-
-        const message = `${errorMessage}${outOfProcDataLabel}${JSON.stringify(state)}`;
-        super(message);
-        this.stack = error.stack;
-    }
-}
+import { OrchestratorState } from "../classes";
+/** @hidden */
+const outOfProcDataLabel = "\n\n$OutOfProcData$:";
+
+/**
+ * @hidden
+ * A wrapper for all errors thrown within an orchestrator function. This exception will embed
+ * the orchestrator state in a way that the C# extension knows how to read so that it can replay the
+ * actions scheduled before throwing an exception. This prevents non-determinism errors in Durable Task.
+ *
+ * Note that making any changes to the following schema to OrchestrationFailureError.message could be considered a breaking change:
+ *
+ * "<error message as a string>\n\n$OutOfProcData$<json representation of state>"
+ */
+export class OrchestrationFailureError extends Error {
+    constructor(error: any, state: OrchestratorState) {
+        let errorMessage: string;
+        if (error instanceof Error) {
+            errorMessage = error.message;
+        } else if (typeof error === "string") {
+            errorMessage = error;
+        } else {
+            errorMessage = JSON.stringify(error);
+        }
+
+        const message = `${errorMessage}${outOfProcDataLabel}${JSON.stringify(state)}`;
+        super(message);
+        this.stack = error.stack;
+    }
+}
diff --git a/src/durablehttprequest.ts b/src/http/DurableHttpRequest.ts
similarity index 97%
rename from src/durablehttprequest.ts
rename to src/http/DurableHttpRequest.ts
index 8273978..129057c 100644
--- a/src/durablehttprequest.ts
+++ b/src/http/DurableHttpRequest.ts
@@ -1,34 +1,34 @@
-import { TokenSource } from "durable-functions";
-
-/**
- * Data structure representing a durable HTTP request.
- */
-export class DurableHttpRequest {
-    /**
-     * Creates a new instance of DurableHttpRequest with the
-     * specified parameters.
-     *
-     * @param method The HTTP request method.
-     * @param uri The HTTP request URL.
-     * @param content The HTTP request content.
-     * @param headers The HTTP request headers.
-     * @param tokenSource The source of OAuth tokens to add to the request.
-     * @param asynchronousPatternEnabled Specifies whether the DurableHttpRequest should handle the asynchronous pattern.
-     */
-    constructor(
-        /** The HTTP request method. */
-        public readonly method: string,
-        /** The HTTP request URL. */
-        public readonly uri: string,
-        /** The HTTP request content. */
-        public readonly content?: string,
-        /** The HTTP request headers. */
-        public readonly headers?: {
-            [key: string]: string;
-        },
-        /** The source of OAuth token to add to the request. */
-        public readonly tokenSource?: TokenSource,
-        /**  Whether the DurableHttpRequest should handle the asynchronous pattern. **/
-        public readonly asynchronousPatternEnabled: boolean = true
-    ) {}
-}
+import { TokenSource } from "durable-functions";
+
+/**
+ * Data structure representing a durable HTTP request.
+ */
+export class DurableHttpRequest {
+    /**
+     * Creates a new instance of DurableHttpRequest with the
+     * specified parameters.
+     *
+     * @param method The HTTP request method.
+     * @param uri The HTTP request URL.
+     * @param content The HTTP request content.
+     * @param headers The HTTP request headers.
+     * @param tokenSource The source of OAuth tokens to add to the request.
+     * @param asynchronousPatternEnabled Specifies whether the DurableHttpRequest should handle the asynchronous pattern.
+     */
+    constructor(
+        /** The HTTP request method. */
+        public readonly method: string,
+        /** The HTTP request URL. */
+        public readonly uri: string,
+        /** The HTTP request content. */
+        public readonly content?: string,
+        /** The HTTP request headers. */
+        public readonly headers?: {
+            [key: string]: string;
+        },
+        /** The source of OAuth token to add to the request. */
+        public readonly tokenSource?: TokenSource,
+        /**  Whether the DurableHttpRequest should handle the asynchronous pattern. **/
+        public readonly asynchronousPatternEnabled: boolean = true
+    ) {}
+}
diff --git a/src/durablehttpresponse.ts b/src/http/DurableHttpResponse.ts
similarity index 96%
rename from src/durablehttpresponse.ts
rename to src/http/DurableHttpResponse.ts
index 5631046..e1daf5f 100644
--- a/src/durablehttpresponse.ts
+++ b/src/http/DurableHttpResponse.ts
@@ -1,40 +1,40 @@
-/**
- * Data structure representing a durable HTTP response.
- */
-export class DurableHttpResponse {
-    /**
-     * Creates a new instance of DurableHttpResponse with the
-     * specified parameters.
-     *
-     * @param statusCode The HTTP response status code.
-     * @param content The HTTP response content.
-     * @param headers The HTTP response headers.
-     */
-    constructor(
-        /** The HTTP response status code. */
-        public statusCode: number,
-
-        /** The HTTP response content. */
-        public content?: string,
-
-        /** The HTTP response headers. */
-        public headers?: {
-            [key: string]: string;
-        }
-    ) {}
-
-    // returns the specified header, case insensitively
-    // returns undefined if the header is not set
-    public getHeader(name: string): string | undefined {
-        if (this.headers) {
-            const lowerCaseName = name.toLowerCase();
-            const foundKey = Object.keys(this.headers).find(
-                (key) => key.toLowerCase() === lowerCaseName
-            );
-            if (foundKey) {
-                return this.headers[foundKey];
-            }
-        }
-        return undefined;
-    }
-}
+/**
+ * Data structure representing a durable HTTP response.
+ */
+export class DurableHttpResponse {
+    /**
+     * Creates a new instance of DurableHttpResponse with the
+     * specified parameters.
+     *
+     * @param statusCode The HTTP response status code.
+     * @param content The HTTP response content.
+     * @param headers The HTTP response headers.
+     */
+    constructor(
+        /** The HTTP response status code. */
+        public statusCode: number,
+
+        /** The HTTP response content. */
+        public content?: string,
+
+        /** The HTTP response headers. */
+        public headers?: {
+            [key: string]: string;
+        }
+    ) {}
+
+    // returns the specified header, case insensitively
+    // returns undefined if the header is not set
+    public getHeader(name: string): string | undefined {
+        if (this.headers) {
+            const lowerCaseName = name.toLowerCase();
+            const foundKey = Object.keys(this.headers).find(
+                (key) => key.toLowerCase() === lowerCaseName
+            );
+            if (foundKey) {
+                return this.headers[foundKey];
+            }
+        }
+        return undefined;
+    }
+}
diff --git a/src/httpcreationpayload.ts b/src/http/HttpCreationPayload.ts
similarity index 96%
rename from src/httpcreationpayload.ts
rename to src/http/HttpCreationPayload.ts
index db27f51..ff8e61e 100644
--- a/src/httpcreationpayload.ts
+++ b/src/http/HttpCreationPayload.ts
@@ -1,6 +1,6 @@
-/** @hidden */
-export class HttpCreationPayload {
-    [key: string]: string;
-
-    constructor(public createNewInstancePostUri: string, public waitOnNewInstancePostUri: string) {}
-}
+/** @hidden */
+export class HttpCreationPayload {
+    [key: string]: string;
+
+    constructor(public createNewInstancePostUri: string, public waitOnNewInstancePostUri: string) {}
+}
diff --git a/src/httpmanagementpayload.ts b/src/http/HttpManagementPayload.ts
similarity index 96%
rename from src/httpmanagementpayload.ts
rename to src/http/HttpManagementPayload.ts
index 9e4c11f..d76349d 100644
--- a/src/httpmanagementpayload.ts
+++ b/src/http/HttpManagementPayload.ts
@@ -1,16 +1,16 @@
-import * as types from "durable-functions";
-
-export class HttpManagementPayload implements types.HttpManagementPayload {
-    /** @hidden */
-    [key: string]: string;
-
-    /** @hidden */
-    constructor(
-        public readonly id: string,
-        public readonly statusQueryGetUri: string,
-        public readonly sendEventPostUri: string,
-        public readonly terminatePostUri: string,
-        public readonly rewindPostUri: string,
-        public readonly purgeHistoryDeleteUri: string
-    ) {}
-}
+import * as types from "durable-functions";
+
+export class HttpManagementPayload implements types.HttpManagementPayload {
+    /** @hidden */
+    [key: string]: string;
+
+    /** @hidden */
+    constructor(
+        public readonly id: string,
+        public readonly statusQueryGetUri: string,
+        public readonly sendEventPostUri: string,
+        public readonly terminatePostUri: string,
+        public readonly rewindPostUri: string,
+        public readonly purgeHistoryDeleteUri: string
+    ) {}
+}
diff --git a/src/index.ts b/src/index.ts
index 7e8f406..a9bf1ac 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,7 +7,7 @@ import {
 } from "./classes";
 import { getClient } from "./durableorchestrationclient";
 import { app, input, trigger } from "./shim";
-import { DummyEntityContext } from "./testingUtils";
+import { DummyEntityContext } from "./util/testingUtils";
 import { ManagedIdentityTokenSource } from "./tokensource";
 
 export {
diff --git a/src/retryoptions.ts b/src/retryoptions.ts
index e836b90..04db8e5 100644
--- a/src/retryoptions.ts
+++ b/src/retryoptions.ts
@@ -1,4 +1,4 @@
-import { Utils } from "./utils";
+import { Utils } from "./util/Utils";
 import * as types from "durable-functions";
 
 export class RetryOptions implements types.RetryOptions {
diff --git a/src/shim.ts b/src/shim.ts
index d1f38fe..70ff5d3 100644
--- a/src/shim.ts
+++ b/src/shim.ts
@@ -18,9 +18,9 @@ import {
     EntityContext,
 } from "durable-functions";
 import { Entity, EntityState, Orchestrator } from "./classes";
-import { DurableEntityBindingInfo } from "./durableentitybindinginfo";
+import { DurableEntityBindingInfo } from "./entities/DurableEntityBindingInfo";
 import { OrchestratorState } from "./orchestratorstate";
-import { DurableOrchestrationInput } from "./testingUtils";
+import { DurableOrchestrationInput } from "./util/testingUtils";
 
 type EntityFunction<T> = FunctionHandler &
     ((entityTrigger: DurableEntityBindingInfo, context: EntityContext<T>) => Promise<EntityState>);
diff --git a/src/task.ts b/src/task.ts
deleted file mode 100644
index fa7037f..0000000
--- a/src/task.ts
+++ /dev/null
@@ -1,575 +0,0 @@
-import { RetryOptions } from ".";
-import { IAction, CreateTimerAction, CallHttpAction, DurableHttpResponse } from "./classes";
-import { TaskOrchestrationExecutor } from "./taskorchestrationexecutor";
-import moment = require("moment");
-import { DurableOrchestrationContext } from "./durableorchestrationcontext";
-import { Task, TimerTask } from "durable-functions";
-
-/**
- * @hidden
- * The states a task can be in
- */
-export enum TaskState {
-    Running,
-    Failed,
-    Completed,
-}
-
-/**
- * @hidden
- * A taskID, either a `string` for external events,
- * or either `false` or a `number` for un-awaited
- * and awaited tasks respectively.
- */
-export type TaskID = number | string | false;
-
-/**
- * @hidden
- * A backing action, either a proper action or "noOp" for an internal-only task
- */
-export type BackingAction = IAction | "noOp";
-
-/**
- * @hidden
- * Base class for all Tasks, defines the basic state transitions for all tasks.
- */
-export abstract class TaskBase {
-    public state: TaskState;
-    public parent: CompoundTask | undefined;
-    public isPlayed: boolean;
-    public result: unknown;
-
-    /**
-     * @hidden
-     *
-     * Construct a task.
-     * @param id
-     *  The task's ID
-     * @param action
-     *  The task's backing action
-     */
-    constructor(public id: TaskID, protected action: BackingAction) {
-        this.state = TaskState.Running;
-    }
-
-    /** Get this task's backing action */
-    get actionObj(): BackingAction {
-        return this.action;
-    }
-
-    /** Get this task's current state */
-    get stateObj(): TaskState {
-        return this.state;
-    }
-
-    /** Whether this task is not in the Running state */
-    get hasResult(): boolean {
-        return this.state !== TaskState.Running;
-    }
-
-    get isFaulted(): boolean {
-        return this.state === TaskState.Failed;
-    }
-
-    get isCompleted(): boolean {
-        return this.state === TaskState.Completed;
-    }
-
-    /** Change this task from the Running state to a completed state */
-    private changeState(state: TaskState): void {
-        if (state === TaskState.Running) {
-            throw Error("Cannot change Task to the RUNNING state.");
-        }
-        this.state = state;
-    }
-
-    /** Attempt to set a result for this task, and notifies parents, if any */
-    public setValue(isError: boolean, value: unknown): void {
-        let newState: TaskState;
-
-        if (isError) {
-            if (!(value instanceof Error)) {
-                const errMessage = `Task ID ${this.id} failed but it's value was not an Exception`;
-                throw new Error(errMessage);
-            }
-            newState = TaskState.Failed;
-        } else {
-            newState = TaskState.Completed;
-        }
-
-        this.changeState(newState);
-        this.result = value;
-        this.propagate();
-    }
-
-    /**
-     * @hidden
-     * Notifies this task's parents about its state change.
-     */
-    private propagate(): void {
-        const hasCompleted = this.state !== TaskState.Running;
-        if (hasCompleted && this.parent !== undefined) {
-            this.parent.handleCompletion(this);
-        }
-    }
-}
-
-/**
- * @hidden
- *
- * A task created only to facilitate replay, it should not communicate any
- * actions to the DF extension.
- *
- * We internally track these kinds of tasks to reason over the completion of
- * DF APIs that decompose into smaller DF APIs that the user didn't explicitly
- * schedule.
- */
-export class NoOpTask extends TaskBase {
-    constructor() {
-        super(false, "noOp");
-    }
-}
-
-/**
- * @hidden
- * A task that should result in an Action being communicated to the DF extension.
- */
-export class DFTask extends TaskBase implements Task {
-    protected action: IAction;
-    public alreadyScheduled = false;
-
-    /** Get this task's backing action */
-    get actionObj(): IAction {
-        return this.action;
-    }
-}
-
-/**
- * @hidden
- *
- * A task that depends on the completion of other (sub-) tasks.
- */
-export abstract class CompoundTask extends DFTask {
-    protected firstError: Error | undefined;
-
-    /**
-     * @hidden
-     * Construct a Compound Task.
-     * Primarily sets the parent pointer of each sub-task to be `this`.
-     *
-     * @param children
-     *  The sub-tasks that this task depends on
-     * @param action
-     *  An action representing this compound task
-     */
-    constructor(public children: TaskBase[], protected action: IAction) {
-        super(false, action);
-        children.map((c) => (c.parent = this));
-        this.firstError = undefined;
-
-        // If the task has no children, throw an error
-        // See issue here for why this isn't allowed: https://github.com/Azure/azure-functions-durable-js/issues/424
-        if (children.length == 0) {
-            const message =
-                "When constructing a CompoundTask (such as Task.all() or Task.any()), you must specify at least one Task.";
-            throw new Error(message);
-        }
-    }
-
-    /**
-     * @hidden
-     * Tries to set this task's result based on the completion of a sub-task
-     * @param child
-     *  A sub-task of this task.
-     */
-    public handleCompletion(child: TaskBase): void {
-        if (!this.isPlayed) {
-            this.isPlayed = child.isPlayed;
-        }
-        this.trySetValue(child);
-    }
-
-    /**
-     * @hidden
-     *
-     * Task-internal logic for attempting to set this tasks' result
-     * after any of its sub-tasks completes.
-     * @param child
-     *  A sub-task
-     */
-    abstract trySetValue(child: TaskBase): void;
-}
-
-export class AtomicTask extends DFTask {}
-
-/**
- * @hidden
- * A timer task. This is the internal implementation to the user-exposed TimerTask interface, which
- * has a more restricted API.
- */
-export class DFTimerTask extends AtomicTask implements TimerTask {
-    /**
-     * @hidden
-     * Construct a Timer Task.
-     *
-     * @param id
-     *  The task's ID
-     * @param action
-     *  The backing action of this task
-     */
-    constructor(public id: TaskID, public action: CreateTimerAction) {
-        super(id, action);
-    }
-
-    /** Whether this timer task is canceled */
-    get isCanceled(): boolean {
-        return this.action.isCanceled;
-    }
-
-    /**
-     * @hidden
-     * Cancel this timer task.
-     * It errors out if the task has already completed.
-     */
-    public cancel(): void {
-        if (this.hasResult) {
-            throw Error("Cannot cancel a completed task.");
-        }
-        this.action.isCanceled = true;
-    }
-}
-
-/**
- * @hidden
- *
- * A WhenAll task.
- */
-export class WhenAllTask extends CompoundTask {
-    /**
-     * @hidden
-     * Construct a WhenAll task.
-     *
-     * @param children
-     *  Sub-tasks to wait on.
-     * @param action
-     *  A the backing action representing this task.
-     */
-    constructor(public children: TaskBase[], protected action: IAction) {
-        super(children, action);
-    }
-
-    /**
-     * @hidden
-     * Attempts to set a value to this task, given a completed sub-task
-     *
-     * @param child
-     *  The sub-task that just completed
-     */
-    public trySetValue(child: AtomicTask): void {
-        if (child.stateObj === TaskState.Completed) {
-            // We set the result only after all sub-tasks have completed
-            if (this.children.every((c) => c.stateObj === TaskState.Completed)) {
-                // The result is a list of all sub-task's results
-                const results = this.children.map((c) => c.result);
-                this.setValue(false, results);
-            }
-        } else {
-            // If any task failed, we fail the entire compound task
-            if (this.firstError === undefined) {
-                this.firstError = child.result as Error;
-                this.setValue(true, this.firstError);
-            }
-        }
-    }
-}
-
-/**
- * @hidden
- *
- * A WhenAny task.
- */
-export class WhenAnyTask extends CompoundTask {
-    /**
-     * @hidden
-     * Attempts to set a value to this task, given a completed sub-task
-     *
-     * @param child
-     *  The sub-task that just completed
-     */
-    public trySetValue(child: TaskBase): void {
-        // For a Task to have isError = true, it needs to contain within an Exception/Error datatype.
-        // However, WhenAny only contains Tasks as its result, so technically it "never errors out".
-        // The isError flag is used simply to determine if the result of the task should be fed in
-        // as a value, or as a raised exception to the generator code. For WhenAny, we always feed
-        // in the result as a value.
-        if (this.state === TaskState.Running) {
-            this.setValue(false, child);
-        }
-    }
-}
-
-/**
- * @hidden
- *
- * CallHttp Task with polling logic
- *
- * If the HTTP requests returns a 202 status code with a 'Location' header,
- * then a timer task is created, after which another HTTP request is made,
- * until a different status code is returned.
- *
- * Any other result from the HTTP requests is the result of the whole task.
- *
- * The duration of the timer is specified by the 'Retry-After' header (in seconds)
- * of the 202 response, or a default value specified by the durable extension is used.
- *
- */
-export class CallHttpWithPollingTask extends CompoundTask {
-    protected action: CallHttpAction;
-    private readonly defaultHttpAsyncRequestSleepDuration: moment.Duration;
-
-    public constructor(
-        id: TaskID,
-        action: CallHttpAction,
-        private readonly orchestrationContext: DurableOrchestrationContext,
-        private readonly executor: TaskOrchestrationExecutor,
-        defaultHttpAsyncRequestSleepTimeMillseconds: number
-    ) {
-        super([new AtomicTask(id, action)], action);
-        this.id = id;
-        this.action = action;
-        this.defaultHttpAsyncRequestSleepDuration = moment.duration(
-            defaultHttpAsyncRequestSleepTimeMillseconds,
-            "ms"
-        );
-    }
-
-    public trySetValue(child: TaskBase): void {
-        if (child.stateObj === TaskState.Completed) {
-            if (child.actionObj instanceof CallHttpAction) {
-                const resultObj = child.result as DurableHttpResponse;
-                const result = new DurableHttpResponse(
-                    resultObj.statusCode,
-                    resultObj.content,
-                    resultObj.headers
-                );
-                if (result.statusCode === 202 && result.getHeader("Location")) {
-                    const retryAfterHeaderValue = result.getHeader("Retry-After");
-                    const delay: moment.Duration = retryAfterHeaderValue
-                        ? moment.duration(retryAfterHeaderValue, "s")
-                        : this.defaultHttpAsyncRequestSleepDuration;
-
-                    const currentTime = this.orchestrationContext.currentUtcDateTime;
-                    const timerFireTime = moment(currentTime).add(delay).toDate();
-
-                    // this should be safe since both types returned by this call
-                    // (DFTimerTask and LongTimerTask) are TaskBase-conforming
-                    const timerTask = (this.orchestrationContext.createTimer(
-                        timerFireTime
-                    ) as unknown) as TaskBase;
-                    const callHttpTask = new AtomicTask(
-                        false,
-                        new CallHttpAction(this.action.httpRequest)
-                    );
-
-                    this.addNewChildren([timerTask, callHttpTask]);
-                } else {
-                    // Set the value of a non-redirect HTTP response as the value of the entire
-                    // compound task
-                    this.setValue(false, result);
-                }
-            }
-        } else {
-            // If any subtask failed, we fail the entire compound task
-            if (this.firstError === undefined) {
-                this.firstError = child.result as Error;
-                this.setValue(true, this.firstError);
-            }
-        }
-    }
-
-    private addNewChildren(children: TaskBase[]): void {
-        children.map((child) => {
-            child.parent = this;
-            this.children.push(child);
-            this.executor.trackOpenTask(child);
-        });
-    }
-}
-
-/**
- * @hidden
- *
- * A long Timer Task.
- *
- * This Task is created when a timer is created with a duration
- * longer than the maximum timer duration supported by storage infrastructure.
- *
- * It extends `WhenAllTask` because it decomposes into
- * several smaller sub-`TimerTask`s
- */
-export class LongTimerTask extends WhenAllTask implements TimerTask {
-    public id: TaskID;
-    public action: CreateTimerAction;
-    private readonly executor: TaskOrchestrationExecutor;
-    private readonly maximumTimerDuration: moment.Duration;
-    private readonly orchestrationContext: DurableOrchestrationContext;
-    private readonly longRunningTimerIntervalDuration: moment.Duration;
-
-    public constructor(
-        id: TaskID,
-        action: CreateTimerAction,
-        orchestrationContext: DurableOrchestrationContext,
-        executor: TaskOrchestrationExecutor,
-        maximumTimerLength: string,
-        longRunningTimerIntervalLength: string
-    ) {
-        const maximumTimerDuration = moment.duration(maximumTimerLength);
-        const longRunningTimerIntervalDuration = moment.duration(longRunningTimerIntervalLength);
-        const currentTime = orchestrationContext.currentUtcDateTime;
-        const finalFireTime = action.fireAt;
-        const durationUntilFire = moment.duration(moment(finalFireTime).diff(currentTime));
-
-        const nextFireTime: Date =
-            durationUntilFire > maximumTimerDuration
-                ? moment(currentTime).add(longRunningTimerIntervalDuration).toDate()
-                : finalFireTime;
-
-        const nextTimerAction = new CreateTimerAction(nextFireTime);
-        const nextTimerTask = new DFTimerTask(false, nextTimerAction);
-        super([nextTimerTask], action);
-
-        this.id = id;
-        this.action = action;
-        this.orchestrationContext = orchestrationContext;
-        this.executor = executor;
-        this.maximumTimerDuration = maximumTimerDuration;
-        this.longRunningTimerIntervalDuration = longRunningTimerIntervalDuration;
-    }
-
-    get isCanceled(): boolean {
-        return this.action.isCanceled;
-    }
-
-    /**
-     * @hidden
-     * Cancel this timer task.
-     * It errors out if the task has already completed.
-     */
-    public cancel(): void {
-        if (this.hasResult) {
-            throw Error("Cannot cancel a completed task.");
-        }
-        this.action.isCanceled = true;
-    }
-
-    /**
-     * @hidden
-     * Attempts to set a value to this timer, given a completed sub-timer
-     *
-     * @param child
-     * The sub-timer that just completed
-     */
-    public trySetValue(child: DFTimerTask): void {
-        const currentTime = this.orchestrationContext.currentUtcDateTime;
-        const finalFireTime = this.action.fireAt;
-        if (finalFireTime > currentTime) {
-            const nextTimer: DFTimerTask = this.getNextTimerTask(finalFireTime, currentTime);
-            this.addNewChild(nextTimer);
-        }
-        super.trySetValue(child);
-    }
-
-    private getNextTimerTask(finalFireTime: Date, currentTime: Date): DFTimerTask {
-        const durationUntilFire = moment.duration(moment(finalFireTime).diff(currentTime));
-        const nextFireTime: Date =
-            durationUntilFire > this.maximumTimerDuration
-                ? moment(currentTime).add(this.longRunningTimerIntervalDuration).toDate()
-                : finalFireTime;
-        return new DFTimerTask(false, new CreateTimerAction(nextFireTime));
-    }
-
-    private addNewChild(childTimer: DFTimerTask): void {
-        childTimer.parent = this;
-        this.children.push(childTimer);
-        this.executor.trackOpenTask(childTimer);
-    }
-}
-
-/**
- * @hidden
- *
- * A `-WithRetry` Task.
- * It is modeled after a `WhenAllTask` because it decomposes
- * into several sub-tasks (a growing sequence of timers and atomic tasks)
- * that all need to complete before this task reaches an end-value.
- */
-export class RetryableTask extends WhenAllTask {
-    private isWaitingOnTimer: boolean;
-    private attemptNumber: number;
-    private error: any;
-
-    /**
-     * @hidden
-     * Construct a retriable task.
-     *
-     * @param innerTask
-     *  The task representing the work to retry
-     * @param retryOptions
-     *  The retrying settings
-     * @param executor
-     *  The taskOrchestrationExecutor managing the replay,
-     *  we use to to scheduling new tasks (timers and retries)
-     */
-    constructor(
-        public innerTask: DFTask,
-        private retryOptions: RetryOptions,
-        private executor: TaskOrchestrationExecutor
-    ) {
-        super([innerTask], innerTask.actionObj);
-        this.attemptNumber = 1;
-        this.isWaitingOnTimer = false;
-    }
-
-    /**
-     * @hidden
-     * Attempts to set a value to this task, given a completed sub-task
-     *
-     * @param child
-     *  The sub-task that just completed
-     */
-    public trySetValue(child: TaskBase): void {
-        // Case 1 - child is a timer task
-        if (this.isWaitingOnTimer) {
-            this.isWaitingOnTimer = false;
-
-            // If we're out of retry attempts, we can set the output value
-            // of this task to be that of the last error we encountered
-            if (this.attemptNumber > this.retryOptions.maxNumberOfAttempts) {
-                this.setValue(true, this.error);
-            } else {
-                // If we still have more attempts available, we re-schedule the
-                // original task. Since these sub-tasks are not user-managed,
-                // they are declared as internal tasks.
-                const rescheduledTask = new NoOpTask();
-                rescheduledTask.parent = this;
-                this.children.push(rescheduledTask);
-                this.executor.trackOpenTask(rescheduledTask);
-            }
-        } // Case 2 - child is the API to retry, and it succeeded
-        else if (child.stateObj === TaskState.Completed) {
-            // If we have a successful non-timer task, we accept its result
-            this.setValue(false, child.result);
-        } // Case 3 - child is the API to retry, and it failed
-        else {
-            // If the sub-task failed, schedule timer to retry again.
-            // Since these sub-tasks are not user-managed, they are declared as internal tasks.
-            const rescheduledTask = new NoOpTask();
-            rescheduledTask.parent = this;
-            this.children.push(rescheduledTask);
-            this.executor.trackOpenTask(rescheduledTask);
-            this.isWaitingOnTimer = true;
-            this.error = child.result;
-            this.attemptNumber++;
-        }
-    }
-}
diff --git a/src/task/AtomicTask.ts b/src/task/AtomicTask.ts
new file mode 100644
index 0000000..7595c4a
--- /dev/null
+++ b/src/task/AtomicTask.ts
@@ -0,0 +1,3 @@
+import { DFTask } from "./DFTask";
+
+export class AtomicTask extends DFTask {}
diff --git a/src/task/CallHttpWithPollingTask.ts b/src/task/CallHttpWithPollingTask.ts
new file mode 100644
index 0000000..be53d90
--- /dev/null
+++ b/src/task/CallHttpWithPollingTask.ts
@@ -0,0 +1,94 @@
+import { CallHttpAction, DurableHttpResponse } from "../classes";
+import { CompoundTask } from "./CompoundTask";
+import { AtomicTask, TaskBase, TaskID, TaskState } from ".";
+import { DurableOrchestrationContext } from "../durableorchestrationcontext";
+import { TaskOrchestrationExecutor } from "src/taskorchestrationexecutor";
+import moment = require("moment");
+
+/**
+ * @hidden
+ *
+ * CallHttp Task with polling logic
+ *
+ * If the HTTP requests returns a 202 status code with a 'Location' header,
+ * then a timer task is created, after which another HTTP request is made,
+ * until a different status code is returned.
+ *
+ * Any other result from the HTTP requests is the result of the whole task.
+ *
+ * The duration of the timer is specified by the 'Retry-After' header (in seconds)
+ * of the 202 response, or a default value specified by the durable extension is used.
+ *
+ */
+export class CallHttpWithPollingTask extends CompoundTask {
+    protected action: CallHttpAction;
+    private readonly defaultHttpAsyncRequestSleepDuration: moment.Duration;
+
+    public constructor(
+        id: TaskID,
+        action: CallHttpAction,
+        private readonly orchestrationContext: DurableOrchestrationContext,
+        private readonly executor: TaskOrchestrationExecutor,
+        defaultHttpAsyncRequestSleepTimeMillseconds: number
+    ) {
+        super([new AtomicTask(id, action)], action);
+        this.id = id;
+        this.action = action;
+        this.defaultHttpAsyncRequestSleepDuration = moment.duration(
+            defaultHttpAsyncRequestSleepTimeMillseconds,
+            "ms"
+        );
+    }
+
+    public trySetValue(child: TaskBase): void {
+        if (child.stateObj === TaskState.Completed) {
+            if (child.actionObj instanceof CallHttpAction) {
+                const resultObj = child.result as DurableHttpResponse;
+                const result = new DurableHttpResponse(
+                    resultObj.statusCode,
+                    resultObj.content,
+                    resultObj.headers
+                );
+                if (result.statusCode === 202 && result.getHeader("Location")) {
+                    const retryAfterHeaderValue = result.getHeader("Retry-After");
+                    const delay: moment.Duration = retryAfterHeaderValue
+                        ? moment.duration(retryAfterHeaderValue, "s")
+                        : this.defaultHttpAsyncRequestSleepDuration;
+
+                    const currentTime = this.orchestrationContext.currentUtcDateTime;
+                    const timerFireTime = moment(currentTime).add(delay).toDate();
+
+                    // this should be safe since both types returned by this call
+                    // (DFTimerTask and LongTimerTask) are TaskBase-conforming
+                    const timerTask = (this.orchestrationContext.createTimer(
+                        timerFireTime
+                    ) as unknown) as TaskBase;
+                    const callHttpTask = new AtomicTask(
+                        false,
+                        new CallHttpAction(this.action.httpRequest)
+                    );
+
+                    this.addNewChildren([timerTask, callHttpTask]);
+                } else {
+                    // Set the value of a non-redirect HTTP response as the value of the entire
+                    // compound task
+                    this.setValue(false, result);
+                }
+            }
+        } else {
+            // If any subtask failed, we fail the entire compound task
+            if (this.firstError === undefined) {
+                this.firstError = child.result as Error;
+                this.setValue(true, this.firstError);
+            }
+        }
+    }
+
+    private addNewChildren(children: TaskBase[]): void {
+        children.map((child) => {
+            child.parent = this;
+            this.children.push(child);
+            this.executor.trackOpenTask(child);
+        });
+    }
+}
diff --git a/src/task/CompoundTask.ts b/src/task/CompoundTask.ts
new file mode 100644
index 0000000..a6619ba
--- /dev/null
+++ b/src/task/CompoundTask.ts
@@ -0,0 +1,59 @@
+import { IAction } from "../classes";
+import { DFTask } from "./DFTask";
+import { TaskBase } from "./TaskBase";
+
+/**
+ * @hidden
+ *
+ * A task that depends on the completion of other (sub-) tasks.
+ */
+export abstract class CompoundTask extends DFTask {
+    protected firstError: Error | undefined;
+
+    /**
+     * @hidden
+     * Construct a Compound Task.
+     * Primarily sets the parent pointer of each sub-task to be `this`.
+     *
+     * @param children
+     *  The sub-tasks that this task depends on
+     * @param action
+     *  An action representing this compound task
+     */
+    constructor(public children: TaskBase[], protected action: IAction) {
+        super(false, action);
+        children.map((c) => (c.parent = this));
+        this.firstError = undefined;
+
+        // If the task has no children, throw an error
+        // See issue here for why this isn't allowed: https://github.com/Azure/azure-functions-durable-js/issues/424
+        if (children.length == 0) {
+            const message =
+                "When constructing a CompoundTask (such as Task.all() or Task.any()), you must specify at least one Task.";
+            throw new Error(message);
+        }
+    }
+
+    /**
+     * @hidden
+     * Tries to set this task's result based on the completion of a sub-task
+     * @param child
+     *  A sub-task of this task.
+     */
+    public handleCompletion(child: TaskBase): void {
+        if (!this.isPlayed) {
+            this.isPlayed = child.isPlayed;
+        }
+        this.trySetValue(child);
+    }
+
+    /**
+     * @hidden
+     *
+     * Task-internal logic for attempting to set this tasks' result
+     * after any of its sub-tasks completes.
+     * @param child
+     *  A sub-task
+     */
+    abstract trySetValue(child: TaskBase): void;
+}
diff --git a/src/task/DFTask.ts b/src/task/DFTask.ts
new file mode 100644
index 0000000..6cff05c
--- /dev/null
+++ b/src/task/DFTask.ts
@@ -0,0 +1,17 @@
+import { Task } from "durable-functions";
+import { TaskBase } from "./TaskBase";
+import { IAction } from "../classes";
+
+/**
+ * @hidden
+ * A task that should result in an Action being communicated to the DF extension.
+ */
+export class DFTask extends TaskBase implements Task {
+    protected action: IAction;
+    public alreadyScheduled = false;
+
+    /** Get this task's backing action */
+    get actionObj(): IAction {
+        return this.action;
+    }
+}
diff --git a/src/task/DFTimerTask.ts b/src/task/DFTimerTask.ts
new file mode 100644
index 0000000..9830d93
--- /dev/null
+++ b/src/task/DFTimerTask.ts
@@ -0,0 +1,41 @@
+import { TimerTask } from "durable-functions";
+import { AtomicTask } from "./AtomicTask";
+import { TaskID } from ".";
+import { CreateTimerAction } from "../classes";
+
+/**
+ * @hidden
+ * A timer task. This is the internal implementation to the user-exposed TimerTask interface, which
+ * has a more restricted API.
+ */
+export class DFTimerTask extends AtomicTask implements TimerTask {
+    /**
+     * @hidden
+     * Construct a Timer Task.
+     *
+     * @param id
+     *  The task's ID
+     * @param action
+     *  The backing action of this task
+     */
+    constructor(public id: TaskID, public action: CreateTimerAction) {
+        super(id, action);
+    }
+
+    /** Whether this timer task is canceled */
+    get isCanceled(): boolean {
+        return this.action.isCanceled;
+    }
+
+    /**
+     * @hidden
+     * Cancel this timer task.
+     * It errors out if the task has already completed.
+     */
+    public cancel(): void {
+        if (this.hasResult) {
+            throw Error("Cannot cancel a completed task.");
+        }
+        this.action.isCanceled = true;
+    }
+}
diff --git a/src/task/LongTimerTask.ts b/src/task/LongTimerTask.ts
new file mode 100644
index 0000000..2f9780c
--- /dev/null
+++ b/src/task/LongTimerTask.ts
@@ -0,0 +1,106 @@
+import { DurableOrchestrationContext, TimerTask } from "durable-functions";
+import { WhenAllTask } from "./WhenAllTask";
+import moment = require("moment");
+import { CreateTimerAction } from "../classes";
+import { TaskOrchestrationExecutor } from "../taskorchestrationexecutor";
+import { TaskID } from ".";
+import { DFTimerTask } from "./DFTimerTask";
+
+/**
+ * @hidden
+ *
+ * A long Timer Task.
+ *
+ * This Task is created when a timer is created with a duration
+ * longer than the maximum timer duration supported by storage infrastructure.
+ *
+ * It extends `WhenAllTask` because it decomposes into
+ * several smaller sub-`TimerTask`s
+ */
+export class LongTimerTask extends WhenAllTask implements TimerTask {
+    public id: TaskID;
+    public action: CreateTimerAction;
+    private readonly executor: TaskOrchestrationExecutor;
+    private readonly maximumTimerDuration: moment.Duration;
+    private readonly orchestrationContext: DurableOrchestrationContext;
+    private readonly longRunningTimerIntervalDuration: moment.Duration;
+
+    public constructor(
+        id: TaskID,
+        action: CreateTimerAction,
+        orchestrationContext: DurableOrchestrationContext,
+        executor: TaskOrchestrationExecutor,
+        maximumTimerLength: string,
+        longRunningTimerIntervalLength: string
+    ) {
+        const maximumTimerDuration = moment.duration(maximumTimerLength);
+        const longRunningTimerIntervalDuration = moment.duration(longRunningTimerIntervalLength);
+        const currentTime = orchestrationContext.currentUtcDateTime;
+        const finalFireTime = action.fireAt;
+        const durationUntilFire = moment.duration(moment(finalFireTime).diff(currentTime));
+
+        const nextFireTime: Date =
+            durationUntilFire > maximumTimerDuration
+                ? moment(currentTime).add(longRunningTimerIntervalDuration).toDate()
+                : finalFireTime;
+
+        const nextTimerAction = new CreateTimerAction(nextFireTime);
+        const nextTimerTask = new DFTimerTask(false, nextTimerAction);
+        super([nextTimerTask], action);
+
+        this.id = id;
+        this.action = action;
+        this.orchestrationContext = orchestrationContext;
+        this.executor = executor;
+        this.maximumTimerDuration = maximumTimerDuration;
+        this.longRunningTimerIntervalDuration = longRunningTimerIntervalDuration;
+    }
+
+    get isCanceled(): boolean {
+        return this.action.isCanceled;
+    }
+
+    /**
+     * @hidden
+     * Cancel this timer task.
+     * It errors out if the task has already completed.
+     */
+    public cancel(): void {
+        if (this.hasResult) {
+            throw Error("Cannot cancel a completed task.");
+        }
+        this.action.isCanceled = true;
+    }
+
+    /**
+     * @hidden
+     * Attempts to set a value to this timer, given a completed sub-timer
+     *
+     * @param child
+     * The sub-timer that just completed
+     */
+    public trySetValue(child: DFTimerTask): void {
+        const currentTime = this.orchestrationContext.currentUtcDateTime;
+        const finalFireTime = this.action.fireAt;
+        if (finalFireTime > currentTime) {
+            const nextTimer: DFTimerTask = this.getNextTimerTask(finalFireTime, currentTime);
+            this.addNewChild(nextTimer);
+        }
+        super.trySetValue(child);
+    }
+
+    private getNextTimerTask(finalFireTime: Date, currentTime: Date): DFTimerTask {
+        const durationUntilFire = moment.duration(moment(finalFireTime).diff(currentTime));
+        const nextFireTime: Date =
+            durationUntilFire > this.maximumTimerDuration
+                ? moment(currentTime).add(this.longRunningTimerIntervalDuration).toDate()
+                : finalFireTime;
+        return new DFTimerTask(false, new CreateTimerAction(nextFireTime));
+    }
+
+    private addNewChild(childTimer: DFTimerTask): void {
+        childTimer.parent = this;
+        this.children.push(childTimer);
+        this.executor.trackOpenTask(childTimer);
+    }
+}
diff --git a/src/task/NoOpTask.ts b/src/task/NoOpTask.ts
new file mode 100644
index 0000000..b010abe
--- /dev/null
+++ b/src/task/NoOpTask.ts
@@ -0,0 +1,17 @@
+import { TaskBase } from "./TaskBase";
+
+/**
+ * @hidden
+ *
+ * A task created only to facilitate replay, it should not communicate any
+ * actions to the DF extension.
+ *
+ * We internally track these kinds of tasks to reason over the completion of
+ * DF APIs that decompose into smaller DF APIs that the user didn't explicitly
+ * schedule.
+ */
+export class NoOpTask extends TaskBase {
+    constructor() {
+        super(false, "noOp");
+    }
+}
diff --git a/src/task/RetryableTask.ts b/src/task/RetryableTask.ts
new file mode 100644
index 0000000..ce04912
--- /dev/null
+++ b/src/task/RetryableTask.ts
@@ -0,0 +1,86 @@
+import { RetryOptions } from "durable-functions";
+import { TaskOrchestrationExecutor } from "../taskorchestrationexecutor";
+import { TaskState } from ".";
+import { DFTask } from "./DFTask";
+import { NoOpTask } from "./NoOpTask";
+import { TaskBase } from "./TaskBase";
+import { WhenAllTask } from "./WhenAllTask";
+
+/**
+ * @hidden
+ *
+ * A `-WithRetry` Task.
+ * It is modeled after a `WhenAllTask` because it decomposes
+ * into several sub-tasks (a growing sequence of timers and atomic tasks)
+ * that all need to complete before this task reaches an end-value.
+ */
+export class RetryableTask extends WhenAllTask {
+    private isWaitingOnTimer: boolean;
+    private attemptNumber: number;
+    private error: any;
+
+    /**
+     * @hidden
+     * Construct a retriable task.
+     *
+     * @param innerTask
+     *  The task representing the work to retry
+     * @param retryOptions
+     *  The retrying settings
+     * @param executor
+     *  The taskOrchestrationExecutor managing the replay,
+     *  we use to to scheduling new tasks (timers and retries)
+     */
+    constructor(
+        public innerTask: DFTask,
+        private retryOptions: RetryOptions,
+        private executor: TaskOrchestrationExecutor
+    ) {
+        super([innerTask], innerTask.actionObj);
+        this.attemptNumber = 1;
+        this.isWaitingOnTimer = false;
+    }
+
+    /**
+     * @hidden
+     * Attempts to set a value to this task, given a completed sub-task
+     *
+     * @param child
+     *  The sub-task that just completed
+     */
+    public trySetValue(child: TaskBase): void {
+        // Case 1 - child is a timer task
+        if (this.isWaitingOnTimer) {
+            this.isWaitingOnTimer = false;
+
+            // If we're out of retry attempts, we can set the output value
+            // of this task to be that of the last error we encountered
+            if (this.attemptNumber > this.retryOptions.maxNumberOfAttempts) {
+                this.setValue(true, this.error);
+            } else {
+                // If we still have more attempts available, we re-schedule the
+                // original task. Since these sub-tasks are not user-managed,
+                // they are declared as internal tasks.
+                const rescheduledTask = new NoOpTask();
+                rescheduledTask.parent = this;
+                this.children.push(rescheduledTask);
+                this.executor.trackOpenTask(rescheduledTask);
+            }
+        } // Case 2 - child is the API to retry, and it succeeded
+        else if (child.stateObj === TaskState.Completed) {
+            // If we have a successful non-timer task, we accept its result
+            this.setValue(false, child.result);
+        } // Case 3 - child is the API to retry, and it failed
+        else {
+            // If the sub-task failed, schedule timer to retry again.
+            // Since these sub-tasks are not user-managed, they are declared as internal tasks.
+            const rescheduledTask = new NoOpTask();
+            rescheduledTask.parent = this;
+            this.children.push(rescheduledTask);
+            this.executor.trackOpenTask(rescheduledTask);
+            this.isWaitingOnTimer = true;
+            this.error = child.result;
+            this.attemptNumber++;
+        }
+    }
+}
diff --git a/src/task/TaskBase.ts b/src/task/TaskBase.ts
new file mode 100644
index 0000000..f02bf36
--- /dev/null
+++ b/src/task/TaskBase.ts
@@ -0,0 +1,87 @@
+import { BackingAction, TaskID, TaskState } from ".";
+import { CompoundTask } from "./CompoundTask";
+
+/**
+ * @hidden
+ * Base class for all Tasks, defines the basic state transitions for all tasks.
+ */
+export abstract class TaskBase {
+    public state: TaskState;
+    public parent: CompoundTask | undefined;
+    public isPlayed: boolean;
+    public result: unknown;
+
+    /**
+     * @hidden
+     *
+     * Construct a task.
+     * @param id
+     *  The task's ID
+     * @param action
+     *  The task's backing action
+     */
+    constructor(public id: TaskID, protected action: BackingAction) {
+        this.state = TaskState.Running;
+    }
+
+    /** Get this task's backing action */
+    get actionObj(): BackingAction {
+        return this.action;
+    }
+
+    /** Get this task's current state */
+    get stateObj(): TaskState {
+        return this.state;
+    }
+
+    /** Whether this task is not in the Running state */
+    get hasResult(): boolean {
+        return this.state !== TaskState.Running;
+    }
+
+    get isFaulted(): boolean {
+        return this.state === TaskState.Failed;
+    }
+
+    get isCompleted(): boolean {
+        return this.state === TaskState.Completed;
+    }
+
+    /** Change this task from the Running state to a completed state */
+    private changeState(state: TaskState): void {
+        if (state === TaskState.Running) {
+            throw Error("Cannot change Task to the RUNNING state.");
+        }
+        this.state = state;
+    }
+
+    /** Attempt to set a result for this task, and notifies parents, if any */
+    public setValue(isError: boolean, value: unknown): void {
+        let newState: TaskState;
+
+        if (isError) {
+            if (!(value instanceof Error)) {
+                const errMessage = `Task ID ${this.id} failed but it's value was not an Exception`;
+                throw new Error(errMessage);
+            }
+            newState = TaskState.Failed;
+        } else {
+            newState = TaskState.Completed;
+        }
+
+        this.changeState(newState);
+        this.result = value;
+        this.propagate();
+    }
+
+    /**
+     * @hidden
+     * Notifies this task's parents about its state change.
+     */
+    private propagate(): void {
+        const hasCompleted = this.state !== TaskState.Running;
+        if (hasCompleted && this.parent !== undefined) {
+            this.parent.handleCompletion(this);
+        }
+    }
+}
diff --git a/src/task/WhenAllTask.ts b/src/task/WhenAllTask.ts
new file mode 100644
index 0000000..2a689ce
--- /dev/null
+++ b/src/task/WhenAllTask.ts
@@ -0,0 +1,49 @@
+import { IAction } from "../classes";
+import { CompoundTask } from "./CompoundTask";
+import { TaskBase } from "./TaskBase";
+import { AtomicTask } from "./AtomicTask";
+import { TaskState } from ".";
+
+/**
+ * @hidden
+ *
+ * A WhenAll task.
+ */
+export class WhenAllTask extends CompoundTask {
+    /**
+     * @hidden
+     * Construct a WhenAll task.
+     *
+     * @param children
+     *  Sub-tasks to wait on.
+     * @param action
+     *  A the backing action representing this task.
+     */
+    constructor(public children: TaskBase[], protected action: IAction) {
+        super(children, action);
+    }
+
+    /**
+     * @hidden
+     * Attempts to set a value to this task, given a completed sub-task
+     *
+     * @param child
+     *  The sub-task that just completed
+     */
+    public trySetValue(child: AtomicTask): void {
+        if (child.stateObj === TaskState.Completed) {
+            // We set the result only after all sub-tasks have completed
+            if (this.children.every((c) => c.stateObj === TaskState.Completed)) {
+                // The result is a list of all sub-task's results
+                const results = this.children.map((c) => c.result);
+                this.setValue(false, results);
+            }
+        } else {
+            // If any task failed, we fail the entire compound task
+            if (this.firstError === undefined) {
+                this.firstError = child.result as Error;
+                this.setValue(true, this.firstError);
+            }
+        }
+    }
+}
diff --git a/src/task/WhenAnyTask.ts b/src/task/WhenAnyTask.ts
new file mode 100644
index 0000000..10d992c
--- /dev/null
+++ b/src/task/WhenAnyTask.ts
@@ -0,0 +1,28 @@
+import { TaskState } from ".";
+import { CompoundTask } from "./CompoundTask";
+import { TaskBase } from "./TaskBase";
+
+/**
+ * @hidden
+ *
+ * A WhenAny task.
+ */
+export class WhenAnyTask extends CompoundTask {
+    /**
+     * @hidden
+     * Attempts to set a value to this task, given a completed sub-task
+     *
+     * @param child
+     *  The sub-task that just completed
+     */
+    public trySetValue(child: TaskBase): void {
+        // For a Task to have isError = true, it needs to contain within an Exception/Error datatype.
+        // However, WhenAny only contains Tasks as its result, so technically it "never errors out".
+        // The isError flag is used simply to determine if the result of the task should be fed in
+        // as a value, or as a raised exception to the generator code. For WhenAny, we always feed
+        // in the result as a value.
+        if (this.state === TaskState.Running) {
+            this.setValue(false, child);
+        }
+    }
+}
diff --git a/src/task/index.ts b/src/task/index.ts
new file mode 100644
index 0000000..4263862
--- /dev/null
+++ b/src/task/index.ts
@@ -0,0 +1,37 @@
+import { IAction } from "../classes";
+
+/**
+ * @hidden
+ * The states a task can be in
+ */
+export enum TaskState {
+    Running,
+    Failed,
+    Completed,
+}
+
+/**
+ * @hidden
+ * A taskID, either a `string` for external events,
+ * or either `false` or a `number` for un-awaited
+ * and awaited tasks respectively.
+ */
+export type TaskID = number | string | false;
+
+/**
+ * @hidden
+ * A backing action, either a proper action or "noOp" for an internal-only task
+ */
+export type BackingAction = IAction | "noOp";
+
+export * from "./TaskBase";
+export * from "./DFTask";
+export * from "./CompoundTask";
+export * from "./AtomicTask";
+export * from "./NoOpTask";
+export * from "./DFTimerTask";
+export * from "./CallHttpWithPollingTask";
+export * from "./LongTimerTask";
+export * from "./RetryableTask";
+export * from "./WhenAllTask";
+export * from "./WhenAnyTask";
diff --git a/src/taskorchestrationexecutor.ts b/src/taskorchestrationexecutor.ts
index 0341f99..37da832 100644
--- a/src/taskorchestrationexecutor.ts
+++ b/src/taskorchestrationexecutor.ts
@@ -11,11 +11,11 @@ import {
     TaskCompletedEvent,
     WaitForExternalEventAction,
 } from "./classes";
-import { OrchestrationFailureError } from "./orchestrationfailureerror";
+import { OrchestrationFailureError } from "./error/OrchestrationFailureError";
 import { OrchestratorState } from "./orchestratorstate";
 import { TaskBase, NoOpTask, DFTask, CompoundTask, TaskState } from "./task";
 import { ReplaySchema } from "./replaySchema";
-import { Utils } from "./utils";
+import { Utils } from "./util/Utils";
 import { DurableOrchestrationContext, OrchestrationContext } from "durable-functions";
 
 /**
diff --git a/src/utils.ts b/src/util/Utils.ts
similarity index 97%
rename from src/utils.ts
rename to src/util/Utils.ts
index 6c296e3..71111f0 100644
--- a/src/utils.ts
+++ b/src/util/Utils.ts
@@ -1,121 +1,121 @@
-/** @hidden */
-export class Utils {
-    public static processInput<T>(input: string | T): string | T {
-        // If we fail to stringify inputs, they may get deserialized incorrectly.
-        // For instance: "13131" might get interpreted as a number.
-        // Somehow this doesn't appear to occur with other datatypes, but we should
-        // investigate that further.
-        if (typeof input === "string") {
-            input = JSON.stringify(input);
-        }
-        return input;
-    }
-    public static getInstancesOf<T>(
-        collection: { [index: string]: unknown },
-        abstractClass: T // this should be an abstract class that contains only the _required_ properties
-    ): T[] {
-        if (collection && abstractClass) {
-            const candidateObjects = Object.values(collection).filter((value) =>
-                this.hasAllPropertiesOf(value, abstractClass)
-            );
-
-            this.parseTimestampsAsDates(candidateObjects);
-
-            return candidateObjects as T[];
-        }
-        return [];
-    }
-
-    public static getHrMilliseconds(times: number[]): number {
-        return times[0] * 1000 + times[1] / 1e6;
-    }
-
-    public static hasStringProperty<X extends {}, Y extends PropertyKey>(
-        obj: X,
-        prop: Y
-    ): obj is X & Record<Y, string> {
-        if (Utils.hasOwnProperty(obj, prop)) {
-            const propKey = prop as keyof typeof obj;
-            const property = obj[propKey];
-            const propertyIsString = typeof property === "string";
-            return propertyIsString;
-        }
-        return false;
-    }
-
-    public static hasOwnProperty<X extends {}, Y extends PropertyKey>(
-        obj: X,
-        prop: Y
-    ): obj is X & Record<Y, unknown> {
-        // informs TS that an object has a property
-        return obj.hasOwnProperty(prop);
-    }
-
-    public static parseTimestampsAsDates(obj: unknown): void {
-        if (typeof obj === "object" && obj != null) {
-            if (this.hasOwnProperty(obj, "Timestamp") && typeof obj.Timestamp === "string") {
-                obj.Timestamp = new Date(obj.Timestamp);
-            }
-            Object.values(obj).map((value) => {
-                // This recursive step ensures _all_ Timestamp properties are converted properly
-                // For example, a payload can contain the history as a property, so if we want to
-                // parse each HistoryEvent's Timestamp, we need to traverse the payload recursively
-                this.parseTimestampsAsDates(value);
-            });
-        }
-    }
-
-    public static hasAllPropertiesOf<T>(obj: unknown, refInstance: T): boolean {
-        return (
-            typeof refInstance === "object" &&
-            typeof obj === "object" &&
-            obj !== null &&
-            Object.keys(refInstance).every((key: string) => {
-                return obj.hasOwnProperty(key);
-            })
-        );
-    }
-
-    public static ensureNonNull<T>(argument: T | undefined, message: string): T {
-        if (argument === undefined) {
-            throw new TypeError(message);
-        }
-
-        return argument;
-    }
-
-    public static async sleep(delayInMilliseconds: number): Promise<void> {
-        await new Promise((resolve) => setTimeout(resolve, delayInMilliseconds));
-    }
-
-    public static throwIfNotInstanceOf<T>(
-        value: unknown,
-        name: string,
-        refInstance: T,
-        type: string
-    ): void {
-        if (!this.hasAllPropertiesOf<T>(value, refInstance)) {
-            throw new TypeError(
-                `${name}: Expected object of type ${type} but got ${typeof value}; are you missing properties?`
-            );
-        }
-    }
-
-    public static throwIfEmpty(value: unknown, name: string): void {
-        if (typeof value !== "string") {
-            throw new TypeError(
-                `${name}: Expected non-empty, non-whitespace string but got ${typeof value}`
-            );
-        } else if (value.trim().length < 1) {
-            throw new Error(
-                `${name}: Expected non-empty, non-whitespace string but got empty string`
-            );
-        }
-    }
-
-    public static throwIfNotNumber(value: unknown, name: string): void {
-        if (typeof value !== "number") {
-            throw new TypeError(`${name}: Expected number but got ${typeof value}`);
-        }
-    }
-}
+/** @hidden */
+export class Utils {
+    public static processInput<T>(input: string | T): string | T {
+        // If we fail to stringify inputs, they may get deserialized incorrectly.
+        // For instance: "13131" might get interpreted as a number.
+        // Somehow this doesn't appear to occur with other datatypes, but we should
+        // investigate that further.
+        if (typeof input === "string") {
+            input = JSON.stringify(input);
+        }
+        return input;
+    }
+    public static getInstancesOf<T>(
+        collection: { [index: string]: unknown },
+        abstractClass: T // this should be an abstract class that contains only the _required_ properties
+    ): T[] {
+        if (collection && abstractClass) {
+            const candidateObjects = Object.values(collection).filter((value) =>
+                this.hasAllPropertiesOf(value, abstractClass)
+            );
+
+            this.parseTimestampsAsDates(candidateObjects);
+
+            return candidateObjects as T[];
+        }
+        return [];
+    }
+
+    public static getHrMilliseconds(times: number[]): number {
+        return times[0] * 1000 + times[1] / 1e6;
+    }
+
+    public static hasStringProperty<X extends {}, Y extends PropertyKey>(
+        obj: X,
+        prop: Y
+    ): obj is X & Record<Y, string> {
+        if (Utils.hasOwnProperty(obj, prop)) {
+            const propKey = prop as keyof typeof obj;
+            const property = obj[propKey];
+            const propertyIsString = typeof property === "string";
+            return propertyIsString;
+        }
+        return false;
+    }
+
+    public static hasOwnProperty<X extends {}, Y extends PropertyKey>(
+        obj: X,
+        prop: Y
+    ): obj is X & Record<Y, unknown> {
+        // informs TS that an object has a property
+        return obj.hasOwnProperty(prop);
+    }
+
+    public static parseTimestampsAsDates(obj: unknown): void {
+        if (typeof obj === "object" && obj != null) {
+            if (this.hasOwnProperty(obj, "Timestamp") && typeof obj.Timestamp === "string") {
+                obj.Timestamp = new Date(obj.Timestamp);
+            }
+            Object.values(obj).map((value) => {
+                // This recursive step ensures _all_ Timestamp properties are converted properly
+                // For example, a payload can contain the history as a property, so if we want to
+                // parse each HistoryEvent's Timestamp, we need to traverse the payload recursively
+                this.parseTimestampsAsDates(value);
+            });
+        }
+    }
+
+    public static hasAllPropertiesOf<T>(obj: unknown, refInstance: T): boolean {
+        return (
+            typeof refInstance === "object" &&
+            typeof obj === "object" &&
+            obj !== null &&
+            Object.keys(refInstance).every((key: string) => {
+                return obj.hasOwnProperty(key);
+            })
+        );
+    }
+
+    public static ensureNonNull<T>(argument: T | undefined, message: string): T {
+        if (argument === undefined) {
+            throw new TypeError(message);
+        }
+
+        return argument;
+    }
+
+    public static async sleep(delayInMilliseconds: number): Promise<void> {
+        await new Promise((resolve) => setTimeout(resolve, delayInMilliseconds));
+    }
+
+    public static throwIfNotInstanceOf<T>(
+        value: unknown,
+        name: string,
+        refInstance: T,
+        type: string
+    ): void {
+        if (!this.hasAllPropertiesOf<T>(value, refInstance)) {
+            throw new TypeError(
+                `${name}: Expected object of type ${type} but got ${typeof value}; are you missing properties?`
+            );
+        }
+    }
+
+    public static throwIfEmpty(value: unknown, name: string): void {
+        if (typeof value !== "string") {
+            throw new TypeError(
+                `${name}: Expected non-empty, non-whitespace string but got ${typeof value}`
+            );
+        } else if (value.trim().length < 1) {
+            throw new Error(
+                `${name}: Expected non-empty, non-whitespace string but got empty string`
+            );
+        }
+    }
+
+    public static throwIfNotNumber(value: unknown, name: string): void {
+        if (typeof value !== "number") {
+            throw new TypeError(`${name}: Expected number but got ${typeof value}`);
+        }
+    }
+}
diff --git a/src/webhookutils.ts b/src/util/WebhookUtils.ts
similarity index 96%
rename from src/webhookutils.ts
rename to src/util/WebhookUtils.ts
index ae50f2c..b3f8c37 100644
--- a/src/webhookutils.ts
+++ b/src/util/WebhookUtils.ts
@@ -1,55 +1,55 @@
-/** @hidden */
-export class WebhookUtils {
-    public static getReadEntityUrl(
-        baseUrl: string,
-        requiredQueryStrings: string,
-        entityName: string,
-        entityKey: string,
-        taskHubName?: string,
-        connectionName?: string
-    ): string {
-        let requestUrl = baseUrl + "/entities/" + entityName + "/" + entityKey + "?";
-
-        const queryStrings: string[] = [];
-        if (taskHubName) {
-            queryStrings.push("taskHub=" + taskHubName);
-        }
-
-        if (connectionName) {
-            queryStrings.push("connection=" + connectionName);
-        }
-
-        queryStrings.push(requiredQueryStrings);
-        requestUrl += queryStrings.join("&");
-        return requestUrl;
-    }
-
-    public static getSignalEntityUrl(
-        baseUrl: string,
-        requiredQueryStrings: string,
-        entityName: string,
-        entityKey: string,
-        operationName?: string,
-        taskHubName?: string,
-        connectionName?: string
-    ): string {
-        let requestUrl = baseUrl + "/entities/" + entityName + "/" + entityKey + "?";
-
-        const queryStrings: string[] = [];
-        if (operationName) {
-            queryStrings.push("op=" + operationName);
-        }
-
-        if (taskHubName) {
-            queryStrings.push("taskHub=" + taskHubName);
-        }
-
-        if (connectionName) {
-            queryStrings.push("connection=" + connectionName);
-        }
-
-        queryStrings.push(requiredQueryStrings);
-        requestUrl += queryStrings.join("&");
-        return requestUrl;
-    }
-}
+/** @hidden */
+export class WebhookUtils {
+    public static getReadEntityUrl(
+        baseUrl: string,
+        requiredQueryStrings: string,
+        entityName: string,
+        entityKey: string,
+        taskHubName?: string,
+        connectionName?: string
+    ): string {
+        let requestUrl = baseUrl + "/entities/" + entityName + "/" + entityKey + "?";
+
+        const queryStrings: string[] = [];
+        if (taskHubName) {
+            queryStrings.push("taskHub=" + taskHubName);
+        }
+
+        if (connectionName) {
+            queryStrings.push("connection=" + connectionName);
+        }
+
+        queryStrings.push(requiredQueryStrings);
+        requestUrl += queryStrings.join("&");
+        return requestUrl;
+    }
+
+    public static getSignalEntityUrl(
+        baseUrl: string,
+        requiredQueryStrings: string,
+        entityName: string,
+        entityKey: string,
+        operationName?: string,
+        taskHubName?: string,
+        connectionName?: string
+    ): string {
+        let requestUrl = baseUrl + "/entities/" + entityName + "/" + entityKey + "?";
+
+        const queryStrings: string[] = [];
+        if (operationName) {
+            queryStrings.push("op=" + operationName);
+        }
+
+        if (taskHubName) {
+            queryStrings.push("taskHub=" + taskHubName);
+        }
+
+        if (connectionName) {
+            queryStrings.push("connection=" + connectionName);
+        }
+
+        queryStrings.push(requiredQueryStrings);
+        requestUrl += queryStrings.join("&");
+        return requestUrl;
+    }
+}


Test patch:
diff --git a/src/testingUtils.ts b/src/util/testingUtils.ts
similarity index 98%
rename from src/testingUtils.ts
rename to src/util/testingUtils.ts
index 697fbb8..0ab88ba 100644
--- a/src/testingUtils.ts
+++ b/src/util/testingUtils.ts
@@ -5,8 +5,8 @@ import {
     HistoryEvent,
     HistoryEventOptions,
     OrchestratorStartedEvent,
-} from "./classes";
-import { ReplaySchema } from "./replaySchema";
+} from "../classes";
+import { ReplaySchema } from "../replaySchema";
 import * as uuidv1 from "uuid/v1";
 import { DurableEntityContext, EntityContext, OrchestrationContext } from "durable-functions";
 import * as types from "durable-functions";
diff --git a/test/integration/entity-spec.ts b/test/integration/entity-spec.ts
index fa0ba05..763f929 100644
--- a/test/integration/entity-spec.ts
+++ b/test/integration/entity-spec.ts
@@ -1,7 +1,7 @@
 import { expect } from "chai";
 import "mocha";
 import { EntityState } from "../../src/classes";
-import { DummyEntityContext } from "../../src/testingUtils";
+import { DummyEntityContext } from "../../src/util/testingUtils";
 import { TestEntities } from "../testobjects/testentities";
 import { TestEntityBatches } from "../testobjects/testentitybatches";
 import { StringStoreOperation } from "../testobjects/testentityoperations";
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index a742d1e..7e421c7 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -25,9 +25,9 @@ import {
     RetryOptions,
     WaitForExternalEventAction,
 } from "../../src/classes";
-import { OrchestrationFailureError } from "../../src/orchestrationfailureerror";
+import { OrchestrationFailureError } from "../../src/error/OrchestrationFailureError";
 import { ReplaySchema } from "../../src/replaySchema";
-import { DurableOrchestrationInput } from "../../src/testingUtils";
+import { DurableOrchestrationInput } from "../../src/util/testingUtils";
 import { TestHistories } from "../testobjects/testhistories";
 import { TestOrchestrations } from "../testobjects/TestOrchestrations";
 import { TestUtils } from "../testobjects/testutils";
diff --git a/test/testobjects/testutils.ts b/test/testobjects/testutils.ts
index 87c4d16..52ba775 100644
--- a/test/testobjects/testutils.ts
+++ b/test/testobjects/testutils.ts
@@ -4,7 +4,7 @@ import {
     IOrchestratorState,
     OrchestrationClientInputData,
 } from "../../src/classes";
-import { OrchestrationFailureError } from "../../src/orchestrationfailureerror";
+import { OrchestrationFailureError } from "../../src/error/OrchestrationFailureError";
 import { TestConstants } from "./testconstants";
 
 export class TestUtils {