Skip to content

Commit 1f8ab6d

Browse files
authored
Merge pull request #109 from resonatehq/main
v0.5.1
2 parents 0ae2081 + 93ad425 commit 1f8ab6d

11 files changed

+157
-60
lines changed

README.md

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
> Resonate is in the **Design Phase**
2-
>
3-
> Our code base is constantly evolving as we are exploring Resonate's programming model. If you are passionate about a dead simple developer experience, join us on this journey of discovery and share your thoughts.
4-
>
5-
> [Join our slack](https://resonatehqcommunity.slack.com)
6-
7-
<br /><br />
81
<p align="center">
92
<img height="170"src="https://raw.githubusercontent.com/resonatehq/resonate/main/docs/img/echo.png">
103
</p>
@@ -38,7 +31,7 @@ Resonate's Distributed Async Await is a new programming model that simplifies co
3831

3932
## Why Resonate?
4033

41-
- **Cloud Computing Made Dead Simple**: Resonate offers a dead simple programming model that simplifies coding for the cloud using an intuitive paradigm you already know — async await.
34+
- **Cloud Computing Made Dead Simple**: Resonate simplifies coding for the cloud using an intuitive paradigm you already know — async await.
4235

4336
- **Single Binary**: Resonate simplifies your deployment and operations with a single binary.
4437

lib/async.ts

+7
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ export class Context {
131131
return this.invocation.counter;
132132
}
133133

134+
/**
135+
* The time the invocation was created. Will use the durable promise creation time if available.
136+
*/
137+
get createdOn() {
138+
return this.invocation.createdOn;
139+
}
140+
134141
/**
135142
* Uniquely identifies the function invocation.
136143
*/

lib/core/execution.ts

+84-34
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@ export abstract class Execution<T> {
2727
return this.promise;
2828
}
2929

30+
// each execution has a fork phase and a join phase
31+
32+
// fork phase is responsible for spawning the invocation,
33+
// which in general means creating a durable promise (if necessary)
3034
const forkPromise = this.fork();
35+
36+
// join phase is responsible for awaiting the invocation,
37+
// if the invocation is backed by a durable promise the durable
38+
// promise is completed before the invocation is completed
3139
const joinPromise = forkPromise.then((f) => this.join(f));
3240

3341
this.promise = new ResonatePromise(this.invocation.id, forkPromise, joinPromise);
@@ -82,15 +90,10 @@ export class OrdinaryExecution<T> extends Execution<T> {
8290
}
8391

8492
protected async fork() {
85-
try {
86-
// acquire lock if necessary
87-
while (this.invocation.opts.lock && !(await this.acquireLock())) {
88-
await new Promise((resolve) => setTimeout(resolve, this.invocation.opts.poll));
89-
}
90-
91-
if (this.invocation.opts.durable) {
92-
// if durable, create a durable promise
93-
const promise =
93+
if (this.invocation.opts.durable) {
94+
// if durable, create a durable promise
95+
try {
96+
this.durablePromise =
9497
this.durablePromise ??
9598
(await DurablePromise.create<T>(
9699
this.resonate.store.promises,
@@ -104,24 +107,59 @@ export class OrdinaryExecution<T> extends Execution<T> {
104107
tags: this.invocation.opts.tags,
105108
},
106109
));
110+
} catch (e) {
111+
// if an error occurs, kill the execution
112+
this.kill(e);
113+
}
114+
}
107115

108-
// override the invocation timeout
109-
this.invocation.timeout = promise.timeout;
116+
if (this.durablePromise) {
117+
// override the invocation creation time
118+
this.invocation.createdOn = this.durablePromise.createdOn;
119+
120+
// override the invocation timeout
121+
this.invocation.timeout = this.durablePromise.timeout;
122+
}
123+
124+
return this.invocation.future;
125+
}
126+
127+
protected async join(future: Future<T>) {
128+
try {
129+
// acquire lock if necessary
130+
while (this.invocation.opts.lock && !(await this.acquireLock())) {
131+
await new Promise((resolve) => setTimeout(resolve, this.invocation.opts.poll));
132+
}
133+
134+
if (this.durablePromise) {
135+
if (this.durablePromise.pending) {
136+
// if durable and pending, invoke the function and resolve/reject the durable promise
137+
let value!: T;
138+
let error: any;
139+
140+
// we need to hold on to a boolean to determine if the function was successful,
141+
// we cannot rely on the value or error as these values could be undefined
142+
let success = true;
110143

111-
if (promise.pending) {
112-
// if pending, invoke the function and resolve/reject the durable promise
113144
try {
114-
await promise.resolve(await this.run(), { idempotencyKey: this.invocation.idempotencyKey });
145+
value = await this.run();
115146
} catch (e) {
116-
await promise.reject(e, { idempotencyKey: this.invocation.idempotencyKey });
147+
error = e;
148+
success = false;
149+
}
150+
151+
if (success) {
152+
await this.durablePromise.resolve(value, { idempotencyKey: this.invocation.idempotencyKey });
153+
} else {
154+
await this.durablePromise.reject(error, { idempotencyKey: this.invocation.idempotencyKey });
117155
}
118156
}
119157

120-
// resolve/reject the invocation
121-
if (promise.resolved) {
122-
this.invocation.resolve(promise.value());
123-
} else if (promise.rejected || promise.canceled || promise.timedout) {
124-
this.invocation.reject(promise.error());
158+
// if durable resolve/reject the invocation
159+
if (this.durablePromise.resolved) {
160+
this.invocation.resolve(this.durablePromise.value());
161+
} else if (this.durablePromise.rejected || this.durablePromise.canceled || this.durablePromise.timedout) {
162+
this.invocation.reject(this.durablePromise.error());
125163
}
126164
} else {
127165
// if not durable, invoke the function and resolve/reject the invocation
@@ -141,10 +179,6 @@ export class OrdinaryExecution<T> extends Execution<T> {
141179
this.kill(e);
142180
}
143181

144-
return this.invocation.future;
145-
}
146-
147-
protected async join(future: Future<T>) {
148182
return await future.promise;
149183
}
150184

@@ -171,10 +205,12 @@ export class OrdinaryExecution<T> extends Execution<T> {
171205
}
172206

173207
export class DeferredExecution<T> extends Execution<T> {
208+
private durablePromise: DurablePromise<T> | null = null;
209+
174210
protected async fork() {
175211
try {
176212
// create a durable promise
177-
const promise = await DurablePromise.create<T>(
213+
this.durablePromise = await DurablePromise.create<T>(
178214
this.resonate.store.promises,
179215
this.invocation.opts.encoder,
180216
this.invocation.id,
@@ -188,13 +224,11 @@ export class DeferredExecution<T> extends Execution<T> {
188224
},
189225
);
190226

191-
// override the invocation timeout
192-
this.invocation.timeout = promise.timeout;
227+
// override the invocation creation time
228+
this.invocation.createdOn = this.durablePromise.createdOn;
193229

194-
// poll the completion of the durable promise
195-
promise.completed.then((p) =>
196-
p.resolved ? this.invocation.resolve(p.value()) : this.invocation.reject(p.error()),
197-
);
230+
// override the invocation timeout
231+
this.invocation.timeout = this.durablePromise.timeout;
198232
} catch (e) {
199233
// if an error occurs, kill the execution
200234
this.kill(e);
@@ -204,6 +238,17 @@ export class DeferredExecution<T> extends Execution<T> {
204238
}
205239

206240
protected async join(future: Future<T>) {
241+
if (this.durablePromise) {
242+
// poll the completion of the durable promise
243+
await this.durablePromise.completed;
244+
245+
if (this.durablePromise.resolved) {
246+
this.invocation.resolve(this.durablePromise.value());
247+
} else {
248+
this.invocation.reject(this.durablePromise.error());
249+
}
250+
}
251+
207252
return await future.promise;
208253
}
209254
}
@@ -237,16 +282,21 @@ export class GeneratorExecution<T> extends Execution<T> {
237282
},
238283
));
239284

240-
// override the invocation timeout
241-
this.invocation.timeout = this.durablePromise.timeout;
242-
243285
// resolve/reject the invocation if already completed
244286
if (this.durablePromise.resolved) {
245287
this.invocation.resolve(this.durablePromise.value());
246288
} else if (this.durablePromise.rejected || this.durablePromise.canceled || this.durablePromise.timedout) {
247289
this.invocation.reject(this.durablePromise.error());
248290
}
249291
}
292+
293+
if (this.durablePromise) {
294+
// override the invocation creation time
295+
this.invocation.createdOn = this.durablePromise.createdOn;
296+
297+
// override the invocation timeout
298+
this.invocation.timeout = this.durablePromise.timeout;
299+
}
250300
} catch (e) {
251301
// if an error occurs, kill the execution
252302
this.kill(e);

lib/core/future.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class ResonatePromise<T> extends Promise<T> {
2323
*/
2424
constructor(
2525
public id: string,
26-
created: Promise<any>,
26+
created: Promise<Future<T>>,
2727
completed: Promise<T>,
2828
) {
2929
// bind the promise to the completed promise
@@ -32,22 +32,25 @@ export class ResonatePromise<T> extends Promise<T> {
3232
});
3333

3434
// expose the id when the durable promise has been created
35-
this.created = created.then(() => this.id);
35+
this.created = created.then((future) => future.id);
3636
}
3737
}
3838

3939
/////////////////////////////////////////////////////////////////////
4040
// Future
4141
/////////////////////////////////////////////////////////////////////
4242

43-
export type Value<T> = { kind: "pending" } | { kind: "resolved"; value: T } | { kind: "rejected"; error: unknown };
43+
export type FutureValue<T> =
44+
| { kind: "pending" }
45+
| { kind: "resolved"; value: T }
46+
| { kind: "rejected"; error: unknown };
4447

4548
export class Future<T> {
4649
// a discriminate property
4750
readonly kind = "future";
4851

4952
// initial value
50-
private _value: Value<T> = { kind: "pending" };
53+
private _value: FutureValue<T> = { kind: "pending" };
5154

5255
/**
5356
* Represents the eventual return value of a Resonate function.

lib/core/invocation.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class Invocation<T> {
1616

1717
killed: boolean = false;
1818

19-
createdAt: number = Date.now();
19+
createdOn: number = Date.now();
2020
counter: number = 0;
2121
attempt: number = 0;
2222

@@ -68,7 +68,7 @@ export class Invocation<T> {
6868
// the timeout is the minimum of:
6969
// - the current time plus the user provided relative time
7070
// - the parent timeout
71-
this.timeout = Math.min(this.createdAt + this.opts.timeout, this.parent?.timeout ?? Infinity);
71+
this.timeout = Math.min(this.createdOn + this.opts.timeout, this.parent?.timeout ?? Infinity);
7272
}
7373

7474
addChild(child: Invocation<any>) {

lib/core/promises/promises.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export type CompleteOptions = {
1919
};
2020

2121
export class DurablePromise<T> {
22-
readonly created: Promise<DurablePromise<T>>;
2322
readonly completed: Promise<DurablePromise<T>>;
2423
private complete!: (value: DurablePromise<T>) => void;
2524

@@ -31,7 +30,6 @@ export class DurablePromise<T> {
3130
private promise: PendingPromise | ResolvedPromise | RejectedPromise | CanceledPromise | TimedoutPromise,
3231
poll?: number,
3332
) {
34-
this.created = Promise.resolve(this);
3533
this.completed = new Promise((resolve) => {
3634
this.complete = resolve;
3735
});
@@ -53,6 +51,10 @@ export class DurablePromise<T> {
5351
return this.promise.idempotencyKeyForComplete;
5452
}
5553

54+
get createdOn() {
55+
return this.promise.createdOn;
56+
}
57+
5658
get timeout() {
5759
return this.promise.timeout;
5860
}

lib/generator.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ export class Resonate extends ResonateBase {
135135
* @param name The schedule name.
136136
* @param cron The schedule cron expression.
137137
* @param func The registered function name.
138-
* @param version The registered function version.
139138
* @param args The function arguments.
140139
* @returns The schedule object.
141140
*/
@@ -205,6 +204,13 @@ export class Context {
205204
return this.invocation.counter;
206205
}
207206

207+
/**
208+
* The time the invocation was created. Will use the durable promise creation time if available.
209+
*/
210+
get createdOn() {
211+
return this.invocation.createdOn;
212+
}
213+
208214
/**
209215
* Uniquely identifies the function invocation.
210216
*/

lib/index.ts

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,44 @@
11
// resonate
2-
export { ResonatePromises, ResonateSchedules } from "./resonate";
2+
export * from "./resonate";
33

4-
// async
5-
export { Resonate, Context } from "./async";
4+
// async (default)
5+
export * from "./async";
6+
export * as async from "./async";
67

7-
// resonate configurations
8-
export { Retry } from "./core/retries/retry";
8+
// generator
9+
export * as generator from "./generator";
10+
11+
// errors
12+
export * from "./core/errors";
13+
14+
// futures
15+
export * from "./core/future";
16+
17+
// options
18+
export * from "./core/options";
19+
20+
// promises
21+
export * as promises from "./core/promises/promises";
22+
23+
// schedules
24+
export * as schedules from "./core/schedules/schedules";
25+
26+
// interfaces
27+
export * from "./core/encoder";
28+
export * from "./core/logger";
29+
export * from "./core/retry";
30+
export * from "./core/storage";
31+
export * from "./core/store";
32+
33+
// implementations
34+
export * from "./core/encoders/base64";
35+
export * from "./core/encoders/json";
36+
export * from "./core/loggers/logger";
37+
export * from "./core/retries/retry";
38+
export * from "./core/storages/memory";
39+
export * from "./core/storages/withTimeout";
40+
export * from "./core/stores/local";
41+
export * from "./core/stores/remote";
42+
43+
// utils
44+
export * as utils from "./core/utils";

lib/resonate.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export abstract class ResonateBase {
155155
* @template T The return type of the function.
156156
* @param id A unique id for the function invocation.
157157
* @param name The function name.
158-
* @param args The function arguments.
158+
* @param argsWithOpts The function arguments.
159159
* @returns A promise that resolve to the function return value.
160160
*/
161161
run<T>(name: string, id: string, ...argsWithOpts: [...any, PartialOptions?]): ResonatePromise<T> {

0 commit comments

Comments
 (0)