Repository: Azure/azure-functions-durable-js (ts)

Issue:
Simplify the `client.startNew()` experience
Based on discussion [here](https://github.com/ejizba/func-nodejs-prototype/pull/14#discussion_r1044721159). Currently, the default HTTP starter `client.startNew()` call looks like this:

```TS
const instanceId = await client.startNew(request.params.orchestratorName, undefined, request.text());
```

Ideally, `client.startNew()` should accept one single object as an argument, rather than 3 distinct arguments, so that:

1. It's clearer what each argument is for
2. Order doesn't matter
3. Unnecessary arguments can be omitted instead of passing in `undefined`.

Since this would be a breaking change, it might be better to include this change with the rest of the v3.x new programming model changes. 

Pull request:
Switch `client.startNew()` to using object for optional arguments
Fixes #415. See issue for details

Patch:
diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml
index b267a3f..d3f26c7 100644
--- a/azure-pipelines-release.yml
+++ b/azure-pipelines-release.yml
@@ -16,7 +16,7 @@ resources:
         - pipeline: DurableJSCI
           project: "Azure Functions"
           source: azure-functions-durable-js
-          branch: main
+          branch: v3.x
 
 jobs:
     - job: Release
diff --git a/src/durablehttprequest.ts b/src/durablehttprequest.ts
index 3b9686b..04f0c91 100644
--- a/src/durablehttprequest.ts
+++ b/src/durablehttprequest.ts
@@ -1,4 +1,4 @@
-import { TokenSource } from "./tokensource";
+import { TokenSource } from "./types";
 
 /**
  * Data structure representing a durable HTTP request.
diff --git a/src/durableorchestrationclient.ts b/src/durableorchestrationclient.ts
index 197eef4..a726a33 100644
--- a/src/durableorchestrationclient.ts
+++ b/src/durableorchestrationclient.ts
@@ -24,7 +24,7 @@ import {
     PurgeHistoryResult,
     Utils,
 } from "./classes";
-import { DurableClientInput } from "./types";
+import { StartNewOptions, DurableClientInput } from "./types";
 import { WebhookUtils } from "./webhookutils";
 
 /** @hidden */
@@ -34,17 +34,7 @@ const URL = url.URL;
  * Returns an OrchestrationClient instance.
  * @param context The context object of the Azure function whose body
  *  calls this method.
- * @example Get an orchestration client instance
- * ```javascript
- * const df = require("durable-functions");
  *
- * module.exports = async function (context, req) {
- *     const client = df.getClient(context);
- *     const instanceId = await client.startNew(req.params.functionName, undefined, req.body);
- *
- *     return client.createCheckStatusResponse(req, instanceId);
- * };
- * ```
  */
 export function getClient(
     context: InvocationContext,
@@ -648,17 +638,12 @@ export class DurableOrchestrationClient {
      * existing instance will be silently replaced by this new instance.
      * @param orchestratorFunctionName The name of the orchestrator function to
      *  start.
-     * @param instanceId The ID to use for the new orchestration instance. If
-     *  no instanceId is specified, the Durable Functions extension will
-     *  generate a random GUID (recommended).
-     * @param input JSON-serializable input value for the orchestrator
-     *  function.
+     * @param options optional object to control the scheduled orchestrator (e.g provide input, instanceID)
      * @returns The ID of the new orchestration instance.
      */
     public async startNew(
         orchestratorFunctionName: string,
-        instanceId?: string,
-        input?: unknown
+        options?: StartNewOptions
     ): Promise<string> {
         if (!orchestratorFunctionName) {
             throw new Error("orchestratorFunctionName must be a valid string.");
@@ -666,10 +651,11 @@ export class DurableOrchestrationClient {
 
         // TODO: Add support for specifying a task hub and a connection name
         let requestUrl: string;
+        const instanceIdPath: string = options?.instanceId ? `/${options.instanceId}` : "";
         if (this.clientData.rpcBaseUrl) {
             // Fast local RPC path
             requestUrl = new URL(
-                `orchestrators/${orchestratorFunctionName}${instanceId ? `/${instanceId}` : ""}`,
+                `orchestrators/${orchestratorFunctionName}${instanceIdPath}`,
                 this.clientData.rpcBaseUrl
             ).href;
         } else {
@@ -677,13 +663,11 @@ export class DurableOrchestrationClient {
             requestUrl = this.clientData.creationUrls.createNewInstancePostUri;
             requestUrl = requestUrl
                 .replace(this.functionNamePlaceholder, orchestratorFunctionName)
-                .replace(this.instanceIdPlaceholder, instanceId ? `/${instanceId}` : "");
+                .replace(this.instanceIdPlaceholder, instanceIdPath);
         }
 
-        const response = await this.axiosInstance.post(
-            requestUrl,
-            input !== undefined ? JSON.stringify(input) : ""
-        );
+        const input: unknown = options?.input !== undefined ? JSON.stringify(options.input) : "";
+        const response = await this.axiosInstance.post(requestUrl, input);
         if (response.data && response.status <= 202) {
             return (response.data as HttpManagementPayload).id;
         } else {
diff --git a/src/durableorchestrationcontext.ts b/src/durableorchestrationcontext.ts
index 05c95e4..2ee589d 100644
--- a/src/durableorchestrationcontext.ts
+++ b/src/durableorchestrationcontext.ts
@@ -1,4 +1,3 @@
-import { TokenSource } from "./tokensource";
 import {
     EntityId,
     RetryOptions,
@@ -33,6 +32,7 @@ import {
 } from "./task";
 import moment = require("moment");
 import { ReplaySchema } from "./replaySchema";
+import { CallHttpOptions } from "./types";
 
 /**
  * Parameter data for orchestration bindings that can be used to schedule
@@ -301,28 +301,24 @@ export class DurableOrchestrationContext {
      *
      * @param req The durable HTTP request to schedule.
      */
-    public callHttp(
-        method: string,
-        uri: string,
-        content?: string | object,
-        headers?: { [key: string]: string },
-        tokenSource?: TokenSource,
-        asynchronousPatternEnabled = true
-    ): Task {
+    public callHttp(options: CallHttpOptions): Task {
+        let content = options.body;
         if (content && typeof content !== "string") {
             content = JSON.stringify(content);
         }
 
-        const req = new DurableHttpRequest(
-            method,
-            uri,
+        const request = new DurableHttpRequest(
+            options.method,
+            options.url,
             content as string,
-            headers,
-            tokenSource,
-            asynchronousPatternEnabled
+            options.headers,
+            options.tokenSource,
+            options.asynchronousPatternEnabled === undefined
+                ? true
+                : options.asynchronousPatternEnabled
         );
-        const newAction = new CallHttpAction(req);
-        if (this.schemaVersion >= ReplaySchema.V3 && req.asynchronousPatternEnabled) {
+        const newAction = new CallHttpAction(request);
+        if (this.schemaVersion >= ReplaySchema.V3 && request.asynchronousPatternEnabled) {
             if (!this.defaultHttpAsyncRequestSleepTimeMillseconds) {
                 throw Error(
                     "A framework-internal error was detected: replay schema version >= V3 is being used, " +
diff --git a/src/tokensource.ts b/src/tokensource.ts
index 648f14e..6b29c56 100644
--- a/src/tokensource.ts
+++ b/src/tokensource.ts
@@ -31,6 +31,3 @@ export class ManagedIdentityTokenSource {
         public readonly resource: string
     ) {}
 }
-
-// Over time we will likely add more implementations
-export type TokenSource = ManagedIdentityTokenSource;
diff --git a/src/types.ts b/src/types.ts
index 3e1a078..fbbd4dd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,6 +7,7 @@ import {
 } from "@azure/functions";
 import { IEntityFunctionContext } from "../src/ientityfunctioncontext";
 import { IOrchestrationFunctionContext } from "../src/iorchestrationfunctioncontext";
+import { ManagedIdentityTokenSource } from "./tokensource";
 
 // orchestrations
 export type OrchestrationHandler = (
@@ -21,6 +22,40 @@ export interface OrchestrationTrigger extends FunctionTrigger {
     type: "orchestrationTrigger";
 }
 
+/**
+ * Options object provided to `callHttp()` methods on orchestration contexts
+ */
+export interface CallHttpOptions {
+    /**
+     * The HTTP request method.
+     */
+    method: string;
+    /**
+     * The HTTP request URL.
+     */
+    url: string;
+    /**
+     * The HTTP request body.
+     */
+    body?: string | object;
+    /**
+     * The HTTP request headers.
+     */
+    headers?: { [key: string]: string };
+    /**
+     * The source of the OAuth token to add to the request.
+     */
+    tokenSource?: TokenSource;
+    /**
+     * Specifies whether the DurableHttpRequest should handle the asynchronous pattern.
+     * @default true
+     */
+    asynchronousPatternEnabled?: boolean;
+}
+
+// Over time we will likely add more implementations
+export type TokenSource = ManagedIdentityTokenSource;
+
 // entities
 export type EntityHandler<T> = (context: IEntityFunctionContext<T>) => void;
 
@@ -49,3 +84,20 @@ export interface ActivityTrigger extends FunctionTrigger {
 export interface DurableClientInput extends FunctionInput {
     type: "durableClient";
 }
+
+/**
+ * Options object provided as an optional second argument to the `client.startNew()` method
+ */
+export interface StartNewOptions {
+    /**
+     *  The ID to use for the new orchestration instance. If
+     *  no instanceId is specified, the Durable Functions extension will
+     *  generate a random GUID (recommended).
+     */
+    instanceId?: string;
+
+    /**
+     * JSON-serializable input value for the orchestrator function.
+     */
+    input?: unknown;
+}


Test patch:
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index 79efe33..15c3ff1 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -318,14 +318,14 @@ export class TestOrchestrations {
 
     public static SendHttpRequest: any = createOrchestrator(function* (context) {
         const input = context.df.getInput() as df.DurableHttpRequest;
-        const output = yield context.df.callHttp(
-            input.method,
-            input.uri,
-            input.content,
-            input.headers,
-            input.tokenSource,
-            input.asynchronousPatternEnabled
-        );
+        const output = yield context.df.callHttp({
+            method: input.method,
+            url: input.uri,
+            body: input.content,
+            headers: input.headers,
+            tokenSource: input.tokenSource,
+            asynchronousPatternEnabled: input.asynchronousPatternEnabled,
+        });
         return output;
     });
 
diff --git a/test/unit/orchestrationclient-spec.ts b/test/unit/orchestrationclient-spec.ts
index 1e4c4ec..96066bf 100644
--- a/test/unit/orchestrationclient-spec.ts
+++ b/test/unit/orchestrationclient-spec.ts
@@ -999,7 +999,10 @@ describe("Orchestration Client", () => {
                 )
                 .reply(202, new HttpManagementPayload(defaultInstanceId, "", "", "", "", ""));
 
-            const result = await client.startNew(functionName, defaultInstanceId, testData);
+            const result = await client.startNew(functionName, {
+                instanceId: defaultInstanceId,
+                input: testData,
+            });
             expect(scope.isDone()).to.be.equal(true);
             expect(result).to.equal(defaultInstanceId);
         });