@hotmeshio/hotmesh 0.0.31 → 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 +69 -66
- package/build/package.json +1 -1
- package/build/services/durable/client.js +2 -2
- package/build/services/signaler/stream.js +1 -1
- package/build/types/durable.d.ts +1 -1
- package/package.json +1 -1
- package/services/durable/client.ts +2 -2
- package/services/signaler/stream.ts +1 -1
- package/types/durable.ts +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# HotMesh
|
|
2
2
|

|
|
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
|
-
|
|
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
|
[](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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
48
|
-
|
|
49
|
+
//client.ts
|
|
50
|
+
import { Durable } from '@hotmeshio/hotmesh';
|
|
49
51
|
import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
|
|
50
|
-
import {
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
}
|
|
60
|
+
});
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 `
|
|
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.
|
package/build/package.json
CHANGED
|
@@ -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
|
|
@@ -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
|
|
228
|
+
//await sleepFor(BLOCK_TIME_MS);
|
|
229
229
|
}
|
|
230
230
|
cancelThrottle() {
|
|
231
231
|
if (this.currentTimerId !== undefined) {
|
package/build/types/durable.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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
|
|
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)
|