@hotmeshio/hotmesh 0.0.57 → 0.0.59
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 +1 -1
- package/build/modules/enums.js +10 -1
- package/build/modules/key.d.ts +38 -0
- package/build/modules/key.js +46 -4
- package/build/modules/utils.d.ts +9 -0
- package/build/modules/utils.js +19 -1
- package/build/package.json +2 -1
- package/build/services/activities/activity.d.ts +28 -0
- package/build/services/activities/activity.js +46 -1
- package/build/services/activities/await.js +4 -0
- package/build/services/activities/cycle.d.ts +7 -0
- package/build/services/activities/cycle.js +16 -1
- package/build/services/activities/hook.d.ts +6 -0
- package/build/services/activities/hook.js +12 -2
- package/build/services/activities/interrupt.js +8 -0
- package/build/services/activities/signal.d.ts +6 -0
- package/build/services/activities/signal.js +15 -0
- package/build/services/activities/trigger.d.ts +4 -0
- package/build/services/activities/trigger.js +7 -1
- package/build/services/activities/worker.js +4 -0
- package/build/services/collator/index.d.ts +70 -0
- package/build/services/collator/index.js +91 -1
- package/build/services/compiler/deployer.js +38 -6
- package/build/services/compiler/index.d.ts +15 -0
- package/build/services/compiler/index.js +20 -0
- package/build/services/compiler/validator.d.ts +3 -0
- package/build/services/compiler/validator.js +25 -0
- package/build/services/connector/clients/ioredis.js +2 -0
- package/build/services/connector/clients/redis.js +2 -0
- package/build/services/connector/index.js +2 -0
- package/build/services/durable/client.d.ts +20 -0
- package/build/services/durable/client.js +25 -0
- package/build/services/durable/exporter.d.ts +22 -0
- package/build/services/durable/exporter.js +30 -1
- package/build/services/durable/handle.d.ts +36 -0
- package/build/services/durable/handle.js +46 -0
- package/build/services/durable/index.d.ts +4 -0
- package/build/services/durable/index.js +4 -0
- package/build/services/durable/schemas/factory.d.ts +29 -0
- package/build/services/durable/schemas/factory.js +29 -0
- package/build/services/durable/search.d.ts +97 -0
- package/build/services/durable/search.js +99 -0
- package/build/services/durable/worker.js +35 -6
- package/build/services/durable/workflow.d.ts +118 -0
- package/build/services/durable/workflow.js +153 -6
- package/build/services/engine/index.d.ts +5 -0
- package/build/services/engine/index.js +43 -1
- package/build/services/exporter/index.d.ts +27 -0
- package/build/services/exporter/index.js +33 -0
- package/build/services/hotmesh/index.js +8 -0
- package/build/services/logger/index.js +2 -0
- package/build/services/mapper/index.d.ts +14 -0
- package/build/services/mapper/index.js +14 -0
- package/build/services/pipe/functions/date.d.ts +7 -0
- package/build/services/pipe/functions/date.js +7 -0
- package/build/services/pipe/functions/math.js +2 -0
- package/build/services/pipe/index.d.ts +16 -0
- package/build/services/pipe/index.js +45 -3
- package/build/services/quorum/index.d.ts +7 -0
- package/build/services/quorum/index.js +21 -0
- package/build/services/reporter/index.d.ts +5 -0
- package/build/services/reporter/index.js +9 -0
- package/build/services/router/index.d.ts +9 -0
- package/build/services/router/index.js +30 -2
- package/build/services/serializer/index.js +23 -6
- package/build/services/store/cache.d.ts +19 -0
- package/build/services/store/cache.js +19 -0
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +55 -0
- package/build/services/store/index.js +81 -5
- package/build/services/stream/clients/ioredis.js +4 -1
- package/build/services/task/index.d.ts +9 -0
- package/build/services/task/index.js +31 -0
- package/build/services/telemetry/index.d.ts +7 -0
- package/build/services/telemetry/index.js +13 -1
- package/build/services/worker/index.d.ts +4 -0
- package/build/services/worker/index.js +6 -2
- package/build/types/activity.d.ts +81 -0
- package/build/types/durable.d.ts +255 -0
- package/build/types/exporter.d.ts +13 -0
- package/build/types/hotmesh.d.ts +10 -1
- package/build/types/hotmesh.js +3 -0
- package/build/types/index.js +1 -1
- package/build/types/job.d.ts +85 -0
- package/build/types/pipe.d.ts +65 -0
- package/build/types/quorum.d.ts +14 -0
- package/build/types/redis.d.ts +6 -0
- package/build/types/stream.d.ts +58 -0
- package/build/types/stream.js +4 -0
- package/package.json +2 -1
|
@@ -10,11 +10,47 @@ export declare class WorkflowHandleService {
|
|
|
10
10
|
workflowId: string;
|
|
11
11
|
constructor(hotMesh: HotMesh, workflowTopic: string, workflowId: string);
|
|
12
12
|
export(options?: ExportOptions): Promise<DurableJobExport>;
|
|
13
|
+
/**
|
|
14
|
+
* Sends a signal to the workflow. This is a way to send
|
|
15
|
+
* a message to a workflow that is paused due to having
|
|
16
|
+
* executed `Durable.workflow.waitFor`. The workflow
|
|
17
|
+
* will awaken if no other signals are pending.
|
|
18
|
+
*/
|
|
13
19
|
signal(signalId: string, data: Record<any, any>): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Returns the job state of the workflow. If the workflow has completed
|
|
22
|
+
* this is also the job output. If the workflow is still running, this
|
|
23
|
+
* is the current state of the job, but it may change depending upon
|
|
24
|
+
* the activities that remain.
|
|
25
|
+
*/
|
|
14
26
|
state(metadata?: boolean): Promise<Record<string, any>>;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the current search state of the workflow. This is
|
|
29
|
+
* different than the job state or individual activity state.
|
|
30
|
+
* Search state represents name/value pairs that were added
|
|
31
|
+
* to the workflow. As the workflow is stored in a Redis hash,
|
|
32
|
+
* this is a way to store additional data that is indexed
|
|
33
|
+
* and searchable using the RediSearch module.
|
|
34
|
+
*/
|
|
15
35
|
queryState(fields: string[]): Promise<Record<string, any>>;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the current status of the workflow. This is a semaphore
|
|
38
|
+
* value that represents the current state of the workflow, where
|
|
39
|
+
* 0 is complete and a negative value represents that the flow was
|
|
40
|
+
* interrupted.
|
|
41
|
+
*/
|
|
16
42
|
status(): Promise<number>;
|
|
43
|
+
/**
|
|
44
|
+
* Interrupts a running workflow. Standard Job Completion tasks will
|
|
45
|
+
* run. Subscribers will be notified and the job hash will be expired.
|
|
46
|
+
*/
|
|
17
47
|
interrupt(options?: JobInterruptOptions): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Waits for the workflow to complete and returns the result. If
|
|
50
|
+
* the workflow response includes an error, this method will rethrow
|
|
51
|
+
* the error, including the stack trace if available.
|
|
52
|
+
* Wrap calls in a try/catch as necessary to avoid unhandled exceptions.
|
|
53
|
+
*/
|
|
18
54
|
result<T>(config?: {
|
|
19
55
|
state?: boolean;
|
|
20
56
|
throwOnError?: boolean;
|
|
@@ -12,9 +12,21 @@ class WorkflowHandleService {
|
|
|
12
12
|
async export(options) {
|
|
13
13
|
return this.exporter.export(this.workflowId, options);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Sends a signal to the workflow. This is a way to send
|
|
17
|
+
* a message to a workflow that is paused due to having
|
|
18
|
+
* executed `Durable.workflow.waitFor`. The workflow
|
|
19
|
+
* will awaken if no other signals are pending.
|
|
20
|
+
*/
|
|
15
21
|
async signal(signalId, data) {
|
|
16
22
|
await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
|
|
17
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns the job state of the workflow. If the workflow has completed
|
|
26
|
+
* this is also the job output. If the workflow is still running, this
|
|
27
|
+
* is the current state of the job, but it may change depending upon
|
|
28
|
+
* the activities that remain.
|
|
29
|
+
*/
|
|
18
30
|
async state(metadata = false) {
|
|
19
31
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
20
32
|
if (!state.data && state.metadata.err) {
|
|
@@ -22,25 +34,56 @@ class WorkflowHandleService {
|
|
|
22
34
|
}
|
|
23
35
|
return metadata ? state : state.data;
|
|
24
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns the current search state of the workflow. This is
|
|
39
|
+
* different than the job state or individual activity state.
|
|
40
|
+
* Search state represents name/value pairs that were added
|
|
41
|
+
* to the workflow. As the workflow is stored in a Redis hash,
|
|
42
|
+
* this is a way to store additional data that is indexed
|
|
43
|
+
* and searchable using the RediSearch module.
|
|
44
|
+
*/
|
|
25
45
|
async queryState(fields) {
|
|
26
46
|
return await this.hotMesh.getQueryState(this.workflowId, fields);
|
|
27
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns the current status of the workflow. This is a semaphore
|
|
50
|
+
* value that represents the current state of the workflow, where
|
|
51
|
+
* 0 is complete and a negative value represents that the flow was
|
|
52
|
+
* interrupted.
|
|
53
|
+
*/
|
|
28
54
|
async status() {
|
|
29
55
|
return await this.hotMesh.getStatus(this.workflowId);
|
|
30
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Interrupts a running workflow. Standard Job Completion tasks will
|
|
59
|
+
* run. Subscribers will be notified and the job hash will be expired.
|
|
60
|
+
*/
|
|
31
61
|
async interrupt(options) {
|
|
32
62
|
return await this.hotMesh.interrupt(`${this.hotMesh.appId}.execute`, this.workflowId, options);
|
|
33
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Waits for the workflow to complete and returns the result. If
|
|
66
|
+
* the workflow response includes an error, this method will rethrow
|
|
67
|
+
* the error, including the stack trace if available.
|
|
68
|
+
* Wrap calls in a try/catch as necessary to avoid unhandled exceptions.
|
|
69
|
+
*/
|
|
34
70
|
async result(config) {
|
|
35
71
|
const topic = `${this.hotMesh.appId}.executed.${this.workflowId}`;
|
|
36
72
|
let isResolved = false;
|
|
37
73
|
return new Promise(async (resolve, reject) => {
|
|
74
|
+
/**
|
|
75
|
+
* rejects/resolves the promise based on the `throwOnError`
|
|
76
|
+
* default behavior is to throw if error
|
|
77
|
+
*/
|
|
38
78
|
const safeReject = (err) => {
|
|
39
79
|
if (config?.throwOnError === false) {
|
|
40
80
|
return resolve(err);
|
|
41
81
|
}
|
|
42
82
|
reject(err);
|
|
43
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* Common completion function that unsubscribes from the topic/returns
|
|
86
|
+
*/
|
|
44
87
|
const complete = async (response, err) => {
|
|
45
88
|
if (isResolved)
|
|
46
89
|
return;
|
|
@@ -63,6 +106,7 @@ class WorkflowHandleService {
|
|
|
63
106
|
}
|
|
64
107
|
resolve(response);
|
|
65
108
|
};
|
|
109
|
+
//more expensive; fetches the entire job, not just the `status`
|
|
66
110
|
if (config?.state) {
|
|
67
111
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
68
112
|
if (state?.data?.done && !state.data?.$error) {
|
|
@@ -75,6 +119,7 @@ class WorkflowHandleService {
|
|
|
75
119
|
return complete(null, JSON.parse(state.metadata.err));
|
|
76
120
|
}
|
|
77
121
|
}
|
|
122
|
+
//subscribe to 'done' topic
|
|
78
123
|
this.hotMesh.sub(topic, async (_topic, state) => {
|
|
79
124
|
this.hotMesh.unsub(topic);
|
|
80
125
|
if (state.data.done && !state.data?.$error) {
|
|
@@ -88,6 +133,7 @@ class WorkflowHandleService {
|
|
|
88
133
|
return await complete(null, error);
|
|
89
134
|
}
|
|
90
135
|
});
|
|
136
|
+
//check state in case completed during wiring
|
|
91
137
|
const status = await this.hotMesh.getStatus(this.workflowId);
|
|
92
138
|
if (status <= 0) {
|
|
93
139
|
await complete();
|
|
@@ -10,6 +10,10 @@ export declare const Durable: {
|
|
|
10
10
|
Search: typeof Search;
|
|
11
11
|
Worker: typeof WorkerService;
|
|
12
12
|
workflow: typeof WorkflowService;
|
|
13
|
+
/**
|
|
14
|
+
* Shutdown everything. All connections, workers, and clients will be closed.
|
|
15
|
+
* Include in your signal handlers to ensure a clean shutdown.
|
|
16
|
+
*/
|
|
13
17
|
shutdown(): Promise<void>;
|
|
14
18
|
};
|
|
15
19
|
export type { ContextType };
|
|
@@ -13,6 +13,10 @@ exports.Durable = {
|
|
|
13
13
|
Search: search_1.Search,
|
|
14
14
|
Worker: worker_1.WorkerService,
|
|
15
15
|
workflow: workflow_1.WorkflowService,
|
|
16
|
+
/**
|
|
17
|
+
* Shutdown everything. All connections, workers, and clients will be closed.
|
|
18
|
+
* Include in your signal handlers to ensure a clean shutdown.
|
|
19
|
+
*/
|
|
16
20
|
async shutdown() {
|
|
17
21
|
await client_1.ClientService.shutdown();
|
|
18
22
|
await worker_1.WorkerService.shutdown();
|
|
@@ -1,4 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*********** HOTMESH 'DURABLE' MODULE APPLICATION GRAPH **********
|
|
3
|
+
*
|
|
4
|
+
* This HotMesh application spec uses 50 activities and 25 transitions
|
|
5
|
+
* to model and emulate the Temporal Application & Query servers using
|
|
6
|
+
* Redis as the backend.
|
|
7
|
+
*
|
|
8
|
+
* It's particularly useful for organizations with high-speed, high-volume
|
|
9
|
+
* use cases as it uses in-memory Redis Streams for transactional,
|
|
10
|
+
* workflow processing, while adhering to Temporal's developer-friendly syntax.
|
|
11
|
+
*
|
|
12
|
+
* This YAML file can also serve as a useful starting point for building
|
|
13
|
+
* Integration/BPM/Workflow servers in general (MuleSoft, etc) without the need
|
|
14
|
+
* for a physical application server.
|
|
15
|
+
*
|
|
16
|
+
* Possible use cases include:
|
|
17
|
+
* * Orchestration servers
|
|
18
|
+
* * Integration servers
|
|
19
|
+
* * BPMN engines
|
|
20
|
+
* * Reentrant process servers
|
|
21
|
+
* * Service Meshes
|
|
22
|
+
* * Master Data Management systems
|
|
23
|
+
*/
|
|
1
24
|
declare const APP_VERSION = "1";
|
|
2
25
|
declare const APP_ID = "durable";
|
|
26
|
+
/**
|
|
27
|
+
* returns a new durable workflow schema
|
|
28
|
+
* @param {string} app - app name (e.g., 'durable')
|
|
29
|
+
* @param {string} version - number as string (e.g., '1')
|
|
30
|
+
* @returns {string} HotMesh App YAML
|
|
31
|
+
*/
|
|
3
32
|
declare const getWorkflowYAML: (app: string, version: string) => string;
|
|
4
33
|
export { getWorkflowYAML, APP_VERSION, APP_ID, };
|
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
*********** HOTMESH 'DURABLE' MODULE APPLICATION GRAPH **********
|
|
4
|
+
*
|
|
5
|
+
* This HotMesh application spec uses 50 activities and 25 transitions
|
|
6
|
+
* to model and emulate the Temporal Application & Query servers using
|
|
7
|
+
* Redis as the backend.
|
|
8
|
+
*
|
|
9
|
+
* It's particularly useful for organizations with high-speed, high-volume
|
|
10
|
+
* use cases as it uses in-memory Redis Streams for transactional,
|
|
11
|
+
* workflow processing, while adhering to Temporal's developer-friendly syntax.
|
|
12
|
+
*
|
|
13
|
+
* This YAML file can also serve as a useful starting point for building
|
|
14
|
+
* Integration/BPM/Workflow servers in general (MuleSoft, etc) without the need
|
|
15
|
+
* for a physical application server.
|
|
16
|
+
*
|
|
17
|
+
* Possible use cases include:
|
|
18
|
+
* * Orchestration servers
|
|
19
|
+
* * Integration servers
|
|
20
|
+
* * BPMN engines
|
|
21
|
+
* * Reentrant process servers
|
|
22
|
+
* * Service Meshes
|
|
23
|
+
* * Master Data Management systems
|
|
24
|
+
*/
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
26
|
exports.APP_ID = exports.APP_VERSION = exports.getWorkflowYAML = void 0;
|
|
4
27
|
const APP_VERSION = '1';
|
|
5
28
|
exports.APP_VERSION = APP_VERSION;
|
|
6
29
|
const APP_ID = 'durable';
|
|
7
30
|
exports.APP_ID = APP_ID;
|
|
31
|
+
/**
|
|
32
|
+
* returns a new durable workflow schema
|
|
33
|
+
* @param {string} app - app name (e.g., 'durable')
|
|
34
|
+
* @param {string} version - number as string (e.g., '1')
|
|
35
|
+
* @returns {string} HotMesh App YAML
|
|
36
|
+
*/
|
|
8
37
|
const getWorkflowYAML = (app, version) => {
|
|
9
38
|
return `app:
|
|
10
39
|
id: ${app}
|
|
@@ -10,14 +10,111 @@ export declare class Search {
|
|
|
10
10
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
11
11
|
cachedFields: Record<string, string>;
|
|
12
12
|
constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string);
|
|
13
|
+
/**
|
|
14
|
+
* Prefixes the key with an underscore to keep separate from the
|
|
15
|
+
* activity and job history (and searchable via HKEYS)
|
|
16
|
+
* @param {string} key - the key to be sanitized. Wrap in quotes to avoid sanitization.
|
|
17
|
+
* @returns {string} - the sanitized key
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
13
20
|
safeKey(key: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
23
|
+
* this method will configure the search index for the workflow. For all
|
|
24
|
+
* others, this method will exit/fail gracefully and not index
|
|
25
|
+
* the fields in the HASH. All values are searchable via HKEYS/HSC/HGET
|
|
26
|
+
* @param {HotMesh} hotMeshClient - the hotmesh client
|
|
27
|
+
* @param {WorkflowSearchOptions} search - the search options
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
* @example
|
|
30
|
+
* const search = {
|
|
31
|
+
* index: 'my_search_index',
|
|
32
|
+
* prefix: ['my_workflow_prefix'],
|
|
33
|
+
* schema: {
|
|
34
|
+
* field1: { type: 'TEXT', sortable: true },
|
|
35
|
+
* field2: { type: 'NUMERIC', sortable: true }
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* await Search.configureSearchIndex(hotMeshClient, search);
|
|
39
|
+
*/
|
|
14
40
|
static configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
43
|
+
* this method will list all search indexes.
|
|
44
|
+
*
|
|
45
|
+
* @param {HotMesh} hotMeshClient - the hotmesh client
|
|
46
|
+
* @returns {Promise<string[]>} - the list of search indexes
|
|
47
|
+
* @example
|
|
48
|
+
* const searchIndexes = await Search.listSearchIndexes(hotMeshClient);
|
|
49
|
+
*/
|
|
15
50
|
static listSearchIndexes(hotMeshClient: HotMesh): Promise<string[]>;
|
|
51
|
+
/**
|
|
52
|
+
* increments the index to return a unique search session guid when
|
|
53
|
+
* calling any method that produces side effects (changes the value)
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
16
56
|
getSearchSessionGuid(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Sets the fields listed in args. Returns the
|
|
59
|
+
* count of new fields that were set (does not
|
|
60
|
+
* count fields that were updated)
|
|
61
|
+
* @param args
|
|
62
|
+
* @returns {number}
|
|
63
|
+
* @example
|
|
64
|
+
* const search = new Search();
|
|
65
|
+
* const count = await search.set('field1', 'value1', 'field2', 'value2');
|
|
66
|
+
*/
|
|
17
67
|
set(...args: string[]): Promise<number>;
|
|
68
|
+
/**
|
|
69
|
+
* Returns the value of the field in the HASH stored at key.
|
|
70
|
+
* @param key
|
|
71
|
+
* @returns {string}
|
|
72
|
+
* @example
|
|
73
|
+
* const search = new Search();
|
|
74
|
+
* const value = await search.get('field1');
|
|
75
|
+
*/
|
|
18
76
|
get(key: string): Promise<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Returns the values of all specified fields in the HASH stored at key.
|
|
79
|
+
* @param args
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
19
82
|
mget(...args: string[]): Promise<string[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Deletes the fields provided as args. Returns the
|
|
85
|
+
* count of fields that were deleted.
|
|
86
|
+
*
|
|
87
|
+
* @param args
|
|
88
|
+
* @returns {number}
|
|
89
|
+
* @example
|
|
90
|
+
* sont search = new Search();
|
|
91
|
+
* const count = await search.del('field1', 'field2', 'field3');
|
|
92
|
+
*/
|
|
20
93
|
del(...args: string[]): Promise<number | void>;
|
|
94
|
+
/**
|
|
95
|
+
* Increments the value of a float field by the given amount. Returns the
|
|
96
|
+
* new value of the field after the increment. Pass a negative
|
|
97
|
+
* number to decrement the value.
|
|
98
|
+
*
|
|
99
|
+
* @param key - the key to increment
|
|
100
|
+
* @param val - the value to increment by
|
|
101
|
+
* @returns {number} - the new value
|
|
102
|
+
* @example
|
|
103
|
+
* const search = new Search();
|
|
104
|
+
* const count = await search.incr('field1', 1.5);
|
|
105
|
+
*/
|
|
21
106
|
incr(key: string, val: number): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Multiplies the value of a field by the given amount. Returns the
|
|
109
|
+
* new value of the field after the multiplication. NOTE:
|
|
110
|
+
* this is exponential multiplication.
|
|
111
|
+
*
|
|
112
|
+
* @param key - the key to multiply
|
|
113
|
+
* @param val - the value to multiply by
|
|
114
|
+
* @returns {number} - the new product of the multiplication
|
|
115
|
+
* @example
|
|
116
|
+
* const search = new Search();
|
|
117
|
+
* const product = await search.mult('field1', 1.5);
|
|
118
|
+
*/
|
|
22
119
|
mult(key: string, val: number): Promise<number>;
|
|
23
120
|
}
|
|
@@ -16,12 +16,38 @@ class Search {
|
|
|
16
16
|
this.hotMeshClient = hotMeshClient;
|
|
17
17
|
this.store = hotMeshClient.engine.store;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Prefixes the key with an underscore to keep separate from the
|
|
21
|
+
* activity and job history (and searchable via HKEYS)
|
|
22
|
+
* @param {string} key - the key to be sanitized. Wrap in quotes to avoid sanitization.
|
|
23
|
+
* @returns {string} - the sanitized key
|
|
24
|
+
* @private
|
|
25
|
+
*/
|
|
19
26
|
safeKey(key) {
|
|
20
27
|
if (key.startsWith('"')) {
|
|
21
28
|
return key.slice(1, -1);
|
|
22
29
|
}
|
|
23
30
|
return `_${key}`;
|
|
24
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
34
|
+
* this method will configure the search index for the workflow. For all
|
|
35
|
+
* others, this method will exit/fail gracefully and not index
|
|
36
|
+
* the fields in the HASH. All values are searchable via HKEYS/HSC/HGET
|
|
37
|
+
* @param {HotMesh} hotMeshClient - the hotmesh client
|
|
38
|
+
* @param {WorkflowSearchOptions} search - the search options
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
* @example
|
|
41
|
+
* const search = {
|
|
42
|
+
* index: 'my_search_index',
|
|
43
|
+
* prefix: ['my_workflow_prefix'],
|
|
44
|
+
* schema: {
|
|
45
|
+
* field1: { type: 'TEXT', sortable: true },
|
|
46
|
+
* field2: { type: 'NUMERIC', sortable: true }
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* await Search.configureSearchIndex(hotMeshClient, search);
|
|
50
|
+
*/
|
|
25
51
|
static async configureSearchIndex(hotMeshClient, search) {
|
|
26
52
|
if (search?.schema) {
|
|
27
53
|
const store = hotMeshClient.engine.store;
|
|
@@ -57,6 +83,15 @@ class Search {
|
|
|
57
83
|
}
|
|
58
84
|
}
|
|
59
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
88
|
+
* this method will list all search indexes.
|
|
89
|
+
*
|
|
90
|
+
* @param {HotMesh} hotMeshClient - the hotmesh client
|
|
91
|
+
* @returns {Promise<string[]>} - the list of search indexes
|
|
92
|
+
* @example
|
|
93
|
+
* const searchIndexes = await Search.listSearchIndexes(hotMeshClient);
|
|
94
|
+
*/
|
|
60
95
|
static async listSearchIndexes(hotMeshClient) {
|
|
61
96
|
try {
|
|
62
97
|
const store = hotMeshClient.engine.store;
|
|
@@ -68,9 +103,25 @@ class Search {
|
|
|
68
103
|
return [];
|
|
69
104
|
}
|
|
70
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* increments the index to return a unique search session guid when
|
|
108
|
+
* calling any method that produces side effects (changes the value)
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
71
111
|
getSearchSessionGuid() {
|
|
112
|
+
//return the search session as it would exist in the search session index
|
|
72
113
|
return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
|
|
73
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Sets the fields listed in args. Returns the
|
|
117
|
+
* count of new fields that were set (does not
|
|
118
|
+
* count fields that were updated)
|
|
119
|
+
* @param args
|
|
120
|
+
* @returns {number}
|
|
121
|
+
* @example
|
|
122
|
+
* const search = new Search();
|
|
123
|
+
* const count = await search.set('field1', 'value1', 'field2', 'value2');
|
|
124
|
+
*/
|
|
74
125
|
async set(...args) {
|
|
75
126
|
const ssGuid = this.getSearchSessionGuid();
|
|
76
127
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
@@ -87,9 +138,18 @@ class Search {
|
|
|
87
138
|
return Number(replay[ssGuid]);
|
|
88
139
|
}
|
|
89
140
|
const fieldCount = await this.store.exec('HSET', this.jobId, ...safeArgs);
|
|
141
|
+
//no need to wait; set this interim value in the replay
|
|
90
142
|
this.store.exec('HSET', this.jobId, ssGuid, fieldCount.toString());
|
|
91
143
|
return Number(fieldCount);
|
|
92
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns the value of the field in the HASH stored at key.
|
|
147
|
+
* @param key
|
|
148
|
+
* @returns {string}
|
|
149
|
+
* @example
|
|
150
|
+
* const search = new Search();
|
|
151
|
+
* const value = await search.get('field1');
|
|
152
|
+
*/
|
|
93
153
|
async get(key) {
|
|
94
154
|
try {
|
|
95
155
|
if (key in this.cachedFields) {
|
|
@@ -104,6 +164,11 @@ class Search {
|
|
|
104
164
|
return '';
|
|
105
165
|
}
|
|
106
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Returns the values of all specified fields in the HASH stored at key.
|
|
169
|
+
* @param args
|
|
170
|
+
* @returns
|
|
171
|
+
*/
|
|
107
172
|
async mget(...args) {
|
|
108
173
|
let isCached = true;
|
|
109
174
|
const values = [];
|
|
@@ -134,6 +199,16 @@ class Search {
|
|
|
134
199
|
return [];
|
|
135
200
|
}
|
|
136
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Deletes the fields provided as args. Returns the
|
|
204
|
+
* count of fields that were deleted.
|
|
205
|
+
*
|
|
206
|
+
* @param args
|
|
207
|
+
* @returns {number}
|
|
208
|
+
* @example
|
|
209
|
+
* sont search = new Search();
|
|
210
|
+
* const count = await search.del('field1', 'field2', 'field3');
|
|
211
|
+
*/
|
|
137
212
|
async del(...args) {
|
|
138
213
|
const ssGuid = this.getSearchSessionGuid();
|
|
139
214
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
@@ -152,6 +227,18 @@ class Search {
|
|
|
152
227
|
this.store.exec('HSET', this.jobId, ssGuid, formattedResponse.toString());
|
|
153
228
|
return formattedResponse;
|
|
154
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Increments the value of a float field by the given amount. Returns the
|
|
232
|
+
* new value of the field after the increment. Pass a negative
|
|
233
|
+
* number to decrement the value.
|
|
234
|
+
*
|
|
235
|
+
* @param key - the key to increment
|
|
236
|
+
* @param val - the value to increment by
|
|
237
|
+
* @returns {number} - the new value
|
|
238
|
+
* @example
|
|
239
|
+
* const search = new Search();
|
|
240
|
+
* const count = await search.incr('field1', 1.5);
|
|
241
|
+
*/
|
|
155
242
|
async incr(key, val) {
|
|
156
243
|
delete this.cachedFields[key];
|
|
157
244
|
const ssGuid = this.getSearchSessionGuid();
|
|
@@ -164,6 +251,18 @@ class Search {
|
|
|
164
251
|
this.store.exec('HSET', this.jobId, ssGuid, num.toString());
|
|
165
252
|
return Number(num);
|
|
166
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Multiplies the value of a field by the given amount. Returns the
|
|
256
|
+
* new value of the field after the multiplication. NOTE:
|
|
257
|
+
* this is exponential multiplication.
|
|
258
|
+
*
|
|
259
|
+
* @param key - the key to multiply
|
|
260
|
+
* @param val - the value to multiply by
|
|
261
|
+
* @returns {number} - the new product of the multiplication
|
|
262
|
+
* @example
|
|
263
|
+
* const search = new Search();
|
|
264
|
+
* const product = await search.mult('field1', 1.5);
|
|
265
|
+
*/
|
|
167
266
|
async mult(key, val) {
|
|
168
267
|
delete this.cachedFields[key];
|
|
169
268
|
const ssGuid = this.getSearchSessionGuid();
|
|
@@ -9,11 +9,11 @@ const ms_1 = __importDefault(require("ms"));
|
|
|
9
9
|
const enums_1 = require("../../modules/enums");
|
|
10
10
|
const errors_1 = require("../../modules/errors");
|
|
11
11
|
const storage_1 = require("../../modules/storage");
|
|
12
|
+
const utils_1 = require("../../modules/utils");
|
|
12
13
|
const factory_1 = require("./schemas/factory");
|
|
13
14
|
const hotmesh_1 = require("../hotmesh");
|
|
14
15
|
const search_1 = require("./search");
|
|
15
16
|
const stream_1 = require("../../types/stream");
|
|
16
|
-
const utils_1 = require("../../modules/utils");
|
|
17
17
|
class WorkerService {
|
|
18
18
|
static async activateWorkflow(hotMesh) {
|
|
19
19
|
const app = await hotMesh.engine.store.getApp(hotMesh.engine.appId);
|
|
@@ -61,6 +61,7 @@ class WorkerService {
|
|
|
61
61
|
const baseTopic = `${config.taskQueue}-${workflowFunctionName}`;
|
|
62
62
|
const activityTopic = `${baseTopic}-activity`;
|
|
63
63
|
const workflowTopic = `${baseTopic}`;
|
|
64
|
+
//initialize supporting workflows
|
|
64
65
|
const worker = new WorkerService();
|
|
65
66
|
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
66
67
|
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
@@ -103,9 +104,11 @@ class WorkerService {
|
|
|
103
104
|
WorkerService.instances.set(activityTopic, hotMeshWorker);
|
|
104
105
|
return hotMeshWorker;
|
|
105
106
|
}
|
|
107
|
+
//this is the linked worker function in the reentrant workflow test
|
|
106
108
|
wrapActivityFunctions() {
|
|
107
109
|
return async (data) => {
|
|
108
110
|
try {
|
|
111
|
+
//always run the activity function when instructed; return the response
|
|
109
112
|
const activityInput = data.data;
|
|
110
113
|
const activityName = activityInput.activityName;
|
|
111
114
|
const activityFunction = WorkerService.activityRegistry[activityName];
|
|
@@ -121,6 +124,8 @@ class WorkerService {
|
|
|
121
124
|
if (!(err instanceof errors_1.DurableTimeoutError) &&
|
|
122
125
|
!(err instanceof errors_1.DurableMaxedError) &&
|
|
123
126
|
!(err instanceof errors_1.DurableFatalError)) {
|
|
127
|
+
//use code 599 as a proxy for all retryable errors
|
|
128
|
+
// (basically anything not 596, 597, 598)
|
|
124
129
|
return {
|
|
125
130
|
status: stream_1.StreamStatus.SUCCESS,
|
|
126
131
|
code: enums_1.HMSH_CODE_DURABLE_RETRYABLE,
|
|
@@ -135,6 +140,8 @@ class WorkerService {
|
|
|
135
140
|
};
|
|
136
141
|
}
|
|
137
142
|
return {
|
|
143
|
+
//always returrn success (the Durable module is just fine);
|
|
144
|
+
// it's the user's function that has failed
|
|
138
145
|
status: stream_1.StreamStatus.SUCCESS,
|
|
139
146
|
code: err.code,
|
|
140
147
|
stack: err.stack,
|
|
@@ -172,8 +179,10 @@ class WorkerService {
|
|
|
172
179
|
wrapWorkflowFunction(workflowFunction, workflowTopic, config) {
|
|
173
180
|
return async (data) => {
|
|
174
181
|
const counter = { counter: 0 };
|
|
175
|
-
const interruptionRegistry =
|
|
182
|
+
const interruptionRegistry = new Array();
|
|
183
|
+
let isProcessing = false;
|
|
176
184
|
try {
|
|
185
|
+
//incoming data payload has arguments and workflowId
|
|
177
186
|
const workflowInput = data.data;
|
|
178
187
|
const context = new Map();
|
|
179
188
|
context.set('canRetry', workflowInput.canRetry);
|
|
@@ -183,14 +192,20 @@ class WorkerService {
|
|
|
183
192
|
context.set('raw', data);
|
|
184
193
|
context.set('workflowId', workflowInput.workflowId);
|
|
185
194
|
if (workflowInput.originJobId) {
|
|
195
|
+
//if present there is an origin job to which this job is subordinated;
|
|
196
|
+
// garbage collect (expire) this job when originJobId is expired
|
|
186
197
|
context.set('originJobId', workflowInput.originJobId);
|
|
187
198
|
}
|
|
188
199
|
let replayQuery = '';
|
|
189
200
|
if (workflowInput.workflowDimension) {
|
|
201
|
+
//every hook function runs in an isolated dimension controlled
|
|
202
|
+
//by the index assigned when the signal was received; even if the
|
|
203
|
+
//hook function re-runs, its scope will always remain constant
|
|
190
204
|
context.set('workflowDimension', workflowInput.workflowDimension);
|
|
191
205
|
replayQuery = `-*${workflowInput.workflowDimension}-*`;
|
|
192
206
|
}
|
|
193
207
|
else {
|
|
208
|
+
//last letter of words like 'hook', 'sleep', 'wait', 'signal', 'search', 'start', 'proxy', 'child', 'collator'
|
|
194
209
|
replayQuery = '-*[ehklptydr]-*';
|
|
195
210
|
}
|
|
196
211
|
context.set('workflowTopic', workflowTopic);
|
|
@@ -200,7 +215,7 @@ class WorkerService {
|
|
|
200
215
|
const store = this.workflowRunner.engine.store;
|
|
201
216
|
const [cursor, replay] = await store.findJobFields(workflowInput.workflowId, replayQuery, 50000, 5000);
|
|
202
217
|
context.set('replay', replay);
|
|
203
|
-
context.set('cursor', cursor);
|
|
218
|
+
context.set('cursor', cursor); // if != 0, more remain
|
|
204
219
|
const workflowResponse = await storage_1.asyncLocalStorage.run(context, async () => {
|
|
205
220
|
return await workflowFunction.apply(this, workflowInput.arguments);
|
|
206
221
|
});
|
|
@@ -212,11 +227,16 @@ class WorkerService {
|
|
|
212
227
|
};
|
|
213
228
|
}
|
|
214
229
|
catch (err) {
|
|
230
|
+
if (isProcessing) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
215
233
|
if (err instanceof errors_1.DurableWaitForError || interruptionRegistry.length > 1) {
|
|
234
|
+
isProcessing = true;
|
|
235
|
+
//NOTE: this type is spawned when `Promise.all` is used OR if the interruption is a `waitFor`
|
|
216
236
|
const workflowInput = data.data;
|
|
217
237
|
const execIndex = counter.counter - interruptionRegistry.length + 1;
|
|
218
238
|
const { workflowId, workflowTopic, workflowDimension, originJobId } = workflowInput;
|
|
219
|
-
const collatorFlowId =
|
|
239
|
+
const collatorFlowId = `${(0, utils_1.guid)()}$C`;
|
|
220
240
|
return {
|
|
221
241
|
status: stream_1.StreamStatus.SUCCESS,
|
|
222
242
|
code: enums_1.HMSH_CODE_DURABLE_ALL,
|
|
@@ -235,6 +255,8 @@ class WorkerService {
|
|
|
235
255
|
};
|
|
236
256
|
}
|
|
237
257
|
else if (err instanceof errors_1.DurableSleepError) {
|
|
258
|
+
//return the sleep interruption
|
|
259
|
+
isProcessing = true;
|
|
238
260
|
return {
|
|
239
261
|
status: stream_1.StreamStatus.SUCCESS,
|
|
240
262
|
code: err.code,
|
|
@@ -249,6 +271,8 @@ class WorkerService {
|
|
|
249
271
|
};
|
|
250
272
|
}
|
|
251
273
|
else if (err instanceof errors_1.DurableProxyError) {
|
|
274
|
+
//return the proxyActivity interruption
|
|
275
|
+
isProcessing = true;
|
|
252
276
|
return {
|
|
253
277
|
status: stream_1.StreamStatus.SUCCESS,
|
|
254
278
|
code: err.code,
|
|
@@ -271,10 +295,12 @@ class WorkerService {
|
|
|
271
295
|
};
|
|
272
296
|
}
|
|
273
297
|
else if (err instanceof errors_1.DurableChildError) {
|
|
298
|
+
//return the child interruption
|
|
299
|
+
isProcessing = true;
|
|
274
300
|
const msg = {
|
|
275
301
|
message: err.message,
|
|
276
302
|
workflowId: err.workflowId,
|
|
277
|
-
dimension: err.workflowDimension
|
|
303
|
+
dimension: err.workflowDimension,
|
|
278
304
|
};
|
|
279
305
|
return {
|
|
280
306
|
status: stream_1.StreamStatus.SUCCESS,
|
|
@@ -297,6 +323,9 @@ class WorkerService {
|
|
|
297
323
|
}
|
|
298
324
|
};
|
|
299
325
|
}
|
|
326
|
+
// ALL other errors are actual fatal errors (598, 597, 596)
|
|
327
|
+
// OR will be retried (599)
|
|
328
|
+
isProcessing = true;
|
|
300
329
|
return {
|
|
301
330
|
status: stream_1.StreamStatus.SUCCESS,
|
|
302
331
|
code: err.code || new errors_1.DurableRetryError(err.message).code,
|
|
@@ -321,7 +350,7 @@ class WorkerService {
|
|
|
321
350
|
}
|
|
322
351
|
}
|
|
323
352
|
_a = WorkerService;
|
|
324
|
-
WorkerService.activityRegistry = {};
|
|
353
|
+
WorkerService.activityRegistry = {}; //user's activities
|
|
325
354
|
WorkerService.instances = new Map();
|
|
326
355
|
WorkerService.getHotMesh = async (workflowTopic, config, options) => {
|
|
327
356
|
if (WorkerService.instances.has(workflowTopic)) {
|