@hotmeshio/hotmesh 0.0.31 → 0.0.33

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/README.md CHANGED
@@ -1,13 +1,9 @@
1
1
  # HotMesh
2
2
  ![alpha release](https://img.shields.io/badge/release-alpha-yellow)
3
3
 
4
- Elevate Redis from an in-memory data cache, and turn your unpredictable functions into unbreakable workflows.
4
+ Elevate Redis from an in-memory data cache, and turn your unpredictable functions into unbreakable workflows. HotMesh is a distributed orchestration engine that governs the execution of your functions, ensuring they always complete successfully.
5
5
 
6
- **HotMesh** is a wrapper for Redis that exposes concepts like ‘activities’, ‘workflows’, and 'jobs'. Behind the scenes, it uses *Redis Data* (Hash, ZSet, List); *Redis Streams* (XReadGroup, XAdd, XLen, etc); and *Redis Publish/Subscribe*.
7
-
8
- It's still Redis in the background, but your functions are run as *reentrant processes* and are executed in a distributed environment, with all the benefits of a distributed system, including fault tolerance, scalability, and high availability.
9
-
10
- Write functions in your own preferred style, and let Redis govern their execution with its unmatched performance.
6
+ *Write functions in your own preferred style, and let Redis govern their execution.*
11
7
 
12
8
  ## Install
13
9
  [![npm version](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh.svg)](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
@@ -15,102 +11,157 @@ Write functions in your own preferred style, and let Redis govern their executio
15
11
  ```sh
16
12
  npm install @hotmeshio/hotmesh
17
13
  ```
18
-
19
14
  ## Design
20
- 1. Start with any ordinary class. Pay attention to unpredictable functions: those that execute slowly, cause problems at scale, or simply fail to return now and then. *Note how the `flaky` function throws an error 50% of the time. This is exactly the type of function that can be fixed using HotMesh.*
21
- ```javascript
22
- //myworkflow.ts
23
-
24
- export class MyWorkflow {
25
15
 
26
- async run(name: string): Promise<string> {
27
- const hi = await this.flaky(name);
28
- const hello = await this.greet(name);
29
- return `${hi} ${hello}`;
30
- }
31
-
32
- //this function is unpredictable and will fail 50% of the time
33
- async flaky(name: string): Promise<string> {
34
- if (Math.random() < 0.5) {
35
- throw new Error('Ooops!');
36
- }
37
- return `Hi, ${name}!`;
38
- }
16
+ 1. Start by defining **activities**. Activities can be written in any style, using any framework, and can even be legacy functions you've already written. The only requirement is that they return a Promise. *Note how the `saludar` example throws an error 50% of the time. It doesn't matter how unpredictable your functions are, HotMesh will retry as necessary until they succeed.*
17
+ ```javascript
18
+ //activities.ts
19
+ export async function greet(name: string): Promise<string> {
20
+ return `Hello, ${name}!`;
21
+ }
39
22
 
40
- async greet(name: string): Promise<string> {
41
- return `Hello, ${name}!`;
23
+ export async function saludar(nombre: string): Promise<string> {
24
+ if (Math.random() > 0.5) throw new Error('Random error');
25
+ return `¡Hola, ${nombre}!`;
26
+ }
27
+ ```
28
+ 2. Define your **workflow** logic. Include conditional branching, loops, etc to control activity execution. It's vanilla code written in your own coding style. The only requirement is to use `proxyActivities`, ensuring your activities are executed with HotMesh's durability guarantee.
29
+ ```javascript
30
+ //workflows.ts
31
+ import { Durable } from '@hotmeshio/hotmesh';
32
+ import * as activities from './activities';
33
+
34
+ const { greet, saludar } = Durable.workflow
35
+ .proxyActivities<typeof activities>({
36
+ activities
37
+ });
38
+
39
+ export async function example(name: string, lang: string): Promise<string> {
40
+ if (lang === 'es') {
41
+ return await saludar(name);
42
+ } else {
43
+ return await greet(name);
42
44
  }
43
45
  }
44
46
  ```
45
- 2. Import and configure `Redis` and `MeshOS` as shown. List those functions that Redis should govern as durable workflows (like `run` and `flaky`). And that's it! *Your functions don't actually change; rather, their governance does.*
47
+ 3. Instance a HotMesh **client** to invoke the workflow.
46
48
  ```javascript
47
- //myworkflow.ts
48
-
49
+ //client.ts
50
+ import { Durable } from '@hotmeshio/hotmesh';
49
51
  import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
50
- import { MeshOS } from '@hotmeshio/hotmesh';
51
-
52
- export class MyWorkflow extends MeshOS {
53
-
54
- //configure Redis
55
- redisClass = Redis;
56
- redisOptions = { host: 'localhost', port: 6379 };
57
-
58
- //list functions to run as durable workflows
59
- workflowFunctions = ['run'];
60
-
61
- //list functions to retry and cache
62
- proxyFunctions = ['flaky'];
63
-
64
- //no need to change anything else!
65
-
66
- async run(name: string): Promise<string> {
67
- const hi = await this.flaky(name);
68
- const hello = await this.greet(name);
69
- return `${hi} ${hello}`;
70
- }
52
+ import { nanoid } from 'nanoid';
71
53
 
72
- async flaky(name: string): Promise<string> {
73
- if (Math.random() < 0.5) {
74
- throw new Error('Ooops!');
54
+ async function run(): Promise<string> {
55
+ const client = new Durable.Client({
56
+ connection: {
57
+ class: Redis,
58
+ options: { host: 'localhost', port: 6379 }
75
59
  }
76
- return `Hi, ${name}!`;
77
- }
60
+ });
78
61
 
79
- async greet(name: string): Promise<string> {
80
- return `Hello, ${name}!`;
81
- }
62
+ const handle = await client.workflow.start({
63
+ args: ['HotMesh', 'es'],
64
+ taskQueue: 'hello-world',
65
+ workflowName: 'example',
66
+ workflowId: nanoid()
67
+ });
68
+
69
+ return await handle.result();
70
+ //returns '¡Hola, HotMesh!'
82
71
  }
83
72
  ```
84
- 3. Invoke your class, providing a unique id (it's now an idempotent workflow and needs a GUID). Nothing changes from the outside, *but Redis now governs the end-to-end execution.* It's guaranteed to succeed, even if it breaks a few times along the way.
73
+ 4. Finally, create a **worker** and link your workflow function. Workers listen for tasks on their assigned Redis stream and invoke your workflow function each time they receive an event.
85
74
  ```javascript
86
- //mycaller.ts
87
-
88
- const workflow = new MyWorkflow({ id: 'my123', await: true });
89
- const response = await workflow.run('World');
90
- //Hi, World! Hello, World!
75
+ //worker.ts
76
+ import { Durable } from '@hotmeshio/hotmesh';
77
+ import Redis from 'ioredis';
78
+ import * as workflows from './workflows';
79
+
80
+ async function run() {
81
+ const worker = await Durable.Worker.create({
82
+ connection: {
83
+ class: Redis,
84
+ options: { host: 'localhost', port: 6379 },
85
+ },
86
+ taskQueue: 'hello-world',
87
+ workflow: workflows.example,
88
+ });
89
+
90
+ await worker.run();
91
+ }
91
92
  ```
92
93
 
93
- Redis governance delivers more than just reliability. Externalizing state fundamentally changes the execution profile for your functions, allowing you to design long-running, durable workflows. The `MeshOS` base class (shown in the examples above) provides additional methods for solving the most common state management challenges.
94
-
95
- - `waitForSignal` | Pause your function and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once all signals have arrived.
96
- - `signal` | Send a signal (and optional payload) to any paused function.
97
- - `hook` | Redis governance converts your functions into 're-entrant processes'. Optionally use the *hook* method to spawn parallel execution threads to augment a running workflow.
98
- - `sleep` | Pause function execution for a ridiculous amount of time (months, years, etc). There's no risk of information loss, as Redis governs function state. When your function awakens, function state is efficiently (and automatically) restored and your function will resume right where it left off.
99
- - `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
100
- - `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
101
- - `startChild` | Call another durable function, but do not await the response.
102
- - `search` | Instance a search session (e.g, `const search = MeshOS.search()`)
103
- - `set` | Set one or more name/value pairs (e.g, `search.set('name1', 'value1', 'name2', 'value2')`)
104
- - `get` | Get a single value by name(e.g, `search.get('name')`)
105
- - `mget` | Get multiple values by name (e.g, `search.mget('name1', 'name2')`)
106
- - `del` | Delete one or more entries by name and return the number deleted (e.g, `search.del('name1', 'name2')`)
107
- - `incr` | Increment (or decrement) a number (e.g, `search.incr('name', -99)`)
108
- - `mult` | Multiply a number (e.g, `search.mult('name', 12)`)
109
- - `find` | Find workflows using the native Redis [FT.*](https://redis.io/commands/ft.search/) search commands
110
- - `findWhere` | Find workflows using a simplified, JSON-based search syntax that overlays the native Redis FT.SEARCH syntax.
111
- - `createIndex` | Create a searchable index in Redis using simplified, JSON-based syntax that overlays the native Redis FT.CREATE syntax.
112
- - `startWorkers` | Start the workers necessary to govern your class (typically called at server startup).
113
- - `stopWorkers` | Stop all workers (typically called at server shutdown)
94
+ ### Workflow Extensions
95
+ Redis governance delivers more than just reliability. Externalizing state fundamentally changes the execution profile for your functions, allowing you to design long-running, durable workflows. The `Durable.workflow` base class (shown in the examples above) provides additional methods for solving the most common state management challenges.
96
+
97
+ - `waitForSignal` Pause your function and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once all signals have arrived.
98
+ ```javascript
99
+ const signals = [a, b] = await Durable.workflow.waitForSignal('sig1', 'sig2')`
100
+ ```
101
+ - `signal` Send a signal (and optional payload) to a paused function awaiting the signal.
102
+ ```javascript
103
+ await Durable.workflow.signal('sig1', {payload: 'hi!'});
104
+ ```
105
+ - `hook` Redis governance converts your functions into 're-entrant processes'. Optionally use the *hook* method to spawn parallel execution threads to augment a running workflow.
106
+ ```javascript
107
+ await Durable.workflow.hook({
108
+ workflowName: 'newsletter',
109
+ taskQueue: 'default',
110
+ args: []
111
+ });
112
+ ```
113
+ - `sleepFor` Pause function execution for a ridiculous amount of time (months, years, etc). There's no risk of information loss, as Redis governs function state. When your function awakens, function state is efficiently (and automatically) restored and your function will resume right where it left off.
114
+ ```javascript
115
+ await Durable.workflow.sleepFor('1 month');
116
+ ```
117
+ - `random` Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
118
+ ```javascript
119
+ const random = await Durable.workflow.random();
120
+ ```
121
+ - `executeChild` Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
122
+ ```javascript
123
+ const jobResponse = await Durable.workflow.executeChild({
124
+ workflowName: 'newsletter',
125
+ taskQueue: 'default',
126
+ args: [{ id, user_id, etc }],
127
+ });
128
+ ```
129
+ - `startChild` Call another durable function, but do not await the response.
130
+ ```javascript
131
+ const jobId = await Durable.workflow.startChild({
132
+ workflowName: 'newsletter',
133
+ taskQueue: 'default',
134
+ args: [{ id, user_id, etc }],
135
+ });
136
+ ```
137
+ - `search` Instance a search session
138
+ ```javascript
139
+ const search = await Durable.workflow.search();
140
+ ```
141
+ - `set` Set one or more name/value pairs
142
+ ```javascript
143
+ await search.set('name1', 'value1', 'name2', 'value2');
144
+ ```
145
+ - `get` Get a single value by name
146
+ ```javascript
147
+ const value = await search.get('name');
148
+ ```
149
+ - `mget` Get multiple values by name
150
+ ```javascript
151
+ const [val1, val2] = await search.mget('name1', 'name2');
152
+ ```
153
+ - `del` Delete one or more entries by name and return the number deleted
154
+ ```javascript
155
+ const count = await search.del('name1', 'name2');
156
+ ```
157
+ - `incr` Increment (or decrement) a number
158
+ ```javascript
159
+ const value = await search.incr('name', 12);
160
+ ```
161
+ - `mult` Multiply a number
162
+ ```javascript
163
+ const value = await search.mult('name', 12);
164
+ ```
114
165
 
115
166
  Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
116
167
 
@@ -28,6 +28,13 @@ declare class DurableSleepError extends Error {
28
28
  dimension: string;
29
29
  constructor(message: string, duration: number, index: number, dimension: string);
30
30
  }
31
+ declare class DurableSleepForError extends Error {
32
+ code: number;
33
+ duration: number;
34
+ index: number;
35
+ dimension: string;
36
+ constructor(message: string, duration: number, index: number, dimension: string);
37
+ }
31
38
  declare class DurableTimeoutError extends Error {
32
39
  code: number;
33
40
  constructor(message: string);
@@ -63,4 +70,4 @@ declare class CollationError extends Error {
63
70
  fault: CollationFaultType;
64
71
  constructor(status: number, leg: ActivityDuplex, stage: CollationStage, fault?: CollationFaultType);
65
72
  }
66
- export { CollationError, DurableTimeoutError, DurableMaxedError, DurableFatalError, DurableRetryError, DurableWaitForSignalError, DurableIncompleteSignalError, DurableSleepError, DuplicateJobError, GetStateError, SetStateError, MapDataError, RegisterTimeoutError, ExecActivityError };
73
+ export { CollationError, DurableTimeoutError, DurableMaxedError, DurableFatalError, DurableRetryError, DurableWaitForSignalError, DurableIncompleteSignalError, DurableSleepError, DurableSleepForError, DuplicateJobError, GetStateError, SetStateError, MapDataError, RegisterTimeoutError, ExecActivityError };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ExecActivityError = exports.RegisterTimeoutError = exports.MapDataError = exports.SetStateError = exports.GetStateError = exports.DuplicateJobError = exports.DurableSleepError = exports.DurableIncompleteSignalError = exports.DurableWaitForSignalError = exports.DurableRetryError = exports.DurableFatalError = exports.DurableMaxedError = exports.DurableTimeoutError = exports.CollationError = void 0;
3
+ exports.ExecActivityError = exports.RegisterTimeoutError = exports.MapDataError = exports.SetStateError = exports.GetStateError = exports.DuplicateJobError = exports.DurableSleepForError = exports.DurableSleepError = exports.DurableIncompleteSignalError = exports.DurableWaitForSignalError = exports.DurableRetryError = exports.DurableFatalError = exports.DurableMaxedError = exports.DurableTimeoutError = exports.CollationError = void 0;
4
4
  class GetStateError extends Error {
5
5
  constructor() {
6
6
  super("Error occurred while getting job state");
@@ -31,6 +31,7 @@ class DurableWaitForSignalError extends Error {
31
31
  }
32
32
  }
33
33
  exports.DurableWaitForSignalError = DurableWaitForSignalError;
34
+ /* @deprecated */
34
35
  class DurableSleepError extends Error {
35
36
  constructor(message, duration, index, dimension) {
36
37
  super(message);
@@ -41,6 +42,16 @@ class DurableSleepError extends Error {
41
42
  }
42
43
  }
43
44
  exports.DurableSleepError = DurableSleepError;
45
+ class DurableSleepForError extends Error {
46
+ constructor(message, duration, index, dimension) {
47
+ super(message);
48
+ this.duration = duration;
49
+ this.index = index;
50
+ this.dimension = dimension;
51
+ this.code = 592;
52
+ }
53
+ }
54
+ exports.DurableSleepForError = DurableSleepForError;
44
55
  class DurableTimeoutError extends Error {
45
56
  constructor(message) {
46
57
  super(message);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -76,8 +76,8 @@ class ClientService {
76
76
  };
77
77
  this.workflow = {
78
78
  start: async (options) => {
79
- const taskQueueName = options.taskQueue;
80
- const workflowName = options.workflowName;
79
+ const taskQueueName = options.entity ?? options.taskQueue;
80
+ const workflowName = options.entity ?? options.workflowName;
81
81
  const trc = options.workflowTrace;
82
82
  const spn = options.workflowSpan;
83
83
  //topic is concat of taskQueue and workflowName
@@ -93,13 +93,12 @@ class ClientService {
93
93
  };
94
94
  const context = { metadata: { trc, spn }, data: {} };
95
95
  const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context);
96
- //seed search data if present
96
+ // Seed search data if present
97
97
  if (jobId && options.search?.data) {
98
98
  const searchSessionId = `-search-0`;
99
99
  const search = new search_1.Search(jobId, hotMeshClient, searchSessionId);
100
- for (const [key, value] of Object.entries(options.search.data)) {
101
- search.set(key, value);
102
- }
100
+ const entries = Object.entries(options.search.data).flat();
101
+ search.set(...entries);
103
102
  }
104
103
  return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
105
104
  },
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * ERROR CODES:
8
8
  * 594: waitforsignal
9
- * 595: sleep
9
+ * 592: sleepFor
10
+ * 595: sleep (deprecated)
10
11
  * 596, 597, 598: fatal
11
12
  * 599: retry
12
13
  */
@@ -9,7 +9,8 @@ exports.DEFAULT_COEFFICIENT = exports.APP_ID = exports.APP_VERSION = exports.get
9
9
  *
10
10
  * ERROR CODES:
11
11
  * 594: waitforsignal
12
- * 595: sleep
12
+ * 592: sleepFor
13
+ * 595: sleep (deprecated)
13
14
  * 596, 597, 598: fatal
14
15
  * 599: retry
15
16
  */
@@ -129,6 +130,20 @@ const getWorkflowYAML = (app, version) => {
129
130
  maps:
130
131
  duration: '{$self.output.data.duration}'
131
132
  index: '{$self.output.data.index}'
133
+ 592:
134
+ schema:
135
+ type: object
136
+ properties:
137
+ duration:
138
+ type: number
139
+ description: sleepFor duration in seconds
140
+ index:
141
+ type: number
142
+ description: the current index
143
+ maps:
144
+ duration: '{$self.output.data.duration}'
145
+ index: '{$self.output.data.index}'
146
+
132
147
  job:
133
148
  maps:
134
149
  response: '{$self.output.data.response}'
@@ -231,7 +246,20 @@ const getWorkflowYAML = (app, version) => {
231
246
  maps:
232
247
  duration: '{$self.output.data.duration}'
233
248
  index: '{$self.output.data.index}'
234
-
249
+ 592:
250
+ schema:
251
+ type: object
252
+ properties:
253
+ duration:
254
+ type: number
255
+ description: sleepFor duration in seconds
256
+ index:
257
+ type: number
258
+ description: the current index
259
+ maps:
260
+ duration: '{$self.output.data.duration}'
261
+ index: '{$self.output.data.index}'
262
+
235
263
  siga594:
236
264
  title: Signal In - Wait for signals
237
265
  type: await
@@ -334,6 +362,19 @@ const getWorkflowYAML = (app, version) => {
334
362
  maps:
335
363
  duration: '{siga1.output.data.duration}'
336
364
 
365
+ siga592:
366
+ title: Signal In - Sleep For a duration and then cycle/goto
367
+ type: hook
368
+ sleep: '{sigw1.output.data.duration}'
369
+
370
+ sigc592:
371
+ title: Signal In - Goto Activity a1 after sleeping for a duration
372
+ type: cycle
373
+ ancestor: siga1
374
+ input:
375
+ maps:
376
+ duration: '{siga1.output.data.duration}'
377
+
337
378
  siga599:
338
379
  title: Signal In - Sleep exponentially longer and retry
339
380
  type: hook
@@ -452,6 +493,19 @@ const getWorkflowYAML = (app, version) => {
452
493
  maps:
453
494
  duration: '{a1.output.data.duration}'
454
495
 
496
+ a592:
497
+ title: Sleep For a duration and then cycle/goto
498
+ type: hook
499
+ sleep: '{w1.output.data.duration}'
500
+
501
+ c592:
502
+ title: Goto Activity a1 after sleeping for a duration
503
+ type: cycle
504
+ ancestor: a1
505
+ input:
506
+ maps:
507
+ duration: '{a1.output.data.duration}'
508
+
455
509
  a599:
456
510
  title: Sleep exponentially longer before retrying
457
511
  type: hook
@@ -655,6 +709,9 @@ const getWorkflowYAML = (app, version) => {
655
709
  - to: siga595
656
710
  conditions:
657
711
  code: 595
712
+ - to: siga592
713
+ conditions:
714
+ code: 592
658
715
  - to: siga599
659
716
  conditions:
660
717
  code: 599
@@ -666,6 +723,8 @@ const getWorkflowYAML = (app, version) => {
666
723
  - to: sigc595
667
724
  conditions:
668
725
  code: 202
726
+ siga592:
727
+ - to: sigc592
669
728
  siga599:
670
729
  - to: sigc599
671
730
  a1:
@@ -677,6 +736,9 @@ const getWorkflowYAML = (app, version) => {
677
736
  - to: a595
678
737
  conditions:
679
738
  code: 595
739
+ - to: a592
740
+ conditions:
741
+ code: 592
680
742
  - to: a599
681
743
  conditions:
682
744
  code: 599
@@ -703,6 +765,8 @@ const getWorkflowYAML = (app, version) => {
703
765
  - to: c595
704
766
  conditions:
705
767
  code: 202
768
+ a592:
769
+ - to: c592
706
770
  a599:
707
771
  - to: c599
708
772
 
@@ -178,7 +178,22 @@ class WorkerService {
178
178
  }
179
179
  catch (err) {
180
180
  //not an error...just a trigger to sleep
181
- if (err instanceof errors_1.DurableSleepError) {
181
+ if (err instanceof errors_1.DurableSleepForError) {
182
+ return {
183
+ status: stream_1.StreamStatus.SUCCESS,
184
+ code: err.code,
185
+ metadata: { ...data.metadata },
186
+ data: {
187
+ code: err.code,
188
+ message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
189
+ duration: err.duration,
190
+ index: err.index,
191
+ dimension: err.dimension
192
+ }
193
+ };
194
+ //deprecated format; not an error...just a trigger to sleep
195
+ }
196
+ else if (err instanceof errors_1.DurableSleepError) {
182
197
  return {
183
198
  status: stream_1.StreamStatus.SUCCESS,
184
199
  code: err.code,
@@ -81,10 +81,21 @@ export declare class WorkflowService {
81
81
  */
82
82
  static hook(options: HookOptions): Promise<string>;
83
83
  /**
84
- * Sleeps for a duration.
84
+ * Sleeps the workflow for a duration. As the function is reentrant,
85
+ * upon reentry, the function will traverse prior execution paths up
86
+ * until the sleep command and then resume execution from that point.
85
87
  * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
86
88
  * @returns {Promise<number>}
87
89
  */
90
+ static sleepFor(duration: string): Promise<number>;
91
+ /**
92
+ * Sleeps the workflow for a duration. As the function is reentrant,
93
+ * upon reentry, the function will traverse prior execution paths up
94
+ * until the sleep command and then resume execution from that point.
95
+ * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
96
+ * @returns {Promise<number>}
97
+ * @deprecated - use `sleepFor` instead
98
+ */
88
99
  static sleep(duration: string): Promise<number>;
89
100
  /**
90
101
  * Waits for a signal to awaken
@@ -262,10 +262,37 @@ class WorkflowService {
262
262
  }
263
263
  }
264
264
  /**
265
- * Sleeps for a duration.
265
+ * Sleeps the workflow for a duration. As the function is reentrant,
266
+ * upon reentry, the function will traverse prior execution paths up
267
+ * until the sleep command and then resume execution from that point.
266
268
  * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
267
269
  * @returns {Promise<number>}
268
270
  */
271
+ static async sleepFor(duration) {
272
+ const seconds = (0, ms_1.default)(duration) / 1000;
273
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
274
+ const namespace = store.get('namespace');
275
+ const workflowTopic = store.get('workflowTopic');
276
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
277
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'sleep')) {
278
+ const workflowId = store.get('workflowId');
279
+ const workflowDimension = store.get('workflowDimension') ?? '';
280
+ const COUNTER = store.get('counter');
281
+ const execIndex = COUNTER.counter;
282
+ // spawn a new sleep job if error code 592 is thrown by the worker
283
+ // NOTE: If this message appears in the stack trace, the `.sleepFor()` method in your workflow code was NOT awaited.
284
+ throw new errors_1.DurableSleepForError(workflowId, seconds, execIndex, workflowDimension);
285
+ }
286
+ return seconds;
287
+ }
288
+ /**
289
+ * Sleeps the workflow for a duration. As the function is reentrant,
290
+ * upon reentry, the function will traverse prior execution paths up
291
+ * until the sleep command and then resume execution from that point.
292
+ * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
293
+ * @returns {Promise<number>}
294
+ * @deprecated - use `sleepFor` instead
295
+ */
269
296
  static async sleep(duration) {
270
297
  const seconds = (0, ms_1.default)(duration) / 1000;
271
298
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -284,7 +311,6 @@ class WorkflowService {
284
311
  }
285
312
  catch (e) {
286
313
  // spawn a new sleep job if error code 595 is thrown by the worker)
287
- // NOTE: If this message shows up in your stack trace, you forgot to await `Durable.workflow.sleep()` in your workflow code.
288
314
  throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
289
315
  }
290
316
  }
@@ -225,7 +225,7 @@ class StreamSignaler {
225
225
  this.shouldConsume = false;
226
226
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
227
227
  this.cancelThrottle();
228
- await (0, utils_1.sleepFor)(BLOCK_TIME_MS);
228
+ //await sleepFor(BLOCK_TIME_MS);
229
229
  }
230
230
  cancelThrottle() {
231
231
  if (this.currentTimerId !== undefined) {
@@ -46,7 +46,7 @@ type WorkflowSearchOptions = {
46
46
  };
47
47
  type WorkflowOptions = {
48
48
  namespace?: string;
49
- taskQueue: string;
49
+ taskQueue?: string;
50
50
  args: any[];
51
51
  workflowId?: string;
52
52
  entity?: string;
package/modules/errors.ts CHANGED
@@ -33,6 +33,7 @@ class DurableWaitForSignalError extends Error {
33
33
  }
34
34
  }
35
35
 
36
+ /* @deprecated */
36
37
  class DurableSleepError extends Error {
37
38
  code: number;
38
39
  duration: number; //seconds
@@ -46,6 +47,19 @@ class DurableSleepError extends Error {
46
47
  this.code = 595;
47
48
  }
48
49
  }
50
+ class DurableSleepForError extends Error {
51
+ code: number;
52
+ duration: number; //seconds
53
+ index: number; //execution order in the workflow
54
+ dimension: string; //hook dimension (e.g., ',0,1,0') (uses empty string for `null`)
55
+ constructor(message: string, duration: number, index: number, dimension: string) {
56
+ super(message);
57
+ this.duration = duration;
58
+ this.index = index;
59
+ this.dimension = dimension;
60
+ this.code = 592;
61
+ }
62
+ }
49
63
  class DurableTimeoutError extends Error {
50
64
  code: number;
51
65
  constructor(message: string) {
@@ -124,6 +138,7 @@ export {
124
138
  DurableWaitForSignalError,
125
139
  DurableIncompleteSignalError,
126
140
  DurableSleepError,
141
+ DurableSleepForError,
127
142
  DuplicateJobError,
128
143
  GetStateError,
129
144
  SetStateError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -92,8 +92,8 @@ export class ClientService {
92
92
 
93
93
  workflow = {
94
94
  start: async (options: WorkflowOptions): Promise<WorkflowHandleService> => {
95
- const taskQueueName = options.taskQueue;
96
- const workflowName = options.workflowName;
95
+ const taskQueueName = options.entity ?? options.taskQueue;
96
+ const workflowName = options.entity ?? options.workflowName;
97
97
  const trc = options.workflowTrace;
98
98
  const spn = options.workflowSpan;
99
99
  //topic is concat of taskQueue and workflowName
@@ -113,13 +113,12 @@ export class ClientService {
113
113
  payload,
114
114
  context as JobState
115
115
  );
116
- //seed search data if present
116
+ // Seed search data if present
117
117
  if (jobId && options.search?.data) {
118
118
  const searchSessionId = `-search-0`;
119
119
  const search = new Search(jobId, hotMeshClient, searchSessionId);
120
- for (const [key, value] of Object.entries(options.search.data)) {
121
- search.set(key, value);
122
- }
120
+ const entries = Object.entries(options.search.data).flat();
121
+ search.set(...entries);
123
122
  }
124
123
  return new WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
125
124
  },
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * ERROR CODES:
8
8
  * 594: waitforsignal
9
- * 595: sleep
9
+ * 592: sleepFor
10
+ * 595: sleep (deprecated)
10
11
  * 596, 597, 598: fatal
11
12
  * 599: retry
12
13
  */
@@ -126,6 +127,20 @@ const getWorkflowYAML = (app: string, version: string) => {
126
127
  maps:
127
128
  duration: '{$self.output.data.duration}'
128
129
  index: '{$self.output.data.index}'
130
+ 592:
131
+ schema:
132
+ type: object
133
+ properties:
134
+ duration:
135
+ type: number
136
+ description: sleepFor duration in seconds
137
+ index:
138
+ type: number
139
+ description: the current index
140
+ maps:
141
+ duration: '{$self.output.data.duration}'
142
+ index: '{$self.output.data.index}'
143
+
129
144
  job:
130
145
  maps:
131
146
  response: '{$self.output.data.response}'
@@ -228,7 +243,20 @@ const getWorkflowYAML = (app: string, version: string) => {
228
243
  maps:
229
244
  duration: '{$self.output.data.duration}'
230
245
  index: '{$self.output.data.index}'
231
-
246
+ 592:
247
+ schema:
248
+ type: object
249
+ properties:
250
+ duration:
251
+ type: number
252
+ description: sleepFor duration in seconds
253
+ index:
254
+ type: number
255
+ description: the current index
256
+ maps:
257
+ duration: '{$self.output.data.duration}'
258
+ index: '{$self.output.data.index}'
259
+
232
260
  siga594:
233
261
  title: Signal In - Wait for signals
234
262
  type: await
@@ -331,6 +359,19 @@ const getWorkflowYAML = (app: string, version: string) => {
331
359
  maps:
332
360
  duration: '{siga1.output.data.duration}'
333
361
 
362
+ siga592:
363
+ title: Signal In - Sleep For a duration and then cycle/goto
364
+ type: hook
365
+ sleep: '{sigw1.output.data.duration}'
366
+
367
+ sigc592:
368
+ title: Signal In - Goto Activity a1 after sleeping for a duration
369
+ type: cycle
370
+ ancestor: siga1
371
+ input:
372
+ maps:
373
+ duration: '{siga1.output.data.duration}'
374
+
334
375
  siga599:
335
376
  title: Signal In - Sleep exponentially longer and retry
336
377
  type: hook
@@ -449,6 +490,19 @@ const getWorkflowYAML = (app: string, version: string) => {
449
490
  maps:
450
491
  duration: '{a1.output.data.duration}'
451
492
 
493
+ a592:
494
+ title: Sleep For a duration and then cycle/goto
495
+ type: hook
496
+ sleep: '{w1.output.data.duration}'
497
+
498
+ c592:
499
+ title: Goto Activity a1 after sleeping for a duration
500
+ type: cycle
501
+ ancestor: a1
502
+ input:
503
+ maps:
504
+ duration: '{a1.output.data.duration}'
505
+
452
506
  a599:
453
507
  title: Sleep exponentially longer before retrying
454
508
  type: hook
@@ -652,6 +706,9 @@ const getWorkflowYAML = (app: string, version: string) => {
652
706
  - to: siga595
653
707
  conditions:
654
708
  code: 595
709
+ - to: siga592
710
+ conditions:
711
+ code: 592
655
712
  - to: siga599
656
713
  conditions:
657
714
  code: 599
@@ -663,6 +720,8 @@ const getWorkflowYAML = (app: string, version: string) => {
663
720
  - to: sigc595
664
721
  conditions:
665
722
  code: 202
723
+ siga592:
724
+ - to: sigc592
666
725
  siga599:
667
726
  - to: sigc599
668
727
  a1:
@@ -674,6 +733,9 @@ const getWorkflowYAML = (app: string, version: string) => {
674
733
  - to: a595
675
734
  conditions:
676
735
  code: 595
736
+ - to: a592
737
+ conditions:
738
+ code: 592
677
739
  - to: a599
678
740
  conditions:
679
741
  code: 599
@@ -700,6 +762,8 @@ const getWorkflowYAML = (app: string, version: string) => {
700
762
  - to: c595
701
763
  conditions:
702
764
  code: 202
765
+ a592:
766
+ - to: c592
703
767
  a599:
704
768
  - to: c599
705
769
 
@@ -4,6 +4,7 @@ import {
4
4
  DurableMaxedError,
5
5
  DurableRetryError,
6
6
  DurableSleepError,
7
+ DurableSleepForError,
7
8
  DurableTimeoutError,
8
9
  DurableWaitForSignalError} from '../../modules/errors';
9
10
  import { asyncLocalStorage } from './asyncLocalStorage';
@@ -226,7 +227,22 @@ export class WorkerService {
226
227
  } catch (err) {
227
228
 
228
229
  //not an error...just a trigger to sleep
229
- if (err instanceof DurableSleepError) {
230
+ if (err instanceof DurableSleepForError) {
231
+ return {
232
+ status: StreamStatus.SUCCESS,
233
+ code: err.code,
234
+ metadata: { ...data.metadata },
235
+ data: {
236
+ code: err.code,
237
+ message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
238
+ duration: err.duration,
239
+ index: err.index,
240
+ dimension: err.dimension
241
+ }
242
+ } as StreamDataResponse;
243
+
244
+ //deprecated format; not an error...just a trigger to sleep
245
+ } else if (err instanceof DurableSleepError) {
230
246
  return {
231
247
  status: StreamStatus.SUCCESS,
232
248
  code: err.code,
@@ -3,6 +3,7 @@ import ms from 'ms';
3
3
  import {
4
4
  DurableIncompleteSignalError,
5
5
  DurableSleepError,
6
+ DurableSleepForError,
6
7
  DurableWaitForSignalError } from '../../modules/errors';
7
8
  import { KeyService, KeyType } from '../../modules/key';
8
9
  import { asyncLocalStorage } from './asyncLocalStorage';
@@ -289,10 +290,38 @@ export class WorkflowService {
289
290
  }
290
291
 
291
292
  /**
292
- * Sleeps for a duration.
293
+ * Sleeps the workflow for a duration. As the function is reentrant,
294
+ * upon reentry, the function will traverse prior execution paths up
295
+ * until the sleep command and then resume execution from that point.
293
296
  * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
294
297
  * @returns {Promise<number>}
295
298
  */
299
+ static async sleepFor(duration: string): Promise<number> {
300
+ const seconds = ms(duration) / 1000;
301
+ const store = asyncLocalStorage.getStore();
302
+ const namespace = store.get('namespace');
303
+ const workflowTopic = store.get('workflowTopic');
304
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
305
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'sleep')) {
306
+ const workflowId = store.get('workflowId');
307
+ const workflowDimension = store.get('workflowDimension') ?? '';
308
+ const COUNTER = store.get('counter');
309
+ const execIndex = COUNTER.counter;
310
+ // spawn a new sleep job if error code 592 is thrown by the worker
311
+ // NOTE: If this message appears in the stack trace, the `.sleepFor()` method in your workflow code was NOT awaited.
312
+ throw new DurableSleepForError(workflowId, seconds, execIndex, workflowDimension);
313
+ }
314
+ return seconds;
315
+ }
316
+
317
+ /**
318
+ * Sleeps the workflow for a duration. As the function is reentrant,
319
+ * upon reentry, the function will traverse prior execution paths up
320
+ * until the sleep command and then resume execution from that point.
321
+ * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
322
+ * @returns {Promise<number>}
323
+ * @deprecated - use `sleepFor` instead
324
+ */
296
325
  static async sleep(duration: string): Promise<number> {
297
326
  const seconds = ms(duration) / 1000;
298
327
 
@@ -312,7 +341,6 @@ export class WorkflowService {
312
341
  return seconds;
313
342
  } catch (e) {
314
343
  // spawn a new sleep job if error code 595 is thrown by the worker)
315
- // NOTE: If this message shows up in your stack trace, you forgot to await `Durable.workflow.sleep()` in your workflow code.
316
344
  throw new DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
317
345
  }
318
346
  }
@@ -260,7 +260,7 @@ class StreamSignaler {
260
260
  this.shouldConsume = false;
261
261
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
262
262
  this.cancelThrottle();
263
- await sleepFor(BLOCK_TIME_MS);
263
+ //await sleepFor(BLOCK_TIME_MS);
264
264
  }
265
265
 
266
266
  cancelThrottle() {
package/types/durable.ts CHANGED
@@ -54,11 +54,11 @@ type WorkflowSearchOptions = {
54
54
 
55
55
  type WorkflowOptions = {
56
56
  namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
57
- taskQueue: string;
57
+ taskQueue?: string; //optional if entity is provided
58
58
  args: any[]; //input arguments to pass in
59
59
  workflowId?: string; //execution id (the job id)
60
60
  entity?: string; //If invoking a workflow, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options.
61
- workflowName?: string; //the name of the user's workflow function
61
+ workflowName?: string; //the name of the user's workflow function; optional if 'entity' is provided
62
62
  parentWorkflowId?: string; //system reserved; the id of the parent; if present the flow will not self-clean until the parent that spawned it self-cleans
63
63
  workflowTrace?: string;
64
64
  workflowSpan?: string;
@@ -68,7 +68,7 @@ type WorkflowOptions = {
68
68
 
69
69
  type HookOptions = {
70
70
  namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
71
- taskQueue?: string;
71
+ taskQueue?: string; //optional if 'entity' is provided
72
72
  args: any[]; //input arguments to pass into the hook
73
73
  entity?: string; //If invoking a hook, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options.
74
74
  workflowId?: string; //execution id (the job id to hook into)