@hotmeshio/hotmesh 0.0.32 → 0.0.34

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
@@ -44,7 +44,7 @@ npm install @hotmeshio/hotmesh
44
44
  }
45
45
  }
46
46
  ```
47
- 3. Although you could call your workflow directly (it's just a vanilla function), it's only durable when invoked and orchestrated via HotMesh.
47
+ 3. Instance a HotMesh **client** to invoke the workflow.
48
48
  ```javascript
49
49
  //client.ts
50
50
  import { Durable } from '@hotmeshio/hotmesh';
@@ -91,29 +91,77 @@ npm install @hotmeshio/hotmesh
91
91
  }
92
92
  ```
93
93
 
94
+ ### Workflow Extensions
94
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.
95
96
 
96
- Include statements like `await Durable.workflow.sleep('1 month')` to pause your workflow function for a month, or `await Durable.workflow.waitForSignal('signal1', 'signal2')` to pause your function until all signals have arrived.
97
-
98
- - `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.
99
- - `signal` | Send a signal (and optional payload) to any paused function.
100
- - `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.
101
- - `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.
102
- - `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
103
- - `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
104
- - `startChild` | Call another durable function, but do not await the response.
105
- - `search` | Instance a search session (e.g, `const search = MeshOS.search()`)
106
- - `set` | Set one or more name/value pairs (e.g, `search.set('name1', 'value1', 'name2', 'value2')`)
107
- - `get` | Get a single value by name(e.g, `search.get('name')`)
108
- - `mget` | Get multiple values by name (e.g, `search.mget('name1', 'name2')`)
109
- - `del` | Delete one or more entries by name and return the number deleted (e.g, `search.del('name1', 'name2')`)
110
- - `incr` | Increment (or decrement) a number (e.g, `search.incr('name', -99)`)
111
- - `mult` | Multiply a number (e.g, `search.mult('name', 12)`)
112
- - `find` | Find workflows using the native Redis [FT.*](https://redis.io/commands/ft.search/) search commands
113
- - `findWhere` | Find workflows using a simplified, JSON-based search syntax that overlays the native Redis FT.SEARCH syntax.
114
- - `createIndex` | Create a searchable index in Redis using simplified, JSON-based syntax that overlays the native Redis FT.CREATE syntax.
115
- - `startWorkers` | Start the workers necessary to govern your class (typically called at server startup).
116
- - `stopWorkers` | Stop all workers (typically called at server shutdown)
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
+ ```
117
165
 
118
166
  Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
119
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.32",
3
+ "version": "0.0.34",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -23,6 +23,7 @@
23
23
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
24
24
  "test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
25
25
  "test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/index.test.ts --detectOpenHandles --forceExit --verbose",
26
+ "test:trigger": "NODE_ENV=test jest ./tests/unit/services/activities/trigger.test.ts --detectOpenHandles --forceExit --verbose",
26
27
  "test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
27
28
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
28
29
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
@@ -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
  },
@@ -179,9 +178,8 @@ class ClientService {
179
178
  }
180
179
  }
181
180
  static async shutdown() {
182
- for (const [key, value] of ClientService.instances) {
183
- const hotMesh = await value;
184
- await hotMesh.stop();
181
+ for (const [_, hotMeshInstance] of ClientService.instances) {
182
+ (await hotMeshInstance).stop();
185
183
  }
186
184
  }
187
185
  }
@@ -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
 
@@ -12,5 +12,10 @@ export declare const Durable: {
12
12
  MeshOS: typeof MeshOSService;
13
13
  Worker: typeof WorkerService;
14
14
  workflow: typeof WorkflowService;
15
+ /**
16
+ * Shutdown everything. All connections, workers, and clients will be closed.
17
+ * Include in your signal handlers to ensure a clean shutdown.
18
+ */
19
+ shutdown(): Promise<void>;
15
20
  };
16
21
  export type { ContextType };
@@ -7,6 +7,7 @@ const meshos_1 = require("./meshos");
7
7
  const search_1 = require("./search");
8
8
  const worker_1 = require("./worker");
9
9
  const workflow_1 = require("./workflow");
10
+ const hotmesh_1 = require("../hotmesh");
10
11
  exports.Durable = {
11
12
  Client: client_1.ClientService,
12
13
  Connection: connection_1.ConnectionService,
@@ -14,4 +15,13 @@ exports.Durable = {
14
15
  MeshOS: meshos_1.MeshOSService,
15
16
  Worker: worker_1.WorkerService,
16
17
  workflow: workflow_1.WorkflowService,
18
+ /**
19
+ * Shutdown everything. All connections, workers, and clients will be closed.
20
+ * Include in your signal handlers to ensure a clean shutdown.
21
+ */
22
+ async shutdown() {
23
+ await client_1.ClientService.shutdown();
24
+ await worker_1.WorkerService.shutdown();
25
+ await hotmesh_1.HotMeshService.stop();
26
+ }
17
27
  };
@@ -8,7 +8,6 @@ const client_1 = require("./client");
8
8
  const search_1 = require("./search");
9
9
  const worker_1 = require("./worker");
10
10
  const workflow_1 = require("./workflow");
11
- const stream_1 = require("../signaler/stream");
12
11
  /**
13
12
  * The base class for running MeshOS workflows.
14
13
  * Extend this class, add your Redis config, and add functions to
@@ -47,9 +46,7 @@ class MeshOSService {
47
46
  * @returns {Promise<void>}
48
47
  */
49
48
  static async stopWorkers() {
50
- await _1.Durable.Client.shutdown();
51
- await _1.Durable.Worker.shutdown();
52
- await stream_1.StreamSignaler.stopConsuming();
49
+ await _1.Durable.shutdown();
53
50
  }
54
51
  /**
55
52
  * Initializes the worker(s). This is a static
@@ -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,
@@ -225,9 +240,8 @@ class WorkerService {
225
240
  };
226
241
  }
227
242
  static async shutdown() {
228
- for (const [key, value] of WorkerService.instances) {
229
- const hotMesh = await value;
230
- await hotMesh.stop();
243
+ for (const [_, hotMeshInstance] of WorkerService.instances) {
244
+ (await hotMeshInstance).stop();
231
245
  }
232
246
  }
233
247
  }
@@ -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
  }
@@ -16,6 +16,7 @@ declare class HotMeshService {
16
16
  quorum: QuorumService | null;
17
17
  workers: WorkerService[];
18
18
  logger: ILogger;
19
+ static disconnecting: boolean;
19
20
  verifyAndSetNamespace(namespace?: string): void;
20
21
  verifyAndSetAppId(appId: string): void;
21
22
  static init(config: HotMeshConfig): Promise<HotMeshService>;
@@ -42,7 +43,8 @@ declare class HotMeshService {
42
43
  scrub(jobId: string): Promise<void>;
43
44
  hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
44
45
  hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
45
- stop(): Promise<void>;
46
+ static stop(): Promise<void>;
47
+ stop(): void;
46
48
  compress(terms: string[]): Promise<boolean>;
47
49
  }
48
50
  export { HotMeshService };
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HotMeshService = void 0;
4
4
  const nanoid_1 = require("nanoid");
5
5
  const key_1 = require("../../modules/key");
6
+ const redis_1 = require("../connector/clients/redis");
7
+ const ioredis_1 = require("../connector/clients/ioredis");
6
8
  const engine_1 = require("../engine");
7
9
  const logger_1 = require("../logger");
8
10
  const stream_1 = require("../signaler/stream");
@@ -128,8 +130,15 @@ class HotMeshService {
128
130
  async hookAll(hookTopic, data, query, queryFacets = []) {
129
131
  return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
130
132
  }
131
- async stop() {
132
- await stream_1.StreamSignaler.stopConsuming();
133
+ static async stop() {
134
+ if (!this.disconnecting) {
135
+ this.disconnecting = true;
136
+ await stream_1.StreamSignaler.stopConsuming();
137
+ await redis_1.RedisConnection.disconnectAll();
138
+ await ioredis_1.RedisConnection.disconnectAll();
139
+ }
140
+ }
141
+ stop() {
133
142
  this.engine?.task.cancelCleanup();
134
143
  }
135
144
  async compress(terms) {
@@ -137,3 +146,4 @@ class HotMeshService {
137
146
  }
138
147
  }
139
148
  exports.HotMeshService = HotMeshService;
149
+ HotMeshService.disconnecting = false;
@@ -220,12 +220,12 @@ class StreamSignaler {
220
220
  for (const instance of [...StreamSignaler.signalers]) {
221
221
  instance.stopConsuming();
222
222
  }
223
+ await (0, utils_1.sleepFor)(BLOCK_TIME_MS);
223
224
  }
224
225
  async stopConsuming() {
225
226
  this.shouldConsume = false;
226
227
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
227
228
  this.cancelThrottle();
228
- //await sleepFor(BLOCK_TIME_MS);
229
229
  }
230
230
  cancelThrottle() {
231
231
  if (this.currentTimerId !== undefined) {
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.32",
3
+ "version": "0.0.34",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -23,6 +23,7 @@
23
23
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
24
24
  "test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
25
25
  "test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/index.test.ts --detectOpenHandles --forceExit --verbose",
26
+ "test:trigger": "NODE_ENV=test jest ./tests/unit/services/activities/trigger.test.ts --detectOpenHandles --forceExit --verbose",
26
27
  "test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
27
28
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
28
29
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
@@ -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
  },
@@ -199,9 +198,8 @@ export class ClientService {
199
198
  }
200
199
 
201
200
  static async shutdown(): Promise<void> {
202
- for (const [key, value] of ClientService.instances) {
203
- const hotMesh = await value;
204
- await hotMesh.stop();
201
+ for (const [_, hotMeshInstance] of ClientService.instances) {
202
+ (await hotMeshInstance).stop();
205
203
  }
206
204
  }
207
205
  }
@@ -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 { MeshOSService } from './meshos';
4
4
  import { Search } from './search';
5
5
  import { WorkerService } from './worker';
6
6
  import { WorkflowService } from './workflow';
7
+ import { HotMeshService } from '../hotmesh';
7
8
  import { ContextType } from '../../types/durable';
8
9
 
9
10
  export const Durable = {
@@ -13,6 +14,16 @@ export const Durable = {
13
14
  MeshOS: MeshOSService,
14
15
  Worker: WorkerService,
15
16
  workflow: WorkflowService,
17
+
18
+ /**
19
+ * Shutdown everything. All connections, workers, and clients will be closed.
20
+ * Include in your signal handlers to ensure a clean shutdown.
21
+ */
22
+ async shutdown(): Promise<void> {
23
+ await ClientService.shutdown();
24
+ await WorkerService.shutdown();
25
+ await HotMeshService.stop();
26
+ }
16
27
  };
17
28
 
18
29
  export type { ContextType };
@@ -7,7 +7,6 @@ import { WorkflowHandleService } from './handle';
7
7
  import { Search } from './search';
8
8
  import { WorkerService as Worker } from './worker';
9
9
  import { WorkflowService } from './workflow';
10
- import { StreamSignaler } from '../signaler/stream';
11
10
  import {
12
11
  FindOptions,
13
12
  FindWhereOptions,
@@ -135,9 +134,7 @@ export class MeshOSService {
135
134
  * @returns {Promise<void>}
136
135
  */
137
136
  static async stopWorkers(): Promise<void> {
138
- await Durable.Client.shutdown();
139
- await Durable.Worker.shutdown();
140
- await StreamSignaler.stopConsuming();
137
+ await Durable.shutdown();
141
138
  }
142
139
 
143
140
  /**
@@ -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,
@@ -275,9 +291,8 @@ export class WorkerService {
275
291
  }
276
292
 
277
293
  static async shutdown(): Promise<void> {
278
- for (const [key, value] of WorkerService.instances) {
279
- const hotMesh = await value;
280
- await hotMesh.stop();
294
+ for (const [_, hotMeshInstance] of WorkerService.instances) {
295
+ (await hotMeshInstance).stop();
281
296
  }
282
297
  }
283
298
  }
@@ -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
  }
@@ -1,5 +1,7 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { HMNS } from '../../modules/key';
3
+ import { RedisConnection } from '../connector/clients/redis';
4
+ import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
3
5
  import { EngineService } from '../engine';
4
6
  import { LoggerService, ILogger } from '../logger';
5
7
  import { StreamSignaler } from '../signaler/stream';
@@ -32,6 +34,8 @@ class HotMeshService {
32
34
  workers: WorkerService[] = [];
33
35
  logger: ILogger;
34
36
 
37
+ static disconnecting = false;
38
+
35
39
  verifyAndSetNamespace(namespace?: string) {
36
40
  if (!namespace) {
37
41
  this.namespace = HMNS;
@@ -176,8 +180,16 @@ class HotMeshService {
176
180
  return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
177
181
  }
178
182
 
179
- async stop() {
180
- await StreamSignaler.stopConsuming();
183
+ static async stop() {
184
+ if (!this.disconnecting) {
185
+ this.disconnecting = true;
186
+ await StreamSignaler.stopConsuming();
187
+ await RedisConnection.disconnectAll();
188
+ await IORedisConnection.disconnectAll();
189
+ }
190
+ }
191
+
192
+ stop() {
181
193
  this.engine?.task.cancelCleanup();
182
194
  }
183
195
 
@@ -254,13 +254,13 @@ class StreamSignaler {
254
254
  for (const instance of [...StreamSignaler.signalers]) {
255
255
  instance.stopConsuming();
256
256
  }
257
+ await sleepFor(BLOCK_TIME_MS);
257
258
  }
258
259
 
259
260
  async stopConsuming() {
260
261
  this.shouldConsume = false;
261
262
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
262
263
  this.cancelThrottle();
263
- //await sleepFor(BLOCK_TIME_MS);
264
264
  }
265
265
 
266
266
  cancelThrottle() {