@hotmeshio/hotmesh 0.0.30 → 0.0.32

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,82 +11,89 @@ 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
-
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
15
 
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. Although you could call your workflow directly (it's just a vanilla function), it's only durable when invoked and orchestrated via HotMesh.
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
+ 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
+ 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.
94
97
 
95
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.
96
99
  - `signal` | Send a signal (and optional payload) to any paused function.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
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
@@ -31,16 +31,16 @@ class WorkflowService {
31
31
  const workflowSpan = store.get('workflowSpan');
32
32
  const COUNTER = store.get('counter');
33
33
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
34
- const prefix = options.prefix ?? '';
35
- //this is risky but MUST be allowed. Users MAY set the workflowId,
36
- //but if there is a naming collision, the data from the target entity will be used
37
- //as there is know way of knowing if the item was generated via a prior run of the workflow
38
- const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
34
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
35
+ //if the hash is a helper, a dash begins it, so it isn't indexed
36
+ const entityOrEmptyString = options.entity ?? '';
37
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
38
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
39
39
  const parentWorkflowId = `${workflowId}-f`;
40
40
  const client = new client_1.ClientService({
41
41
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
42
42
  });
43
- let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId, namespace);
43
+ let handle = await client.workflow.getHandle(options.entity ?? options.taskQueue, options.entity ?? options.workflowName, childJobId, namespace);
44
44
  try {
45
45
  return await handle.result(true);
46
46
  }
@@ -73,10 +73,12 @@ class WorkflowService {
73
73
  const workflowSpan = store.get('workflowSpan');
74
74
  const COUNTER = store.get('counter');
75
75
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
76
- const prefix = options.prefix ?? '';
77
- const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
76
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
77
+ const entityOrEmptyString = options.entity ?? '';
78
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
79
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
78
80
  const parentWorkflowId = `${workflowId}-f`;
79
- const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
81
+ const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
80
82
  try {
81
83
  //get the status; if there is no error, return childJobId (what was spawned)
82
84
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
@@ -247,8 +249,8 @@ class WorkflowService {
247
249
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
248
250
  const workflowId = options.workflowId ?? store.get('workflowId');
249
251
  let workflowTopic = store.get('workflowTopic');
250
- if (options.taskQueue && options.workflowName) {
251
- workflowTopic = `${options.taskQueue}-${options.workflowName}`;
252
+ if (options.entity || (options.taskQueue && options.workflowName)) {
253
+ workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
252
254
  } //else this is essentially recursion as the function calls itself
253
255
  const payload = {
254
256
  arguments: [...options.args],
@@ -220,13 +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);
224
223
  }
225
224
  async stopConsuming() {
226
225
  this.shouldConsume = false;
227
226
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
228
227
  this.cancelThrottle();
229
- await (0, utils_1.sleepFor)(BLOCK_TIME_MS);
228
+ //await sleepFor(BLOCK_TIME_MS);
230
229
  }
231
230
  cancelThrottle() {
232
231
  if (this.currentTimerId !== undefined) {
@@ -46,10 +46,10 @@ 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
- prefix?: string;
52
+ entity?: string;
53
53
  workflowName?: string;
54
54
  parentWorkflowId?: string;
55
55
  workflowTrace?: string;
@@ -61,6 +61,7 @@ type HookOptions = {
61
61
  namespace?: string;
62
62
  taskQueue?: string;
63
63
  args: any[];
64
+ entity?: string;
64
65
  workflowId?: string;
65
66
  workflowName?: string;
66
67
  search?: WorkflowSearchOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
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
@@ -39,11 +39,11 @@ export class WorkflowService {
39
39
  const workflowSpan = store.get('workflowSpan');
40
40
  const COUNTER = store.get('counter');
41
41
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
42
- const prefix = options.prefix ?? '';
43
- //this is risky but MUST be allowed. Users MAY set the workflowId,
44
- //but if there is a naming collision, the data from the target entity will be used
45
- //as there is know way of knowing if the item was generated via a prior run of the workflow
46
- const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
42
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
43
+ //if the hash is a helper, a dash begins it, so it isn't indexed
44
+ const entityOrEmptyString = options.entity ?? '';
45
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
46
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
47
47
  const parentWorkflowId = `${workflowId}-f`;
48
48
 
49
49
  const client = new Client({
@@ -51,8 +51,8 @@ export class WorkflowService {
51
51
  });
52
52
 
53
53
  let handle = await client.workflow.getHandle(
54
- options.taskQueue,
55
- options.workflowName,
54
+ options.entity ?? options.taskQueue,
55
+ options.entity ?? options.workflowName,
56
56
  childJobId,
57
57
  namespace,
58
58
  );
@@ -89,10 +89,12 @@ export class WorkflowService {
89
89
  const workflowSpan = store.get('workflowSpan');
90
90
  const COUNTER = store.get('counter');
91
91
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
92
- const prefix = options.prefix ?? '';
93
- const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
92
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
93
+ const entityOrEmptyString = options.entity ?? '';
94
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
95
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
94
96
  const parentWorkflowId = `${workflowId}-f`;
95
- const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
97
+ const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
96
98
 
97
99
  try {
98
100
  //get the status; if there is no error, return childJobId (what was spawned)
@@ -273,8 +275,8 @@ export class WorkflowService {
273
275
  const store = asyncLocalStorage.getStore();
274
276
  const workflowId = options.workflowId ?? store.get('workflowId');
275
277
  let workflowTopic = store.get('workflowTopic');
276
- if (options.taskQueue && options.workflowName) {
277
- workflowTopic = `${options.taskQueue}-${options.workflowName}`;
278
+ if (options.entity || (options.taskQueue && options.workflowName)) {
279
+ workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
278
280
  } //else this is essentially recursion as the function calls itself
279
281
  const payload = {
280
282
  arguments: [...options.args],
@@ -254,14 +254,13 @@ class StreamSignaler {
254
254
  for (const instance of [...StreamSignaler.signalers]) {
255
255
  instance.stopConsuming();
256
256
  }
257
- await sleepFor(BLOCK_TIME_MS);
258
257
  }
259
258
 
260
259
  async stopConsuming() {
261
260
  this.shouldConsume = false;
262
261
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
263
262
  this.cancelThrottle();
264
- await sleepFor(BLOCK_TIME_MS);
263
+ //await sleepFor(BLOCK_TIME_MS);
265
264
  }
266
265
 
267
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
- prefix?: string; //If invoking a hook, the prefix ensures the FT.SEARCH index is properly scoped to those documents with this prefix
61
- workflowName?: string; //the name of the user's workflow function
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; 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,8 +68,9 @@ 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
+ 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.
73
74
  workflowId?: string; //execution id (the job id to hook into)
74
75
  workflowName?: string; //the name of the user's hook function
75
76
  search?: WorkflowSearchOptions //bind additional search terms immediately before hook reentry