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

Issue:
Support DurableOrchestrationContext.GetGUID
`DurableOrchestrationContext` should support the `getGUID` method to deterministically generate GUIDs as documented [here](https://azure.github.io/azure-functions-durable-extension/api/Microsoft.Azure.WebJobs.DurableOrchestrationContext.html#Microsoft_Azure_WebJobs_DurableOrchestrationContext_NewGuid). This will require work on the extension side.

Pull request description:
Added DurableOrchestrationClient.newGuid method.
Fixes #31.

Patch:
diff --git a/src/classes.ts b/src/classes.ts
index 28d368d..1f9bae1 100644
--- a/src/classes.ts
+++ b/src/classes.ts
@@ -54,3 +54,5 @@ export { IHttpResponse } from "./ihttpresponse";
 
 export { DurableOrchestrationStatus } from "./durableorchestrationstatus";
 export { OrchestrationRuntimeStatus } from "./orchestrationruntimestatus";
+
+export { GuidManager } from "./guidmanager";
diff --git a/src/durableorchestrationcontext.ts b/src/durableorchestrationcontext.ts
index 52116a2..66fc786 100644
--- a/src/durableorchestrationcontext.ts
+++ b/src/durableorchestrationcontext.ts
@@ -147,6 +147,19 @@ export class DurableOrchestrationContext {
         throw new Error("This is a placeholder.");
     }
 
+    /**
+     * Creates a new GUID that is safe for replay within an orchestration or
+     * operation.
+     *
+     * The default implementation of this method creates a name-based UUID
+     * using the algorithm from RFC 4122 §4.3. The name input used to generate
+     * this value is a combination of the orchestration instance ID and an
+     * internally managed sequence number.
+     */
+    public newGuid(): string {
+        throw new Error("This is a placeholder.");
+    }
+
     /**
      * Sets the JSON-serializable status of the current orchestrator function.
      *
diff --git a/src/guidmanager.ts b/src/guidmanager.ts
new file mode 100644
index 0000000..0c81e25
--- /dev/null
+++ b/src/guidmanager.ts
@@ -0,0 +1,31 @@
+import * as crypto from "crypto";
+import uuidv5 = require("uuid/v5");
+import { Utils } from "./classes";
+
+/** @hidden */
+export class GuidManager {
+    // I don't anticipate these changing often.
+    public static DnsNamespaceValue: string = "9e952958-5e33-4daf-827f-2fa12937b875";
+    public static UrlNamespaceValue: string = "9e952958-5e33-4daf-827f-2fa12937b875";
+    public static IsoOidNamespaceValue: string = "9e952958-5e33-4daf-827f-2fa12937b875";
+
+    public static createDeterministicGuid(namespaceValue: string, name: string): string {
+        return this.createDeterministicGuidCore(namespaceValue, name, DeterministicGuidVersion.V5);
+    }
+
+    private static createDeterministicGuidCore(namespaceValue: string, name: string, version: DeterministicGuidVersion): string {
+        Utils.throwIfEmpty(namespaceValue, "namespaceValue");
+        Utils.throwIfEmpty(name, "name");
+
+        const hash = crypto.createHash("sha1");
+        hash.update(name);
+        const bytes: number[] = Array.prototype.slice.call(hash.digest(), 0, 16);
+
+        return uuidv5(namespaceValue, bytes);
+    }
+}
+
+enum DeterministicGuidVersion {
+    V3 = 0,
+    V5 = 1,
+}
diff --git a/src/orchestrator.ts b/src/orchestrator.ts
index 607d1cf..fe83024 100644
--- a/src/orchestrator.ts
+++ b/src/orchestrator.ts
@@ -1,17 +1,22 @@
 import * as debug from "debug";
 import { CallActivityAction, CallActivityWithRetryAction, CallSubOrchestratorAction,
-    CallSubOrchestratorWithRetryAction, Constants, ContinueAsNewAction, CreateTimerAction,
-    DurableOrchestrationBindingInfo, EventRaisedEvent, HistoryEvent, HistoryEventType, IAction, IFunctionContext,
-    OrchestratorState, RetryOptions, SubOrchestrationInstanceCompletedEvent, SubOrchestrationInstanceCreatedEvent,
-    SubOrchestrationInstanceFailedEvent, Task, TaskCompletedEvent, TaskFailedEvent, TaskScheduledEvent, TaskSet,
-    TimerCreatedEvent, TimerFiredEvent, TimerTask, Utils, WaitForExternalEventAction } from "./classes";
+    CallSubOrchestratorWithRetryAction, ContinueAsNewAction, CreateTimerAction,
+    DurableOrchestrationBindingInfo, EventRaisedEvent, GuidManager, HistoryEvent,
+    HistoryEventType, IAction, IFunctionContext, OrchestratorState, RetryOptions,
+    SubOrchestrationInstanceCompletedEvent, SubOrchestrationInstanceCreatedEvent,
+    SubOrchestrationInstanceFailedEvent, Task, TaskCompletedEvent, TaskFailedEvent,
+    TaskScheduledEvent, TaskSet, TimerCreatedEvent, TimerFiredEvent, TimerTask,
+    Utils, WaitForExternalEventAction
+} from "./classes";
 
 /** @hidden */
 const log = debug("orchestrator");
 
 /** @hidden */
 export class Orchestrator {
+    private currentUtcDateTime: Date;
     private customStatus: unknown;
+    private newGuidCounter: number;
 
     constructor(public fn: (context: IFunctionContext) => IterableIterator<unknown>) { }
 
@@ -29,14 +34,21 @@ export class Orchestrator {
 
         const state: HistoryEvent[] = orchestrationBinding.history;
         const input: unknown = orchestrationBinding.input;
+        const instanceId: string = orchestrationBinding.instanceId;
 
         // Initialize currentUtcDateTime
         let decisionStartedEvent: HistoryEvent = state.find((e) =>
             (e.EventType === HistoryEventType.OrchestratorStarted));
+        this.currentUtcDateTime = decisionStartedEvent
+            ? decisionStartedEvent.Timestamp
+            : undefined;
+        
+        // Reset newGuidCounter
+        this.newGuidCounter = 0;
 
         // Create durable orchestration context
         context.df = {
-            instanceId: orchestrationBinding.instanceId,
+            instanceId,
             isReplaying: orchestrationBinding.isReplaying,
             parentInstanceId: orchestrationBinding.parentInstanceId,
             callActivity: this.callActivity.bind(this, state),
@@ -46,15 +58,14 @@ export class Orchestrator {
             continueAsNew: this.continueAsNew.bind(this, state),
             createTimer: this.createTimer.bind(this, state),
             getInput: this.getInput.bind(this, input),
+            newGuid: this.newGuid.bind(this, instanceId),
             setCustomStatus: this.setCustomStatus.bind(this),
             waitForExternalEvent: this.waitForExternalEvent.bind(this, state),
             Task: {
                 all: this.all.bind(this, state),
                 any: this.any.bind(this, state),
             },
-            currentUtcDateTime: decisionStartedEvent
-                ? decisionStartedEvent.Timestamp
-                : undefined,
+            currentUtcDateTime: this.currentUtcDateTime,
         };
 
         // Setup
@@ -102,7 +113,9 @@ export class Orchestrator {
                 decisionStartedEvent = state.find((e) =>
                     e.EventType === HistoryEventType.OrchestratorStarted &&
                     e.Timestamp > decisionStartedEvent.Timestamp);
-                context.df.currentUtcDateTime = decisionStartedEvent ? decisionStartedEvent.Timestamp : undefined;
+                context.df.currentUtcDateTime = this.currentUtcDateTime = decisionStartedEvent
+                    ? decisionStartedEvent.Timestamp
+                    : undefined;
 
                 g = gen.next(partialResult ? partialResult.result : undefined);
             }
@@ -316,6 +329,12 @@ export class Orchestrator {
         return input;
     }
 
+    private newGuid(instanceId: string): string {
+        const guidNameValue = `${instanceId}_${this.currentUtcDateTime.valueOf()}_${this.newGuidCounter}`;
+        this.newGuidCounter++;
+        return GuidManager.createDeterministicGuid(GuidManager.UrlNamespaceValue, guidNameValue);
+    }
+
     private setCustomStatus(customStatusObject: unknown): void {
         this.customStatus = customStatusObject;
     }


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index 92febdb..b5f3b99 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -982,6 +982,41 @@ describe("Orchestrator", () => {
         });
     });
 
+    describe("newGuid()", () => {
+        it("generates consistent GUIDs", () => {
+            const orchestrator = TestOrchestrations.GuidGenerator;
+            const currentUtcDateTime = moment.utc().toDate();
+            const instanceId = uuidv1();
+
+            const mockContext1 = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetOrchestratorStart(
+                        "GuidGenerator",
+                        currentUtcDateTime,
+                    ),
+                    undefined,
+                    instanceId,
+                ),
+            });
+            const mockContext2 = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetOrchestratorStart(
+                        "GuidGenerator",
+                        currentUtcDateTime,
+                    ),
+                    undefined,
+                    instanceId,
+                ),
+            });            
+
+            orchestrator(mockContext1);
+            orchestrator(mockContext2);
+
+            expect(mockContext1.doneValue.isDone).to.equal(true);
+            expect(mockContext1.doneValue).to.deep.equal(mockContext2.doneValue);
+        });
+    });
+
     describe("setCustomStatus()", () => {
         it("sets a custom status", async () => {
             const orchestrator = TestOrchestrations.SayHelloWithCustomStatus;
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index 9fc7586..3a402e0 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -46,6 +46,16 @@ export class TestOrchestrations {
         return totalBytes;
     });
 
+    public static GuidGenerator: any = df.orchestrator(function*(context: any) {
+        const outputs: string[] = [];
+
+        for (let i = 0; i < 3; i++) {
+            outputs.push(context.df.newGuid());
+        }
+
+        return outputs;
+    });
+
     public static SayHelloInline: any = df.orchestrator(function*(context: any) {
         const input = context.df.getInput();
         return `Hello, ${input}!`;
diff --git a/test/unit/guidmanager-spec.ts b/test/unit/guidmanager-spec.ts
new file mode 100644
index 0000000..357bb2b
--- /dev/null
+++ b/test/unit/guidmanager-spec.ts
@@ -0,0 +1,43 @@
+import { expect } from "chai";
+import * as crypto from "crypto";
+import "mocha";
+import moment = require("moment");
+import uuidv1 = require("uuid/v1");
+import uuidv5 = require("uuid/v5");
+import { isUUID } from "validator";
+import { GuidManager } from "../../src/classes";
+
+describe("GuidManager", () => {
+    describe("createDeterministicGuid()", async () => {
+        it("throws if namespaceValue is empty", () => {
+            expect(() => GuidManager.createDeterministicGuid(undefined, "name"))
+                .to.throw("namespaceValue: Expected non-empty, non-whitespace string but got undefined");
+        });
+
+        it("throws if name is empty", () => {
+            expect(() => GuidManager.createDeterministicGuid("namespaceValue", undefined))
+                .to.throw("name: Expected non-empty, non-whitespace string but got undefined");
+        });
+
+        it("returns consistent GUID for namespace and name", () => {
+            const namespace = GuidManager.UrlNamespaceValue;
+            const instanceId = uuidv1();
+            const currentUtcDateTime = moment.utc().toDate().valueOf();
+
+            const name1 = `${instanceId}_${currentUtcDateTime}_0`;
+            const name2 = `${instanceId}_${currentUtcDateTime}_12`;
+
+            const result1a = GuidManager.createDeterministicGuid(namespace, name1);
+            const result1b = GuidManager.createDeterministicGuid(namespace, name1);
+
+            const result2a = GuidManager.createDeterministicGuid(namespace, name2);
+            const result2b = GuidManager.createDeterministicGuid(namespace, name2);
+
+            expect(isUUID(result1a, "5")).to.equal(true);
+            expect(isUUID(result2a, "5")).to.equal(true);
+            expect(result1a).to.equal(result1b);
+            expect(result2a).to.equal(result2b);
+            expect(result1a).to.not.equal(result2a);
+        });
+    });
+});