@hotmeshio/hotmesh 0.0.28 → 0.0.30
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 +32 -26
- package/build/package.json +2 -2
- package/build/services/durable/client.js +15 -4
- package/build/services/durable/index.d.ts +2 -0
- package/build/services/durable/index.js +2 -0
- package/build/services/durable/meshos.d.ts +8 -5
- package/build/services/durable/meshos.js +6 -3
- package/build/services/durable/workflow.d.ts +56 -11
- package/build/services/durable/workflow.js +77 -12
- package/build/types/durable.d.ts +32 -1
- package/build/types/index.d.ts +1 -1
- package/package.json +2 -2
- package/services/durable/client.ts +18 -6
- package/services/durable/index.ts +2 -0
- package/services/durable/meshos.ts +8 -5
- package/services/durable/workflow.ts +79 -12
- package/types/durable.ts +44 -4
- package/types/index.ts +1 -0
package/README.md
CHANGED
|
@@ -17,9 +17,7 @@ npm install @hotmeshio/hotmesh
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Design
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
1. Start with any ordinary class. Pay attention to unpredictable functions: those that execute slowly, cause problems at scale, or simply fail to return. *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.*
|
|
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.*
|
|
23
21
|
```javascript
|
|
24
22
|
//myworkflow.ts
|
|
25
23
|
|
|
@@ -44,7 +42,7 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
|
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
```
|
|
47
|
-
2. Import `Redis` and `MeshOS`
|
|
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.*
|
|
48
46
|
```javascript
|
|
49
47
|
//myworkflow.ts
|
|
50
48
|
|
|
@@ -53,6 +51,7 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
|
|
|
53
51
|
|
|
54
52
|
export class MyWorkflow extends MeshOS {
|
|
55
53
|
|
|
54
|
+
//configure Redis
|
|
56
55
|
redisClass = Redis;
|
|
57
56
|
redisOptions = { host: 'localhost', port: 6379 };
|
|
58
57
|
|
|
@@ -62,13 +61,14 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
|
|
|
62
61
|
//list functions to retry and cache
|
|
63
62
|
proxyFunctions = ['flaky'];
|
|
64
63
|
|
|
64
|
+
//no need to change anything else!
|
|
65
|
+
|
|
65
66
|
async run(name: string): Promise<string> {
|
|
66
67
|
const hi = await this.flaky(name);
|
|
67
68
|
const hello = await this.greet(name);
|
|
68
69
|
return `${hi} ${hello}`;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
//this function is now durable and will be retried until it succeeds!
|
|
72
72
|
async flaky(name: string): Promise<string> {
|
|
73
73
|
if (Math.random() < 0.5) {
|
|
74
74
|
throw new Error('Ooops!');
|
|
@@ -81,7 +81,7 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
```
|
|
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
|
|
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.
|
|
85
85
|
```javascript
|
|
86
86
|
//mycaller.ts
|
|
87
87
|
|
|
@@ -90,21 +90,27 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
|
|
|
90
90
|
//Hi, World! Hello, World!
|
|
91
91
|
```
|
|
92
92
|
|
|
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.
|
|
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
94
|
|
|
95
|
-
- `waitForSignal` | Pause and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once
|
|
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
96
|
- `signal` | Send a signal (and optional payload) to any paused function.
|
|
97
|
-
- `hook` | Redis governance
|
|
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.
|
|
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
99
|
- `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
|
|
100
100
|
- `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
|
|
101
101
|
- `startChild` | Call another durable function, but do not await the response.
|
|
102
|
-
- `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
108
114
|
|
|
109
115
|
Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
|
|
110
116
|
|
|
@@ -216,31 +222,31 @@ const hotMesh = await HotMesh.init({
|
|
|
216
222
|
```
|
|
217
223
|
|
|
218
224
|
### Observability
|
|
219
|
-
Workflows and activities are run according to the rules you define, offering [Graph-Oriented](https://github.com/hotmeshio/sdk-typescript/
|
|
225
|
+
Workflows and activities are run according to the rules you define, offering [Graph-Oriented](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/system_lifecycle.md#telemetry) telemetry insights into your legacy function executions.
|
|
220
226
|
|
|
221
227
|
## FAQ
|
|
222
|
-
Refer to the [FAQ](https://github.com/hotmeshio/sdk-typescript/
|
|
228
|
+
Refer to the [FAQ](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/faq.md) for terminology, definitions, and an exploration of how HotMesh facilitates orchestration use cases.
|
|
223
229
|
|
|
224
230
|
## Quick Start
|
|
225
|
-
Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/
|
|
231
|
+
Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/quickstart.md) for sample flows you can easily copy, paste, and modify to get started.
|
|
226
232
|
|
|
227
233
|
## Developer Guide
|
|
228
|
-
For more details on the complete development process, including information about schemas, APIs, and deployment, consult the [Developer Guide](https://github.com/hotmeshio/sdk-typescript/
|
|
234
|
+
For more details on the complete development process, including information about schemas, APIs, and deployment, consult the [Developer Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/developer_guide.md).
|
|
229
235
|
|
|
230
236
|
## Model Driven Development
|
|
231
|
-
[Model Driven Development](https://github.com/hotmeshio/sdk-typescript/
|
|
237
|
+
[Model Driven Development](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/model_driven_development.md) is an established strategy for managing process-oriented tasks. Check out this guide to understand its foundational principles.
|
|
232
238
|
|
|
233
239
|
## Data Mapping
|
|
234
|
-
Exchanging data between activities is central to HotMesh. For detailed information on supported functions and the functional mapping syntax (@pipes), see the [Data Mapping Overview](https://github.com/hotmeshio/sdk-typescript/
|
|
240
|
+
Exchanging data between activities is central to HotMesh. For detailed information on supported functions and the functional mapping syntax (@pipes), see the [Data Mapping Overview](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/data_mapping.md).
|
|
235
241
|
|
|
236
242
|
## Composition
|
|
237
|
-
While the simplest graphs are linear, detailing a consistent sequence of non-cyclical activities, graphs can be layered to represent intricate business scenarios. Some can even be designed to accommodate long-lasting workflows that span months. For more details, check out the [Composable Workflow Guide](https://github.com/hotmeshio/sdk-typescript/
|
|
243
|
+
While the simplest graphs are linear, detailing a consistent sequence of non-cyclical activities, graphs can be layered to represent intricate business scenarios. Some can even be designed to accommodate long-lasting workflows that span months. For more details, check out the [Composable Workflow Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/composable_workflow.md).
|
|
238
244
|
|
|
239
245
|
## Distributed Orchestration
|
|
240
|
-
HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchestration Guide](https://github.com/hotmeshio/sdk-typescript/
|
|
246
|
+
HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchestration Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/distributed_orchestration.md) for a detailed breakdown of the approach.
|
|
241
247
|
|
|
242
248
|
## System Lifecycle
|
|
243
|
-
Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/
|
|
249
|
+
Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/system_lifecycle.md).
|
|
244
250
|
|
|
245
251
|
## Alpha Release
|
|
246
|
-
So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/
|
|
252
|
+
So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/alpha.md)?
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
55
55
|
"test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
|
|
56
56
|
},
|
|
57
|
-
"keywords": [],
|
|
57
|
+
"keywords": ["durable workflow", "hotmesh", "service mesh", "workflows", "operational data", "redis"],
|
|
58
58
|
"author": "luke.birdeau@gmail.com",
|
|
59
59
|
"license": "SEE LICENSE IN LICENSE",
|
|
60
60
|
"dependencies": {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClientService = void 0;
|
|
4
|
-
const nanoid_1 = require("nanoid");
|
|
5
4
|
const factory_1 = require("./factory");
|
|
6
5
|
const handle_1 = require("./handle");
|
|
7
6
|
const hotmesh_1 = require("../hotmesh");
|
|
@@ -70,6 +69,9 @@ class ClientService {
|
|
|
70
69
|
};
|
|
71
70
|
this.search = async (hotMeshClient, index, query) => {
|
|
72
71
|
const store = hotMeshClient.engine.store;
|
|
72
|
+
if (query[0]?.startsWith('FT.')) {
|
|
73
|
+
return await store.exec(...query);
|
|
74
|
+
}
|
|
73
75
|
return await store.exec('FT.SEARCH', index, ...query);
|
|
74
76
|
};
|
|
75
77
|
this.workflow = {
|
|
@@ -85,14 +87,14 @@ class ClientService {
|
|
|
85
87
|
const payload = {
|
|
86
88
|
arguments: [...options.args],
|
|
87
89
|
parentWorkflowId: options.parentWorkflowId,
|
|
88
|
-
workflowId: options.workflowId ||
|
|
90
|
+
workflowId: options.workflowId || hotmesh_1.HotMeshService.guid(),
|
|
89
91
|
workflowTopic: workflowTopic,
|
|
90
92
|
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
91
93
|
};
|
|
92
94
|
const context = { metadata: { trc, spn }, data: {} };
|
|
93
95
|
const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context);
|
|
96
|
+
//seed search data if present
|
|
94
97
|
if (jobId && options.search?.data) {
|
|
95
|
-
//job successfully kicked off; there is default job data to persist
|
|
96
98
|
const searchSessionId = `-search-0`;
|
|
97
99
|
const search = new search_1.Search(jobId, hotMeshClient, searchSessionId);
|
|
98
100
|
for (const [key, value] of Object.entries(options.search.data)) {
|
|
@@ -122,8 +124,17 @@ class ClientService {
|
|
|
122
124
|
workflowTopic,
|
|
123
125
|
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
124
126
|
};
|
|
127
|
+
//seed search data if presentthe hook before entering
|
|
125
128
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
126
|
-
|
|
129
|
+
const msgId = await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, types_1.StreamStatus.PENDING, 202);
|
|
130
|
+
if (options.search?.data) {
|
|
131
|
+
const searchSessionId = `-search-${hotmesh_1.HotMeshService.guid()}-0`;
|
|
132
|
+
const search = new search_1.Search(options.workflowId, hotMeshClient, searchSessionId);
|
|
133
|
+
for (const [key, value] of Object.entries(options.search.data)) {
|
|
134
|
+
search.set(key, value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return msgId;
|
|
127
138
|
},
|
|
128
139
|
getHandle: async (taskQueue, workflowName, workflowId, namespace) => {
|
|
129
140
|
const workflowTopic = `${taskQueue}-${workflowName}`;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { ClientService } from './client';
|
|
2
2
|
import { ConnectionService } from './connection';
|
|
3
3
|
import { MeshOSService } from './meshos';
|
|
4
|
+
import { Search } from './search';
|
|
4
5
|
import { WorkerService } from './worker';
|
|
5
6
|
import { WorkflowService } from './workflow';
|
|
6
7
|
import { ContextType } from '../../types/durable';
|
|
7
8
|
export declare const Durable: {
|
|
8
9
|
Client: typeof ClientService;
|
|
9
10
|
Connection: typeof ConnectionService;
|
|
11
|
+
Search: typeof Search;
|
|
10
12
|
MeshOS: typeof MeshOSService;
|
|
11
13
|
Worker: typeof WorkerService;
|
|
12
14
|
workflow: typeof WorkflowService;
|
|
@@ -4,11 +4,13 @@ exports.Durable = void 0;
|
|
|
4
4
|
const client_1 = require("./client");
|
|
5
5
|
const connection_1 = require("./connection");
|
|
6
6
|
const meshos_1 = require("./meshos");
|
|
7
|
+
const search_1 = require("./search");
|
|
7
8
|
const worker_1 = require("./worker");
|
|
8
9
|
const workflow_1 = require("./workflow");
|
|
9
10
|
exports.Durable = {
|
|
10
11
|
Client: client_1.ClientService,
|
|
11
12
|
Connection: connection_1.ConnectionService,
|
|
13
|
+
Search: search_1.Search,
|
|
12
14
|
MeshOS: meshos_1.MeshOSService,
|
|
13
15
|
Worker: worker_1.WorkerService,
|
|
14
16
|
workflow: workflow_1.WorkflowService,
|
|
@@ -91,19 +91,22 @@ export declare class MeshOSService {
|
|
|
91
91
|
*/
|
|
92
92
|
static startWorkers(options?: MeshOSWorkerOptions): Promise<void>;
|
|
93
93
|
/**
|
|
94
|
-
* executes the redis FT search query
|
|
94
|
+
* executes the redis FT search query; optionally specify other commands
|
|
95
95
|
* @example '@_quantity:[89 89]'
|
|
96
|
+
* @example '@_quantity:[89 89] @_name:"John"'
|
|
97
|
+
* @example 'FT.search my-index @_quantity:[89 89]'
|
|
98
|
+
* @param {FindOptions} options
|
|
96
99
|
* @param {any[]} args
|
|
97
|
-
* @returns {string}
|
|
100
|
+
* @returns {Promise<string[] | [number] | Array<number, string | number | string[]>>}
|
|
98
101
|
*/
|
|
99
|
-
static find(options: FindOptions, ...args: string[]): Promise<string[] | [number]
|
|
102
|
+
static find(options: FindOptions, ...args: string[]): Promise<string[] | [number] | Array<string | number | string[]>>;
|
|
100
103
|
/**
|
|
101
104
|
* Provides a JSON abstraction for the Redis FT.search command
|
|
102
105
|
* (e.g, `count`, `query`, `return`, `limit`)
|
|
103
106
|
* @param {FindWhereOptions} options
|
|
104
|
-
* @returns {Promise<string[] | [number]
|
|
107
|
+
* @returns {Promise<string[] | [number] | Array<string | number | string[]>>}
|
|
105
108
|
*/
|
|
106
|
-
static findWhere(options: FindWhereOptions): Promise<string[] | [number]
|
|
109
|
+
static findWhere(options: FindWhereOptions): Promise<string[] | [number] | Array<string | number | string[]>>;
|
|
107
110
|
static generateSearchQuery(query: FindWhereQuery[]): string;
|
|
108
111
|
/**
|
|
109
112
|
* returns the workflow handle. The handle can then be
|
|
@@ -140,10 +140,13 @@ class MeshOSService {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
-
* executes the redis FT search query
|
|
143
|
+
* executes the redis FT search query; optionally specify other commands
|
|
144
144
|
* @example '@_quantity:[89 89]'
|
|
145
|
+
* @example '@_quantity:[89 89] @_name:"John"'
|
|
146
|
+
* @example 'FT.search my-index @_quantity:[89 89]'
|
|
147
|
+
* @param {FindOptions} options
|
|
145
148
|
* @param {any[]} args
|
|
146
|
-
* @returns {string}
|
|
149
|
+
* @returns {Promise<string[] | [number] | Array<number, string | number | string[]>>}
|
|
147
150
|
*/
|
|
148
151
|
static async find(options, ...args) {
|
|
149
152
|
const my = new this();
|
|
@@ -170,7 +173,7 @@ class MeshOSService {
|
|
|
170
173
|
* Provides a JSON abstraction for the Redis FT.search command
|
|
171
174
|
* (e.g, `count`, `query`, `return`, `limit`)
|
|
172
175
|
* @param {FindWhereOptions} options
|
|
173
|
-
* @returns {Promise<string[] | [number]
|
|
176
|
+
* @returns {Promise<string[] | [number] | Array<string | number | string[]>>}
|
|
174
177
|
*/
|
|
175
178
|
static async findWhere(options) {
|
|
176
179
|
const args = [this.generateSearchQuery(options.query)];
|
|
@@ -1,52 +1,97 @@
|
|
|
1
1
|
import { Search } from './search';
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
-
import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
|
|
3
|
+
import { ActivityConfig, HookOptions, ProxyType, WorkflowContext, WorkflowOptions } from "../../types/durable";
|
|
4
4
|
export declare class WorkflowService {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Spawns a child workflow. await and return the result.
|
|
7
|
+
* @template T - the result type
|
|
8
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
9
|
+
* @returns {Promise<T>} - the result of the child workflow
|
|
7
10
|
*/
|
|
8
11
|
static executeChild<T>(options: WorkflowOptions): Promise<T>;
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
13
|
+
* Spawns a child workflow. return the childJobId.
|
|
14
|
+
* This method is used when the result of the child workflow is not needed.
|
|
15
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
16
|
+
* @returns {Promise<string>} - the childJobId
|
|
11
17
|
*/
|
|
12
18
|
static startChild<T>(options: WorkflowOptions): Promise<string>;
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
20
|
+
* Wraps activities in a proxy that will durably run them
|
|
21
|
+
* @param {ActivityConfig} options - the activity configuration
|
|
22
|
+
* that will be used to wrap the activities. You must pass an
|
|
23
|
+
* `activities` object to this configuration. The activities object
|
|
24
|
+
* should be a key-value pair of activity names and their respective
|
|
25
|
+
* functions. This is typically done by importing the activities.
|
|
26
|
+
*
|
|
27
|
+
* @returns {ProxyType<ACT>} - a proxy object with the same keys as the
|
|
28
|
+
* activities object, but with the values replaced by a wrapped function
|
|
29
|
+
* @example
|
|
30
|
+
* // import the activities
|
|
31
|
+
* import * as activities from './activities';
|
|
32
|
+
* const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
33
|
+
*
|
|
34
|
+
* //or destructure the proxy object, as the function names are the keys
|
|
35
|
+
* const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
15
36
|
*/
|
|
16
37
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
|
|
17
38
|
/**
|
|
18
|
-
*
|
|
39
|
+
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
40
|
+
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
41
|
+
* @returns {Promise<Search>} - a search session
|
|
19
42
|
*/
|
|
20
43
|
static search(): Promise<Search>;
|
|
21
44
|
/**
|
|
22
|
-
*
|
|
45
|
+
* Return a handle to the hotmesh client currently running the workflow
|
|
46
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
23
47
|
*/
|
|
24
48
|
static getHotMesh(): Promise<HotMesh>;
|
|
25
49
|
/**
|
|
26
|
-
*
|
|
50
|
+
* Returns the current workflow context
|
|
51
|
+
* @returns {WorkflowContext} - the current workflow context
|
|
52
|
+
*/
|
|
53
|
+
static getContext(): WorkflowContext;
|
|
54
|
+
/**
|
|
55
|
+
* Those methods that may only be called once must be protected by flagging
|
|
27
56
|
* their execution with a unique key (the key is stored in the HASH alongside
|
|
28
57
|
* process state and job state)
|
|
58
|
+
* @private
|
|
29
59
|
*/
|
|
30
60
|
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
31
61
|
/**
|
|
32
|
-
*
|
|
62
|
+
* Returns a random number between 0 and 1. This number is deterministic
|
|
33
63
|
* and will never vary for a given seed. This is useful for randomizing
|
|
34
64
|
* pathways in a workflow that can be safely replayed.
|
|
35
|
-
* @returns {number}
|
|
65
|
+
* @returns {number} - a random number between 0 and 1
|
|
36
66
|
*/
|
|
37
67
|
static random(): number;
|
|
38
68
|
/**
|
|
39
|
-
*
|
|
69
|
+
* Sends signal data into any other paused thread (which is paused and
|
|
40
70
|
* awaiting the signal) from within a hook-thread or the main-thread
|
|
71
|
+
* @param {string} signalId - the signal id
|
|
72
|
+
* @param {Record<any, any>} data - the signal data
|
|
73
|
+
* @returns {Promise<string>} - the stream id
|
|
41
74
|
*/
|
|
42
75
|
static signal(signalId: string, data: Record<any, any>): Promise<string>;
|
|
43
76
|
/**
|
|
44
|
-
*
|
|
77
|
+
* Spawns a hook from either the main thread or a hook thread with
|
|
45
78
|
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
46
79
|
* default to the current workflowId/WorkflowTopic if not provided
|
|
80
|
+
* @param {HookOptions} options - the hook options
|
|
47
81
|
*/
|
|
48
82
|
static hook(options: HookOptions): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Sleeps for a duration.
|
|
85
|
+
* @param {string} duration - for example: '1 minute', '2 hours', '3 days'
|
|
86
|
+
* @returns {Promise<number>}
|
|
87
|
+
*/
|
|
49
88
|
static sleep(duration: string): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Waits for a signal to awaken
|
|
91
|
+
* @param {string[]} signals - the signals to wait for
|
|
92
|
+
* @param {Record<string, string>} options - the options
|
|
93
|
+
* @returns {Promise<Record<any, any>[]>}
|
|
94
|
+
*/
|
|
50
95
|
static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
|
|
51
96
|
static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
|
|
52
97
|
}
|
|
@@ -17,7 +17,10 @@ const stream_1 = require("../../types/stream");
|
|
|
17
17
|
const utils_1 = require("../../modules/utils");
|
|
18
18
|
class WorkflowService {
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Spawns a child workflow. await and return the result.
|
|
21
|
+
* @template T - the result type
|
|
22
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
23
|
+
* @returns {Promise<T>} - the result of the child workflow
|
|
21
24
|
*/
|
|
22
25
|
static async executeChild(options) {
|
|
23
26
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -28,10 +31,11 @@ class WorkflowService {
|
|
|
28
31
|
const workflowSpan = store.get('workflowSpan');
|
|
29
32
|
const COUNTER = store.get('counter');
|
|
30
33
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
34
|
+
const prefix = options.prefix ?? '';
|
|
31
35
|
//this is risky but MUST be allowed. Users MAY set the workflowId,
|
|
32
36
|
//but if there is a naming collision, the data from the target entity will be used
|
|
33
37
|
//as there is know way of knowing if the item was generated via a prior run of the workflow
|
|
34
|
-
const childJobId = options.workflowId ??
|
|
38
|
+
const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
35
39
|
const parentWorkflowId = `${workflowId}-f`;
|
|
36
40
|
const client = new client_1.ClientService({
|
|
37
41
|
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
@@ -55,7 +59,10 @@ class WorkflowService {
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
62
|
+
* Spawns a child workflow. return the childJobId.
|
|
63
|
+
* This method is used when the result of the child workflow is not needed.
|
|
64
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
65
|
+
* @returns {Promise<string>} - the childJobId
|
|
59
66
|
*/
|
|
60
67
|
static async startChild(options) {
|
|
61
68
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -66,7 +73,8 @@ class WorkflowService {
|
|
|
66
73
|
const workflowSpan = store.get('workflowSpan');
|
|
67
74
|
const COUNTER = store.get('counter');
|
|
68
75
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
69
|
-
const
|
|
76
|
+
const prefix = options.prefix ?? '';
|
|
77
|
+
const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
70
78
|
const parentWorkflowId = `${workflowId}-f`;
|
|
71
79
|
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
72
80
|
try {
|
|
@@ -91,7 +99,22 @@ class WorkflowService {
|
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
|
-
*
|
|
102
|
+
* Wraps activities in a proxy that will durably run them
|
|
103
|
+
* @param {ActivityConfig} options - the activity configuration
|
|
104
|
+
* that will be used to wrap the activities. You must pass an
|
|
105
|
+
* `activities` object to this configuration. The activities object
|
|
106
|
+
* should be a key-value pair of activity names and their respective
|
|
107
|
+
* functions. This is typically done by importing the activities.
|
|
108
|
+
*
|
|
109
|
+
* @returns {ProxyType<ACT>} - a proxy object with the same keys as the
|
|
110
|
+
* activities object, but with the values replaced by a wrapped function
|
|
111
|
+
* @example
|
|
112
|
+
* // import the activities
|
|
113
|
+
* import * as activities from './activities';
|
|
114
|
+
* const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
115
|
+
*
|
|
116
|
+
* //or destructure the proxy object, as the function names are the keys
|
|
117
|
+
* const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
95
118
|
*/
|
|
96
119
|
static proxyActivities(options) {
|
|
97
120
|
if (options.activities) {
|
|
@@ -108,7 +131,9 @@ class WorkflowService {
|
|
|
108
131
|
return proxy;
|
|
109
132
|
}
|
|
110
133
|
/**
|
|
111
|
-
*
|
|
134
|
+
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
135
|
+
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
136
|
+
* @returns {Promise<Search>} - a search session
|
|
112
137
|
*/
|
|
113
138
|
static async search() {
|
|
114
139
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -124,7 +149,8 @@ class WorkflowService {
|
|
|
124
149
|
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
125
150
|
}
|
|
126
151
|
/**
|
|
127
|
-
*
|
|
152
|
+
* Return a handle to the hotmesh client currently running the workflow
|
|
153
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
128
154
|
*/
|
|
129
155
|
static async getHotMesh() {
|
|
130
156
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -133,9 +159,33 @@ class WorkflowService {
|
|
|
133
159
|
return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
134
160
|
}
|
|
135
161
|
/**
|
|
136
|
-
*
|
|
162
|
+
* Returns the current workflow context
|
|
163
|
+
* @returns {WorkflowContext} - the current workflow context
|
|
164
|
+
*/
|
|
165
|
+
static getContext() {
|
|
166
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
167
|
+
const workflowId = store.get('workflowId');
|
|
168
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
169
|
+
const workflowTopic = store.get('workflowTopic');
|
|
170
|
+
const namespace = store.get('namespace');
|
|
171
|
+
const workflowTrace = store.get('workflowTrace');
|
|
172
|
+
const workflowSpan = store.get('workflowSpan');
|
|
173
|
+
const COUNTER = store.get('counter');
|
|
174
|
+
return {
|
|
175
|
+
counter: COUNTER.counter,
|
|
176
|
+
namespace,
|
|
177
|
+
workflowId,
|
|
178
|
+
workflowDimension,
|
|
179
|
+
workflowTopic,
|
|
180
|
+
workflowTrace,
|
|
181
|
+
workflowSpan,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Those methods that may only be called once must be protected by flagging
|
|
137
186
|
* their execution with a unique key (the key is stored in the HASH alongside
|
|
138
187
|
* process state and job state)
|
|
188
|
+
* @private
|
|
139
189
|
*/
|
|
140
190
|
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
141
191
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -153,10 +203,10 @@ class WorkflowService {
|
|
|
153
203
|
return guidValue === 1;
|
|
154
204
|
}
|
|
155
205
|
/**
|
|
156
|
-
*
|
|
206
|
+
* Returns a random number between 0 and 1. This number is deterministic
|
|
157
207
|
* and will never vary for a given seed. This is useful for randomizing
|
|
158
208
|
* pathways in a workflow that can be safely replayed.
|
|
159
|
-
* @returns {number}
|
|
209
|
+
* @returns {number} - a random number between 0 and 1
|
|
160
210
|
*/
|
|
161
211
|
static random() {
|
|
162
212
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -165,8 +215,11 @@ class WorkflowService {
|
|
|
165
215
|
return (0, utils_1.deterministicRandom)(seed);
|
|
166
216
|
}
|
|
167
217
|
/**
|
|
168
|
-
*
|
|
218
|
+
* Sends signal data into any other paused thread (which is paused and
|
|
169
219
|
* awaiting the signal) from within a hook-thread or the main-thread
|
|
220
|
+
* @param {string} signalId - the signal id
|
|
221
|
+
* @param {Record<any, any>} data - the signal data
|
|
222
|
+
* @returns {Promise<string>} - the stream id
|
|
170
223
|
*/
|
|
171
224
|
static async signal(signalId, data) {
|
|
172
225
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -180,9 +233,10 @@ class WorkflowService {
|
|
|
180
233
|
}
|
|
181
234
|
}
|
|
182
235
|
/**
|
|
183
|
-
*
|
|
236
|
+
* Spawns a hook from either the main thread or a hook thread with
|
|
184
237
|
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
185
238
|
* default to the current workflowId/WorkflowTopic if not provided
|
|
239
|
+
* @param {HookOptions} options - the hook options
|
|
186
240
|
*/
|
|
187
241
|
static async hook(options) {
|
|
188
242
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -205,6 +259,11 @@ class WorkflowService {
|
|
|
205
259
|
return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
|
|
206
260
|
}
|
|
207
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Sleeps for a duration.
|
|
264
|
+
* @param {string} duration - for example: '1 minute', '2 hours', '3 days'
|
|
265
|
+
* @returns {Promise<number>}
|
|
266
|
+
*/
|
|
208
267
|
static async sleep(duration) {
|
|
209
268
|
const seconds = (0, ms_1.default)(duration) / 1000;
|
|
210
269
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -227,6 +286,12 @@ class WorkflowService {
|
|
|
227
286
|
throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
|
|
228
287
|
}
|
|
229
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Waits for a signal to awaken
|
|
291
|
+
* @param {string[]} signals - the signals to wait for
|
|
292
|
+
* @param {Record<string, string>} options - the options
|
|
293
|
+
* @returns {Promise<Record<any, any>[]>}
|
|
294
|
+
*/
|
|
230
295
|
static async waitForSignal(signals, options) {
|
|
231
296
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
232
297
|
const COUNTER = store.get('counter');
|
package/build/types/durable.d.ts
CHANGED
|
@@ -5,6 +5,36 @@ type WorkflowConfig = {
|
|
|
5
5
|
maximumInterval?: string;
|
|
6
6
|
initialInterval?: string;
|
|
7
7
|
};
|
|
8
|
+
type WorkflowContext = {
|
|
9
|
+
/**
|
|
10
|
+
* the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count.
|
|
11
|
+
*/
|
|
12
|
+
counter: number;
|
|
13
|
+
/**
|
|
14
|
+
* the HotMesh App namespace. `durable` is the default.
|
|
15
|
+
*/
|
|
16
|
+
namespace: string;
|
|
17
|
+
/**
|
|
18
|
+
* the workflow/job ID
|
|
19
|
+
*/
|
|
20
|
+
workflowId: string;
|
|
21
|
+
/**
|
|
22
|
+
* the dimensional isolation for the reentrant hook, expressed in the format `0,0`, `0,1`, etc
|
|
23
|
+
*/
|
|
24
|
+
workflowDimension: string;
|
|
25
|
+
/**
|
|
26
|
+
* a concatenation of the task queue and workflow name (e.g., `${taskQueueName}-${workflowName}`)
|
|
27
|
+
*/
|
|
28
|
+
workflowTopic: string;
|
|
29
|
+
/**
|
|
30
|
+
* the open telemetry trace context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
|
|
31
|
+
*/
|
|
32
|
+
workflowTrace: string;
|
|
33
|
+
/**
|
|
34
|
+
* the open telemetry span context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
|
|
35
|
+
*/
|
|
36
|
+
workflowSpan: string;
|
|
37
|
+
};
|
|
8
38
|
type WorkflowSearchOptions = {
|
|
9
39
|
index?: string;
|
|
10
40
|
prefix?: string[];
|
|
@@ -19,6 +49,7 @@ type WorkflowOptions = {
|
|
|
19
49
|
taskQueue: string;
|
|
20
50
|
args: any[];
|
|
21
51
|
workflowId?: string;
|
|
52
|
+
prefix?: string;
|
|
22
53
|
workflowName?: string;
|
|
23
54
|
parentWorkflowId?: string;
|
|
24
55
|
workflowTrace?: string;
|
|
@@ -142,4 +173,4 @@ type ActivityConfig = {
|
|
|
142
173
|
maximumInterval: string;
|
|
143
174
|
};
|
|
144
175
|
};
|
|
145
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
|
|
176
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, WorkflowContext, };
|
package/build/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
|
|
|
3
3
|
export { AsyncSignal } from './async';
|
|
4
4
|
export { CacheMode } from './cache';
|
|
5
5
|
export { CollationFaultType, CollationStage } from './collator';
|
|
6
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
6
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowContext, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
7
7
|
export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
|
|
8
8
|
export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
|
|
9
9
|
export { ILogger } from './logger';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
55
55
|
"test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
|
|
56
56
|
},
|
|
57
|
-
"keywords": [],
|
|
57
|
+
"keywords": ["durable workflow", "hotmesh", "service mesh", "workflows", "operational data", "redis"],
|
|
58
58
|
"author": "luke.birdeau@gmail.com",
|
|
59
59
|
"license": "SEE LICENSE IN LICENSE",
|
|
60
60
|
"dependencies": {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { nanoid } from 'nanoid';
|
|
2
1
|
import { APP_ID, APP_VERSION, DEFAULT_COEFFICIENT, getWorkflowYAML } from './factory';
|
|
3
2
|
import { WorkflowHandleService } from './handle';
|
|
4
3
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
@@ -85,6 +84,9 @@ export class ClientService {
|
|
|
85
84
|
|
|
86
85
|
search = async (hotMeshClient: HotMesh, index: string, query: string[]): Promise<string[]> => {
|
|
87
86
|
const store = hotMeshClient.engine.store;
|
|
87
|
+
if (query[0]?.startsWith('FT.')) {
|
|
88
|
+
return await store.exec(...query) as string[];
|
|
89
|
+
}
|
|
88
90
|
return await store.exec('FT.SEARCH', index, ...query) as string[];
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -101,7 +103,7 @@ export class ClientService {
|
|
|
101
103
|
const payload = {
|
|
102
104
|
arguments: [...options.args],
|
|
103
105
|
parentWorkflowId: options.parentWorkflowId,
|
|
104
|
-
workflowId: options.workflowId ||
|
|
106
|
+
workflowId: options.workflowId || HotMesh.guid(),
|
|
105
107
|
workflowTopic: workflowTopic,
|
|
106
108
|
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
107
109
|
}
|
|
@@ -109,9 +111,10 @@ export class ClientService {
|
|
|
109
111
|
const jobId = await hotMeshClient.pub(
|
|
110
112
|
`${options.namespace ?? APP_ID}.execute`,
|
|
111
113
|
payload,
|
|
112
|
-
context as JobState
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
context as JobState
|
|
115
|
+
);
|
|
116
|
+
//seed search data if present
|
|
117
|
+
if (jobId && options.search?.data) {
|
|
115
118
|
const searchSessionId = `-search-0`;
|
|
116
119
|
const search = new Search(jobId, hotMeshClient, searchSessionId);
|
|
117
120
|
for (const [key, value] of Object.entries(options.search.data)) {
|
|
@@ -143,8 +146,17 @@ export class ClientService {
|
|
|
143
146
|
workflowTopic,
|
|
144
147
|
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
145
148
|
}
|
|
149
|
+
//seed search data if presentthe hook before entering
|
|
146
150
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
147
|
-
|
|
151
|
+
const msgId = await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, StreamStatus.PENDING, 202);
|
|
152
|
+
if (options.search?.data) {
|
|
153
|
+
const searchSessionId = `-search-${HotMesh.guid()}-0`;
|
|
154
|
+
const search = new Search(options.workflowId, hotMeshClient, searchSessionId);
|
|
155
|
+
for (const [key, value] of Object.entries(options.search.data)) {
|
|
156
|
+
search.set(key, value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return msgId;
|
|
148
160
|
},
|
|
149
161
|
|
|
150
162
|
getHandle: async (taskQueue: string, workflowName: string, workflowId: string, namespace?: string): Promise<WorkflowHandleService> => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ClientService } from './client';
|
|
2
2
|
import { ConnectionService } from './connection';
|
|
3
3
|
import { MeshOSService } from './meshos';
|
|
4
|
+
import { Search } from './search';
|
|
4
5
|
import { WorkerService } from './worker';
|
|
5
6
|
import { WorkflowService } from './workflow';
|
|
6
7
|
import { ContextType } from '../../types/durable';
|
|
@@ -8,6 +9,7 @@ import { ContextType } from '../../types/durable';
|
|
|
8
9
|
export const Durable = {
|
|
9
10
|
Client: ClientService,
|
|
10
11
|
Connection: ConnectionService,
|
|
12
|
+
Search,
|
|
11
13
|
MeshOS: MeshOSService,
|
|
12
14
|
Worker: WorkerService,
|
|
13
15
|
workflow: WorkflowService,
|
|
@@ -236,12 +236,15 @@ export class MeshOSService {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
|
-
* executes the redis FT search query
|
|
239
|
+
* executes the redis FT search query; optionally specify other commands
|
|
240
240
|
* @example '@_quantity:[89 89]'
|
|
241
|
+
* @example '@_quantity:[89 89] @_name:"John"'
|
|
242
|
+
* @example 'FT.search my-index @_quantity:[89 89]'
|
|
243
|
+
* @param {FindOptions} options
|
|
241
244
|
* @param {any[]} args
|
|
242
|
-
* @returns {string}
|
|
245
|
+
* @returns {Promise<string[] | [number] | Array<number, string | number | string[]>>}
|
|
243
246
|
*/
|
|
244
|
-
static async find(options: FindOptions, ...args: string[]): Promise<string[] | [number]
|
|
247
|
+
static async find(options: FindOptions, ...args: string[]): Promise<string[] | [number] | Array<string | number | string[]>> {
|
|
245
248
|
const my = new this();
|
|
246
249
|
const client = new Client({ connection: {
|
|
247
250
|
class: my.redisClass,
|
|
@@ -272,9 +275,9 @@ export class MeshOSService {
|
|
|
272
275
|
* Provides a JSON abstraction for the Redis FT.search command
|
|
273
276
|
* (e.g, `count`, `query`, `return`, `limit`)
|
|
274
277
|
* @param {FindWhereOptions} options
|
|
275
|
-
* @returns {Promise<string[] | [number]
|
|
278
|
+
* @returns {Promise<string[] | [number] | Array<string | number | string[]>>}
|
|
276
279
|
*/
|
|
277
|
-
static async findWhere(options: FindWhereOptions): Promise<string[] | [number]
|
|
280
|
+
static async findWhere(options: FindWhereOptions): Promise<string[] | [number] | Array<string | number | string[]>> {
|
|
278
281
|
const args: string[] = [this.generateSearchQuery(options.query)];
|
|
279
282
|
if (options.count) {
|
|
280
283
|
args.push('LIMIT', '0', '0');
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ActivityConfig,
|
|
17
17
|
HookOptions,
|
|
18
18
|
ProxyType,
|
|
19
|
+
WorkflowContext,
|
|
19
20
|
WorkflowOptions } from "../../types/durable";
|
|
20
21
|
import { JobOutput, JobState } from '../../types/job';
|
|
21
22
|
import { StreamStatus } from '../../types/stream';
|
|
@@ -24,7 +25,10 @@ import { deterministicRandom } from '../../modules/utils';
|
|
|
24
25
|
export class WorkflowService {
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
+
* Spawns a child workflow. await and return the result.
|
|
29
|
+
* @template T - the result type
|
|
30
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
31
|
+
* @returns {Promise<T>} - the result of the child workflow
|
|
28
32
|
*/
|
|
29
33
|
static async executeChild<T>(options: WorkflowOptions): Promise<T> {
|
|
30
34
|
const store = asyncLocalStorage.getStore();
|
|
@@ -35,10 +39,11 @@ export class WorkflowService {
|
|
|
35
39
|
const workflowSpan = store.get('workflowSpan');
|
|
36
40
|
const COUNTER = store.get('counter');
|
|
37
41
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
42
|
+
const prefix = options.prefix ?? '';
|
|
38
43
|
//this is risky but MUST be allowed. Users MAY set the workflowId,
|
|
39
44
|
//but if there is a naming collision, the data from the target entity will be used
|
|
40
45
|
//as there is know way of knowing if the item was generated via a prior run of the workflow
|
|
41
|
-
const childJobId = options.workflowId ??
|
|
46
|
+
const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
42
47
|
const parentWorkflowId = `${workflowId}-f`;
|
|
43
48
|
|
|
44
49
|
const client = new Client({
|
|
@@ -70,7 +75,10 @@ export class WorkflowService {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/**
|
|
73
|
-
*
|
|
78
|
+
* Spawns a child workflow. return the childJobId.
|
|
79
|
+
* This method is used when the result of the child workflow is not needed.
|
|
80
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
81
|
+
* @returns {Promise<string>} - the childJobId
|
|
74
82
|
*/
|
|
75
83
|
static async startChild<T>(options: WorkflowOptions): Promise<string> {
|
|
76
84
|
const store = asyncLocalStorage.getStore();
|
|
@@ -81,7 +89,8 @@ export class WorkflowService {
|
|
|
81
89
|
const workflowSpan = store.get('workflowSpan');
|
|
82
90
|
const COUNTER = store.get('counter');
|
|
83
91
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
84
|
-
const
|
|
92
|
+
const prefix = options.prefix ?? '';
|
|
93
|
+
const childJobId = options.workflowId ?? `${prefix}-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
85
94
|
const parentWorkflowId = `${workflowId}-f`;
|
|
86
95
|
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
87
96
|
|
|
@@ -108,7 +117,22 @@ export class WorkflowService {
|
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
|
111
|
-
*
|
|
120
|
+
* Wraps activities in a proxy that will durably run them
|
|
121
|
+
* @param {ActivityConfig} options - the activity configuration
|
|
122
|
+
* that will be used to wrap the activities. You must pass an
|
|
123
|
+
* `activities` object to this configuration. The activities object
|
|
124
|
+
* should be a key-value pair of activity names and their respective
|
|
125
|
+
* functions. This is typically done by importing the activities.
|
|
126
|
+
*
|
|
127
|
+
* @returns {ProxyType<ACT>} - a proxy object with the same keys as the
|
|
128
|
+
* activities object, but with the values replaced by a wrapped function
|
|
129
|
+
* @example
|
|
130
|
+
* // import the activities
|
|
131
|
+
* import * as activities from './activities';
|
|
132
|
+
* const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
133
|
+
*
|
|
134
|
+
* //or destructure the proxy object, as the function names are the keys
|
|
135
|
+
* const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
112
136
|
*/
|
|
113
137
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
|
|
114
138
|
if (options.activities) {
|
|
@@ -127,7 +151,9 @@ export class WorkflowService {
|
|
|
127
151
|
}
|
|
128
152
|
|
|
129
153
|
/**
|
|
130
|
-
*
|
|
154
|
+
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
155
|
+
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
156
|
+
* @returns {Promise<Search>} - a search session
|
|
131
157
|
*/
|
|
132
158
|
static async search(): Promise<Search> {
|
|
133
159
|
const store = asyncLocalStorage.getStore();
|
|
@@ -144,7 +170,8 @@ export class WorkflowService {
|
|
|
144
170
|
}
|
|
145
171
|
|
|
146
172
|
/**
|
|
147
|
-
*
|
|
173
|
+
* Return a handle to the hotmesh client currently running the workflow
|
|
174
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
148
175
|
*/
|
|
149
176
|
static async getHotMesh(): Promise<HotMesh> {
|
|
150
177
|
const store = asyncLocalStorage.getStore();
|
|
@@ -154,9 +181,34 @@ export class WorkflowService {
|
|
|
154
181
|
}
|
|
155
182
|
|
|
156
183
|
/**
|
|
157
|
-
*
|
|
184
|
+
* Returns the current workflow context
|
|
185
|
+
* @returns {WorkflowContext} - the current workflow context
|
|
186
|
+
*/
|
|
187
|
+
static getContext(): WorkflowContext {
|
|
188
|
+
const store = asyncLocalStorage.getStore();
|
|
189
|
+
const workflowId = store.get('workflowId');
|
|
190
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
191
|
+
const workflowTopic = store.get('workflowTopic');
|
|
192
|
+
const namespace = store.get('namespace');
|
|
193
|
+
const workflowTrace = store.get('workflowTrace');
|
|
194
|
+
const workflowSpan = store.get('workflowSpan');
|
|
195
|
+
const COUNTER = store.get('counter');
|
|
196
|
+
return {
|
|
197
|
+
counter: COUNTER.counter,
|
|
198
|
+
namespace,
|
|
199
|
+
workflowId,
|
|
200
|
+
workflowDimension,
|
|
201
|
+
workflowTopic,
|
|
202
|
+
workflowTrace,
|
|
203
|
+
workflowSpan,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Those methods that may only be called once must be protected by flagging
|
|
158
209
|
* their execution with a unique key (the key is stored in the HASH alongside
|
|
159
210
|
* process state and job state)
|
|
211
|
+
* @private
|
|
160
212
|
*/
|
|
161
213
|
static async isSideEffectAllowed(hotMeshClient: HotMesh, prefix:string): Promise<boolean> {
|
|
162
214
|
const store = asyncLocalStorage.getStore();
|
|
@@ -175,10 +227,10 @@ export class WorkflowService {
|
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
/**
|
|
178
|
-
*
|
|
230
|
+
* Returns a random number between 0 and 1. This number is deterministic
|
|
179
231
|
* and will never vary for a given seed. This is useful for randomizing
|
|
180
232
|
* pathways in a workflow that can be safely replayed.
|
|
181
|
-
* @returns {number}
|
|
233
|
+
* @returns {number} - a random number between 0 and 1
|
|
182
234
|
*/
|
|
183
235
|
static random(): number {
|
|
184
236
|
const store = asyncLocalStorage.getStore();
|
|
@@ -188,8 +240,11 @@ export class WorkflowService {
|
|
|
188
240
|
}
|
|
189
241
|
|
|
190
242
|
/**
|
|
191
|
-
*
|
|
243
|
+
* Sends signal data into any other paused thread (which is paused and
|
|
192
244
|
* awaiting the signal) from within a hook-thread or the main-thread
|
|
245
|
+
* @param {string} signalId - the signal id
|
|
246
|
+
* @param {Record<any, any>} data - the signal data
|
|
247
|
+
* @returns {Promise<string>} - the stream id
|
|
193
248
|
*/
|
|
194
249
|
static async signal(signalId: string, data: Record<any, any>): Promise<string> {
|
|
195
250
|
const store = asyncLocalStorage.getStore();
|
|
@@ -204,9 +259,10 @@ export class WorkflowService {
|
|
|
204
259
|
}
|
|
205
260
|
|
|
206
261
|
/**
|
|
207
|
-
*
|
|
262
|
+
* Spawns a hook from either the main thread or a hook thread with
|
|
208
263
|
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
209
264
|
* default to the current workflowId/WorkflowTopic if not provided
|
|
265
|
+
* @param {HookOptions} options - the hook options
|
|
210
266
|
*/
|
|
211
267
|
static async hook(options: HookOptions): Promise<string> {
|
|
212
268
|
const store = asyncLocalStorage.getStore();
|
|
@@ -230,6 +286,11 @@ export class WorkflowService {
|
|
|
230
286
|
}
|
|
231
287
|
}
|
|
232
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Sleeps for a duration.
|
|
291
|
+
* @param {string} duration - for example: '1 minute', '2 hours', '3 days'
|
|
292
|
+
* @returns {Promise<number>}
|
|
293
|
+
*/
|
|
233
294
|
static async sleep(duration: string): Promise<number> {
|
|
234
295
|
const seconds = ms(duration) / 1000;
|
|
235
296
|
|
|
@@ -254,6 +315,12 @@ export class WorkflowService {
|
|
|
254
315
|
}
|
|
255
316
|
}
|
|
256
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Waits for a signal to awaken
|
|
320
|
+
* @param {string[]} signals - the signals to wait for
|
|
321
|
+
* @param {Record<string, string>} options - the options
|
|
322
|
+
* @returns {Promise<Record<any, any>[]>}
|
|
323
|
+
*/
|
|
257
324
|
static async waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]> {
|
|
258
325
|
const store = asyncLocalStorage.getStore();
|
|
259
326
|
const COUNTER = store.get('counter');
|
package/types/durable.ts
CHANGED
|
@@ -7,6 +7,44 @@ type WorkflowConfig = {
|
|
|
7
7
|
initialInterval?: string; //default 1s
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
type WorkflowContext = {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count.
|
|
14
|
+
*/
|
|
15
|
+
counter: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* the HotMesh App namespace. `durable` is the default.
|
|
19
|
+
*/
|
|
20
|
+
namespace: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* the workflow/job ID
|
|
24
|
+
*/
|
|
25
|
+
workflowId: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* the dimensional isolation for the reentrant hook, expressed in the format `0,0`, `0,1`, etc
|
|
29
|
+
*/
|
|
30
|
+
workflowDimension: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* a concatenation of the task queue and workflow name (e.g., `${taskQueueName}-${workflowName}`)
|
|
34
|
+
*/
|
|
35
|
+
workflowTopic: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* the open telemetry trace context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
|
|
39
|
+
*/
|
|
40
|
+
workflowTrace: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* the open telemetry span context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
|
|
44
|
+
*/
|
|
45
|
+
workflowSpan: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
type WorkflowSearchOptions = {
|
|
11
49
|
index?: string; //FT index name (myapp:myindex)
|
|
12
50
|
prefix?: string[]; //FT prefixes (['myapp:myindex:prefix1', 'myapp:myindex:prefix2'])
|
|
@@ -15,11 +53,12 @@ type WorkflowSearchOptions = {
|
|
|
15
53
|
}
|
|
16
54
|
|
|
17
55
|
type WorkflowOptions = {
|
|
18
|
-
namespace?: string;
|
|
56
|
+
namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
|
|
19
57
|
taskQueue: string;
|
|
20
|
-
args: any[];
|
|
21
|
-
workflowId?: string;
|
|
22
|
-
|
|
58
|
+
args: any[]; //input arguments to pass in
|
|
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
|
|
23
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
|
|
24
63
|
workflowTrace?: string;
|
|
25
64
|
workflowSpan?: string;
|
|
@@ -188,4 +227,5 @@ export {
|
|
|
188
227
|
WorkflowSearchOptions,
|
|
189
228
|
WorkflowDataType,
|
|
190
229
|
WorkflowOptions,
|
|
230
|
+
WorkflowContext,
|
|
191
231
|
};
|