Skip to content

Commit f5a44af

Browse files
authored
Merge pull request #179 from Azure/dev
Updating release version to 1.4.0
2 parents 6e25d7f + e2e8106 commit f5a44af

21 files changed

+1110
-112
lines changed

package-lock.json

+918-52
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "durable-functions",
3-
"version": "1.3.3",
3+
"version": "1.4.0",
44
"description": "Durable Functions library for Node.js Azure Functions",
55
"license": "MIT",
66
"repository": {
@@ -37,30 +37,31 @@
3737
"debug": "~2.6.9",
3838
"lodash": "^4.17.15",
3939
"rimraf": "~2.5.4",
40+
"typedoc": "^0.17.1",
4041
"uuid": "~3.3.2",
4142
"validator": "~10.8.0"
4243
},
4344
"devDependencies": {
4445
"@types/chai": "~4.1.6",
45-
"@types/chai-string": "~1.4.1",
4646
"@types/chai-as-promised": "~7.1.0",
47+
"@types/chai-string": "~1.4.1",
4748
"@types/commander": "~2.3.31",
4849
"@types/debug": "0.0.29",
49-
"@types/mocha": "~5.2.5",
50+
"@types/mocha": "~7.0.2",
5051
"@types/nock": "^9.3.0",
5152
"@types/node": "~6.14.7",
5253
"@types/rimraf": "0.0.28",
5354
"@types/sinon": "~5.0.5",
5455
"chai": "~4.2.0",
55-
"chai-string": "~1.5.0",
5656
"chai-as-promised": "~7.1.1",
57-
"mocha": "~5.2.0",
57+
"chai-string": "~1.5.0",
58+
"mocha": "~7.1.1",
5859
"moment": "~2.22.2",
5960
"nock": "^10.0.6",
6061
"sinon": "~7.1.1",
6162
"ts-node": "~1.0.0",
6263
"tslint": "^5.11.0",
63-
"typescript": "~3.2.4"
64+
"typescript": "^3.8.3"
6465
},
6566
"engines": {
6667
"node": ">=6.5.0"
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"bindings": [
3+
{
4+
"name": "context",
5+
"type": "entityTrigger",
6+
"direction": "in"
7+
}
8+
],
9+
"disabled": false
10+
}

