@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
|
@@ -3,10 +3,10 @@ var _a;
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.WorkerService = void 0;
|
|
5
5
|
const errors_1 = require("../../modules/errors");
|
|
6
|
-
const key_1 = require("../../modules/key");
|
|
7
6
|
const asyncLocalStorage_1 = require("./asyncLocalStorage");
|
|
8
7
|
const factory_1 = require("./factory");
|
|
9
8
|
const hotmesh_1 = require("../hotmesh");
|
|
9
|
+
const search_1 = require("./search");
|
|
10
10
|
const stream_1 = require("../../types/stream");
|
|
11
11
|
class WorkerService {
|
|
12
12
|
static async activateWorkflow(hotMesh) {
|
|
@@ -45,39 +45,6 @@ class WorkerService {
|
|
|
45
45
|
}
|
|
46
46
|
return WorkerService.activityRegistry;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* For those deployments with a redis stack backend (with the FT module),
|
|
50
|
-
* this method will configure the search index for the workflow. For all
|
|
51
|
-
* others, this method will fail gracefully. In all cases, the values
|
|
52
|
-
* will be stored in the workflow's central HASH data structure, allowing
|
|
53
|
-
* for manual traversal and inspection as well.
|
|
54
|
-
*/
|
|
55
|
-
static async configureSearchIndex(hotMeshClient, search) {
|
|
56
|
-
if (search?.schema) {
|
|
57
|
-
const store = hotMeshClient.engine.store;
|
|
58
|
-
const schema = [];
|
|
59
|
-
for (const [key, value] of Object.entries(search.schema)) {
|
|
60
|
-
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
61
|
-
schema.push(`_${key}`);
|
|
62
|
-
schema.push(value.type);
|
|
63
|
-
if (value.sortable) {
|
|
64
|
-
schema.push('SORTABLE');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
const keyParams = {
|
|
69
|
-
appId: hotMeshClient.appId,
|
|
70
|
-
jobId: ''
|
|
71
|
-
};
|
|
72
|
-
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
73
|
-
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
74
|
-
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
48
|
static async create(config) {
|
|
82
49
|
WorkerService.connection = config.connection;
|
|
83
50
|
const workflow = config.workflow;
|
|
@@ -89,7 +56,7 @@ class WorkerService {
|
|
|
89
56
|
const worker = new WorkerService();
|
|
90
57
|
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
91
58
|
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
92
|
-
|
|
59
|
+
search_1.Search.configureSearchIndex(worker.workflowRunner, config.search);
|
|
93
60
|
await WorkerService.activateWorkflow(worker.workflowRunner);
|
|
94
61
|
return worker;
|
|
95
62
|
}
|
|
@@ -10,11 +10,18 @@ export declare class WorkflowService {
|
|
|
10
10
|
* spawn a child workflow. return the childJobId.
|
|
11
11
|
*/
|
|
12
12
|
static startChild<T>(options: WorkflowOptions): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* wrap all activities in a proxy that will durably run them
|
|
15
|
+
*/
|
|
13
16
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
|
|
17
|
+
/**
|
|
18
|
+
* return a search session for use when reading/writing to the workflow HASH
|
|
19
|
+
*/
|
|
14
20
|
static search(): Promise<Search>;
|
|
15
21
|
/**
|
|
16
22
|
* those methods that may only be called once must be protected by flagging
|
|
17
|
-
* their execution with a unique key (the key is stored in the
|
|
23
|
+
* their execution with a unique key (the key is stored in the HASH alongside
|
|
24
|
+
* process state and job state)
|
|
18
25
|
*/
|
|
19
26
|
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
20
27
|
/**
|
|
@@ -89,6 +89,9 @@ class WorkflowService {
|
|
|
89
89
|
return childJobId;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* wrap all activities in a proxy that will durably run them
|
|
94
|
+
*/
|
|
92
95
|
static proxyActivities(options) {
|
|
93
96
|
if (options.activities) {
|
|
94
97
|
worker_1.WorkerService.registerActivities(options.activities);
|
|
@@ -103,6 +106,9 @@ class WorkflowService {
|
|
|
103
106
|
}
|
|
104
107
|
return proxy;
|
|
105
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* return a search session for use when reading/writing to the workflow HASH
|
|
111
|
+
*/
|
|
106
112
|
static async search() {
|
|
107
113
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
108
114
|
const workflowId = store.get('workflowId');
|
|
@@ -111,14 +117,15 @@ class WorkflowService {
|
|
|
111
117
|
const namespace = store.get('namespace');
|
|
112
118
|
const COUNTER = store.get('counter');
|
|
113
119
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
114
|
-
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
115
120
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
121
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
116
122
|
const searchSessionId = `-search${workflowDimension}-${execIndex}`;
|
|
117
123
|
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
118
124
|
}
|
|
119
125
|
/**
|
|
120
126
|
* those methods that may only be called once must be protected by flagging
|
|
121
|
-
* their execution with a unique key (the key is stored in the
|
|
127
|
+
* their execution with a unique key (the key is stored in the HASH alongside
|
|
128
|
+
* process state and job state)
|
|
122
129
|
*/
|
|
123
130
|
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
124
131
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
@@ -126,15 +133,12 @@ class WorkflowService {
|
|
|
126
133
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
127
134
|
const COUNTER = store.get('counter');
|
|
128
135
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
129
|
-
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
130
136
|
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
131
|
-
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
132
137
|
const keyParams = {
|
|
133
138
|
appId: hotMeshClient.appId,
|
|
134
|
-
jobId:
|
|
139
|
+
jobId: workflowId
|
|
135
140
|
};
|
|
136
|
-
const
|
|
137
|
-
const workflowGuid = `${hotMeshPrefix}${workflowId}`;
|
|
141
|
+
const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
138
142
|
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
|
|
139
143
|
return guidValue === 1;
|
|
140
144
|
}
|
|
@@ -18,6 +18,7 @@ import { JobState, JobData, JobMetadata, JobOutput, JobStatus } from '../../type
|
|
|
18
18
|
import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '../../types/hotmesh';
|
|
19
19
|
import { JobMessageCallback } from '../../types/quorum';
|
|
20
20
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
21
|
+
import { StringAnyType } from '../../types/serializer';
|
|
21
22
|
import { GetStatsOptions, IdsResponse, JobStatsInput, StatsResponse } from '../../types/stats';
|
|
22
23
|
import { StreamCode, StreamData, StreamDataResponse, StreamError, StreamStatus } from '../../types/stream';
|
|
23
24
|
declare class EngineService {
|
|
@@ -82,6 +83,7 @@ declare class EngineService {
|
|
|
82
83
|
runJobCompletionTasks(context: JobState, emit?: boolean): Promise<void>;
|
|
83
84
|
getStatus(jobId: string): Promise<JobStatus>;
|
|
84
85
|
getState(topic: string, jobId: string): Promise<JobOutput>;
|
|
86
|
+
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
85
87
|
compress(terms: string[]): Promise<boolean>;
|
|
86
88
|
}
|
|
87
89
|
export { EngineService };
|
|
@@ -523,6 +523,9 @@ class EngineService {
|
|
|
523
523
|
}
|
|
524
524
|
return stateTree;
|
|
525
525
|
}
|
|
526
|
+
async getQueryState(jobId, fields) {
|
|
527
|
+
return await this.store.getQueryState(jobId, fields);
|
|
528
|
+
}
|
|
526
529
|
async compress(terms) {
|
|
527
530
|
const existingSymbols = await this.store.getSymbolValues();
|
|
528
531
|
const startIndex = Object.keys(existingSymbols).length;
|
|
@@ -7,6 +7,7 @@ import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
|
|
|
7
7
|
import { JobMessageCallback } from '../../types/quorum';
|
|
8
8
|
import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
|
|
9
9
|
import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
|
|
10
|
+
import { StringAnyType } from '../../types/serializer';
|
|
10
11
|
declare class HotMeshService {
|
|
11
12
|
namespace: string;
|
|
12
13
|
appId: string;
|
|
@@ -21,7 +22,7 @@ declare class HotMeshService {
|
|
|
21
22
|
static guid(): string;
|
|
22
23
|
initEngine(config: HotMeshConfig, logger: ILogger): Promise<void>;
|
|
23
24
|
initQuorum(config: HotMeshConfig, engine: EngineService, logger: ILogger): Promise<void>;
|
|
24
|
-
|
|
25
|
+
doWork(config: HotMeshConfig, logger: ILogger): Promise<void>;
|
|
25
26
|
pub(topic: string, data?: JobData, context?: JobState): Promise<string>;
|
|
26
27
|
sub(topic: string, callback: JobMessageCallback): Promise<void>;
|
|
27
28
|
unsub(topic: string): Promise<void>;
|
|
@@ -35,6 +36,7 @@ declare class HotMeshService {
|
|
|
35
36
|
getStats(topic: string, query: JobStatsInput): Promise<StatsResponse>;
|
|
36
37
|
getStatus(jobId: string): Promise<JobStatus>;
|
|
37
38
|
getState(topic: string, jobId: string): Promise<JobOutput>;
|
|
39
|
+
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
38
40
|
getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
|
|
39
41
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
40
42
|
scrub(jobId: string): Promise<void>;
|
|
@@ -45,7 +45,7 @@ class HotMeshService {
|
|
|
45
45
|
instance.logger = new logger_1.LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
|
|
46
46
|
await instance.initEngine(config, instance.logger);
|
|
47
47
|
await instance.initQuorum(config, instance.engine, instance.logger);
|
|
48
|
-
await instance.
|
|
48
|
+
await instance.doWork(config, instance.logger);
|
|
49
49
|
return instance;
|
|
50
50
|
}
|
|
51
51
|
static guid() {
|
|
@@ -62,7 +62,7 @@ class HotMeshService {
|
|
|
62
62
|
this.quorum = await quorum_1.QuorumService.init(this.namespace, this.appId, this.guid, config, engine, logger);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
async
|
|
65
|
+
async doWork(config, logger) {
|
|
66
66
|
this.workers = await worker_1.WorkerService.init(this.namespace, this.appId, this.guid, config, logger);
|
|
67
67
|
}
|
|
68
68
|
// ************* PUB/SUB METHODS *************
|
|
@@ -108,6 +108,9 @@ class HotMeshService {
|
|
|
108
108
|
async getState(topic, jobId) {
|
|
109
109
|
return this.engine?.getState(topic, jobId);
|
|
110
110
|
}
|
|
111
|
+
async getQueryState(jobId, fields) {
|
|
112
|
+
return await this.engine?.getQueryState(jobId, fields);
|
|
113
|
+
}
|
|
111
114
|
async getIds(topic, query, queryFacets = []) {
|
|
112
115
|
return await this.engine?.getIds(topic, query, queryFacets);
|
|
113
116
|
}
|
|
@@ -61,6 +61,11 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
61
61
|
setStatus(collationKeyStatus: number, jobId: string, appId: string, multi?: U): Promise<any>;
|
|
62
62
|
getStatus(jobId: string, appId: string): Promise<number>;
|
|
63
63
|
setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi?: U): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* returns custom search fields and values. The fields param
|
|
66
|
+
* should not prefix items with an underscore.
|
|
67
|
+
*/
|
|
68
|
+
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
64
69
|
getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
|
|
65
70
|
collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi?: U): Promise<number>;
|
|
66
71
|
setStateNX(jobId: string, appId: string): Promise<boolean>;
|
|
@@ -394,6 +394,20 @@ class StoreService {
|
|
|
394
394
|
await (multi || this.redisClient)[this.commands.hset](hashKey, hashData);
|
|
395
395
|
return jobId;
|
|
396
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* returns custom search fields and values. The fields param
|
|
399
|
+
* should not prefix items with an underscore.
|
|
400
|
+
*/
|
|
401
|
+
async getQueryState(jobId, fields) {
|
|
402
|
+
const key = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
403
|
+
const _fields = fields.map(field => `_${field}`);
|
|
404
|
+
const jobDataArray = await this.redisClient[this.commands.hmget](key, _fields);
|
|
405
|
+
const jobData = {};
|
|
406
|
+
fields.forEach((field, index) => {
|
|
407
|
+
jobData[field] = jobDataArray[index];
|
|
408
|
+
});
|
|
409
|
+
return jobData;
|
|
410
|
+
}
|
|
397
411
|
async getState(jobId, consumes, dIds) {
|
|
398
412
|
//get abbreviated field list (the symbols for the paths)
|
|
399
413
|
const key = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
package/build/types/durable.d.ts
CHANGED
|
@@ -52,12 +52,28 @@ type WorkflowDataType = {
|
|
|
52
52
|
workflowId: string;
|
|
53
53
|
workflowTopic: string;
|
|
54
54
|
};
|
|
55
|
+
type MeshDBClassConfig = {
|
|
56
|
+
namespace: string;
|
|
57
|
+
taskQueue: string;
|
|
58
|
+
redisOptions: RedisOptions;
|
|
59
|
+
redisClass: RedisClass;
|
|
60
|
+
};
|
|
61
|
+
type MeshDBConfig = {
|
|
62
|
+
taskQueue?: string;
|
|
63
|
+
index?: {
|
|
64
|
+
index: string;
|
|
65
|
+
prefix: string[];
|
|
66
|
+
schema: Record<string, {
|
|
67
|
+
type: 'TEXT' | 'NUMERIC' | 'TAG';
|
|
68
|
+
sortable: boolean;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
55
72
|
type ConnectionConfig = {
|
|
56
73
|
class: RedisClass;
|
|
57
74
|
options: RedisOptions;
|
|
58
75
|
};
|
|
59
76
|
type Connection = ConnectionConfig;
|
|
60
|
-
type NativeConnection = ConnectionConfig;
|
|
61
77
|
type ClientConfig = {
|
|
62
78
|
connection: Connection;
|
|
63
79
|
};
|
|
@@ -95,4 +111,4 @@ type ActivityConfig = {
|
|
|
95
111
|
maximumInterval: string;
|
|
96
112
|
};
|
|
97
113
|
};
|
|
98
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection,
|
|
114
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, HookOptions, MeshDBClassConfig, MeshDBConfig, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
|
package/build/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
|
|
|
3
3
|
export { AsyncSignal } from './async';
|
|
4
4
|
export { CacheMode } from './cache';
|
|
5
5
|
export { CollationFaultType, CollationStage } from './collator';
|
|
6
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection,
|
|
6
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, HookOptions, MeshDBClassConfig, MeshDBConfig, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
7
7
|
export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
|
|
8
8
|
export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
|
|
9
9
|
export { ILogger } from './logger';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.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,5 @@
|
|
|
1
1
|
import { Connection, ConnectionConfig } from "../../types/durable";
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
Here is an example of how the methods in this file are used:
|
|
5
|
-
|
|
6
|
-
./client.ts
|
|
7
|
-
|
|
8
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
9
|
-
import Redis from 'ioredis';
|
|
10
|
-
import { nanoid } from 'nanoid';
|
|
11
|
-
|
|
12
|
-
async function run() {
|
|
13
|
-
const connection = await Durable.Connection.connect({
|
|
14
|
-
class: Redis,
|
|
15
|
-
options: {
|
|
16
|
-
host: 'localhost',
|
|
17
|
-
port: 6379,
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const client = new Durable.Client({
|
|
22
|
-
connection,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const handle = await client.workflow.start(example, {
|
|
26
|
-
taskQueue: 'hello-world',
|
|
27
|
-
args: ['HotMesh'],
|
|
28
|
-
workflowName: 'example',
|
|
29
|
-
workflowId: nanoid(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
console.log(`Started workflow ${handle.workflowId}`);
|
|
33
|
-
console.log(await handle.result());
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
run().catch((err) => {
|
|
37
|
-
console.error(err);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
3
|
export class ConnectionService {
|
|
44
4
|
static async connect(config: ConnectionConfig): Promise<Connection> {
|
|
45
5
|
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
|
const getWorkflowYAML = (app: string, version: string) => {
|
|
12
14
|
return `app:
|
|
@@ -16,6 +16,22 @@ export class WorkflowHandleService {
|
|
|
16
16
|
await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
async state(metadata = false): Promise<Record<string, any>> {
|
|
20
|
+
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
21
|
+
if (!state.data && state.metadata.err) {
|
|
22
|
+
throw new Error(JSON.parse(state.metadata.err));
|
|
23
|
+
}
|
|
24
|
+
return metadata ? state : state.data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async queryState(fields: string[]): Promise<Record<string, any>> {
|
|
28
|
+
return await this.hotMesh.getQueryState(this.workflowId, fields);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async status(): Promise<number> {
|
|
32
|
+
return await this.hotMesh.getStatus(this.workflowId);
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
async result(loadState?: boolean): Promise<any> {
|
|
20
36
|
if (loadState) {
|
|
21
37
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
@@ -26,8 +42,7 @@ export class WorkflowHandleService {
|
|
|
26
42
|
//child flows are never technically 'done' as they have an open hook
|
|
27
43
|
//that is tied to the parent flow's completion. so, we need to check
|
|
28
44
|
//the 'done' flag on the child flow's payload (not the 'js' metadata field
|
|
29
|
-
//which is typically used); the loadState parameter
|
|
30
|
-
//check happens early
|
|
45
|
+
//which is typically used); the `loadState` parameter triggers this
|
|
31
46
|
return state.data.response;
|
|
32
47
|
}
|
|
33
48
|
}
|
|
@@ -1,58 +1,14 @@
|
|
|
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
7
|
|
|
8
|
-
/**
|
|
9
|
-
* As a durable integration platform, HotMesh
|
|
10
|
-
* can model and emulate other durable systems
|
|
11
|
-
* (like Temporal). As you review the code in
|
|
12
|
-
* this file, note the following:
|
|
13
|
-
*
|
|
14
|
-
* 1) There is no central governing server.
|
|
15
|
-
* HotMesh is a client-side SDK that connects to Redis
|
|
16
|
-
* using CQRS principles to implicitly drive
|
|
17
|
-
* orchestrations using a headless quorum. Stream
|
|
18
|
-
* semantics guarantee that all events are
|
|
19
|
-
* processed by the quorum of connected clients.
|
|
20
|
-
*
|
|
21
|
-
* 2) Every developer-defined `workflow` function
|
|
22
|
-
* is assigned a HotMesh workflow (which runs in
|
|
23
|
-
* the background) to support it.
|
|
24
|
-
*
|
|
25
|
-
* If the HotMesh workflow is not yet defined,
|
|
26
|
-
* it will be deployed and activated on-the-fly.
|
|
27
|
-
* (The generated DAG will have one Trigger Activity
|
|
28
|
-
* and one Worker Activity.) The Worker Activity
|
|
29
|
-
* is configured to catch execution errors and
|
|
30
|
-
* return them, using a 'pending' status to
|
|
31
|
-
* indicate that the workflow is still running.
|
|
32
|
-
* It is possible for workflow activities to throw
|
|
33
|
-
* errors that will force the entire workflow to
|
|
34
|
-
* fail. This is not the case here. The workflow will
|
|
35
|
-
* continue to run until it is completed or cancelled.
|
|
36
|
-
*
|
|
37
|
-
* 2) Every developer-defined `activity` function
|
|
38
|
-
* is assigned a HotMesh workflow (which runs in
|
|
39
|
-
* the background) to support it.
|
|
40
|
-
*
|
|
41
|
-
* (The generated DAG will have one Trigger Activity and one
|
|
42
|
-
* Worker Activity.) The JOB ID for Worker Activity executions
|
|
43
|
-
* is derived from the containing JOB ID,
|
|
44
|
-
* allowing Activity state to be 'replayed' when the workflow
|
|
45
|
-
* is run again. The Activity Function Runner (activity proxy)
|
|
46
|
-
* is configured similar to how the workflow worker is and will
|
|
47
|
-
* catch execution errors and return them to the caller, using a
|
|
48
|
-
* 'pending' status to indicate that the activity is still running.
|
|
49
|
-
* This allows the activity to be retried until it succeeds.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
8
|
export const Durable = {
|
|
53
9
|
Client: ClientService,
|
|
54
10
|
Connection: ConnectionService,
|
|
55
|
-
|
|
11
|
+
MeshDB: MeshDBService,
|
|
56
12
|
Worker: WorkerService,
|
|
57
13
|
workflow: WorkflowService,
|
|
58
14
|
};
|