@resonatehq/sdk 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/errors.d.ts +4 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +5 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/options.d.ts +12 -5
- package/dist/core/options.d.ts.map +1 -1
- package/dist/core/options.js +11 -1
- package/dist/core/options.js.map +1 -1
- package/dist/core/promises/promises.d.ts.map +1 -1
- package/dist/core/retry.d.ts +1 -0
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +20 -1
- package/dist/core/retry.js.map +1 -1
- package/dist/core/schedules/schedules.d.ts +1 -1
- package/dist/core/schedules/schedules.d.ts.map +1 -1
- package/dist/core/schedules/schedules.js.map +1 -1
- package/dist/core/stores/local.js +2 -2
- package/dist/core/stores/local.js.map +1 -1
- package/dist/core/stores/remote.d.ts.map +1 -1
- package/dist/core/stores/remote.js +14 -1
- package/dist/core/stores/remote.js.map +1 -1
- package/dist/core/utils.d.ts +55 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +67 -1
- package/dist/core/utils.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/resonate.d.ts +165 -46
- package/dist/resonate.d.ts.map +1 -1
- package/dist/resonate.js +622 -169
- package/dist/resonate.js.map +1 -1
- package/package.json +1 -1
- package/dist/async.d.ts +0 -219
- package/dist/async.d.ts.map +0 -1
- package/dist/async.js +0 -403
- package/dist/async.js.map +0 -1
- package/dist/core/calls.d.ts +0 -23
- package/dist/core/calls.d.ts.map +0 -1
- package/dist/core/calls.js +0 -13
- package/dist/core/calls.js.map +0 -1
- package/dist/core/execution.d.ts +0 -37
- package/dist/core/execution.d.ts.map +0 -1
- package/dist/core/execution.js +0 -215
- package/dist/core/execution.js.map +0 -1
- package/dist/core/future.d.ts +0 -56
- package/dist/core/future.d.ts.map +0 -1
- package/dist/core/future.js +0 -107
- package/dist/core/future.js.map +0 -1
- package/dist/core/invocation.d.ts +0 -41
- package/dist/core/invocation.d.ts.map +0 -1
- package/dist/core/invocation.js +0 -81
- package/dist/core/invocation.js.map +0 -1
package/dist/resonate.js
CHANGED
|
@@ -23,42 +23,32 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.
|
|
26
|
+
exports.InvocationHandle = exports.Context = exports.Resonate = void 0;
|
|
27
27
|
const json_1 = require("./core/encoders/json");
|
|
28
|
+
const errors_1 = require("./core/errors");
|
|
28
29
|
const logger_1 = require("./core/loggers/logger");
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const options_1 = require("./core/options");
|
|
31
|
+
const durablePromises = __importStar(require("./core/promises/promises"));
|
|
32
|
+
const retryPolicies = __importStar(require("./core/retry"));
|
|
33
|
+
const retry_1 = require("./core/retry");
|
|
31
34
|
const schedules = __importStar(require("./core/schedules/schedules"));
|
|
32
35
|
const local_1 = require("./core/stores/local");
|
|
33
36
|
const remote_1 = require("./core/stores/remote");
|
|
34
37
|
const utils = __importStar(require("./core/utils"));
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const utils_1 = require("./core/utils");
|
|
39
|
+
class Resonate {
|
|
40
|
+
#registeredFunctions = {};
|
|
41
|
+
#invocationHandles;
|
|
42
|
+
#interval;
|
|
43
|
+
store;
|
|
44
|
+
logger;
|
|
45
|
+
defaultInvocationOptions;
|
|
40
46
|
promises;
|
|
41
47
|
schedules;
|
|
42
|
-
pid;
|
|
43
|
-
poll;
|
|
44
|
-
timeout;
|
|
45
|
-
tags;
|
|
46
|
-
encoder;
|
|
47
|
-
logger;
|
|
48
|
-
retry;
|
|
49
|
-
store;
|
|
50
|
-
interval;
|
|
51
48
|
constructor({ auth = undefined, encoder = new json_1.JSONEncoder(), heartbeat = 15000, // 15s
|
|
52
|
-
logger = new logger_1.Logger(), pid = utils.randomId(),
|
|
53
|
-
|
|
49
|
+
logger = new logger_1.Logger(), pid = utils.randomId(), pollFrequency = 5000, // 5s
|
|
50
|
+
retryPolicy = retryPolicies.exponential(), store = undefined, tags = {}, timeout = 10000, // 10s
|
|
54
51
|
url = undefined, } = {}) {
|
|
55
|
-
this.encoder = encoder;
|
|
56
|
-
this.logger = logger;
|
|
57
|
-
this.pid = pid;
|
|
58
|
-
this.poll = poll;
|
|
59
|
-
this.retry = retry;
|
|
60
|
-
this.tags = tags;
|
|
61
|
-
this.timeout = timeout;
|
|
62
52
|
if (store) {
|
|
63
53
|
this.store = store;
|
|
64
54
|
}
|
|
@@ -78,43 +68,71 @@ class ResonateBase {
|
|
|
78
68
|
pid,
|
|
79
69
|
});
|
|
80
70
|
}
|
|
71
|
+
this.logger = logger;
|
|
72
|
+
this.#invocationHandles = new Map();
|
|
73
|
+
this.defaultInvocationOptions = {
|
|
74
|
+
__resonate: true,
|
|
75
|
+
durable: true,
|
|
76
|
+
eidFn: utils.randomId,
|
|
77
|
+
encoder: encoder,
|
|
78
|
+
idempotencyKeyFn: utils.hash,
|
|
79
|
+
shouldLock: undefined,
|
|
80
|
+
pollFrequency,
|
|
81
|
+
retryPolicy,
|
|
82
|
+
tags,
|
|
83
|
+
timeout,
|
|
84
|
+
version: 0,
|
|
85
|
+
};
|
|
81
86
|
// promises
|
|
82
87
|
this.promises = {
|
|
83
|
-
create: (id, timeout, opts = {}) =>
|
|
84
|
-
resolve: (id, value, opts = {}) =>
|
|
85
|
-
reject: (id, error, opts = {}) =>
|
|
86
|
-
cancel: (id, error, opts = {}) =>
|
|
87
|
-
get: (id) =>
|
|
88
|
-
search: (id, state, tags, limit) =>
|
|
88
|
+
create: (id, timeout, opts = {}) => durablePromises.DurablePromise.create(this.promisesStore, encoder, id, timeout, opts),
|
|
89
|
+
resolve: (id, value, opts = {}) => durablePromises.DurablePromise.resolve(this.promisesStore, encoder, id, value, opts),
|
|
90
|
+
reject: (id, error, opts = {}) => durablePromises.DurablePromise.reject(this.promisesStore, encoder, id, error, opts),
|
|
91
|
+
cancel: (id, error, opts = {}) => durablePromises.DurablePromise.cancel(this.promisesStore, encoder, id, error, opts),
|
|
92
|
+
get: (id) => durablePromises.DurablePromise.get(this.promisesStore, encoder, id),
|
|
93
|
+
search: (id, state, tags, limit) => durablePromises.DurablePromise.search(this.promisesStore, encoder, id, state, tags, limit),
|
|
89
94
|
};
|
|
90
95
|
// schedules
|
|
91
96
|
this.schedules = {
|
|
92
|
-
create: (id, cron, promiseId, promiseTimeout, opts = {}) => schedules.Schedule.create(this.store.schedules,
|
|
93
|
-
get: (id) => schedules.Schedule.get(this.store.schedules,
|
|
94
|
-
search: (id, tags, limit) => schedules.Schedule.search(this.store.schedules,
|
|
97
|
+
create: (id, cron, promiseId, promiseTimeout, opts = {}) => schedules.Schedule.create(this.store.schedules, encoder, id, cron, promiseId, promiseTimeout, opts),
|
|
98
|
+
get: (id) => schedules.Schedule.get(this.store.schedules, encoder, id),
|
|
99
|
+
search: (id, tags, limit) => schedules.Schedule.search(this.store.schedules, encoder, id, tags, limit),
|
|
95
100
|
};
|
|
96
101
|
}
|
|
102
|
+
get promisesStore() {
|
|
103
|
+
return this.store.promises;
|
|
104
|
+
}
|
|
105
|
+
get locksStore() {
|
|
106
|
+
return this.store.locks;
|
|
107
|
+
}
|
|
108
|
+
registeredFunction(funcName, version) {
|
|
109
|
+
if (!this.#registeredFunctions[funcName]?.[version]) {
|
|
110
|
+
throw new Error(`Function ${funcName} version ${version} not registered`);
|
|
111
|
+
}
|
|
112
|
+
return this.#registeredFunctions[funcName][version];
|
|
113
|
+
}
|
|
97
114
|
register(name, func, opts = {}) {
|
|
98
115
|
// set default version
|
|
99
116
|
opts.version = opts.version ?? 1;
|
|
100
|
-
// set default options
|
|
101
|
-
const options = this.
|
|
117
|
+
// set default values for the options options
|
|
118
|
+
const options = this.withDefaultOpts(opts);
|
|
102
119
|
if (options.version <= 0) {
|
|
103
120
|
throw new Error("Version must be greater than 0");
|
|
104
121
|
}
|
|
105
|
-
if (!this
|
|
106
|
-
this
|
|
122
|
+
if (!this.#registeredFunctions[name]) {
|
|
123
|
+
this.#registeredFunctions[name] = {};
|
|
107
124
|
}
|
|
108
|
-
if (this
|
|
125
|
+
if (this.#registeredFunctions[name][options.version]) {
|
|
109
126
|
throw new Error(`Function ${name} version ${options.version} already registered`);
|
|
110
127
|
}
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
// Get the highest version number of existing functions with this name
|
|
129
|
+
const latestVersion = Math.max(...Object.values(this.#registeredFunctions[name]).map((f) => f.opts.version));
|
|
130
|
+
// If the new function's version is higher, register it as the latest (index 0)
|
|
131
|
+
if (options.version > latestVersion) {
|
|
132
|
+
this.#registeredFunctions[name][0] = { func, opts: options };
|
|
114
133
|
}
|
|
115
134
|
// register specific version
|
|
116
|
-
this
|
|
117
|
-
return (id, ...args) => this.run(name, id, ...args, options);
|
|
135
|
+
this.#registeredFunctions[name][options.version] = { func, opts: options };
|
|
118
136
|
}
|
|
119
137
|
registerModule(module, opts = {}) {
|
|
120
138
|
for (const key in module) {
|
|
@@ -122,78 +140,209 @@ class ResonateBase {
|
|
|
122
140
|
}
|
|
123
141
|
}
|
|
124
142
|
/**
|
|
125
|
-
*
|
|
143
|
+
* Start the resonate service which continually checks for pending promises
|
|
144
|
+
* every `delay` ms.
|
|
126
145
|
*
|
|
127
|
-
* @
|
|
128
|
-
* @param id A unique id for the function invocation.
|
|
129
|
-
* @param name The function name.
|
|
130
|
-
* @param argsWithOpts The function arguments.
|
|
131
|
-
* @returns A promise that resolve to the function return value.
|
|
146
|
+
* @param delay Frequency in ms to check for pending promises.
|
|
132
147
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
async start(delay = 5000) {
|
|
149
|
+
clearInterval(this.#interval);
|
|
150
|
+
this.#_start();
|
|
151
|
+
this.#interval = setInterval(this.#_start.bind(this), delay);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Stop the resonate service.
|
|
155
|
+
*/
|
|
156
|
+
async stop() {
|
|
157
|
+
clearInterval(this.#interval);
|
|
158
|
+
}
|
|
159
|
+
async #_start() {
|
|
160
|
+
try {
|
|
161
|
+
for await (const promises of this.promisesStore.search("*", "pending", { "resonate:invocation": "true" })) {
|
|
162
|
+
for (const promiseRecord of promises) {
|
|
163
|
+
const param = this.defaultInvocationOptions.encoder.decode(promiseRecord.param.data);
|
|
164
|
+
if (param &&
|
|
165
|
+
typeof param === "object" &&
|
|
166
|
+
"func" in param &&
|
|
167
|
+
typeof param.func === "string" &&
|
|
168
|
+
"version" in param &&
|
|
169
|
+
typeof param.version === "number" &&
|
|
170
|
+
"args" in param &&
|
|
171
|
+
Array.isArray(param.args) &&
|
|
172
|
+
"retryPolicy" in param &&
|
|
173
|
+
retryPolicies.isRetryPolicy(param.retryPolicy)) {
|
|
174
|
+
// Since the promise is already created on the server, we should use that idempotencyKey.
|
|
175
|
+
// If for whatever reason it is not, we should recalculate it using our defaults.
|
|
176
|
+
const idempotencyKeyFn = (_) => {
|
|
177
|
+
return (promiseRecord.idempotencyKeyForCreate ??
|
|
178
|
+
this.defaultInvocationOptions.idempotencyKeyFn(promiseRecord.id));
|
|
179
|
+
};
|
|
180
|
+
await this.invokeLocal(param.func, promiseRecord.id, ...param.args, (0, options_1.options)({
|
|
181
|
+
retryPolicy: param.retryPolicy,
|
|
182
|
+
version: param.version,
|
|
183
|
+
idempotencyKeyFn,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
140
187
|
}
|
|
141
|
-
tfc = {
|
|
142
|
-
funcName: nameOrTfc,
|
|
143
|
-
id,
|
|
144
|
-
args,
|
|
145
|
-
optsOverrides: {
|
|
146
|
-
durable,
|
|
147
|
-
eidFn,
|
|
148
|
-
idempotencyKeyFn,
|
|
149
|
-
retry,
|
|
150
|
-
tags,
|
|
151
|
-
timeout,
|
|
152
|
-
version,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
188
|
}
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
catch (e) {
|
|
190
|
+
// squash all errors and log,
|
|
191
|
+
// transient errors will be ironed out in the next interval
|
|
192
|
+
this.logger.error(e);
|
|
158
193
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
throw new Error(`Function ${tfc.funcName} version ${tfc.optsOverrides.version} not registered`);
|
|
194
|
+
}
|
|
195
|
+
async run(name, id, ...argsWithOverrides) {
|
|
196
|
+
const handle = await this.invokeLocal(name, id, ...argsWithOverrides);
|
|
197
|
+
return await handle.result();
|
|
198
|
+
}
|
|
199
|
+
async invokeLocal(name, id, ...argsWithOverrides) {
|
|
200
|
+
if (this.#invocationHandles.has(id)) {
|
|
201
|
+
return this.#invocationHandles.get(id);
|
|
168
202
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
203
|
+
const { args, opts: optionOverrides } = utils.split(argsWithOverrides);
|
|
204
|
+
// version 0 means the latest registered version
|
|
205
|
+
const givenVersion = optionOverrides?.version ?? 0;
|
|
206
|
+
// guarantees we only use the overrides in case the user pass an object with more properties
|
|
207
|
+
const { eidFn, idempotencyKeyFn, retryPolicy, tags, timeout, version } = optionOverrides;
|
|
208
|
+
const { func, opts: registeredOpts } = this.registeredFunction(name, givenVersion);
|
|
209
|
+
const opts = utils.mergeObjects({
|
|
210
|
+
eidFn,
|
|
211
|
+
idempotencyKeyFn,
|
|
212
|
+
retryPolicy,
|
|
213
|
+
tags,
|
|
214
|
+
timeout,
|
|
215
|
+
version,
|
|
216
|
+
}, registeredOpts);
|
|
217
|
+
// We want to preserve the registered version.
|
|
177
218
|
opts.version = registeredOpts.version;
|
|
178
|
-
// For tags we need to merge the objects themselves
|
|
219
|
+
// For tags we need to merge the objects themselves
|
|
220
|
+
// giving priority to the passed tags and add the
|
|
179
221
|
// resonate:invocation tag to identify a top level invocation
|
|
180
|
-
opts.tags = { ...registeredOpts.tags, ...
|
|
222
|
+
opts.tags = { ...registeredOpts.tags, ...tags, "resonate:invocation": "true" };
|
|
181
223
|
// lock on top level is true by default
|
|
182
|
-
opts.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
224
|
+
opts.shouldLock = opts.shouldLock ?? true;
|
|
225
|
+
const param = {
|
|
226
|
+
func: name,
|
|
227
|
+
version: opts.version,
|
|
228
|
+
retryPolicy: opts.retryPolicy,
|
|
229
|
+
args,
|
|
230
|
+
};
|
|
231
|
+
const idempotencyKey = opts.idempotencyKeyFn(id);
|
|
232
|
+
const storedPromise = await this.promisesStore.create(id, idempotencyKey, false, undefined, opts.encoder.encode(param), Date.now() + opts.timeout, opts.tags);
|
|
233
|
+
const runFunc = async () => {
|
|
234
|
+
const eid = opts.eidFn(id);
|
|
235
|
+
let value;
|
|
236
|
+
// If the promise that comes back from the server is already completed, resolve or reject right away.
|
|
237
|
+
switch (storedPromise.state) {
|
|
238
|
+
case "RESOLVED":
|
|
239
|
+
return opts.encoder.decode(storedPromise.value.data);
|
|
240
|
+
case "REJECTED":
|
|
241
|
+
throw opts.encoder.decode(storedPromise.value.data);
|
|
242
|
+
case "REJECTED_CANCELED":
|
|
243
|
+
throw new errors_1.ResonateError("Resonate function canceled", errors_1.ErrorCodes.CANCELED, opts.encoder.decode(storedPromise.value.data));
|
|
244
|
+
case "REJECTED_TIMEDOUT":
|
|
245
|
+
throw new errors_1.ResonateError(`Resonate function timedout at ${new Date(storedPromise.timeout).toISOString()}`, errors_1.ErrorCodes.TIMEDOUT);
|
|
246
|
+
}
|
|
247
|
+
// storedPromise.state === "PENDING"
|
|
248
|
+
try {
|
|
249
|
+
// Acquire the lock if necessary
|
|
250
|
+
if (opts.shouldLock) {
|
|
251
|
+
const acquireLock = async () => {
|
|
252
|
+
try {
|
|
253
|
+
return await this.store.locks.tryAcquire(id, eid);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
// if lock is already acquired, return false so we can poll
|
|
257
|
+
if (e instanceof errors_1.ResonateError && e.code === errors_1.ErrorCodes.STORE_FORBIDDEN) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
throw e;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
while (!(await acquireLock())) {
|
|
264
|
+
await (0, utils_1.sleep)(opts.pollFrequency);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
let error;
|
|
268
|
+
const ctx = Context.createRootContext(this, { id, eid, name, opts });
|
|
269
|
+
// we need to hold on to a boolean to determine if the function was successful,
|
|
270
|
+
// we cannot rely on the value or error as these values could be undefined
|
|
271
|
+
let success = true;
|
|
272
|
+
try {
|
|
273
|
+
value = await (0, retry_1.runWithRetry)(async () => {
|
|
274
|
+
ctx.reset();
|
|
275
|
+
return await func(ctx, ...args);
|
|
276
|
+
}, opts.retryPolicy, storedPromise.timeout);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
if (ctx.aborted) {
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
// We need to capture the error to be able to reject the durable promise
|
|
283
|
+
// after that we will then propagate this error by rejecting the result promise
|
|
284
|
+
error = e;
|
|
285
|
+
success = false;
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
await ctx.finalize();
|
|
289
|
+
}
|
|
290
|
+
let completedPromiseRecord;
|
|
291
|
+
if (success) {
|
|
292
|
+
completedPromiseRecord = await this.promisesStore.resolve(id, idempotencyKey, false, storedPromise.value.headers, opts.encoder.encode(value));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
completedPromiseRecord = await this.promisesStore.reject(id, idempotencyKey, false, storedPromise.value.headers, opts.encoder.encode(error));
|
|
296
|
+
}
|
|
297
|
+
// Because of eventual consistency and recovery paths it is possible that we get a
|
|
298
|
+
// rejected promise even if we did call `resolve` on it or the other way around.
|
|
299
|
+
// What should never happen is that we get a "PENDING" promise
|
|
300
|
+
switch (completedPromiseRecord.state) {
|
|
301
|
+
case "RESOLVED":
|
|
302
|
+
return value;
|
|
303
|
+
case "REJECTED":
|
|
304
|
+
throw error;
|
|
305
|
+
case "REJECTED_CANCELED":
|
|
306
|
+
throw new errors_1.ResonateError("Resonate function canceled", errors_1.ErrorCodes.CANCELED, error);
|
|
307
|
+
case "REJECTED_TIMEDOUT":
|
|
308
|
+
throw new errors_1.ResonateError(`Resonate function timedout at ${new Date(completedPromiseRecord.timeout).toISOString()}`, errors_1.ErrorCodes.TIMEDOUT);
|
|
309
|
+
case "PENDING":
|
|
310
|
+
throw new Error("Unreachable");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (err instanceof errors_1.ResonateError && (err.code === errors_1.ErrorCodes.CANCELED || err.code === errors_1.ErrorCodes.TIMEDOUT)) {
|
|
315
|
+
// Cancel and timeout errors just forward them
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
else if (err instanceof errors_1.ResonateError && err.code !== errors_1.ErrorCodes.ABORT) {
|
|
319
|
+
// All other resonate errors we wrap them in an abort.
|
|
320
|
+
throw new errors_1.ResonateError("Unrecoverable Error: Aborting", errors_1.ErrorCodes.ABORT, err);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// - The error was already an abort error, for example, the unrecoverable error happened in one
|
|
324
|
+
// of the children invocations
|
|
325
|
+
// - Error in the user function.
|
|
326
|
+
// Either way, just forward it.
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
// release lock if necessary
|
|
332
|
+
if (opts.shouldLock) {
|
|
333
|
+
await this.locksStore.release(id, eid);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return value;
|
|
337
|
+
};
|
|
338
|
+
const resultPromise = runFunc();
|
|
339
|
+
const handle = new InvocationHandle(resultPromise, id);
|
|
340
|
+
this.#invocationHandles.set(id, handle);
|
|
341
|
+
return handle;
|
|
193
342
|
}
|
|
194
|
-
schedule(name, cron, func, ...argsWithOpts) {
|
|
343
|
+
async schedule(name, cron, func, ...argsWithOpts) {
|
|
195
344
|
const { args, opts: givenOpts } = utils.split(argsWithOpts);
|
|
196
|
-
const opts = this.
|
|
345
|
+
const opts = this.withDefaultOpts(givenOpts);
|
|
197
346
|
if (typeof func === "function") {
|
|
198
347
|
// if function is provided, the default version is 1
|
|
199
348
|
// as opposed to 0 (alias for latest version)
|
|
@@ -201,10 +350,7 @@ class ResonateBase {
|
|
|
201
350
|
this.register(name, func, opts);
|
|
202
351
|
}
|
|
203
352
|
const funcName = typeof func === "string" ? func : name;
|
|
204
|
-
|
|
205
|
-
throw new Error(`Function ${funcName} version ${opts.version} not registered`);
|
|
206
|
-
}
|
|
207
|
-
const { opts: { retry, version, timeout, tags: promiseTags }, } = this.functions[funcName][opts.version];
|
|
353
|
+
const { opts: { retryPolicy: retry, version, timeout, tags: promiseTags }, } = this.registeredFunction(funcName, opts.version);
|
|
208
354
|
const idempotencyKey = opts.idempotencyKeyFn(funcName);
|
|
209
355
|
const promiseParam = {
|
|
210
356
|
func: funcName,
|
|
@@ -212,83 +358,390 @@ class ResonateBase {
|
|
|
212
358
|
retryPolicy: retry,
|
|
213
359
|
args,
|
|
214
360
|
};
|
|
215
|
-
return this.schedules.create(name, cron, "{{.id}}.{{.timestamp}}", timeout, {
|
|
361
|
+
return await this.schedules.create(name, cron, "{{.id}}.{{.timestamp}}", timeout, {
|
|
216
362
|
idempotencyKey,
|
|
217
363
|
promiseParam,
|
|
218
364
|
promiseTags,
|
|
219
365
|
});
|
|
220
366
|
}
|
|
367
|
+
withDefaultOpts(givenOpts = {}) {
|
|
368
|
+
// merge tags
|
|
369
|
+
const tags = { ...this.defaultInvocationOptions.tags, ...givenOpts.tags };
|
|
370
|
+
return {
|
|
371
|
+
...this.defaultInvocationOptions,
|
|
372
|
+
...givenOpts,
|
|
373
|
+
tags,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
exports.Resonate = Resonate;
|
|
378
|
+
class Context {
|
|
379
|
+
#resonate;
|
|
380
|
+
#stopAllPolling = false;
|
|
381
|
+
#invocationHandles;
|
|
382
|
+
#aborted;
|
|
383
|
+
#abortCause;
|
|
384
|
+
childrenCount;
|
|
385
|
+
invocationData;
|
|
386
|
+
parent;
|
|
387
|
+
root;
|
|
388
|
+
constructor(resonate, invocationData, parent) {
|
|
389
|
+
this.#resonate = resonate;
|
|
390
|
+
this.#invocationHandles = new Map();
|
|
391
|
+
this.#aborted = false;
|
|
392
|
+
this.parent = parent;
|
|
393
|
+
this.root = !parent ? this : parent.root;
|
|
394
|
+
this.invocationData = invocationData;
|
|
395
|
+
this.childrenCount = 0;
|
|
396
|
+
}
|
|
397
|
+
static createRootContext(resonate, invocationData) {
|
|
398
|
+
return new Context(resonate, invocationData, undefined);
|
|
399
|
+
}
|
|
400
|
+
static createChildrenContext(parentCtx, invocationData) {
|
|
401
|
+
return new Context(parentCtx.#resonate, invocationData, parentCtx);
|
|
402
|
+
}
|
|
403
|
+
reset() {
|
|
404
|
+
this.childrenCount = 0;
|
|
405
|
+
}
|
|
406
|
+
async finalize() {
|
|
407
|
+
await Promise.allSettled(Array.from(this.#invocationHandles, ([_, handle]) => handle.result()));
|
|
408
|
+
}
|
|
409
|
+
abort(cause) {
|
|
410
|
+
this.root.#aborted = true;
|
|
411
|
+
this.root.#abortCause = cause;
|
|
412
|
+
}
|
|
413
|
+
get aborted() {
|
|
414
|
+
return this.#aborted;
|
|
415
|
+
}
|
|
416
|
+
get abortCause() {
|
|
417
|
+
return this.#abortCause;
|
|
418
|
+
}
|
|
419
|
+
async run(funcOrId, ...argsWithOpts) {
|
|
420
|
+
let handle;
|
|
421
|
+
if (typeof funcOrId === "string") {
|
|
422
|
+
handle = await this.invokeRemote(funcOrId, ...argsWithOpts);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
handle = await this.invokeLocal(funcOrId, ...argsWithOpts);
|
|
426
|
+
}
|
|
427
|
+
return await handle.result();
|
|
428
|
+
}
|
|
429
|
+
async invokeRemote(funcId, ...argsWithOpts) {
|
|
430
|
+
if (this.#invocationHandles.has(funcId)) {
|
|
431
|
+
return this.#invocationHandles.get(funcId);
|
|
432
|
+
}
|
|
433
|
+
const { opts: givenOpts } = utils.split(argsWithOpts);
|
|
434
|
+
const { opts: registeredOpts } = this.#resonate.registeredFunction(this.root.invocationData.name, this.root.invocationData.opts.version);
|
|
435
|
+
const opts = { ...registeredOpts, ...givenOpts };
|
|
436
|
+
// Merge the tags
|
|
437
|
+
opts.tags = { ...registeredOpts.tags, ...givenOpts?.tags };
|
|
438
|
+
// Default lock is false for children execution
|
|
439
|
+
opts.shouldLock = opts.shouldLock ?? false;
|
|
440
|
+
// Children execution do not need params since we don't go trough the recovery path with children
|
|
441
|
+
const param = {};
|
|
442
|
+
const idempotencyKey = opts.idempotencyKeyFn(funcId);
|
|
443
|
+
const storedPromise = await this.#resonate.promisesStore.create(funcId, idempotencyKey, false, undefined, opts.encoder.encode(param), Date.now() + opts.timeout, opts.tags);
|
|
444
|
+
const runFunc = async () => {
|
|
445
|
+
while (!this.#stopAllPolling) {
|
|
446
|
+
const durablePromiseRecord = await this.#resonate.promisesStore.get(storedPromise.id);
|
|
447
|
+
switch (durablePromiseRecord.state) {
|
|
448
|
+
case "RESOLVED":
|
|
449
|
+
return opts.encoder.decode(durablePromiseRecord.value.data);
|
|
450
|
+
case "REJECTED":
|
|
451
|
+
throw opts.encoder.decode(durablePromiseRecord.value.data);
|
|
452
|
+
case "REJECTED_CANCELED":
|
|
453
|
+
throw new errors_1.ResonateError("Resonate function canceled", errors_1.ErrorCodes.CANCELED, opts.encoder.decode(durablePromiseRecord.value.data));
|
|
454
|
+
case "REJECTED_TIMEDOUT":
|
|
455
|
+
throw new errors_1.ResonateError(`Resonate function timedout at ${new Date(durablePromiseRecord.timeout).toISOString()}`, errors_1.ErrorCodes.TIMEDOUT);
|
|
456
|
+
case "PENDING":
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
// TODO: Consider using exponential backoff instead.
|
|
460
|
+
(0, utils_1.sleep)(opts.pollFrequency);
|
|
461
|
+
}
|
|
462
|
+
throw new Error(`Polling of remote invocation with ${funcId} was stopped`);
|
|
463
|
+
};
|
|
464
|
+
const resultPromise = runFunc();
|
|
465
|
+
const invocationHandle = new InvocationHandle(resultPromise, funcId);
|
|
466
|
+
this.#invocationHandles.set(funcId, invocationHandle);
|
|
467
|
+
return invocationHandle;
|
|
468
|
+
}
|
|
469
|
+
async invokeLocal(func, ...argsWithOpts) {
|
|
470
|
+
const { args, opts: givenOpts } = utils.split(argsWithOpts);
|
|
471
|
+
const { opts: registeredOpts } = this.#resonate.registeredFunction(this.root.invocationData.name, this.root.invocationData.opts.version);
|
|
472
|
+
const opts = { ...registeredOpts, ...givenOpts };
|
|
473
|
+
// Merge the tags
|
|
474
|
+
opts.tags = { ...registeredOpts.tags, ...givenOpts.tags };
|
|
475
|
+
// Default lock is false for children execution
|
|
476
|
+
opts.shouldLock = opts.shouldLock ?? false;
|
|
477
|
+
this.childrenCount++;
|
|
478
|
+
// If it is an anonymous function give at anon name nested within the current invocation name
|
|
479
|
+
const name = func.name ? func.name : `${this.invocationData.name}__anon${this.childrenCount}`;
|
|
480
|
+
const id = `${this.invocationData.id}.${this.childrenCount}.${name}`;
|
|
481
|
+
if (this.#invocationHandles.has(id)) {
|
|
482
|
+
return this.#invocationHandles.get(id);
|
|
483
|
+
}
|
|
484
|
+
if (!opts.durable) {
|
|
485
|
+
const eid = opts.eidFn(id);
|
|
486
|
+
const runFunc = async () => {
|
|
487
|
+
const ctx = Context.createChildrenContext(this, { name, id, eid, opts });
|
|
488
|
+
const timeout = Date.now() + opts.timeout;
|
|
489
|
+
return (await (0, retry_1.runWithRetry)(async () => {
|
|
490
|
+
ctx.reset();
|
|
491
|
+
return await func(ctx, ...args);
|
|
492
|
+
}, opts.retryPolicy, timeout));
|
|
493
|
+
};
|
|
494
|
+
const resultPromise = runFunc();
|
|
495
|
+
const handle = new InvocationHandle(resultPromise, id);
|
|
496
|
+
this.#invocationHandles.set(id, handle);
|
|
497
|
+
return handle;
|
|
498
|
+
}
|
|
499
|
+
// Children execution do not need params since we don't go trough the recovery path with children
|
|
500
|
+
const param = {};
|
|
501
|
+
const idempotencyKey = opts.idempotencyKeyFn(id);
|
|
502
|
+
const storedPromise = await this.#resonate.promisesStore.create(id, idempotencyKey, false, undefined, opts.encoder.encode(param), Date.now() + opts.timeout, opts.tags);
|
|
503
|
+
const runFunc = async () => {
|
|
504
|
+
const eid = opts.eidFn(id);
|
|
505
|
+
let value;
|
|
506
|
+
// If the promise that comes back from the server is already completed, resolve or reject right away.
|
|
507
|
+
switch (storedPromise.state) {
|
|
508
|
+
case "RESOLVED":
|
|
509
|
+
return opts.encoder.decode(storedPromise.value.data);
|
|
510
|
+
case "REJECTED":
|
|
511
|
+
throw opts.encoder.decode(storedPromise.value.data);
|
|
512
|
+
case "REJECTED_CANCELED":
|
|
513
|
+
throw new errors_1.ResonateError("Resonate function canceled", errors_1.ErrorCodes.CANCELED, opts.encoder.decode(storedPromise.value.data));
|
|
514
|
+
case "REJECTED_TIMEDOUT":
|
|
515
|
+
throw new errors_1.ResonateError(`Resonate function timedout at ${new Date(storedPromise.timeout).toISOString()}`, errors_1.ErrorCodes.TIMEDOUT);
|
|
516
|
+
}
|
|
517
|
+
// storedPromise.state === "PENDING"
|
|
518
|
+
try {
|
|
519
|
+
// Acquire the lock if necessary
|
|
520
|
+
if (opts.shouldLock) {
|
|
521
|
+
const acquireLock = async () => {
|
|
522
|
+
try {
|
|
523
|
+
return await this.#resonate.locksStore.tryAcquire(id, eid);
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
// if lock is already acquired, return false so we can poll
|
|
527
|
+
if (e instanceof errors_1.ResonateError && e.code === errors_1.ErrorCodes.STORE_FORBIDDEN) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
throw e;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
while (!(await acquireLock())) {
|
|
534
|
+
await (0, utils_1.sleep)(opts.pollFrequency);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let error;
|
|
538
|
+
const ctx = Context.createChildrenContext(this, { id, eid, name, opts });
|
|
539
|
+
// we need to hold on to a boolean to determine if the function was successful,
|
|
540
|
+
// we cannot rely on the value or error as these values could be undefined
|
|
541
|
+
let success = true;
|
|
542
|
+
try {
|
|
543
|
+
value = await (0, retry_1.runWithRetry)(async () => {
|
|
544
|
+
ctx.reset();
|
|
545
|
+
return await func(ctx, ...args);
|
|
546
|
+
}, opts.retryPolicy, storedPromise.timeout);
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
// We need to capture the error to be able to reject the durable promise,
|
|
550
|
+
// after that we will then propagate this error by rejecting the result promise
|
|
551
|
+
error = e;
|
|
552
|
+
success = false;
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
// Resonate will implicitly await all the invocationHandles as the last
|
|
556
|
+
// thing it does with the context before it goes out of scope
|
|
557
|
+
await ctx.finalize();
|
|
558
|
+
}
|
|
559
|
+
if (this.root.aborted) {
|
|
560
|
+
throw new errors_1.ResonateError("Unrecoverable Error: Aborting", errors_1.ErrorCodes.ABORT, this.root.abortCause);
|
|
561
|
+
}
|
|
562
|
+
let completedPromiseRecord;
|
|
563
|
+
if (success) {
|
|
564
|
+
completedPromiseRecord = await this.#resonate.promisesStore.resolve(id, idempotencyKey, false, storedPromise.value.headers, opts.encoder.encode(value));
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
completedPromiseRecord = await this.#resonate.promisesStore.reject(id, idempotencyKey, false, storedPromise.value.headers, opts.encoder.encode(error));
|
|
568
|
+
}
|
|
569
|
+
// Because of eventual consistency and recovery paths it is possible that we get a
|
|
570
|
+
// rejected promise even if we did call `resolve` on it or the other way around.
|
|
571
|
+
// What should never happen is that we get a "PENDING" promise
|
|
572
|
+
switch (completedPromiseRecord.state) {
|
|
573
|
+
case "RESOLVED":
|
|
574
|
+
return value;
|
|
575
|
+
case "REJECTED":
|
|
576
|
+
throw error;
|
|
577
|
+
case "REJECTED_CANCELED":
|
|
578
|
+
throw new errors_1.ResonateError("Resonate function canceled", errors_1.ErrorCodes.CANCELED, error);
|
|
579
|
+
case "REJECTED_TIMEDOUT":
|
|
580
|
+
throw new errors_1.ResonateError(`Resonate function timedout at ${new Date(completedPromiseRecord.timeout).toISOString()}`, errors_1.ErrorCodes.TIMEDOUT);
|
|
581
|
+
case "PENDING":
|
|
582
|
+
throw new Error("Unreachable");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
if (err instanceof errors_1.ResonateError && (err.code === errors_1.ErrorCodes.CANCELED || err.code === errors_1.ErrorCodes.TIMEDOUT)) {
|
|
587
|
+
// Cancel and timeout errors, just forward them
|
|
588
|
+
throw err;
|
|
589
|
+
}
|
|
590
|
+
else if (err instanceof errors_1.ResonateError && err.code !== errors_1.ErrorCodes.ABORT) {
|
|
591
|
+
// Any other instance of ResonateError we must abort the current execution.
|
|
592
|
+
this.abort(err);
|
|
593
|
+
throw new errors_1.ResonateError("Unrecoverable Error: Aborting", errors_1.ErrorCodes.ABORT, err);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
throw err;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
// release lock if necessary
|
|
601
|
+
if (opts.shouldLock) {
|
|
602
|
+
await this.#resonate.locksStore.release(id, eid);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return value;
|
|
606
|
+
};
|
|
607
|
+
const resultPromise = runFunc();
|
|
608
|
+
const invocationHandle = new InvocationHandle(resultPromise, id);
|
|
609
|
+
this.#invocationHandles.set(id, invocationHandle);
|
|
610
|
+
return invocationHandle;
|
|
611
|
+
}
|
|
221
612
|
/**
|
|
222
|
-
*
|
|
613
|
+
* Durable version of sleep.
|
|
614
|
+
* Sleep for the specified time (ms).
|
|
223
615
|
*
|
|
224
|
-
* @param
|
|
225
|
-
* @returns
|
|
616
|
+
* @param ms Amount of time to sleep in milliseconds.
|
|
617
|
+
* @returns A Promise that resolves after the specified time has elapsed.
|
|
226
618
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
619
|
+
async sleep(ms) {
|
|
620
|
+
const id = `${this.invocationData.id}.${this.childrenCount++}`;
|
|
621
|
+
const handle = await this.invokeRemote(id, (0, options_1.options)({ timeout: ms, pollFrequency: ms, tags: { "resonate:timeout": "true" }, durable: true }));
|
|
622
|
+
await handle.result();
|
|
229
623
|
}
|
|
230
624
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
625
|
+
* Creates a Promise that is resolved with an array of results when all of the provided Promises
|
|
626
|
+
* resolve, or rejected when any Promise is rejected.
|
|
233
627
|
*
|
|
234
|
-
* @param
|
|
628
|
+
* @param values An array of Promises.
|
|
629
|
+
* @param opts Optional {@link options}.
|
|
630
|
+
* @returns A new ResonatePromise.
|
|
235
631
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
632
|
+
all(values, opts = {}) {
|
|
633
|
+
// catch all promises to prevent unhandled promise rejections,
|
|
634
|
+
// since Promise.all will not be called in the case where the
|
|
635
|
+
// durable promise already completed
|
|
636
|
+
for (const v of values) {
|
|
637
|
+
if (v instanceof Promise)
|
|
638
|
+
v.catch(() => { });
|
|
639
|
+
}
|
|
640
|
+
// prettier-ignore
|
|
641
|
+
return this.run(() => Promise.all(values), (0, options_1.options)({
|
|
642
|
+
retryPolicy: retryPolicies.never(),
|
|
643
|
+
...opts,
|
|
644
|
+
}));
|
|
240
645
|
}
|
|
241
646
|
/**
|
|
242
|
-
*
|
|
647
|
+
* Creates a Promise that is fulfilled by the first given promise to be fulfilled, or rejected
|
|
648
|
+
* with an AggregateError.
|
|
649
|
+
*
|
|
650
|
+
* @param values An array of Promises.
|
|
651
|
+
* @param opts Optional {@link options}.
|
|
652
|
+
* @returns A new ResonatePromise.
|
|
243
653
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
poll,
|
|
258
|
-
retry,
|
|
259
|
-
tags,
|
|
260
|
-
timeout,
|
|
261
|
-
version,
|
|
262
|
-
};
|
|
654
|
+
any(values, opts = {}) {
|
|
655
|
+
// catch all promises to prevent unhandled promise rejections,
|
|
656
|
+
// since Promise.any will not be called in the case where the
|
|
657
|
+
// durable promise already completed
|
|
658
|
+
for (const v of values) {
|
|
659
|
+
if (v instanceof Promise)
|
|
660
|
+
v.catch(() => { });
|
|
661
|
+
}
|
|
662
|
+
// prettier-ignore
|
|
663
|
+
return this.run(() => Promise.any(values), (0, options_1.options)({
|
|
664
|
+
retryPolicy: retryPolicies.never(),
|
|
665
|
+
...opts,
|
|
666
|
+
}));
|
|
263
667
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const { func, opts } = this.functions[param.func][param.version];
|
|
280
|
-
opts.retry = param.retryPolicy;
|
|
281
|
-
this.execute(param.func, promise.id, func, param.args, opts, promise);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
668
|
+
/**
|
|
669
|
+
* Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
|
|
670
|
+
* or rejected.
|
|
671
|
+
*
|
|
672
|
+
* @param values An array of Promises.
|
|
673
|
+
* @param opts Optional {@link options}.
|
|
674
|
+
* @returns A new ResonatePromise.
|
|
675
|
+
*/
|
|
676
|
+
race(values, opts = {}) {
|
|
677
|
+
// catch all promises to prevent unhandled promise rejections,
|
|
678
|
+
// since Promise.race will not be called in the case where the
|
|
679
|
+
// durable promise already completed
|
|
680
|
+
for (const v of values) {
|
|
681
|
+
if (v instanceof Promise)
|
|
682
|
+
v.catch(() => { });
|
|
285
683
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
684
|
+
// prettier-ignore
|
|
685
|
+
return this.run(() => Promise.race(values), (0, options_1.options)({
|
|
686
|
+
retryPolicy: retryPolicies.never(),
|
|
687
|
+
...opts,
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Creates a Promise that is resolved with an array of results when all of the provided Promises
|
|
692
|
+
* resolve or reject.
|
|
693
|
+
*
|
|
694
|
+
* @param values An array of Promises.
|
|
695
|
+
* @param opts Optional {@link options}.
|
|
696
|
+
* @returns A new Promise.
|
|
697
|
+
*/
|
|
698
|
+
allSettled(values, opts = {}) {
|
|
699
|
+
// catch all promises to prevent unhandled promise rejections,
|
|
700
|
+
// since Promise.allSettled will not be called in the case where the
|
|
701
|
+
// durable promise already completed
|
|
702
|
+
for (const v of values) {
|
|
703
|
+
if (v instanceof Promise)
|
|
704
|
+
v.catch(() => { });
|
|
290
705
|
}
|
|
706
|
+
// prettier-ignore
|
|
707
|
+
return this.run(() => Promise.allSettled(values), (0, options_1.options)({
|
|
708
|
+
retryPolicy: retryPolicies.never(),
|
|
709
|
+
...opts,
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Invoke a Resonate function in detached mode. Functions must first be registered with Resonate.
|
|
714
|
+
* a detached invocation will not be implecitly awaited at the end of the current context, instead
|
|
715
|
+
* it will be "supervised" as a top level invocation.
|
|
716
|
+
*
|
|
717
|
+
* @template R The return type of the function.
|
|
718
|
+
* @param id A unique id for the function invocation.
|
|
719
|
+
* @param name The function name.
|
|
720
|
+
* @param argsWithOverrides The function arguments and options overrides.
|
|
721
|
+
* @returns A Res.
|
|
722
|
+
*/
|
|
723
|
+
async detached(name, id, ...argsWithOverrides) {
|
|
724
|
+
return await this.#resonate.invokeLocal(name, id, ...argsWithOverrides);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
exports.Context = Context;
|
|
728
|
+
class InvocationHandle {
|
|
729
|
+
resultPromise;
|
|
730
|
+
invocationId;
|
|
731
|
+
constructor(resultPromise, invocationId) {
|
|
732
|
+
this.resultPromise = resultPromise;
|
|
733
|
+
this.invocationId = invocationId;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* get the current state of the resultPromise.
|
|
737
|
+
*
|
|
738
|
+
*/
|
|
739
|
+
async state() {
|
|
740
|
+
return await utils.promiseState(this.resultPromise);
|
|
741
|
+
}
|
|
742
|
+
async result() {
|
|
743
|
+
return this.resultPromise;
|
|
291
744
|
}
|
|
292
745
|
}
|
|
293
|
-
exports.
|
|
746
|
+
exports.InvocationHandle = InvocationHandle;
|
|
294
747
|
//# sourceMappingURL=resonate.js.map
|