samples/AsyncCounterEntity/index.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const df = require("durable-functions");
2+
3+
module.exports = df.entity(async function(context) {
4+
await Promise.resolve();
5+
let currentValue = context.df.getState(() => 0);
6+
7+
switch (context.df.operationName) {
8+
case "add":
9+
const amount = context.df.getInput();
10+
currentValue += amount;
11+
break;
12+
case "reset":
13+
currentValue = 0;
14+
break;
15+
case "get":
16+
context.df.return(currentValue);
17+
break;
18+
}
19+
20+
context.df.setState(currentValue);
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"bindings": [
3+
{
4+
"name": "context",
5+
"type": "orchestrationTrigger",
6+
"direction": "in"
7+
}
8+
],
9+
"disabled": false
10+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const df = require("durable-functions");
2+
3+
module.exports = df.orchestrator(function*(context){
4+
const entityId = new df.EntityId("AsyncCounterEntity", "myAsyncCounter");
5+
6+
currentValue = yield context.df.callEntity(entityId, "get");
7+
if (currentValue < 10) {
8+
yield context.df.callEntity(entityId, "add", 1);
9+
}
10+
});

src/actions/callhttpaction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ActionType, DurableHttpRequest, IAction, Utils } from "../classes";
1+
import { ActionType, DurableHttpRequest, IAction } from "../classes";
22

33
/** @hidden */
44
export class CallHttpAction implements IAction {

src/actions/createtimeraction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isDate } from "util";
2-
import { ActionType, Constants, IAction } from "../classes";
2+
import { ActionType, IAction } from "../classes";
33

44
/** @hidden */
55
export class CreateTimerAction implements IAction {

src/durableorchestrationclient.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ export class DurableOrchestrationClient {
465465
case 200: // entity exists
466466
return new EntityStateResponse(true, response.data as T);
467467
case 404: // entity does not exist
468-
return new EntityStateResponse(false, undefined);
468+
return new EntityStateResponse(false);
469469
default:
470470
return Promise.reject(this.createGenericError(response));
471471
}

src/durableorchestrationcontext.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ export class DurableOrchestrationContext {
1515
*/
1616
public readonly instanceId: string;
1717

18+
/**
19+
* The ID of the parent orchestration of the current sub-orchestration
20+
* instance. The value will be available only in sub-orchestrations.
21+
*
22+
* The parent instance ID is generated and fixed when the parent
23+
* orchestrator function is scheduled. It can be either auto-generated, in
24+
* which case it is formatted as a GUID, or it can be user-specified with
25+
* any format.
26+
*/
27+
public readonly parentInstanceId: string | undefined;
28+
1829
/**
1930
* Gets a value indicating whether the orchestrator function is currently
2031
* replaying itself.
@@ -26,18 +37,7 @@ export class DurableOrchestrationContext {
2637
* whether the function is being replayed and then issue the log statements
2738
* when this value is `false`.
2839
*/
29-
public readonly isReplaying: boolean;
30-
31-
/**
32-
* The ID of the parent orchestration of the current sub-orchestration
33-
* instance. The value will be available only in sub-orchestrations.
34-
*
35-
* The parent instance ID is generated and fixed when the parent
36-
* orchestrator function is scheduled. It can be either auto-generated, in
37-
* which case it is formatted as a GUID, or it can be user-specified with
38-
* any format.
39-
*/
40-
public readonly parentInstanceId: string | undefined;
40+
public isReplaying: boolean;
4141

4242
/**
4343
* Gets the current date/time in a way that is safe for use by orchestrator

src/entities/entitystateresponse.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ export class EntityStateResponse<T> {
77
public entityExists: boolean,
88

99
/** The current state of the entity, if it exists, or default value otherwise. */
10-
public entityState: T | undefined,
10+
public entityState?: T,
1111
) {}
1212
}

src/entity.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class Entity {
3030
context.df = this.getCurrentDurableEntityContext(entityBinding, returnState, i, startTime);
3131

3232
try {
33-
this.fn(context);
33+
await Promise.resolve(this.fn(context));
3434
if (!returnState.results[i]) {
3535
const elapsedMs = this.computeElapsedMilliseconds(startTime);
3636
returnState.results[i] = new OperationResult(false, elapsedMs);

src/orchestrator.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class Orchestrator {
2525
private newGuidCounter: number;
2626
private subOrchestratorCounter: number;
2727

28-
constructor(public fn: (context: IOrchestrationFunctionContext) => IterableIterator<unknown>) { }
28+
constructor(public fn: (context: IOrchestrationFunctionContext) => Generator<unknown, unknown, any>) { }
2929

3030
public listen() {
3131
return this.handle.bind(this);
@@ -87,7 +87,7 @@ export class Orchestrator {
8787

8888
try {
8989
// First execution, we have not yet "yielded" any of the tasks.
90-
let g = gen.next(undefined);
90+
let g = gen.next();
9191

9292
while (true) {
9393

@@ -146,6 +146,13 @@ export class Orchestrator {
146146
return;
147147
}
148148

149+
// The first time a task is marked as complete, the history event that finally marked the task as completed
150+
// should not yet have been played by the Durable Task framework, resulting in isReplaying being false.
151+
// On replays, the event will have already been processed by the framework, and IsPlayed will be marked as true.
152+
if (state[partialResult.completionIndex] !== undefined) {
153+
context.df.isReplaying = state[partialResult.completionIndex].IsPlayed;
154+
}
155+
149156
if (TaskFilter.isFailedTask(partialResult)) {
150157
if (!gen.throw) {
151158
throw new Error("Cannot properly throw the execption returned by customer code");
@@ -544,11 +551,10 @@ export class Orchestrator {
544551
}
545552

546553
private all(state: HistoryEvent[], tasks: TaskBase[]): TaskSet {
547-
let maxCompletionIndex: number | undefined = undefined;
554+
let maxCompletionIndex: number | undefined;
548555
const errors: Error[] = [];
549-
const results: unknown[] = [];
550-
for (let index = 0; index < tasks.length; index++) {
551-
const task = tasks[index];
556+
const results: Array<unknown> = [];
557+
for (const task of tasks) {
552558
if (!TaskFilter.isCompletedTask(task)) {
553559
return TaskFactory.UncompletedTaskSet(tasks);
554560
}
@@ -582,9 +588,8 @@ export class Orchestrator {
582588
throw new Error("At least one yieldable task must be provided to wait for.");
583589
}
584590

585-
let firstCompleted: CompletedTask | undefined = undefined;
586-
for (let index = 0; index < tasks.length; index++) {
587-
const task = tasks[index];
591+
let firstCompleted: CompletedTask | undefined;
592+
for (const task of tasks) {
588593
if (TaskFilter.isCompletedTask(task)) {
589594
if (!firstCompleted) {
590595
firstCompleted = task;

src/shim.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Entity, IEntityFunctionContext, IOrchestrationFunctionContext, Orchestr
1414
* });
1515
* ```
1616
*/
17-
export function orchestrator(fn: (context: IOrchestrationFunctionContext) => IterableIterator<unknown>)
17+
export function orchestrator(fn: (context: IOrchestrationFunctionContext) => Generator<unknown, unknown, any>)
1818
: (context: IOrchestrationFunctionContext) => void {
1919
const listener = new Orchestrator(fn).listen();
2020
return (context: IOrchestrationFunctionContext) => {
@@ -25,7 +25,7 @@ export function orchestrator(fn: (context: IOrchestrationFunctionContext) => Ite
2525
export function entity(fn: (context: IEntityFunctionContext) => unknown)
2626
: (context: IEntityFunctionContext) => void {
2727
const listener = new Entity(fn).listen();
28-
return (context: IEntityFunctionContext) => {
29-
listener(context);
28+
return async (context: IEntityFunctionContext) => {
29+
await listener(context);
3030
};
3131
}

test/integration/entity-spec.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe("Entity", () => {
1818
const mockContext = new MockContext({
1919
context: testData.input,
2020
});
21-
entity(mockContext);
21+
await entity(mockContext);
2222

2323
expect(mockContext.doneValue).to.not.equal(undefined);
2424

@@ -36,7 +36,28 @@ describe("Entity", () => {
3636
const mockContext = new MockContext({
3737
context: testData.input,
3838
});
39-
entity(mockContext);
39+
await entity(mockContext);
40+
41+
expect(mockContext.doneValue).to.not.equal(undefined);
42+
43+
if (mockContext.doneValue) {
44+
entityStateMatchesExpected(mockContext.doneValue, testData.output);
45+
}
46+
});
47+
48+
it("AsyncStringStore entity with no initial state.", async () => {
49+
const entity = TestEntities.AsyncStringStore;
50+
const operations: StringStoreOperation[] = [];
51+
operations.push({ kind: "set", value: "set 1"});
52+
operations.push({ kind: "get"});
53+
operations.push({ kind: "set", value: "set 2"});
54+
operations.push({ kind: "get"});
55+
56+
const testData = TestEntityBatches.GetAsyncStringStoreBatch(operations, undefined);
57+
const mockContext = new MockContext({
58+
context: testData.input,
59+
});
60+
await entity(mockContext);
4061

4162
expect(mockContext.doneValue).to.not.equal(undefined);
4263

test/integration/orchestrator-spec.ts

+8-13
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
ActionType, CallActivityAction, CallActivityWithRetryAction, CallEntityAction, CallHttpAction, CallSubOrchestratorAction,
99
CallSubOrchestratorWithRetryAction, ContinueAsNewAction, CreateTimerAction, DurableHttpRequest,
1010
DurableHttpResponse, DurableOrchestrationBindingInfo, DurableOrchestrationContext, EntityId,
11-
ExternalEventType, IOrchestratorState, LockState, OrchestratorState,
12-
RetryOptions, WaitForExternalEventAction,
11+
ExternalEventType, HistoryEvent, IOrchestratorState, LockState,
12+
OrchestratorState, RetryOptions, WaitForExternalEventAction,
1313
} from "../../src/classes";
1414
import { OrchestrationFailureError } from "../../src/orchestrationfailureerror";
1515
import { TestHistories } from "../testobjects/testhistories";
@@ -119,23 +119,18 @@ describe("Orchestrator", () => {
119119
it("assigns isReplaying", async () => {
120120
const orchestrator = TestOrchestrations.SayHelloSequence;
121121
const name = "World";
122-
const replaying = true;
122+
123+
const mockHistory = TestHistories.GetSayHelloWithActivityReplayOne("SayHelloWithActivity", moment.utc().toDate(), name);
123124

124125
const mockContext = new MockContext({
125-
context: new DurableOrchestrationBindingInfo(
126-
TestHistories.GetSayHelloWithActivityReplayOne(
127-
"SayHelloWithActivity",
128-
moment.utc().toDate(),
129-
name),
130-
name,
131-
undefined,
132-
replaying,
133-
),
126+
context: new DurableOrchestrationBindingInfo(mockHistory, name),
134127
});
135128

136129
orchestrator(mockContext);
137130

138-
expect(mockContext.df!.isReplaying).to.be.equal(replaying);
131+
const lastEvent = mockHistory.pop() as HistoryEvent;
132+
133+
expect(mockContext.df!.isReplaying).to.be.equal(lastEvent.IsPlayed);
139134
});
140135

141136
it("assigns parentInstanceId", async () => {

test/testobjects/TestOrchestrations.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,11 @@ export class TestOrchestrations {
305305
return "Timer fired!";
306306
});
307307

308-
public static ThrowsExceptionFromActivity: any = df.orchestrator(function*(context: any)
309-
: IterableIterator<unknown> {
308+
public static ThrowsExceptionFromActivity: any = df.orchestrator(function*(context: any) {
310309
yield context.df.callActivity("ThrowsErrorActivity");
311310
});
312311

313-
public static ThrowsExceptionFromActivityWithCatch: any = df.orchestrator(function*(context: any)
314-
: IterableIterator<unknown> {
312+
public static ThrowsExceptionFromActivityWithCatch: any = df.orchestrator(function*(context: any) {
315313
try {
316314
yield context.df.callActivity("ThrowsErrorActivity");
317315
} catch (e) {
@@ -322,8 +320,7 @@ export class TestOrchestrations {
322320
}
323321
});
324322

325-
public static ThrowsExceptionInline: any = df.orchestrator(function*(context: any)
326-
: IterableIterator<unknown> {
323+
public static ThrowsExceptionInline: any = df.orchestrator(function*(context: any) {
327324
throw Error("Exception from Orchestrator");
328325
});
329326
}

test/testobjects/testentities.ts

+15
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,19 @@ export class TestEntities {
3838
throw Error("Invalid operation");
3939
}
4040
});
41+
42+
public static AsyncStringStore: any = df.entity(async (context: IEntityFunctionContext) => {
43+
await new Promise((resolve) => setTimeout(() => resolve(), 0)); // force onto the event loop and result in a no-op delay
44+
switch (context.df.operationName) {
45+
case "set":
46+
context.df.setState(context.df.getInput());
47+
break;
48+
case "get":
49+
context.df.return(context.df.getState());
50+
break;
51+
default:
52+
throw new Error("No such operation exists");
53+
}
54+
55+
});
4156
}

0 commit comments

Comments
 (0)