@hotmeshio/hotmesh 0.0.29 → 0.0.31
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 +24 -18
- package/build/package.json +2 -2
- package/build/services/durable/client.js +12 -4
- package/build/services/durable/index.d.ts +2 -0
- package/build/services/durable/index.js +2 -0
- package/build/services/durable/workflow.d.ts +56 -11
- package/build/services/durable/workflow.js +86 -19
- package/build/services/signaler/stream.js +0 -1
- package/build/types/durable.d.ts +33 -1
- package/build/types/index.d.ts +1 -1
- package/package.json +2 -2
- package/services/durable/client.ts +15 -6
- package/services/durable/index.ts +2 -0
- package/services/durable/workflow.ts +89 -20
- package/services/signaler/stream.ts +0 -1
- package/types/durable.ts +45 -4
- package/types/index.ts +1 -0
package/README.md
CHANGED
|
@@ -92,19 +92,25 @@ npm install @hotmeshio/hotmesh
|
|
|
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
94
|
|
|
95
|
-
- `waitForSignal` | Pause your function and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once
|
|
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
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.
|
|
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.31",
|
|
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");
|
|
@@ -88,14 +87,14 @@ class ClientService {
|
|
|
88
87
|
const payload = {
|
|
89
88
|
arguments: [...options.args],
|
|
90
89
|
parentWorkflowId: options.parentWorkflowId,
|
|
91
|
-
workflowId: options.workflowId ||
|
|
90
|
+
workflowId: options.workflowId || hotmesh_1.HotMeshService.guid(),
|
|
92
91
|
workflowTopic: workflowTopic,
|
|
93
92
|
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
94
93
|
};
|
|
95
94
|
const context = { metadata: { trc, spn }, data: {} };
|
|
96
95
|
const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context);
|
|
96
|
+
//seed search data if present
|
|
97
97
|
if (jobId && options.search?.data) {
|
|
98
|
-
//job successfully kicked off; there is default job data to persist
|
|
99
98
|
const searchSessionId = `-search-0`;
|
|
100
99
|
const search = new search_1.Search(jobId, hotMeshClient, searchSessionId);
|
|
101
100
|
for (const [key, value] of Object.entries(options.search.data)) {
|
|
@@ -125,8 +124,17 @@ class ClientService {
|
|
|
125
124
|
workflowTopic,
|
|
126
125
|
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
127
126
|
};
|
|
127
|
+
//seed search data if presentthe hook before entering
|
|
128
128
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
129
|
-
|
|
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;
|
|
130
138
|
},
|
|
131
139
|
getHandle: async (taskQueue, workflowName, workflowId, namespace) => {
|
|
132
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,
|
|
@@ -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,15 +31,16 @@ 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;
|
|
31
|
-
//this is
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
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}`;
|
|
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),
|
|
38
42
|
});
|
|
39
|
-
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);
|
|
40
44
|
try {
|
|
41
45
|
return await handle.result(true);
|
|
42
46
|
}
|
|
@@ -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,9 +73,12 @@ 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
|
-
|
|
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}`;
|
|
70
80
|
const parentWorkflowId = `${workflowId}-f`;
|
|
71
|
-
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
81
|
+
const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
72
82
|
try {
|
|
73
83
|
//get the status; if there is no error, return childJobId (what was spawned)
|
|
74
84
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
@@ -91,7 +101,22 @@ class WorkflowService {
|
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
/**
|
|
94
|
-
*
|
|
104
|
+
* Wraps activities in a proxy that will durably run them
|
|
105
|
+
* @param {ActivityConfig} options - the activity configuration
|
|
106
|
+
* that will be used to wrap the activities. You must pass an
|
|
107
|
+
* `activities` object to this configuration. The activities object
|
|
108
|
+
* should be a key-value pair of activity names and their respective
|
|
109
|
+
* functions. This is typically done by importing the activities.
|
|
110
|
+
*
|
|
111
|
+
* @returns {ProxyType<ACT>} - a proxy object with the same keys as the
|
|
112
|
+
* activities object, but with the values replaced by a wrapped function
|
|
113
|
+
* @example
|
|
114
|
+
* // import the activities
|
|
115
|
+
* import * as activities from './activities';
|
|
116
|
+
* const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
117
|
+
*
|
|
118
|
+
* //or destructure the proxy object, as the function names are the keys
|
|
119
|
+
* const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
95
120
|
*/
|
|
96
121
|
static proxyActivities(options) {
|
|
97
122
|
if (options.activities) {
|
|
@@ -108,7 +133,9 @@ class WorkflowService {
|
|
|
108
133
|
return proxy;
|
|
109
134
|
}
|
|
110
135
|
/**
|
|
111
|
-
*
|
|
136
|
+
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
137
|
+
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
138
|
+
* @returns {Promise<Search>} - a search session
|
|
112
139
|
*/
|
|
113
140
|
static async search() {
|
|
114
141
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -124,7 +151,8 @@ class WorkflowService {
|
|
|
124
151
|
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
125
152
|
}
|
|
126
153
|
/**
|
|
127
|
-
*
|
|
154
|
+
* Return a handle to the hotmesh client currently running the workflow
|
|
155
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
128
156
|
*/
|
|
129
157
|
static async getHotMesh() {
|
|
130
158
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -133,9 +161,33 @@ class WorkflowService {
|
|
|
133
161
|
return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
134
162
|
}
|
|
135
163
|
/**
|
|
136
|
-
*
|
|
164
|
+
* Returns the current workflow context
|
|
165
|
+
* @returns {WorkflowContext} - the current workflow context
|
|
166
|
+
*/
|
|
167
|
+
static getContext() {
|
|
168
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
169
|
+
const workflowId = store.get('workflowId');
|
|
170
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
171
|
+
const workflowTopic = store.get('workflowTopic');
|
|
172
|
+
const namespace = store.get('namespace');
|
|
173
|
+
const workflowTrace = store.get('workflowTrace');
|
|
174
|
+
const workflowSpan = store.get('workflowSpan');
|
|
175
|
+
const COUNTER = store.get('counter');
|
|
176
|
+
return {
|
|
177
|
+
counter: COUNTER.counter,
|
|
178
|
+
namespace,
|
|
179
|
+
workflowId,
|
|
180
|
+
workflowDimension,
|
|
181
|
+
workflowTopic,
|
|
182
|
+
workflowTrace,
|
|
183
|
+
workflowSpan,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Those methods that may only be called once must be protected by flagging
|
|
137
188
|
* their execution with a unique key (the key is stored in the HASH alongside
|
|
138
189
|
* process state and job state)
|
|
190
|
+
* @private
|
|
139
191
|
*/
|
|
140
192
|
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
141
193
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -153,10 +205,10 @@ class WorkflowService {
|
|
|
153
205
|
return guidValue === 1;
|
|
154
206
|
}
|
|
155
207
|
/**
|
|
156
|
-
*
|
|
208
|
+
* Returns a random number between 0 and 1. This number is deterministic
|
|
157
209
|
* and will never vary for a given seed. This is useful for randomizing
|
|
158
210
|
* pathways in a workflow that can be safely replayed.
|
|
159
|
-
* @returns {number}
|
|
211
|
+
* @returns {number} - a random number between 0 and 1
|
|
160
212
|
*/
|
|
161
213
|
static random() {
|
|
162
214
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -165,8 +217,11 @@ class WorkflowService {
|
|
|
165
217
|
return (0, utils_1.deterministicRandom)(seed);
|
|
166
218
|
}
|
|
167
219
|
/**
|
|
168
|
-
*
|
|
220
|
+
* Sends signal data into any other paused thread (which is paused and
|
|
169
221
|
* awaiting the signal) from within a hook-thread or the main-thread
|
|
222
|
+
* @param {string} signalId - the signal id
|
|
223
|
+
* @param {Record<any, any>} data - the signal data
|
|
224
|
+
* @returns {Promise<string>} - the stream id
|
|
170
225
|
*/
|
|
171
226
|
static async signal(signalId, data) {
|
|
172
227
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -180,9 +235,10 @@ class WorkflowService {
|
|
|
180
235
|
}
|
|
181
236
|
}
|
|
182
237
|
/**
|
|
183
|
-
*
|
|
238
|
+
* Spawns a hook from either the main thread or a hook thread with
|
|
184
239
|
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
185
240
|
* default to the current workflowId/WorkflowTopic if not provided
|
|
241
|
+
* @param {HookOptions} options - the hook options
|
|
186
242
|
*/
|
|
187
243
|
static async hook(options) {
|
|
188
244
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -193,8 +249,8 @@ class WorkflowService {
|
|
|
193
249
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
194
250
|
const workflowId = options.workflowId ?? store.get('workflowId');
|
|
195
251
|
let workflowTopic = store.get('workflowTopic');
|
|
196
|
-
if (options.taskQueue && options.workflowName) {
|
|
197
|
-
workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
252
|
+
if (options.entity || (options.taskQueue && options.workflowName)) {
|
|
253
|
+
workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
198
254
|
} //else this is essentially recursion as the function calls itself
|
|
199
255
|
const payload = {
|
|
200
256
|
arguments: [...options.args],
|
|
@@ -205,6 +261,11 @@ class WorkflowService {
|
|
|
205
261
|
return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
|
|
206
262
|
}
|
|
207
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Sleeps for a duration.
|
|
266
|
+
* @param {string} duration - for example: '1 minute', '2 hours', '3 days'
|
|
267
|
+
* @returns {Promise<number>}
|
|
268
|
+
*/
|
|
208
269
|
static async sleep(duration) {
|
|
209
270
|
const seconds = (0, ms_1.default)(duration) / 1000;
|
|
210
271
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -227,6 +288,12 @@ class WorkflowService {
|
|
|
227
288
|
throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
|
|
228
289
|
}
|
|
229
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Waits for a signal to awaken
|
|
293
|
+
* @param {string[]} signals - the signals to wait for
|
|
294
|
+
* @param {Record<string, string>} options - the options
|
|
295
|
+
* @returns {Promise<Record<any, any>[]>}
|
|
296
|
+
*/
|
|
230
297
|
static async waitForSignal(signals, options) {
|
|
231
298
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
232
299
|
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
|
+
entity?: string;
|
|
22
53
|
workflowName?: string;
|
|
23
54
|
parentWorkflowId?: string;
|
|
24
55
|
workflowTrace?: string;
|
|
@@ -30,6 +61,7 @@ type HookOptions = {
|
|
|
30
61
|
namespace?: string;
|
|
31
62
|
taskQueue?: string;
|
|
32
63
|
args: any[];
|
|
64
|
+
entity?: string;
|
|
33
65
|
workflowId?: string;
|
|
34
66
|
workflowName?: string;
|
|
35
67
|
search?: WorkflowSearchOptions;
|
|
@@ -142,4 +174,4 @@ type ActivityConfig = {
|
|
|
142
174
|
maximumInterval: string;
|
|
143
175
|
};
|
|
144
176
|
};
|
|
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, };
|
|
177
|
+
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.31",
|
|
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';
|
|
@@ -104,7 +103,7 @@ export class ClientService {
|
|
|
104
103
|
const payload = {
|
|
105
104
|
arguments: [...options.args],
|
|
106
105
|
parentWorkflowId: options.parentWorkflowId,
|
|
107
|
-
workflowId: options.workflowId ||
|
|
106
|
+
workflowId: options.workflowId || HotMesh.guid(),
|
|
108
107
|
workflowTopic: workflowTopic,
|
|
109
108
|
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
110
109
|
}
|
|
@@ -112,9 +111,10 @@ export class ClientService {
|
|
|
112
111
|
const jobId = await hotMeshClient.pub(
|
|
113
112
|
`${options.namespace ?? APP_ID}.execute`,
|
|
114
113
|
payload,
|
|
115
|
-
context as JobState
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
context as JobState
|
|
115
|
+
);
|
|
116
|
+
//seed search data if present
|
|
117
|
+
if (jobId && options.search?.data) {
|
|
118
118
|
const searchSessionId = `-search-0`;
|
|
119
119
|
const search = new Search(jobId, hotMeshClient, searchSessionId);
|
|
120
120
|
for (const [key, value] of Object.entries(options.search.data)) {
|
|
@@ -146,8 +146,17 @@ export class ClientService {
|
|
|
146
146
|
workflowTopic,
|
|
147
147
|
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
148
148
|
}
|
|
149
|
+
//seed search data if presentthe hook before entering
|
|
149
150
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
150
|
-
|
|
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;
|
|
151
160
|
},
|
|
152
161
|
|
|
153
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,
|
|
@@ -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;
|
|
38
|
-
//this is
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
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}`;
|
|
42
47
|
const parentWorkflowId = `${workflowId}-f`;
|
|
43
48
|
|
|
44
49
|
const client = new Client({
|
|
@@ -46,8 +51,8 @@ export class WorkflowService {
|
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
let handle = await client.workflow.getHandle(
|
|
49
|
-
options.taskQueue,
|
|
50
|
-
options.workflowName,
|
|
54
|
+
options.entity ?? options.taskQueue,
|
|
55
|
+
options.entity ?? options.workflowName,
|
|
51
56
|
childJobId,
|
|
52
57
|
namespace,
|
|
53
58
|
);
|
|
@@ -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,9 +89,12 @@ 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
|
-
|
|
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}`;
|
|
85
96
|
const parentWorkflowId = `${workflowId}-f`;
|
|
86
|
-
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
97
|
+
const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
87
98
|
|
|
88
99
|
try {
|
|
89
100
|
//get the status; if there is no error, return childJobId (what was spawned)
|
|
@@ -108,7 +119,22 @@ export class WorkflowService {
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
/**
|
|
111
|
-
*
|
|
122
|
+
* Wraps activities in a proxy that will durably run them
|
|
123
|
+
* @param {ActivityConfig} options - the activity configuration
|
|
124
|
+
* that will be used to wrap the activities. You must pass an
|
|
125
|
+
* `activities` object to this configuration. The activities object
|
|
126
|
+
* should be a key-value pair of activity names and their respective
|
|
127
|
+
* functions. This is typically done by importing the activities.
|
|
128
|
+
*
|
|
129
|
+
* @returns {ProxyType<ACT>} - a proxy object with the same keys as the
|
|
130
|
+
* activities object, but with the values replaced by a wrapped function
|
|
131
|
+
* @example
|
|
132
|
+
* // import the activities
|
|
133
|
+
* import * as activities from './activities';
|
|
134
|
+
* const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
135
|
+
*
|
|
136
|
+
* //or destructure the proxy object, as the function names are the keys
|
|
137
|
+
* const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
|
|
112
138
|
*/
|
|
113
139
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
|
|
114
140
|
if (options.activities) {
|
|
@@ -127,7 +153,9 @@ export class WorkflowService {
|
|
|
127
153
|
}
|
|
128
154
|
|
|
129
155
|
/**
|
|
130
|
-
*
|
|
156
|
+
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
157
|
+
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
158
|
+
* @returns {Promise<Search>} - a search session
|
|
131
159
|
*/
|
|
132
160
|
static async search(): Promise<Search> {
|
|
133
161
|
const store = asyncLocalStorage.getStore();
|
|
@@ -144,7 +172,8 @@ export class WorkflowService {
|
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
/**
|
|
147
|
-
*
|
|
175
|
+
* Return a handle to the hotmesh client currently running the workflow
|
|
176
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
148
177
|
*/
|
|
149
178
|
static async getHotMesh(): Promise<HotMesh> {
|
|
150
179
|
const store = asyncLocalStorage.getStore();
|
|
@@ -154,9 +183,34 @@ export class WorkflowService {
|
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
/**
|
|
157
|
-
*
|
|
186
|
+
* Returns the current workflow context
|
|
187
|
+
* @returns {WorkflowContext} - the current workflow context
|
|
188
|
+
*/
|
|
189
|
+
static getContext(): WorkflowContext {
|
|
190
|
+
const store = asyncLocalStorage.getStore();
|
|
191
|
+
const workflowId = store.get('workflowId');
|
|
192
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
193
|
+
const workflowTopic = store.get('workflowTopic');
|
|
194
|
+
const namespace = store.get('namespace');
|
|
195
|
+
const workflowTrace = store.get('workflowTrace');
|
|
196
|
+
const workflowSpan = store.get('workflowSpan');
|
|
197
|
+
const COUNTER = store.get('counter');
|
|
198
|
+
return {
|
|
199
|
+
counter: COUNTER.counter,
|
|
200
|
+
namespace,
|
|
201
|
+
workflowId,
|
|
202
|
+
workflowDimension,
|
|
203
|
+
workflowTopic,
|
|
204
|
+
workflowTrace,
|
|
205
|
+
workflowSpan,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Those methods that may only be called once must be protected by flagging
|
|
158
211
|
* their execution with a unique key (the key is stored in the HASH alongside
|
|
159
212
|
* process state and job state)
|
|
213
|
+
* @private
|
|
160
214
|
*/
|
|
161
215
|
static async isSideEffectAllowed(hotMeshClient: HotMesh, prefix:string): Promise<boolean> {
|
|
162
216
|
const store = asyncLocalStorage.getStore();
|
|
@@ -175,10 +229,10 @@ export class WorkflowService {
|
|
|
175
229
|
}
|
|
176
230
|
|
|
177
231
|
/**
|
|
178
|
-
*
|
|
232
|
+
* Returns a random number between 0 and 1. This number is deterministic
|
|
179
233
|
* and will never vary for a given seed. This is useful for randomizing
|
|
180
234
|
* pathways in a workflow that can be safely replayed.
|
|
181
|
-
* @returns {number}
|
|
235
|
+
* @returns {number} - a random number between 0 and 1
|
|
182
236
|
*/
|
|
183
237
|
static random(): number {
|
|
184
238
|
const store = asyncLocalStorage.getStore();
|
|
@@ -188,8 +242,11 @@ export class WorkflowService {
|
|
|
188
242
|
}
|
|
189
243
|
|
|
190
244
|
/**
|
|
191
|
-
*
|
|
245
|
+
* Sends signal data into any other paused thread (which is paused and
|
|
192
246
|
* awaiting the signal) from within a hook-thread or the main-thread
|
|
247
|
+
* @param {string} signalId - the signal id
|
|
248
|
+
* @param {Record<any, any>} data - the signal data
|
|
249
|
+
* @returns {Promise<string>} - the stream id
|
|
193
250
|
*/
|
|
194
251
|
static async signal(signalId: string, data: Record<any, any>): Promise<string> {
|
|
195
252
|
const store = asyncLocalStorage.getStore();
|
|
@@ -204,9 +261,10 @@ export class WorkflowService {
|
|
|
204
261
|
}
|
|
205
262
|
|
|
206
263
|
/**
|
|
207
|
-
*
|
|
264
|
+
* Spawns a hook from either the main thread or a hook thread with
|
|
208
265
|
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
209
266
|
* default to the current workflowId/WorkflowTopic if not provided
|
|
267
|
+
* @param {HookOptions} options - the hook options
|
|
210
268
|
*/
|
|
211
269
|
static async hook(options: HookOptions): Promise<string> {
|
|
212
270
|
const store = asyncLocalStorage.getStore();
|
|
@@ -217,8 +275,8 @@ export class WorkflowService {
|
|
|
217
275
|
const store = asyncLocalStorage.getStore();
|
|
218
276
|
const workflowId = options.workflowId ?? store.get('workflowId');
|
|
219
277
|
let workflowTopic = store.get('workflowTopic');
|
|
220
|
-
if (options.taskQueue && options.workflowName) {
|
|
221
|
-
workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
278
|
+
if (options.entity || (options.taskQueue && options.workflowName)) {
|
|
279
|
+
workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
222
280
|
} //else this is essentially recursion as the function calls itself
|
|
223
281
|
const payload = {
|
|
224
282
|
arguments: [...options.args],
|
|
@@ -230,6 +288,11 @@ export class WorkflowService {
|
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Sleeps for a duration.
|
|
293
|
+
* @param {string} duration - for example: '1 minute', '2 hours', '3 days'
|
|
294
|
+
* @returns {Promise<number>}
|
|
295
|
+
*/
|
|
233
296
|
static async sleep(duration: string): Promise<number> {
|
|
234
297
|
const seconds = ms(duration) / 1000;
|
|
235
298
|
|
|
@@ -254,6 +317,12 @@ export class WorkflowService {
|
|
|
254
317
|
}
|
|
255
318
|
}
|
|
256
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Waits for a signal to awaken
|
|
322
|
+
* @param {string[]} signals - the signals to wait for
|
|
323
|
+
* @param {Record<string, string>} options - the options
|
|
324
|
+
* @returns {Promise<Record<any, any>[]>}
|
|
325
|
+
*/
|
|
257
326
|
static async waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]> {
|
|
258
327
|
const store = asyncLocalStorage.getStore();
|
|
259
328
|
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
|
+
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
|
|
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;
|
|
@@ -31,6 +70,7 @@ type HookOptions = {
|
|
|
31
70
|
namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
|
|
32
71
|
taskQueue?: string;
|
|
33
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.
|
|
34
74
|
workflowId?: string; //execution id (the job id to hook into)
|
|
35
75
|
workflowName?: string; //the name of the user's hook function
|
|
36
76
|
search?: WorkflowSearchOptions //bind additional search terms immediately before hook reentry
|
|
@@ -188,4 +228,5 @@ export {
|
|
|
188
228
|
WorkflowSearchOptions,
|
|
189
229
|
WorkflowDataType,
|
|
190
230
|
WorkflowOptions,
|
|
231
|
+
WorkflowContext,
|
|
191
232
|
};
|