@hotmeshio/hotmesh 0.0.22 → 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/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 +11 -7
- 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 +18 -2
- 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 +11 -7
- 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 +18 -2
- 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
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"test:sub:redis": "NODE_ENV=test jest ./tests/functional/sub/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
44
44
|
"test:sub:ioredis": "NODE_ENV=test jest ./tests/functional/sub/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
45
45
|
"test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
46
|
+
"test:durable:meshdb": "NODE_ENV=test jest ./tests/durable/meshdb/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
46
47
|
"test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
47
48
|
"test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
48
49
|
"test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -1,45 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectionService = void 0;
|
|
4
|
-
/*
|
|
5
|
-
Here is an example of how the methods in this file are used:
|
|
6
|
-
|
|
7
|
-
./client.ts
|
|
8
|
-
|
|
9
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
10
|
-
import Redis from 'ioredis';
|
|
11
|
-
import { nanoid } from 'nanoid';
|
|
12
|
-
|
|
13
|
-
async function run() {
|
|
14
|
-
const connection = await Durable.Connection.connect({
|
|
15
|
-
class: Redis,
|
|
16
|
-
options: {
|
|
17
|
-
host: 'localhost',
|
|
18
|
-
port: 6379,
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const client = new Durable.Client({
|
|
23
|
-
connection,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const handle = await client.workflow.start(example, {
|
|
27
|
-
taskQueue: 'hello-world',
|
|
28
|
-
args: ['HotMesh'],
|
|
29
|
-
workflowName: 'example',
|
|
30
|
-
workflowId: nanoid(),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
console.log(`Started workflow ${handle.workflowId}`);
|
|
34
|
-
console.log(await handle.result());
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
run().catch((err) => {
|
|
38
|
-
console.error(err);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
*/
|
|
43
4
|
class ConnectionService {
|
|
44
5
|
static async connect(config) {
|
|
45
6
|
return {
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* NOTE: Using `maxSystemRetries = 3` and `backoffCoefficient = 10`, errant
|
|
3
3
|
* workflows will be retried on the following schedule (8 times in 27 hours):
|
|
4
4
|
* => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
|
|
5
|
+
* TODO: Max Interval, Min Interval, Initial Interval
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* ERROR CODES:
|
|
8
|
+
* 594: waitforsignal
|
|
9
|
+
* 595: sleep
|
|
10
|
+
* 596, 597, 598: fatal
|
|
11
|
+
* 599: retry
|
|
10
12
|
*/
|
|
11
13
|
declare const getWorkflowYAML: (app: string, version: string) => string;
|
|
12
14
|
declare const APP_VERSION = "1";
|
|
@@ -5,11 +5,13 @@ exports.DEFAULT_COEFFICIENT = exports.APP_ID = exports.APP_VERSION = exports.get
|
|
|
5
5
|
* NOTE: Using `maxSystemRetries = 3` and `backoffCoefficient = 10`, errant
|
|
6
6
|
* workflows will be retried on the following schedule (8 times in 27 hours):
|
|
7
7
|
* => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
|
|
8
|
+
* TODO: Max Interval, Min Interval, Initial Interval
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* ERROR CODES:
|
|
11
|
+
* 594: waitforsignal
|
|
12
|
+
* 595: sleep
|
|
13
|
+
* 596, 597, 598: fatal
|
|
14
|
+
* 599: retry
|
|
13
15
|
*/
|
|
14
16
|
const getWorkflowYAML = (app, version) => {
|
|
15
17
|
return `app:
|
|
@@ -5,5 +5,8 @@ export declare class WorkflowHandleService {
|
|
|
5
5
|
workflowId: string;
|
|
6
6
|
constructor(hotMesh: HotMesh, workflowTopic: string, workflowId: string);
|
|
7
7
|
signal(signalId: string, data: Record<any, any>): Promise<void>;
|
|
8
|
+
state(metadata?: boolean): Promise<Record<string, any>>;
|
|
9
|
+
queryState(fields: string[]): Promise<Record<string, any>>;
|
|
10
|
+
status(): Promise<number>;
|
|
8
11
|
result(loadState?: boolean): Promise<any>;
|
|
9
12
|
}
|
|
@@ -10,6 +10,19 @@ class WorkflowHandleService {
|
|
|
10
10
|
async signal(signalId, data) {
|
|
11
11
|
await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
|
|
12
12
|
}
|
|
13
|
+
async state(metadata = false) {
|
|
14
|
+
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
15
|
+
if (!state.data && state.metadata.err) {
|
|
16
|
+
throw new Error(JSON.parse(state.metadata.err));
|
|
17
|
+
}
|
|
18
|
+
return metadata ? state : state.data;
|
|
19
|
+
}
|
|
20
|
+
async queryState(fields) {
|
|
21
|
+
return await this.hotMesh.getQueryState(this.workflowId, fields);
|
|
22
|
+
}
|
|
23
|
+
async status() {
|
|
24
|
+
return await this.hotMesh.getStatus(this.workflowId);
|
|
25
|
+
}
|
|
13
26
|
async result(loadState) {
|
|
14
27
|
if (loadState) {
|
|
15
28
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
@@ -20,8 +33,7 @@ class WorkflowHandleService {
|
|
|
20
33
|
//child flows are never technically 'done' as they have an open hook
|
|
21
34
|
//that is tied to the parent flow's completion. so, we need to check
|
|
22
35
|
//the 'done' flag on the child flow's payload (not the 'js' metadata field
|
|
23
|
-
//which is typically used); the loadState parameter
|
|
24
|
-
//check happens early
|
|
36
|
+
//which is typically used); the `loadState` parameter triggers this
|
|
25
37
|
return state.data.response;
|
|
26
38
|
}
|
|
27
39
|
}
|
|
@@ -1,56 +1,13 @@
|
|
|
1
1
|
import { ClientService } from './client';
|
|
2
2
|
import { ConnectionService } from './connection';
|
|
3
|
-
import {
|
|
3
|
+
import { MeshDBService } from './meshdb';
|
|
4
4
|
import { WorkerService } from './worker';
|
|
5
5
|
import { WorkflowService } from './workflow';
|
|
6
6
|
import { ContextType } from '../../types/durable';
|
|
7
|
-
/**
|
|
8
|
-
* As a durable integration platform, HotMesh
|
|
9
|
-
* can model and emulate other durable systems
|
|
10
|
-
* (like Temporal). As you review the code in
|
|
11
|
-
* this file, note the following:
|
|
12
|
-
*
|
|
13
|
-
* 1) There is no central governing server.
|
|
14
|
-
* HotMesh is a client-side SDK that connects to Redis
|
|
15
|
-
* using CQRS principles to implicitly drive
|
|
16
|
-
* orchestrations using a headless quorum. Stream
|
|
17
|
-
* semantics guarantee that all events are
|
|
18
|
-
* processed by the quorum of connected clients.
|
|
19
|
-
*
|
|
20
|
-
* 2) Every developer-defined `workflow` function
|
|
21
|
-
* is assigned a HotMesh workflow (which runs in
|
|
22
|
-
* the background) to support it.
|
|
23
|
-
*
|
|
24
|
-
* If the HotMesh workflow is not yet defined,
|
|
25
|
-
* it will be deployed and activated on-the-fly.
|
|
26
|
-
* (The generated DAG will have one Trigger Activity
|
|
27
|
-
* and one Worker Activity.) The Worker Activity
|
|
28
|
-
* is configured to catch execution errors and
|
|
29
|
-
* return them, using a 'pending' status to
|
|
30
|
-
* indicate that the workflow is still running.
|
|
31
|
-
* It is possible for workflow activities to throw
|
|
32
|
-
* errors that will force the entire workflow to
|
|
33
|
-
* fail. This is not the case here. The workflow will
|
|
34
|
-
* continue to run until it is completed or cancelled.
|
|
35
|
-
*
|
|
36
|
-
* 2) Every developer-defined `activity` function
|
|
37
|
-
* is assigned a HotMesh workflow (which runs in
|
|
38
|
-
* the background) to support it.
|
|
39
|
-
*
|
|
40
|
-
* (The generated DAG will have one Trigger Activity and one
|
|
41
|
-
* Worker Activity.) The JOB ID for Worker Activity executions
|
|
42
|
-
* is derived from the containing JOB ID,
|
|
43
|
-
* allowing Activity state to be 'replayed' when the workflow
|
|
44
|
-
* is run again. The Activity Function Runner (activity proxy)
|
|
45
|
-
* is configured similar to how the workflow worker is and will
|
|
46
|
-
* catch execution errors and return them to the caller, using a
|
|
47
|
-
* 'pending' status to indicate that the activity is still running.
|
|
48
|
-
* This allows the activity to be retried until it succeeds.
|
|
49
|
-
*/
|
|
50
7
|
export declare const Durable: {
|
|
51
8
|
Client: typeof ClientService;
|
|
52
9
|
Connection: typeof ConnectionService;
|
|
53
|
-
|
|
10
|
+
MeshDB: typeof MeshDBService;
|
|
54
11
|
Worker: typeof WorkerService;
|
|
55
12
|
workflow: typeof WorkflowService;
|
|
56
13
|
};
|
|
@@ -3,56 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Durable = void 0;
|
|
4
4
|
const client_1 = require("./client");
|
|
5
5
|
const connection_1 = require("./connection");
|
|
6
|
-
const
|
|
6
|
+
const meshdb_1 = require("./meshdb");
|
|
7
7
|
const worker_1 = require("./worker");
|
|
8
8
|
const workflow_1 = require("./workflow");
|
|
9
|
-
/**
|
|
10
|
-
* As a durable integration platform, HotMesh
|
|
11
|
-
* can model and emulate other durable systems
|
|
12
|
-
* (like Temporal). As you review the code in
|
|
13
|
-
* this file, note the following:
|
|
14
|
-
*
|
|
15
|
-
* 1) There is no central governing server.
|
|
16
|
-
* HotMesh is a client-side SDK that connects to Redis
|
|
17
|
-
* using CQRS principles to implicitly drive
|
|
18
|
-
* orchestrations using a headless quorum. Stream
|
|
19
|
-
* semantics guarantee that all events are
|
|
20
|
-
* processed by the quorum of connected clients.
|
|
21
|
-
*
|
|
22
|
-
* 2) Every developer-defined `workflow` function
|
|
23
|
-
* is assigned a HotMesh workflow (which runs in
|
|
24
|
-
* the background) to support it.
|
|
25
|
-
*
|
|
26
|
-
* If the HotMesh workflow is not yet defined,
|
|
27
|
-
* it will be deployed and activated on-the-fly.
|
|
28
|
-
* (The generated DAG will have one Trigger Activity
|
|
29
|
-
* and one Worker Activity.) The Worker Activity
|
|
30
|
-
* is configured to catch execution errors and
|
|
31
|
-
* return them, using a 'pending' status to
|
|
32
|
-
* indicate that the workflow is still running.
|
|
33
|
-
* It is possible for workflow activities to throw
|
|
34
|
-
* errors that will force the entire workflow to
|
|
35
|
-
* fail. This is not the case here. The workflow will
|
|
36
|
-
* continue to run until it is completed or cancelled.
|
|
37
|
-
*
|
|
38
|
-
* 2) Every developer-defined `activity` function
|
|
39
|
-
* is assigned a HotMesh workflow (which runs in
|
|
40
|
-
* the background) to support it.
|
|
41
|
-
*
|
|
42
|
-
* (The generated DAG will have one Trigger Activity and one
|
|
43
|
-
* Worker Activity.) The JOB ID for Worker Activity executions
|
|
44
|
-
* is derived from the containing JOB ID,
|
|
45
|
-
* allowing Activity state to be 'replayed' when the workflow
|
|
46
|
-
* is run again. The Activity Function Runner (activity proxy)
|
|
47
|
-
* is configured similar to how the workflow worker is and will
|
|
48
|
-
* catch execution errors and return them to the caller, using a
|
|
49
|
-
* 'pending' status to indicate that the activity is still running.
|
|
50
|
-
* This allows the activity to be retried until it succeeds.
|
|
51
|
-
*/
|
|
52
9
|
exports.Durable = {
|
|
53
10
|
Client: client_1.ClientService,
|
|
54
11
|
Connection: connection_1.ConnectionService,
|
|
55
|
-
|
|
12
|
+
MeshDB: meshdb_1.MeshDBService,
|
|
56
13
|
Worker: worker_1.WorkerService,
|
|
57
14
|
workflow: workflow_1.WorkflowService,
|
|
58
15
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { WorkflowHandleService } from './handle';
|
|
2
|
+
import { WorkflowSearchOptions } from '../../types/durable';
|
|
3
|
+
import { RedisOptions, RedisClass } from '../../types/redis';
|
|
4
|
+
/**
|
|
5
|
+
* A base class for configuration and setup of
|
|
6
|
+
* a reentrant process database. Entities modeled as
|
|
7
|
+
* subclasses of this class will execute as reentrant
|
|
8
|
+
* processes with a 'main' execution thread and 'n'
|
|
9
|
+
* parallel hook threads.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* //RUN (start a workflow)
|
|
13
|
+
* const myInstance = new MeshDB('someIdempotentGuid');
|
|
14
|
+
* const handle = await myInstance.create(100);
|
|
15
|
+
* await handle.result(); //100
|
|
16
|
+
*
|
|
17
|
+
* //UPDATE (update a workflow)
|
|
18
|
+
* const result = await myInstance.decrement(11);
|
|
19
|
+
*/
|
|
20
|
+
export declare class MeshDBService {
|
|
21
|
+
/**
|
|
22
|
+
* The name of the main method. When this method
|
|
23
|
+
* is invoked/proxied, it is assumed that a new
|
|
24
|
+
* workflow instance is being created. In all other
|
|
25
|
+
* cases, the call is assumed to be a hook/update
|
|
26
|
+
*/
|
|
27
|
+
main: string;
|
|
28
|
+
/**
|
|
29
|
+
* The GUID for the workflow (assigned when created). This
|
|
30
|
+
* value should be idempotent and will be rejected if an
|
|
31
|
+
* instance is already running with the same id.
|
|
32
|
+
*/
|
|
33
|
+
id: string;
|
|
34
|
+
/**
|
|
35
|
+
* test value
|
|
36
|
+
*/
|
|
37
|
+
value: number;
|
|
38
|
+
/**
|
|
39
|
+
* The top-level Redis isolation. All workflow data is
|
|
40
|
+
* isolated within this namespace. Values should be
|
|
41
|
+
* lower-case with no spaces (e.g, 'staging', 'prod', 'test',
|
|
42
|
+
* 'routing-stagig', 'reporting-prod', etc.).
|
|
43
|
+
* 1) only url-safe values are allowed;
|
|
44
|
+
* 2) the 'a' symbol is reserved by HotMesh for indexing apps
|
|
45
|
+
*/
|
|
46
|
+
namespace: string;
|
|
47
|
+
/**
|
|
48
|
+
* The second-level isolation. Data is routed to workers
|
|
49
|
+
* that specify this task queue. Setting the task queue
|
|
50
|
+
* when the worker is created will ensure that the worker
|
|
51
|
+
* only receives messages destined for the queue. This
|
|
52
|
+
* allows callers to specify specific workers/containers
|
|
53
|
+
* for specific tasks. Only url-safe values are allowed.
|
|
54
|
+
*/
|
|
55
|
+
taskQueue: string;
|
|
56
|
+
/**
|
|
57
|
+
* The Redis connection options. NOTE: Redis and IORedis
|
|
58
|
+
* use different formats for their connection config.
|
|
59
|
+
*/
|
|
60
|
+
redisOptions: RedisOptions;
|
|
61
|
+
/**
|
|
62
|
+
* The Redis connection class. Import as follows
|
|
63
|
+
* within the base subclass as follows:
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* import Redis from 'ioredis';
|
|
67
|
+
* import * as Redis from 'redis';
|
|
68
|
+
*/
|
|
69
|
+
redisClass: RedisClass | null;
|
|
70
|
+
/**
|
|
71
|
+
* Configuration for the the Redis FT search index.
|
|
72
|
+
*/
|
|
73
|
+
search: WorkflowSearchOptions;
|
|
74
|
+
static getHotMeshClient(redisClass: RedisClass, redisOptions: RedisOptions, namespace: string, taskQueue: string): Promise<import("../hotmesh").HotMeshService>;
|
|
75
|
+
/**
|
|
76
|
+
* mints a new key, using the provided search prefix, ensuring
|
|
77
|
+
* new workflows are properly indexed
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
static mintGuid(): string;
|
|
81
|
+
/**
|
|
82
|
+
* Creates an FT search index
|
|
83
|
+
*/
|
|
84
|
+
static createIndex(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Initialize the worker(s) for the entity. This is a static
|
|
87
|
+
* method that allows for optional task Queue targeting.
|
|
88
|
+
* NOTE: Allow List may be optionally used
|
|
89
|
+
* @param {string} taskQueue
|
|
90
|
+
* @param {string[]} allowList
|
|
91
|
+
*/
|
|
92
|
+
static doWork(taskQueue?: string, allowList?: string[]): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* executes the redis FT search query
|
|
95
|
+
* @example '@_quantity:[89 89]'
|
|
96
|
+
* @param {any[]} args
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
static find(...args: string[]): Promise<string[] | [number]>;
|
|
100
|
+
/**
|
|
101
|
+
* returns the workflow handle (use the handle to call:
|
|
102
|
+
* `state`, `status`, `queryStatus`, and `result`)
|
|
103
|
+
* @param {string} id
|
|
104
|
+
* @returns {Promise<WorkflowHandleService>}
|
|
105
|
+
*/
|
|
106
|
+
static get(id: string): Promise<WorkflowHandleService>;
|
|
107
|
+
/**
|
|
108
|
+
* Initialize with an idempotent workflow identifier.
|
|
109
|
+
* Optionally include a target taskQueue to send
|
|
110
|
+
* events to a specific worker.
|
|
111
|
+
*/
|
|
112
|
+
constructor(id?: string, taskQueue?: string);
|
|
113
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MeshDBService = void 0;
|
|
4
|
+
const nanoid_1 = require("nanoid");
|
|
5
|
+
const client_1 = require("./client");
|
|
6
|
+
const search_1 = require("./search");
|
|
7
|
+
const worker_1 = require("./worker");
|
|
8
|
+
/**
|
|
9
|
+
* A base class for configuration and setup of
|
|
10
|
+
* a reentrant process database. Entities modeled as
|
|
11
|
+
* subclasses of this class will execute as reentrant
|
|
12
|
+
* processes with a 'main' execution thread and 'n'
|
|
13
|
+
* parallel hook threads.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* //RUN (start a workflow)
|
|
17
|
+
* const myInstance = new MeshDB('someIdempotentGuid');
|
|
18
|
+
* const handle = await myInstance.create(100);
|
|
19
|
+
* await handle.result(); //100
|
|
20
|
+
*
|
|
21
|
+
* //UPDATE (update a workflow)
|
|
22
|
+
* const result = await myInstance.decrement(11);
|
|
23
|
+
*/
|
|
24
|
+
class MeshDBService {
|
|
25
|
+
static async getHotMeshClient(redisClass, redisOptions, namespace, taskQueue) {
|
|
26
|
+
const client = new client_1.ClientService({
|
|
27
|
+
connection: {
|
|
28
|
+
class: redisClass,
|
|
29
|
+
options: redisOptions,
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return await client.getHotMeshClient(taskQueue, namespace);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* mints a new key, using the provided search prefix, ensuring
|
|
36
|
+
* new workflows are properly indexed
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
static mintGuid() {
|
|
40
|
+
const my = new this();
|
|
41
|
+
return `${my.search?.prefix?.[0]}${(0, nanoid_1.nanoid)()}}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates an FT search index
|
|
45
|
+
*/
|
|
46
|
+
static async createIndex() {
|
|
47
|
+
const my = new this();
|
|
48
|
+
const hmClient = await MeshDBService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
|
|
49
|
+
search_1.Search.configureSearchIndex(hmClient, my.search);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the worker(s) for the entity. This is a static
|
|
53
|
+
* method that allows for optional task Queue targeting.
|
|
54
|
+
* NOTE: Allow List may be optionally used
|
|
55
|
+
* @param {string} taskQueue
|
|
56
|
+
* @param {string[]} allowList
|
|
57
|
+
*/
|
|
58
|
+
static async doWork(taskQueue, allowList) {
|
|
59
|
+
const my = new this();
|
|
60
|
+
let prototype = Object.getPrototypeOf(my);
|
|
61
|
+
const durablePromises = [];
|
|
62
|
+
const found = [];
|
|
63
|
+
while (prototype !== null && !Object.getOwnPropertyNames(prototype).includes('__proto__')) {
|
|
64
|
+
const promises = Object.getOwnPropertyNames(prototype).map((prop) => {
|
|
65
|
+
if (found.includes(prop) || ['constructor'].includes(prop) || (allowList && !allowList.includes(prop))) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const originalMethod = my[prop];
|
|
69
|
+
if (typeof originalMethod === 'function') {
|
|
70
|
+
found.push(prop);
|
|
71
|
+
return worker_1.WorkerService.create({
|
|
72
|
+
namespace: my.namespace,
|
|
73
|
+
connection: {
|
|
74
|
+
class: my.redisClass,
|
|
75
|
+
options: my.redisOptions,
|
|
76
|
+
},
|
|
77
|
+
taskQueue: taskQueue ?? my.taskQueue,
|
|
78
|
+
workflow: originalMethod,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}).filter(p => p !== undefined); // filter out undefined values
|
|
82
|
+
durablePromises.push(...promises);
|
|
83
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(durablePromises);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* executes the redis FT search query
|
|
89
|
+
* @example '@_quantity:[89 89]'
|
|
90
|
+
* @param {any[]} args
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
static async find(...args) {
|
|
94
|
+
const my = new this();
|
|
95
|
+
const client = new client_1.ClientService({ connection: {
|
|
96
|
+
class: my.redisClass,
|
|
97
|
+
options: my.redisOptions
|
|
98
|
+
} });
|
|
99
|
+
return await client.workflow.search(my.taskQueue, my.main, my.namespace, my.search.index, ...args);
|
|
100
|
+
//[count, [id, fields[], id, fields[], id, fields[], ...]]
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* returns the workflow handle (use the handle to call:
|
|
104
|
+
* `state`, `status`, `queryStatus`, and `result`)
|
|
105
|
+
* @param {string} id
|
|
106
|
+
* @returns {Promise<WorkflowHandleService>}
|
|
107
|
+
*/
|
|
108
|
+
static async get(id) {
|
|
109
|
+
const my = new this();
|
|
110
|
+
const client = new client_1.ClientService({ connection: {
|
|
111
|
+
class: my.redisClass,
|
|
112
|
+
options: my.redisOptions
|
|
113
|
+
} });
|
|
114
|
+
return await client.workflow.getHandle(my.taskQueue, my.main, id, my.namespace);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Initialize with an idempotent workflow identifier.
|
|
118
|
+
* Optionally include a target taskQueue to send
|
|
119
|
+
* events to a specific worker.
|
|
120
|
+
*/
|
|
121
|
+
constructor(id, taskQueue) {
|
|
122
|
+
/**
|
|
123
|
+
* The name of the main method. When this method
|
|
124
|
+
* is invoked/proxied, it is assumed that a new
|
|
125
|
+
* workflow instance is being created. In all other
|
|
126
|
+
* cases, the call is assumed to be a hook/update
|
|
127
|
+
*/
|
|
128
|
+
this.main = 'create';
|
|
129
|
+
/**
|
|
130
|
+
* The top-level Redis isolation. All workflow data is
|
|
131
|
+
* isolated within this namespace. Values should be
|
|
132
|
+
* lower-case with no spaces (e.g, 'staging', 'prod', 'test',
|
|
133
|
+
* 'routing-stagig', 'reporting-prod', etc.).
|
|
134
|
+
* 1) only url-safe values are allowed;
|
|
135
|
+
* 2) the 'a' symbol is reserved by HotMesh for indexing apps
|
|
136
|
+
*/
|
|
137
|
+
this.namespace = 'durable';
|
|
138
|
+
/**
|
|
139
|
+
* The second-level isolation. Data is routed to workers
|
|
140
|
+
* that specify this task queue. Setting the task queue
|
|
141
|
+
* when the worker is created will ensure that the worker
|
|
142
|
+
* only receives messages destined for the queue. This
|
|
143
|
+
* allows callers to specify specific workers/containers
|
|
144
|
+
* for specific tasks. Only url-safe values are allowed.
|
|
145
|
+
*/
|
|
146
|
+
this.taskQueue = 'default';
|
|
147
|
+
/**
|
|
148
|
+
* The Redis connection options. NOTE: Redis and IORedis
|
|
149
|
+
* use different formats for their connection config.
|
|
150
|
+
*/
|
|
151
|
+
this.redisOptions = {
|
|
152
|
+
host: 'localhost',
|
|
153
|
+
port: 6379,
|
|
154
|
+
password: '',
|
|
155
|
+
db: 0,
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* The Redis connection class. Import as follows
|
|
159
|
+
* within the base subclass as follows:
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* import Redis from 'ioredis';
|
|
163
|
+
* import * as Redis from 'redis';
|
|
164
|
+
*/
|
|
165
|
+
this.redisClass = null;
|
|
166
|
+
this.id = id;
|
|
167
|
+
if (taskQueue) {
|
|
168
|
+
this.taskQueue = taskQueue;
|
|
169
|
+
}
|
|
170
|
+
else if (!id && !taskQueue) {
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
return new Proxy(this, {
|
|
174
|
+
get: (target, prop, receiver) => {
|
|
175
|
+
if (typeof target[prop] === 'function') {
|
|
176
|
+
return (...args) => {
|
|
177
|
+
return new Promise(async (resolve, reject) => {
|
|
178
|
+
const client = new client_1.ClientService({ connection: {
|
|
179
|
+
class: this.redisClass,
|
|
180
|
+
options: this.redisOptions
|
|
181
|
+
} });
|
|
182
|
+
if (prop === this.main) {
|
|
183
|
+
//start a new workflow (main method was called)
|
|
184
|
+
return client.workflow.start({
|
|
185
|
+
namespace: this.namespace,
|
|
186
|
+
args,
|
|
187
|
+
taskQueue: this.taskQueue,
|
|
188
|
+
workflowName: prop,
|
|
189
|
+
workflowId: this.id,
|
|
190
|
+
}).then(resolve).catch(reject);
|
|
191
|
+
}
|
|
192
|
+
else if (prop !== 'constructor') {
|
|
193
|
+
//update an existing workflow (hook/signal-in)
|
|
194
|
+
return client.workflow.hook({
|
|
195
|
+
namespace: this.namespace,
|
|
196
|
+
taskQueue: this.taskQueue,
|
|
197
|
+
workflowName: prop,
|
|
198
|
+
workflowId: this.id,
|
|
199
|
+
args,
|
|
200
|
+
}).then(resolve).catch(reject);
|
|
201
|
+
}
|
|
202
|
+
target[prop].apply(this, args).then(resolve).catch(reject);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return Reflect.get(target, prop, receiver);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
exports.MeshDBService = MeshDBService;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
2
2
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
|
+
import { WorkflowSearchOptions } from '../../types/durable';
|
|
4
5
|
export declare class Search {
|
|
5
6
|
jobId: string;
|
|
6
7
|
searchSessionId: string;
|
|
@@ -8,6 +9,14 @@ export declare class Search {
|
|
|
8
9
|
hotMeshClient: HotMesh;
|
|
9
10
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
10
11
|
safeKey(key: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
14
|
+
* this method will configure the search index for the workflow. For all
|
|
15
|
+
* others, this method will exit/fail gracefully and not index
|
|
16
|
+
* the fields in the HASH. However, all values are still available
|
|
17
|
+
* in the HASH.
|
|
18
|
+
*/
|
|
19
|
+
static configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void>;
|
|
11
20
|
constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string);
|
|
12
21
|
/**
|
|
13
22
|
* increments the index to return a unique search session guid when
|
|
@@ -4,10 +4,43 @@ exports.Search = void 0;
|
|
|
4
4
|
const key_1 = require("../../modules/key");
|
|
5
5
|
class Search {
|
|
6
6
|
safeKey(key) {
|
|
7
|
-
//note: protect the execution namespace with a prefix
|
|
8
|
-
//so its design never conflicts with the hotmesh keyspace
|
|
7
|
+
//note: protect the execution namespace with a prefix
|
|
9
8
|
return `_${key}`;
|
|
10
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
12
|
+
* this method will configure the search index for the workflow. For all
|
|
13
|
+
* others, this method will exit/fail gracefully and not index
|
|
14
|
+
* the fields in the HASH. However, all values are still available
|
|
15
|
+
* in the HASH.
|
|
16
|
+
*/
|
|
17
|
+
static async configureSearchIndex(hotMeshClient, search) {
|
|
18
|
+
if (search?.schema) {
|
|
19
|
+
const store = hotMeshClient.engine.store;
|
|
20
|
+
const schema = [];
|
|
21
|
+
for (const [key, value] of Object.entries(search.schema)) {
|
|
22
|
+
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
23
|
+
schema.push(`_${key}`);
|
|
24
|
+
schema.push(value.type);
|
|
25
|
+
if (value.sortable) {
|
|
26
|
+
schema.push('SORTABLE');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const keyParams = {
|
|
31
|
+
appId: hotMeshClient.appId,
|
|
32
|
+
jobId: ''
|
|
33
|
+
};
|
|
34
|
+
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
35
|
+
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
36
|
+
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length.toString(), ...prefixes, 'SCHEMA', ...schema);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(err);
|
|
40
|
+
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
constructor(workflowId, hotMeshClient, searchSessionId) {
|
|
12
45
|
this.searchSessionIndex = 0;
|
|
13
46
|
const keyParams = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
2
|
-
import { Connection, Registry, WorkerConfig, WorkerOptions
|
|
2
|
+
import { Connection, Registry, WorkerConfig, WorkerOptions } from '../../types/durable';
|
|
3
3
|
export declare class WorkerService {
|
|
4
4
|
static activityRegistry: Registry;
|
|
5
5
|
static connection: Connection;
|
|
@@ -9,14 +9,6 @@ export declare class WorkerService {
|
|
|
9
9
|
static getHotMesh: (workflowTopic: string, config?: Partial<WorkerConfig>, options?: WorkerOptions) => Promise<HotMesh>;
|
|
10
10
|
static activateWorkflow(hotMesh: HotMesh): Promise<void>;
|
|
11
11
|
static registerActivities<ACT>(activities: ACT): Registry;
|
|
12
|
-
/**
|
|
13
|
-
* For those deployments with a redis stack backend (with the FT module),
|
|
14
|
-
* this method will configure the search index for the workflow. For all
|
|
15
|
-
* others, this method will fail gracefully. In all cases, the values
|
|
16
|
-
* will be stored in the workflow's central HASH data structure, allowing
|
|
17
|
-
* for manual traversal and inspection as well.
|
|
18
|
-
*/
|
|
19
|
-
static configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void>;
|
|
20
12
|
static create(config: WorkerConfig): Promise<WorkerService>;
|
|
21
13
|
static resolveWorkflowTarget(workflow: object | Function): [string, Function];
|
|
22
14
|
run(): Promise<void>;
|