@resonatehq/sdk 0.5.5 → 0.6.1

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