@hotmeshio/hotmesh 0.0.21 → 0.0.23
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 +0 -3
- package/build/package.json +2 -1
- package/build/services/durable/connection.js +0 -39
- package/build/services/durable/factory.d.ts +6 -4
- package/build/services/durable/factory.js +6 -4
- package/build/services/durable/handle.d.ts +3 -0
- package/build/services/durable/handle.js +14 -2
- package/build/services/durable/index.d.ts +2 -45
- package/build/services/durable/index.js +2 -45
- package/build/services/durable/meshdb.d.ts +113 -0
- package/build/services/durable/meshdb.js +211 -0
- package/build/services/durable/search.d.ts +9 -0
- package/build/services/durable/search.js +35 -2
- package/build/services/durable/worker.d.ts +1 -9
- package/build/services/durable/worker.js +2 -35
- package/build/services/durable/workflow.d.ts +8 -1
- package/build/services/durable/workflow.js +15 -16
- package/build/services/engine/index.d.ts +2 -0
- package/build/services/engine/index.js +3 -0
- package/build/services/hotmesh/index.d.ts +3 -1
- package/build/services/hotmesh/index.js +5 -2
- package/build/services/store/index.d.ts +5 -0
- package/build/services/store/index.js +14 -0
- package/build/types/durable.d.ts +20 -4
- package/build/types/index.d.ts +1 -1
- package/package.json +2 -1
- package/services/durable/connection.ts +0 -40
- package/services/durable/factory.ts +6 -4
- package/services/durable/handle.ts +17 -2
- package/services/durable/index.ts +2 -46
- package/services/durable/meshdb.ts +254 -0
- package/services/durable/search.ts +36 -2
- package/services/durable/worker.ts +3 -37
- package/services/durable/workflow.ts +15 -15
- package/services/engine/index.ts +4 -1
- package/services/hotmesh/index.ts +6 -2
- package/services/store/index.ts +15 -0
- package/types/durable.ts +20 -4
- package/types/index.ts +2 -1
- package/build/services/durable/native.d.ts +0 -4
- package/build/services/durable/native.js +0 -46
- package/services/durable/native.ts +0 -45
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
|
|
3
|
+
import { ClientService as Client } from './client';
|
|
4
|
+
import { WorkflowHandleService } from './handle';
|
|
5
|
+
import { Search } from './search';
|
|
6
|
+
import { WorkerService as Worker } from './worker';
|
|
7
|
+
import { WorkflowSearchOptions } from '../../types/durable';
|
|
8
|
+
import { RedisOptions, RedisClass } from '../../types/redis';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A base class for configuration and setup of
|
|
12
|
+
* a reentrant process database. Entities modeled as
|
|
13
|
+
* subclasses of this class will execute as reentrant
|
|
14
|
+
* processes with a 'main' execution thread and 'n'
|
|
15
|
+
* parallel hook threads.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* //RUN (start a workflow)
|
|
19
|
+
* const myInstance = new MeshDB('someIdempotentGuid');
|
|
20
|
+
* const handle = await myInstance.create(100);
|
|
21
|
+
* await handle.result(); //100
|
|
22
|
+
*
|
|
23
|
+
* //UPDATE (update a workflow)
|
|
24
|
+
* const result = await myInstance.decrement(11);
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export class MeshDBService {
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The name of the main method. When this method
|
|
31
|
+
* is invoked/proxied, it is assumed that a new
|
|
32
|
+
* workflow instance is being created. In all other
|
|
33
|
+
* cases, the call is assumed to be a hook/update
|
|
34
|
+
*/
|
|
35
|
+
main = 'create';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The GUID for the workflow (assigned when created). This
|
|
39
|
+
* value should be idempotent and will be rejected if an
|
|
40
|
+
* instance is already running with the same id.
|
|
41
|
+
*/
|
|
42
|
+
id: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* test value
|
|
46
|
+
*/
|
|
47
|
+
value: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The top-level Redis isolation. All workflow data is
|
|
51
|
+
* isolated within this namespace. Values should be
|
|
52
|
+
* lower-case with no spaces (e.g, 'staging', 'prod', 'test',
|
|
53
|
+
* 'routing-stagig', 'reporting-prod', etc.).
|
|
54
|
+
* 1) only url-safe values are allowed;
|
|
55
|
+
* 2) the 'a' symbol is reserved by HotMesh for indexing apps
|
|
56
|
+
*/
|
|
57
|
+
namespace = 'durable';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The second-level isolation. Data is routed to workers
|
|
61
|
+
* that specify this task queue. Setting the task queue
|
|
62
|
+
* when the worker is created will ensure that the worker
|
|
63
|
+
* only receives messages destined for the queue. This
|
|
64
|
+
* allows callers to specify specific workers/containers
|
|
65
|
+
* for specific tasks. Only url-safe values are allowed.
|
|
66
|
+
*/
|
|
67
|
+
taskQueue = 'default';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The Redis connection options. NOTE: Redis and IORedis
|
|
71
|
+
* use different formats for their connection config.
|
|
72
|
+
*/
|
|
73
|
+
redisOptions: RedisOptions = {
|
|
74
|
+
host: 'localhost',
|
|
75
|
+
port: 6379,
|
|
76
|
+
password: '',
|
|
77
|
+
db: 0,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The Redis connection class. Import as follows
|
|
82
|
+
* within the base subclass as follows:
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* import Redis from 'ioredis';
|
|
86
|
+
* import * as Redis from 'redis';
|
|
87
|
+
*/
|
|
88
|
+
redisClass: RedisClass | null = null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Configuration for the the Redis FT search index.
|
|
92
|
+
*/
|
|
93
|
+
search: WorkflowSearchOptions;
|
|
94
|
+
|
|
95
|
+
static async getHotMeshClient (redisClass: RedisClass, redisOptions: RedisOptions, namespace: string, taskQueue: string) {
|
|
96
|
+
const client = new Client({
|
|
97
|
+
connection: {
|
|
98
|
+
class: redisClass,
|
|
99
|
+
options: redisOptions,
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return await client.getHotMeshClient(taskQueue, namespace);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* mints a new key, using the provided search prefix, ensuring
|
|
107
|
+
* new workflows are properly indexed
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
static mintGuid(): string {
|
|
111
|
+
const my = new this();
|
|
112
|
+
return `${my.search?.prefix?.[0]}${nanoid()}}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates an FT search index
|
|
117
|
+
*/
|
|
118
|
+
static async createIndex() {
|
|
119
|
+
const my = new this();
|
|
120
|
+
const hmClient = await MeshDBService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
|
|
121
|
+
Search.configureSearchIndex(hmClient, my.search)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Initialize the worker(s) for the entity. This is a static
|
|
126
|
+
* method that allows for optional task Queue targeting.
|
|
127
|
+
* NOTE: Allow List may be optionally used
|
|
128
|
+
* @param {string} taskQueue
|
|
129
|
+
* @param {string[]} allowList
|
|
130
|
+
*/
|
|
131
|
+
static async doWork(taskQueue?: string, allowList?: string[]) {
|
|
132
|
+
const my = new this();
|
|
133
|
+
let prototype = Object.getPrototypeOf(my);
|
|
134
|
+
const durablePromises = [];
|
|
135
|
+
const found = [];
|
|
136
|
+
|
|
137
|
+
while (prototype !== null && !Object.getOwnPropertyNames(prototype).includes('__proto__')) {
|
|
138
|
+
const promises = Object.getOwnPropertyNames(prototype).map((prop) => {
|
|
139
|
+
if (found.includes(prop) || ['constructor'].includes(prop) || (allowList && !allowList.includes(prop))) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const originalMethod = my[prop];
|
|
143
|
+
if (typeof originalMethod === 'function') {
|
|
144
|
+
found.push(prop);
|
|
145
|
+
return Worker.create({
|
|
146
|
+
namespace: my.namespace,
|
|
147
|
+
connection: {
|
|
148
|
+
class: my.redisClass,
|
|
149
|
+
options: my.redisOptions,
|
|
150
|
+
},
|
|
151
|
+
taskQueue: taskQueue ?? my.taskQueue,
|
|
152
|
+
workflow: originalMethod,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}).filter(p => p !== undefined); // filter out undefined values
|
|
156
|
+
durablePromises.push(...promises);
|
|
157
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
158
|
+
}
|
|
159
|
+
await Promise.all(durablePromises);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* executes the redis FT search query
|
|
164
|
+
* @example '@_quantity:[89 89]'
|
|
165
|
+
* @param {any[]} args
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
static async find(...args: string[]): Promise<string[] | [number]> {
|
|
169
|
+
const my = new this();
|
|
170
|
+
const client = new Client({ connection: {
|
|
171
|
+
class: my.redisClass,
|
|
172
|
+
options: my.redisOptions
|
|
173
|
+
}});
|
|
174
|
+
return await client.workflow.search(
|
|
175
|
+
my.taskQueue,
|
|
176
|
+
my.main,
|
|
177
|
+
my.namespace,
|
|
178
|
+
my.search.index,
|
|
179
|
+
...args,
|
|
180
|
+
);
|
|
181
|
+
//[count, [id, fields[], id, fields[], id, fields[], ...]]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* returns the workflow handle (use the handle to call:
|
|
186
|
+
* `state`, `status`, `queryStatus`, and `result`)
|
|
187
|
+
* @param {string} id
|
|
188
|
+
* @returns {Promise<WorkflowHandleService>}
|
|
189
|
+
*/
|
|
190
|
+
static async get(id: string): Promise<WorkflowHandleService> {
|
|
191
|
+
const my = new this();
|
|
192
|
+
const client = new Client({ connection: {
|
|
193
|
+
class: my.redisClass,
|
|
194
|
+
options: my.redisOptions
|
|
195
|
+
}});
|
|
196
|
+
return await client.workflow.getHandle(
|
|
197
|
+
my.taskQueue,
|
|
198
|
+
my.main,
|
|
199
|
+
id,
|
|
200
|
+
my.namespace,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Initialize with an idempotent workflow identifier.
|
|
206
|
+
* Optionally include a target taskQueue to send
|
|
207
|
+
* events to a specific worker.
|
|
208
|
+
*/
|
|
209
|
+
constructor(id?: string, taskQueue?: string) {
|
|
210
|
+
this.id = id;
|
|
211
|
+
if (taskQueue) {
|
|
212
|
+
this.taskQueue = taskQueue;
|
|
213
|
+
} else if (!id && !taskQueue) {
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return new Proxy(this, {
|
|
218
|
+
get: (target, prop, receiver) => {
|
|
219
|
+
if (typeof target[prop] === 'function') {
|
|
220
|
+
return (...args: any[]) => {
|
|
221
|
+
|
|
222
|
+
return new Promise(async (resolve, reject) => {
|
|
223
|
+
const client = new Client({ connection: {
|
|
224
|
+
class: this.redisClass,
|
|
225
|
+
options: this.redisOptions
|
|
226
|
+
}});
|
|
227
|
+
if (prop === this.main) {
|
|
228
|
+
//start a new workflow (main method was called)
|
|
229
|
+
return client.workflow.start({
|
|
230
|
+
namespace: this.namespace,
|
|
231
|
+
args,
|
|
232
|
+
taskQueue: this.taskQueue,
|
|
233
|
+
workflowName: prop,
|
|
234
|
+
workflowId: this.id,
|
|
235
|
+
}).then(resolve).catch(reject);
|
|
236
|
+
} else if (prop !== 'constructor') {
|
|
237
|
+
//update an existing workflow (hook/signal-in)
|
|
238
|
+
return client.workflow.hook({
|
|
239
|
+
namespace: this.namespace,
|
|
240
|
+
taskQueue: this.taskQueue,
|
|
241
|
+
workflowName: prop as string,
|
|
242
|
+
workflowId: this.id,
|
|
243
|
+
args,
|
|
244
|
+
}).then(resolve).catch(reject);
|
|
245
|
+
}
|
|
246
|
+
target[prop].apply(this, args).then(resolve).catch(reject);
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return Reflect.get(target, prop, receiver);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -2,6 +2,7 @@ import { HotMeshService as HotMesh } from '../hotmesh'
|
|
|
2
2
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
4
|
import { KeyService, KeyType } from '../../modules/key';
|
|
5
|
+
import { WorkflowSearchOptions } from '../../types/durable';
|
|
5
6
|
|
|
6
7
|
export class Search {
|
|
7
8
|
jobId: string;
|
|
@@ -11,11 +12,44 @@ export class Search {
|
|
|
11
12
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
12
13
|
|
|
13
14
|
safeKey(key:string): string {
|
|
14
|
-
//note: protect the execution namespace with a prefix
|
|
15
|
-
//so its design never conflicts with the hotmesh keyspace
|
|
15
|
+
//note: protect the execution namespace with a prefix
|
|
16
16
|
return `_${key}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
21
|
+
* this method will configure the search index for the workflow. For all
|
|
22
|
+
* others, this method will exit/fail gracefully and not index
|
|
23
|
+
* the fields in the HASH. However, all values are still available
|
|
24
|
+
* in the HASH.
|
|
25
|
+
*/
|
|
26
|
+
static async configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void> {
|
|
27
|
+
if (search?.schema) {
|
|
28
|
+
const store = hotMeshClient.engine.store;
|
|
29
|
+
const schema: string[] = [];
|
|
30
|
+
for (const [key, value] of Object.entries(search.schema)) {
|
|
31
|
+
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
32
|
+
schema.push(`_${key}`);
|
|
33
|
+
schema.push(value.type);
|
|
34
|
+
if (value.sortable) {
|
|
35
|
+
schema.push('SORTABLE');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const keyParams = {
|
|
40
|
+
appId: hotMeshClient.appId,
|
|
41
|
+
jobId: ''
|
|
42
|
+
}
|
|
43
|
+
const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
44
|
+
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
45
|
+
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length.toString(), ...prefixes, 'SCHEMA', ...schema);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(err);
|
|
48
|
+
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string) {
|
|
20
54
|
const keyParams = {
|
|
21
55
|
appId: hotMeshClient.appId,
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
DurableSleepError,
|
|
7
7
|
DurableTimeoutError,
|
|
8
8
|
DurableWaitForSignalError} from '../../modules/errors';
|
|
9
|
-
import { KeyService, KeyType } from '../../modules/key';
|
|
10
9
|
import { asyncLocalStorage } from './asyncLocalStorage';
|
|
11
10
|
import { APP_ID, APP_VERSION, getWorkflowYAML } from './factory';
|
|
12
11
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
@@ -16,9 +15,9 @@ import {
|
|
|
16
15
|
Registry,
|
|
17
16
|
WorkerConfig,
|
|
18
17
|
WorkerOptions,
|
|
19
|
-
WorkflowDataType
|
|
20
|
-
WorkflowSearchOptions} from '../../types/durable';
|
|
18
|
+
WorkflowDataType } from '../../types/durable';
|
|
21
19
|
import { RedisClass, RedisOptions } from '../../types/redis';
|
|
20
|
+
import { Search } from './search';
|
|
22
21
|
import {
|
|
23
22
|
StreamData,
|
|
24
23
|
StreamDataResponse,
|
|
@@ -79,39 +78,6 @@ export class WorkerService {
|
|
|
79
78
|
return WorkerService.activityRegistry;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
/**
|
|
83
|
-
* For those deployments with a redis stack backend (with the FT module),
|
|
84
|
-
* this method will configure the search index for the workflow. For all
|
|
85
|
-
* others, this method will fail gracefully. In all cases, the values
|
|
86
|
-
* will be stored in the workflow's central HASH data structure, allowing
|
|
87
|
-
* for manual traversal and inspection as well.
|
|
88
|
-
*/
|
|
89
|
-
static async configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void> {
|
|
90
|
-
if (search?.schema) {
|
|
91
|
-
const store = hotMeshClient.engine.store;
|
|
92
|
-
const schema: string[] = [];
|
|
93
|
-
for (const [key, value] of Object.entries(search.schema)) {
|
|
94
|
-
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
95
|
-
schema.push(`_${key}`);
|
|
96
|
-
schema.push(value.type);
|
|
97
|
-
if (value.sortable) {
|
|
98
|
-
schema.push('SORTABLE');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
const keyParams = {
|
|
103
|
-
appId: hotMeshClient.appId,
|
|
104
|
-
jobId: ''
|
|
105
|
-
}
|
|
106
|
-
const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
107
|
-
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
108
|
-
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
81
|
static async create(config: WorkerConfig) {
|
|
116
82
|
WorkerService.connection = config.connection;
|
|
117
83
|
const workflow = config.workflow;
|
|
@@ -124,7 +90,7 @@ export class WorkerService {
|
|
|
124
90
|
const worker = new WorkerService();
|
|
125
91
|
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
126
92
|
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
127
|
-
|
|
93
|
+
Search.configureSearchIndex(worker.workflowRunner, config.search)
|
|
128
94
|
await WorkerService.activateWorkflow(worker.workflowRunner);
|
|
129
95
|
return worker;
|
|
130
96
|
}
|
|
@@ -106,6 +106,9 @@ export class WorkflowService {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* wrap all activities in a proxy that will durably run them
|
|
111
|
+
*/
|
|
109
112
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
|
|
110
113
|
if (options.activities) {
|
|
111
114
|
WorkerService.registerActivities(options.activities)
|
|
@@ -122,6 +125,9 @@ export class WorkflowService {
|
|
|
122
125
|
return proxy;
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
/**
|
|
129
|
+
* return a search session for use when reading/writing to the workflow HASH
|
|
130
|
+
*/
|
|
125
131
|
static async search(): Promise<Search> {
|
|
126
132
|
const store = asyncLocalStorage.getStore();
|
|
127
133
|
const workflowId = store.get('workflowId');
|
|
@@ -130,15 +136,16 @@ export class WorkflowService {
|
|
|
130
136
|
const namespace = store.get('namespace');
|
|
131
137
|
const COUNTER = store.get('counter');
|
|
132
138
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
133
|
-
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
134
139
|
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
140
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
135
141
|
const searchSessionId = `-search${workflowDimension}-${execIndex}`;
|
|
136
142
|
return new Search(workflowId, hotMeshClient, searchSessionId);
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
/**
|
|
140
146
|
* those methods that may only be called once must be protected by flagging
|
|
141
|
-
* their execution with a unique key (the key is stored in the
|
|
147
|
+
* their execution with a unique key (the key is stored in the HASH alongside
|
|
148
|
+
* process state and job state)
|
|
142
149
|
*/
|
|
143
150
|
static async isSideEffectAllowed(hotMeshClient: HotMesh, prefix:string): Promise<boolean> {
|
|
144
151
|
const store = asyncLocalStorage.getStore();
|
|
@@ -146,15 +153,12 @@ export class WorkflowService {
|
|
|
146
153
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
147
154
|
const COUNTER = store.get('counter');
|
|
148
155
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
149
|
-
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
150
156
|
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
151
|
-
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
152
157
|
const keyParams = {
|
|
153
158
|
appId: hotMeshClient.appId,
|
|
154
|
-
jobId:
|
|
159
|
+
jobId: workflowId
|
|
155
160
|
}
|
|
156
|
-
const
|
|
157
|
-
const workflowGuid = `${hotMeshPrefix}${workflowId}`;
|
|
161
|
+
const workflowGuid = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
158
162
|
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1') as string);
|
|
159
163
|
return guidValue === 1;
|
|
160
164
|
}
|
|
@@ -183,15 +187,11 @@ export class WorkflowService {
|
|
|
183
187
|
const hotMeshClient = await WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
|
|
184
188
|
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
|
|
185
189
|
const store = asyncLocalStorage.getStore();
|
|
186
|
-
|
|
187
|
-
let workflowTopic
|
|
188
|
-
if (options.
|
|
189
|
-
workflowId = options.workflowId;
|
|
190
|
+
const workflowId = options.workflowId ?? store.get('workflowId');
|
|
191
|
+
let workflowTopic = store.get('workflowTopic');
|
|
192
|
+
if (options.taskQueue && options.workflowName) {
|
|
190
193
|
workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
191
|
-
} else
|
|
192
|
-
workflowId = store.get('workflowId');
|
|
193
|
-
workflowTopic = store.get('workflowTopic');
|
|
194
|
-
}
|
|
194
|
+
} //else this is essentially recursion as the function calls itself
|
|
195
195
|
const payload = {
|
|
196
196
|
arguments: [...options.args],
|
|
197
197
|
id: workflowId,
|
package/services/engine/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
SubscriptionCallback } from '../../types/quorum';
|
|
54
54
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
55
55
|
import { RedisClientType } from '../../types/redisclient';
|
|
56
|
+
import { StringAnyType, StringStringType } from '../../types/serializer';
|
|
56
57
|
import {
|
|
57
58
|
GetStatsOptions,
|
|
58
59
|
IdsResponse,
|
|
@@ -67,7 +68,6 @@ import {
|
|
|
67
68
|
StreamError,
|
|
68
69
|
StreamRole,
|
|
69
70
|
StreamStatus } from '../../types/stream';
|
|
70
|
-
import { StringStringType } from '../../types';
|
|
71
71
|
|
|
72
72
|
//wait time to see if a job is complete
|
|
73
73
|
const OTT_WAIT_TIME = 1000;
|
|
@@ -629,6 +629,9 @@ class EngineService {
|
|
|
629
629
|
}
|
|
630
630
|
return stateTree;
|
|
631
631
|
}
|
|
632
|
+
async getQueryState(jobId: string, fields: string[]): Promise<StringAnyType> {
|
|
633
|
+
return await this.store.getQueryState(jobId, fields);
|
|
634
|
+
}
|
|
632
635
|
|
|
633
636
|
async compress(terms: string[]): Promise<boolean> {
|
|
634
637
|
const existingSymbols = await this.store.getSymbolValues();
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
StatsResponse } from '../../types/stats';
|
|
22
22
|
import { ConnectorService } from '../connector';
|
|
23
23
|
import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
|
|
24
|
+
import { StringAnyType } from '../../types/serializer';
|
|
24
25
|
|
|
25
26
|
class HotMeshService {
|
|
26
27
|
namespace: string;
|
|
@@ -59,7 +60,7 @@ class HotMeshService {
|
|
|
59
60
|
instance.logger = new LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
|
|
60
61
|
await instance.initEngine(config, instance.logger);
|
|
61
62
|
await instance.initQuorum(config, instance.engine, instance.logger);
|
|
62
|
-
await instance.
|
|
63
|
+
await instance.doWork(config, instance.logger);
|
|
63
64
|
return instance;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -97,7 +98,7 @@ class HotMeshService {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
async
|
|
101
|
+
async doWork(config: HotMeshConfig, logger: ILogger) {
|
|
101
102
|
this.workers = await WorkerService.init(
|
|
102
103
|
this.namespace,
|
|
103
104
|
this.appId,
|
|
@@ -152,6 +153,9 @@ class HotMeshService {
|
|
|
152
153
|
async getState(topic: string, jobId: string): Promise<JobOutput> {
|
|
153
154
|
return this.engine?.getState(topic, jobId);
|
|
154
155
|
}
|
|
156
|
+
async getQueryState(jobId: string, fields: string[]): Promise<StringAnyType> {
|
|
157
|
+
return await this.engine?.getQueryState(jobId, fields);
|
|
158
|
+
}
|
|
155
159
|
async getIds(topic: string, query: JobStatsInput, queryFacets = []): Promise<IdsResponse> {
|
|
156
160
|
return await this.engine?.getIds(topic, query, queryFacets);
|
|
157
161
|
}
|
package/services/store/index.ts
CHANGED
|
@@ -476,6 +476,21 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
476
476
|
return jobId;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
/**
|
|
480
|
+
* returns custom search fields and values. The fields param
|
|
481
|
+
* should not prefix items with an underscore.
|
|
482
|
+
*/
|
|
483
|
+
async getQueryState(jobId: string, fields: string[]): Promise<StringAnyType> {
|
|
484
|
+
const key = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
485
|
+
const _fields = fields.map(field => `_${field}`);
|
|
486
|
+
const jobDataArray = await this.redisClient[this.commands.hmget](key, _fields);
|
|
487
|
+
const jobData: StringAnyType = {};
|
|
488
|
+
fields.forEach((field, index) => {
|
|
489
|
+
jobData[field] = jobDataArray[index];
|
|
490
|
+
});
|
|
491
|
+
return jobData;
|
|
492
|
+
}
|
|
493
|
+
|
|
479
494
|
async getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined> {
|
|
480
495
|
//get abbreviated field list (the symbols for the paths)
|
|
481
496
|
const key = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
package/types/durable.ts
CHANGED
|
@@ -29,9 +29,9 @@ type WorkflowOptions = {
|
|
|
29
29
|
|
|
30
30
|
type HookOptions = {
|
|
31
31
|
namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
|
|
32
|
-
taskQueue
|
|
32
|
+
taskQueue?: string;
|
|
33
33
|
args: any[]; //input arguments to pass into the hook
|
|
34
|
-
workflowId
|
|
34
|
+
workflowId?: string; //execution id (the job id to hook into)
|
|
35
35
|
workflowName?: string; //the name of the user's hook function
|
|
36
36
|
search?: WorkflowSearchOptions //bind additional search terms immediately before hook reentry
|
|
37
37
|
config?: WorkflowConfig; //hook function constraints (backoffCoefficient, maximumAttempts, maximumInterval, initialInterval)
|
|
@@ -57,12 +57,27 @@ type WorkflowDataType = {
|
|
|
57
57
|
workflowTopic: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
type MeshDBClassConfig = {
|
|
61
|
+
namespace: string;
|
|
62
|
+
taskQueue: string;
|
|
63
|
+
redisOptions: RedisOptions;
|
|
64
|
+
redisClass: RedisClass;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type MeshDBConfig = {
|
|
68
|
+
taskQueue?: string;
|
|
69
|
+
index?: {
|
|
70
|
+
index: string;
|
|
71
|
+
prefix: string[];
|
|
72
|
+
schema: Record<string, {type: 'TEXT' | 'NUMERIC' | 'TAG', sortable: boolean}>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
type ConnectionConfig = {
|
|
61
77
|
class: RedisClass;
|
|
62
78
|
options: RedisOptions;
|
|
63
79
|
}
|
|
64
80
|
type Connection = ConnectionConfig;
|
|
65
|
-
type NativeConnection = ConnectionConfig;
|
|
66
81
|
|
|
67
82
|
type ClientConfig = {
|
|
68
83
|
connection: Connection;
|
|
@@ -115,11 +130,12 @@ export {
|
|
|
115
130
|
ContextType,
|
|
116
131
|
ConnectionConfig,
|
|
117
132
|
Connection,
|
|
118
|
-
NativeConnection,
|
|
119
133
|
ProxyType,
|
|
120
134
|
Registry,
|
|
121
135
|
SignalOptions,
|
|
122
136
|
HookOptions,
|
|
137
|
+
MeshDBClassConfig,
|
|
138
|
+
MeshDBConfig,
|
|
123
139
|
WorkerConfig,
|
|
124
140
|
WorkflowConfig,
|
|
125
141
|
WorkerOptions,
|
package/types/index.ts
CHANGED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NativeConnectionService = void 0;
|
|
4
|
-
/*
|
|
5
|
-
|
|
6
|
-
Here is an example of how the methods in this file are used:
|
|
7
|
-
|
|
8
|
-
./worker.ts
|
|
9
|
-
|
|
10
|
-
import { Durable: { NativeConnection, Worker } } from '@hotmeshio/hotmesh';
|
|
11
|
-
import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
|
|
12
|
-
|
|
13
|
-
import * as workflows from './workflows';
|
|
14
|
-
|
|
15
|
-
async function run() {
|
|
16
|
-
const connection = await NativeConnection.connect({
|
|
17
|
-
class: Redis,
|
|
18
|
-
options: {
|
|
19
|
-
host: 'localhost',
|
|
20
|
-
port: 6379,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
const worker = await Worker.create({
|
|
24
|
-
connection,
|
|
25
|
-
taskQueue: 'hello-world',
|
|
26
|
-
workflow: workflows.example,
|
|
27
|
-
activities,
|
|
28
|
-
});
|
|
29
|
-
await worker.run();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
run().catch((err) => {
|
|
33
|
-
console.error(err);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
*/
|
|
38
|
-
class NativeConnectionService {
|
|
39
|
-
static async connect(config) {
|
|
40
|
-
return {
|
|
41
|
-
class: config.class,
|
|
42
|
-
options: { ...config.options },
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
exports.NativeConnectionService = NativeConnectionService;
|