@hotmeshio/hotmesh 0.0.18 → 0.0.19
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/modules/utils.js +7 -0
- package/build/package.json +1 -1
- package/build/services/activities/hook.d.ts +2 -2
- package/build/services/activities/hook.js +4 -4
- package/build/services/durable/client.js +9 -41
- package/build/services/durable/worker.d.ts +6 -1
- package/build/services/durable/worker.js +34 -30
- package/build/services/durable/workflow.d.ts +2 -1
- package/build/services/durable/workflow.js +3 -54
- package/build/types/durable.d.ts +5 -3
- package/modules/utils.ts +6 -0
- package/package.json +1 -1
- package/services/activities/hook.ts +5 -5
- package/services/durable/client.ts +9 -42
- package/services/durable/worker.ts +36 -32
- package/services/durable/workflow.ts +3 -50
- package/types/durable.ts +5 -3
package/build/modules/utils.js
CHANGED
|
@@ -6,6 +6,13 @@ async function sleepFor(ms) {
|
|
|
6
6
|
}
|
|
7
7
|
exports.sleepFor = sleepFor;
|
|
8
8
|
function identifyRedisType(redisInstance) {
|
|
9
|
+
const prototype = Object.getPrototypeOf(redisInstance);
|
|
10
|
+
if ('defineCommand' in prototype || Object.keys(prototype).includes('multi')) {
|
|
11
|
+
return 'ioredis';
|
|
12
|
+
}
|
|
13
|
+
else if (Object.keys(prototype).includes('Multi')) {
|
|
14
|
+
return 'redis';
|
|
15
|
+
}
|
|
9
16
|
if (redisInstance.constructor) {
|
|
10
17
|
if (redisInstance.constructor.name === 'Redis' || redisInstance.constructor.name === 'EventEmitter') {
|
|
11
18
|
if ('hset' in redisInstance) {
|
package/build/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { Activity } from './activity';
|
|
1
2
|
import { EngineService } from '../engine';
|
|
2
3
|
import { ActivityData, ActivityMetadata, ActivityType, HookActivity } from '../../types/activity';
|
|
4
|
+
import { HookRule } from '../../types/hook';
|
|
3
5
|
import { JobState, JobStatus } from '../../types/job';
|
|
4
6
|
import { RedisMulti } from '../../types/redis';
|
|
5
|
-
import { HookRule } from '../../types/hook';
|
|
6
|
-
import { Activity } from './activity';
|
|
7
7
|
/**
|
|
8
8
|
* Listens for `webhook`, `timehook`, and `cycle` (repeat) signals
|
|
9
9
|
*/
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Hook = void 0;
|
|
4
4
|
const errors_1 = require("../../modules/errors");
|
|
5
|
+
const activity_1 = require("./activity");
|
|
5
6
|
const collator_1 = require("../collator");
|
|
6
7
|
const pipe_1 = require("../pipe");
|
|
7
8
|
const store_1 = require("../signaler/store");
|
|
8
9
|
const telemetry_1 = require("../telemetry");
|
|
9
|
-
const
|
|
10
|
-
const types_1 = require("../../types");
|
|
10
|
+
const stream_1 = require("../../types/stream");
|
|
11
11
|
/**
|
|
12
12
|
* Listens for `webhook`, `timehook`, and `cycle` (repeat) signals
|
|
13
13
|
*/
|
|
@@ -107,7 +107,7 @@ class Hook extends activity_1.Activity {
|
|
|
107
107
|
if (jobId) {
|
|
108
108
|
//if a webhook signal is sent that includes 'keep_alive' the hook will remain open
|
|
109
109
|
const code = data.keep_alive ? 202 : 200;
|
|
110
|
-
await this.processEvent(
|
|
110
|
+
await this.processEvent(stream_1.StreamStatus.SUCCESS, code, 'hook', jobId);
|
|
111
111
|
if (code === 200) {
|
|
112
112
|
await signaler.deleteWebHookSignal(this.config.hook.topic, data);
|
|
113
113
|
}
|
|
@@ -118,7 +118,7 @@ class Hook extends activity_1.Activity {
|
|
|
118
118
|
jid: jobId,
|
|
119
119
|
aid: this.metadata.aid
|
|
120
120
|
});
|
|
121
|
-
await this.processEvent(
|
|
121
|
+
await this.processEvent(stream_1.StreamStatus.SUCCESS, 200, 'hook');
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
exports.Hook = Hook;
|
|
@@ -6,46 +6,7 @@ const factory_1 = require("./factory");
|
|
|
6
6
|
const handle_1 = require("./handle");
|
|
7
7
|
const hotmesh_1 = require("../hotmesh");
|
|
8
8
|
const key_1 = require("../../modules/key");
|
|
9
|
-
|
|
10
|
-
Here is an example of how the methods in this file are used:
|
|
11
|
-
|
|
12
|
-
./client.ts
|
|
13
|
-
|
|
14
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
15
|
-
import Redis from 'ioredis';
|
|
16
|
-
import { example } from './workflows';
|
|
17
|
-
import { nanoid } from 'nanoid';
|
|
18
|
-
|
|
19
|
-
async function run() {
|
|
20
|
-
const connection = await Durable.Connection.connect({
|
|
21
|
-
class: Redis,
|
|
22
|
-
options: {
|
|
23
|
-
host: 'localhost',
|
|
24
|
-
port: 6379,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const client = new Durable.Client({
|
|
29
|
-
connection,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const handle = await client.workflow.start({
|
|
33
|
-
args: ['HotMesh'],
|
|
34
|
-
taskQueue: 'hello-world',
|
|
35
|
-
workflowName: 'example',
|
|
36
|
-
workflowId: 'workflow-' + nanoid(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
console.log(`Started workflow ${handle.workflowId}`);
|
|
40
|
-
console.log(await handle.result());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
run().catch((err) => {
|
|
44
|
-
console.error(err);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
*/
|
|
9
|
+
const search_1 = require("./search");
|
|
49
10
|
class ClientService {
|
|
50
11
|
constructor(config) {
|
|
51
12
|
this.getHotMeshClient = async (worflowTopic) => {
|
|
@@ -81,7 +42,7 @@ class ClientService {
|
|
|
81
42
|
* this method will configure the search index for the workflow.
|
|
82
43
|
*/
|
|
83
44
|
this.configureSearchIndex = async (hotMeshClient, search) => {
|
|
84
|
-
if (search) {
|
|
45
|
+
if (search?.schema) {
|
|
85
46
|
const store = hotMeshClient.engine.store;
|
|
86
47
|
const schema = [];
|
|
87
48
|
for (const [key, value] of Object.entries(search.schema)) {
|
|
@@ -129,6 +90,13 @@ class ClientService {
|
|
|
129
90
|
};
|
|
130
91
|
const context = { metadata: { trc, spn }, data: {} };
|
|
131
92
|
const jobId = await hotMeshClient.pub(factory_1.SUBSCRIBES_TOPIC, payload, context);
|
|
93
|
+
if (jobId && options.search?.data) {
|
|
94
|
+
//job successfully kicked off; there is default job data to persist
|
|
95
|
+
const search = new search_1.Search(jobId, hotMeshClient);
|
|
96
|
+
for (const [key, value] of Object.entries(options.search.data)) {
|
|
97
|
+
search.set(key, value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
132
100
|
return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
133
101
|
},
|
|
134
102
|
signal: async (signalId, data) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
2
|
-
import { Connection, Registry, WorkerConfig, WorkerOptions } from "../../types/durable";
|
|
2
|
+
import { Connection, Registry, WorkerConfig, WorkerOptions, WorkflowSearchOptions } from "../../types/durable";
|
|
3
3
|
export declare class WorkerService {
|
|
4
4
|
static activityRegistry: Registry;
|
|
5
5
|
static connection: Connection;
|
|
@@ -14,6 +14,11 @@ export declare class WorkerService {
|
|
|
14
14
|
* allowing proxyActivities to succeed.
|
|
15
15
|
*/
|
|
16
16
|
static registerActivities<ACT>(activities: ACT): Registry;
|
|
17
|
+
/**
|
|
18
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
19
|
+
* this method will configure the search index for the workflow.
|
|
20
|
+
*/
|
|
21
|
+
static configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void>;
|
|
17
22
|
static create(config: WorkerConfig): Promise<WorkerService>;
|
|
18
23
|
static resolveWorkflowTarget(workflow: object | Function): [string, Function];
|
|
19
24
|
run(): Promise<void>;
|
|
@@ -7,36 +7,7 @@ const asyncLocalStorage_1 = require("./asyncLocalStorage");
|
|
|
7
7
|
const factory_1 = require("./factory");
|
|
8
8
|
const hotmesh_1 = require("../hotmesh");
|
|
9
9
|
const stream_1 = require("../../types/stream");
|
|
10
|
-
|
|
11
|
-
Here is an example of how the methods in this file are used:
|
|
12
|
-
|
|
13
|
-
./worker.ts
|
|
14
|
-
|
|
15
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
16
|
-
import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
|
|
17
|
-
|
|
18
|
-
import * as workflows from './workflows';
|
|
19
|
-
|
|
20
|
-
async function run() {
|
|
21
|
-
const worker = await Durable.Worker.create({
|
|
22
|
-
connection: {
|
|
23
|
-
class: Redis,
|
|
24
|
-
options: {
|
|
25
|
-
host: 'localhost',
|
|
26
|
-
port: 6379,
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
taskQueue: 'hello-world',
|
|
30
|
-
workflow: workflows.example,
|
|
31
|
-
});
|
|
32
|
-
await worker.run();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
run().catch((err) => {
|
|
36
|
-
console.error(err);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
});
|
|
39
|
-
*/
|
|
10
|
+
const key_1 = require("../../modules/key");
|
|
40
11
|
class WorkerService {
|
|
41
12
|
static async activateWorkflow(hotMesh) {
|
|
42
13
|
const app = await hotMesh.engine.store.getApp(factory_1.APP_ID);
|
|
@@ -79,6 +50,38 @@ class WorkerService {
|
|
|
79
50
|
}
|
|
80
51
|
return WorkerService.activityRegistry;
|
|
81
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
55
|
+
* this method will configure the search index for the workflow.
|
|
56
|
+
*/
|
|
57
|
+
//todo: bind this to the Search service; update constructor to expect hotMeshClient as first param (id is optional
|
|
58
|
+
//refactor and delete other one as well)
|
|
59
|
+
static async configureSearchIndex(hotMeshClient, search) {
|
|
60
|
+
if (search?.schema) {
|
|
61
|
+
const store = hotMeshClient.engine.store;
|
|
62
|
+
const schema = [];
|
|
63
|
+
for (const [key, value] of Object.entries(search.schema)) {
|
|
64
|
+
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
65
|
+
schema.push(`_${key}`);
|
|
66
|
+
schema.push(value.type);
|
|
67
|
+
if (value.sortable) {
|
|
68
|
+
schema.push('SORTABLE');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const keyParams = {
|
|
73
|
+
appId: hotMeshClient.appId,
|
|
74
|
+
jobId: ''
|
|
75
|
+
};
|
|
76
|
+
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
77
|
+
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
78
|
+
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
82
85
|
static async create(config) {
|
|
83
86
|
//always call `registerActivities` before `import`
|
|
84
87
|
WorkerService.connection = config.connection;
|
|
@@ -91,6 +94,7 @@ class WorkerService {
|
|
|
91
94
|
const worker = new WorkerService();
|
|
92
95
|
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
93
96
|
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
97
|
+
WorkerService.configureSearchIndex(worker.workflowRunner, config.search);
|
|
94
98
|
await WorkerService.activateWorkflow(worker.workflowRunner);
|
|
95
99
|
return worker;
|
|
96
100
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
|
|
2
|
+
import { Search } from './search';
|
|
2
3
|
export declare class WorkflowService {
|
|
3
4
|
/**
|
|
4
5
|
* Spawn a child workflow. await the result.
|
|
5
6
|
*/
|
|
6
7
|
static executeChild<T>(options: WorkflowOptions): Promise<T>;
|
|
7
8
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
|
|
8
|
-
static
|
|
9
|
+
static search(): Promise<Search>;
|
|
9
10
|
static sleep(duration: string): Promise<number>;
|
|
10
11
|
static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
|
|
11
12
|
static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
|
|
@@ -12,34 +12,6 @@ const connection_1 = require("./connection");
|
|
|
12
12
|
const factory_1 = require("./factory");
|
|
13
13
|
const errors_1 = require("../../modules/errors");
|
|
14
14
|
const search_1 = require("./search");
|
|
15
|
-
/*
|
|
16
|
-
`proxyActivities` returns a wrapped instance of the
|
|
17
|
-
target activity, so that when the workflow calls a
|
|
18
|
-
proxied activity, it is actually calling the proxy
|
|
19
|
-
function, which in turn calls the activity function.
|
|
20
|
-
|
|
21
|
-
Here is an example of how the methods in this file are used:
|
|
22
|
-
|
|
23
|
-
./workflows.ts
|
|
24
|
-
|
|
25
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
26
|
-
import * as activities from './activities';
|
|
27
|
-
|
|
28
|
-
const { greet } = Durable.workflow.proxyActivities<typeof activities>({
|
|
29
|
-
activities: activities,
|
|
30
|
-
startToCloseTimeout: '1 minute',
|
|
31
|
-
retryPolicy: {
|
|
32
|
-
initialInterval: '5 seconds', // Initial delay between retries
|
|
33
|
-
maximumAttempts: 3, // Max number of retry attempts
|
|
34
|
-
backoffCoefficient: 2.0, // Backoff factor for delay between retries: delay = initialInterval * (backoffCoefficient ^ retry_attempt)
|
|
35
|
-
maximumInterval: '30 seconds', // Max delay between retries
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export async function example(name: string): Promise<string> {
|
|
40
|
-
return await greet(name);
|
|
41
|
-
}
|
|
42
|
-
*/
|
|
43
15
|
class WorkflowService {
|
|
44
16
|
/**
|
|
45
17
|
* Spawn a child workflow. await the result.
|
|
@@ -89,38 +61,15 @@ class WorkflowService {
|
|
|
89
61
|
}
|
|
90
62
|
return proxy;
|
|
91
63
|
}
|
|
92
|
-
static async
|
|
64
|
+
static async search() {
|
|
93
65
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
94
66
|
if (!store) {
|
|
95
67
|
throw new Error('durable-store-not-found');
|
|
96
68
|
}
|
|
97
69
|
const workflowId = store.get('workflowId');
|
|
98
70
|
const workflowTopic = store.get('workflowTopic');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const search = new search_1.Search(workflowId, hotMeshClient);
|
|
102
|
-
if (command === 'get') {
|
|
103
|
-
return await search.get(args[0]);
|
|
104
|
-
}
|
|
105
|
-
else if (command === 'set') {
|
|
106
|
-
await search.set(args[0], args[1]);
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
else if (command === 'del') {
|
|
110
|
-
await search.del(args[0]);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
else if (command === 'incr') {
|
|
114
|
-
return await search.incr(args[0], Number(args[1]));
|
|
115
|
-
}
|
|
116
|
-
else if (command === 'mult') {
|
|
117
|
-
return await search.mult(args[0], Number(args[1]));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (e) {
|
|
121
|
-
console.error(e);
|
|
122
|
-
return '';
|
|
123
|
-
}
|
|
71
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
|
|
72
|
+
return new search_1.Search(workflowId, hotMeshClient);
|
|
124
73
|
}
|
|
125
74
|
static async sleep(duration) {
|
|
126
75
|
const seconds = (0, ms_1.default)(duration) / 1000;
|
package/build/types/durable.d.ts
CHANGED
|
@@ -6,12 +6,13 @@ type WorkflowConfig = {
|
|
|
6
6
|
initialInterval?: string;
|
|
7
7
|
};
|
|
8
8
|
type WorkflowSearchOptions = {
|
|
9
|
-
index
|
|
10
|
-
prefix
|
|
11
|
-
schema
|
|
9
|
+
index?: string;
|
|
10
|
+
prefix?: string[];
|
|
11
|
+
schema?: Record<string, {
|
|
12
12
|
type: 'TEXT' | 'NUMERIC' | 'TAG';
|
|
13
13
|
sortable: boolean;
|
|
14
14
|
}>;
|
|
15
|
+
data?: Record<string, string>;
|
|
15
16
|
};
|
|
16
17
|
type WorkflowOptions = {
|
|
17
18
|
taskQueue: string;
|
|
@@ -59,6 +60,7 @@ type WorkerConfig = {
|
|
|
59
60
|
taskQueue: string;
|
|
60
61
|
workflow: Function;
|
|
61
62
|
options?: WorkerOptions;
|
|
63
|
+
search?: WorkflowSearchOptions;
|
|
62
64
|
};
|
|
63
65
|
type WorkerOptions = {
|
|
64
66
|
maxSystemRetries?: number;
|
package/modules/utils.ts
CHANGED
|
@@ -9,6 +9,12 @@ export async function sleepFor(ms: number) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null {
|
|
12
|
+
const prototype = Object.getPrototypeOf(redisInstance);
|
|
13
|
+
if ('defineCommand' in prototype || Object.keys(prototype).includes('multi')) {
|
|
14
|
+
return 'ioredis';
|
|
15
|
+
} else if (Object.keys(prototype).includes('Multi')) {
|
|
16
|
+
return 'redis';
|
|
17
|
+
}
|
|
12
18
|
if (redisInstance.constructor) {
|
|
13
19
|
if (redisInstance.constructor.name === 'Redis' || redisInstance.constructor.name === 'EventEmitter') {
|
|
14
20
|
if ('hset' in redisInstance) {
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GetStateError } from '../../modules/errors';
|
|
2
|
+
import { Activity } from './activity';
|
|
2
3
|
import { CollatorService } from '../collator';
|
|
3
4
|
import { EngineService } from '../engine';
|
|
4
5
|
import { Pipe } from '../pipe';
|
|
@@ -8,15 +9,14 @@ import {
|
|
|
8
9
|
ActivityData,
|
|
9
10
|
ActivityMetadata,
|
|
10
11
|
ActivityType,
|
|
11
|
-
HookActivity} from '../../types/activity';
|
|
12
|
+
HookActivity } from '../../types/activity';
|
|
13
|
+
import { HookRule } from '../../types/hook';
|
|
12
14
|
import { JobState, JobStatus } from '../../types/job';
|
|
13
15
|
import {
|
|
14
16
|
MultiResponseFlags,
|
|
15
17
|
RedisMulti } from '../../types/redis';
|
|
16
18
|
import { StringScalarType } from '../../types/serializer';
|
|
17
|
-
import {
|
|
18
|
-
import { Activity } from './activity';
|
|
19
|
-
import { StreamStatus } from '../../types';
|
|
19
|
+
import { StreamStatus } from '../../types/stream';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Listens for `webhook`, `timehook`, and `cycle` (repeat) signals
|
|
@@ -9,47 +9,7 @@ import {
|
|
|
9
9
|
WorkflowSearchOptions} from '../../types/durable';
|
|
10
10
|
import { JobState } from '../../types/job';
|
|
11
11
|
import { KeyService, KeyType } from '../../modules/key';
|
|
12
|
-
|
|
13
|
-
/*
|
|
14
|
-
Here is an example of how the methods in this file are used:
|
|
15
|
-
|
|
16
|
-
./client.ts
|
|
17
|
-
|
|
18
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
19
|
-
import Redis from 'ioredis';
|
|
20
|
-
import { example } from './workflows';
|
|
21
|
-
import { nanoid } from 'nanoid';
|
|
22
|
-
|
|
23
|
-
async function run() {
|
|
24
|
-
const connection = await Durable.Connection.connect({
|
|
25
|
-
class: Redis,
|
|
26
|
-
options: {
|
|
27
|
-
host: 'localhost',
|
|
28
|
-
port: 6379,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const client = new Durable.Client({
|
|
33
|
-
connection,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const handle = await client.workflow.start({
|
|
37
|
-
args: ['HotMesh'],
|
|
38
|
-
taskQueue: 'hello-world',
|
|
39
|
-
workflowName: 'example',
|
|
40
|
-
workflowId: 'workflow-' + nanoid(),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
console.log(`Started workflow ${handle.workflowId}`);
|
|
44
|
-
console.log(await handle.result());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
run().catch((err) => {
|
|
48
|
-
console.error(err);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
*/
|
|
12
|
+
import { Search } from './search';
|
|
53
13
|
|
|
54
14
|
export class ClientService {
|
|
55
15
|
|
|
@@ -96,7 +56,7 @@ export class ClientService {
|
|
|
96
56
|
* this method will configure the search index for the workflow.
|
|
97
57
|
*/
|
|
98
58
|
configureSearchIndex = async (hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void> => {
|
|
99
|
-
if (search) {
|
|
59
|
+
if (search?.schema) {
|
|
100
60
|
const store = hotMeshClient.engine.store;
|
|
101
61
|
const schema: string[] = [];
|
|
102
62
|
for (const [key, value] of Object.entries(search.schema)) {
|
|
@@ -148,6 +108,13 @@ export class ClientService {
|
|
|
148
108
|
SUBSCRIBES_TOPIC,
|
|
149
109
|
payload,
|
|
150
110
|
context as JobState);
|
|
111
|
+
if (jobId && options.search?.data) {
|
|
112
|
+
//job successfully kicked off; there is default job data to persist
|
|
113
|
+
const search = new Search(jobId, hotMeshClient);
|
|
114
|
+
for (const [key, value] of Object.entries(options.search.data)) {
|
|
115
|
+
search.set(key, value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
151
118
|
return new WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
152
119
|
},
|
|
153
120
|
|
|
@@ -15,43 +15,14 @@ import {
|
|
|
15
15
|
Registry,
|
|
16
16
|
WorkerConfig,
|
|
17
17
|
WorkerOptions,
|
|
18
|
-
WorkflowDataType
|
|
18
|
+
WorkflowDataType,
|
|
19
|
+
WorkflowSearchOptions} from "../../types/durable";
|
|
19
20
|
import { RedisClass, RedisOptions } from '../../types/redis';
|
|
20
21
|
import {
|
|
21
22
|
StreamData,
|
|
22
23
|
StreamDataResponse,
|
|
23
24
|
StreamStatus } from '../../types/stream';
|
|
24
|
-
|
|
25
|
-
/*
|
|
26
|
-
Here is an example of how the methods in this file are used:
|
|
27
|
-
|
|
28
|
-
./worker.ts
|
|
29
|
-
|
|
30
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
31
|
-
import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
|
|
32
|
-
|
|
33
|
-
import * as workflows from './workflows';
|
|
34
|
-
|
|
35
|
-
async function run() {
|
|
36
|
-
const worker = await Durable.Worker.create({
|
|
37
|
-
connection: {
|
|
38
|
-
class: Redis,
|
|
39
|
-
options: {
|
|
40
|
-
host: 'localhost',
|
|
41
|
-
port: 6379,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
taskQueue: 'hello-world',
|
|
45
|
-
workflow: workflows.example,
|
|
46
|
-
});
|
|
47
|
-
await worker.run();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
run().catch((err) => {
|
|
51
|
-
console.error(err);
|
|
52
|
-
process.exit(1);
|
|
53
|
-
});
|
|
54
|
-
*/
|
|
25
|
+
import { KeyService, KeyType } from '../../modules/key';
|
|
55
26
|
|
|
56
27
|
export class WorkerService {
|
|
57
28
|
static activityRegistry: Registry = {}; //user's activities
|
|
@@ -112,6 +83,38 @@ export class WorkerService {
|
|
|
112
83
|
return WorkerService.activityRegistry;
|
|
113
84
|
}
|
|
114
85
|
|
|
86
|
+
/**
|
|
87
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
88
|
+
* this method will configure the search index for the workflow.
|
|
89
|
+
*/
|
|
90
|
+
//todo: bind this to the Search service; update constructor to expect hotMeshClient as first param (id is optional
|
|
91
|
+
//refactor and delete other one as well)
|
|
92
|
+
static async configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void> {
|
|
93
|
+
if (search?.schema) {
|
|
94
|
+
const store = hotMeshClient.engine.store;
|
|
95
|
+
const schema: string[] = [];
|
|
96
|
+
for (const [key, value] of Object.entries(search.schema)) {
|
|
97
|
+
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
98
|
+
schema.push(`_${key}`);
|
|
99
|
+
schema.push(value.type);
|
|
100
|
+
if (value.sortable) {
|
|
101
|
+
schema.push('SORTABLE');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const keyParams = {
|
|
106
|
+
appId: hotMeshClient.appId,
|
|
107
|
+
jobId: ''
|
|
108
|
+
}
|
|
109
|
+
const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
110
|
+
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
111
|
+
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
115
118
|
static async create(config: WorkerConfig) {
|
|
116
119
|
//always call `registerActivities` before `import`
|
|
117
120
|
WorkerService.connection = config.connection;
|
|
@@ -125,6 +128,7 @@ export class WorkerService {
|
|
|
125
128
|
const worker = new WorkerService();
|
|
126
129
|
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
127
130
|
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
131
|
+
WorkerService.configureSearchIndex(worker.workflowRunner, config.search)
|
|
128
132
|
await WorkerService.activateWorkflow(worker.workflowRunner);
|
|
129
133
|
return worker;
|
|
130
134
|
}
|
|
@@ -10,35 +10,6 @@ import { ACTIVITY_PUBLISHES_TOPIC, ACTIVITY_SUBSCRIBES_TOPIC, SLEEP_SUBSCRIBES_T
|
|
|
10
10
|
import { DurableIncompleteSignalError, DurableSleepError, DurableWaitForSignalError } from '../../modules/errors';
|
|
11
11
|
import { Search } from './search';
|
|
12
12
|
|
|
13
|
-
/*
|
|
14
|
-
`proxyActivities` returns a wrapped instance of the
|
|
15
|
-
target activity, so that when the workflow calls a
|
|
16
|
-
proxied activity, it is actually calling the proxy
|
|
17
|
-
function, which in turn calls the activity function.
|
|
18
|
-
|
|
19
|
-
Here is an example of how the methods in this file are used:
|
|
20
|
-
|
|
21
|
-
./workflows.ts
|
|
22
|
-
|
|
23
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
24
|
-
import * as activities from './activities';
|
|
25
|
-
|
|
26
|
-
const { greet } = Durable.workflow.proxyActivities<typeof activities>({
|
|
27
|
-
activities: activities,
|
|
28
|
-
startToCloseTimeout: '1 minute',
|
|
29
|
-
retryPolicy: {
|
|
30
|
-
initialInterval: '5 seconds', // Initial delay between retries
|
|
31
|
-
maximumAttempts: 3, // Max number of retry attempts
|
|
32
|
-
backoffCoefficient: 2.0, // Backoff factor for delay between retries: delay = initialInterval * (backoffCoefficient ^ retry_attempt)
|
|
33
|
-
maximumInterval: '30 seconds', // Max delay between retries
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export async function example(name: string): Promise<string> {
|
|
38
|
-
return await greet(name);
|
|
39
|
-
}
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
13
|
export class WorkflowService {
|
|
43
14
|
|
|
44
15
|
/**
|
|
@@ -98,7 +69,7 @@ export class WorkflowService {
|
|
|
98
69
|
return proxy;
|
|
99
70
|
}
|
|
100
71
|
|
|
101
|
-
static async
|
|
72
|
+
static async search(): Promise<Search> {
|
|
102
73
|
const store = asyncLocalStorage.getStore();
|
|
103
74
|
if (!store) {
|
|
104
75
|
throw new Error('durable-store-not-found');
|
|
@@ -106,26 +77,8 @@ export class WorkflowService {
|
|
|
106
77
|
const workflowId = store.get('workflowId');
|
|
107
78
|
const workflowTopic = store.get('workflowTopic');
|
|
108
79
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const search = new Search(workflowId, hotMeshClient);
|
|
112
|
-
if (command === 'get') {
|
|
113
|
-
return await search.get(args[0]) as string;
|
|
114
|
-
} else if (command === 'set') {
|
|
115
|
-
await search.set(args[0], args[1]);
|
|
116
|
-
return true;
|
|
117
|
-
} else if (command === 'del') {
|
|
118
|
-
await search.del(args[0]);
|
|
119
|
-
return true;
|
|
120
|
-
} else if (command === 'incr') {
|
|
121
|
-
return await search.incr(args[0], Number(args[1])) as number;
|
|
122
|
-
} else if (command === 'mult') {
|
|
123
|
-
return await search.mult(args[0], Number(args[1])) as number;
|
|
124
|
-
}
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.error(e);
|
|
127
|
-
return '';
|
|
128
|
-
}
|
|
80
|
+
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic);
|
|
81
|
+
return new Search(workflowId, hotMeshClient);
|
|
129
82
|
}
|
|
130
83
|
|
|
131
84
|
static async sleep(duration: string): Promise<number> {
|
package/types/durable.ts
CHANGED
|
@@ -8,9 +8,10 @@ type WorkflowConfig = {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
type WorkflowSearchOptions = {
|
|
11
|
-
index
|
|
12
|
-
prefix
|
|
13
|
-
schema
|
|
11
|
+
index?: string; //FT index name (myapp:myindex)
|
|
12
|
+
prefix?: string[]; //FT prefixes (['myapp:myindex:prefix1', 'myapp:myindex:prefix2'])
|
|
13
|
+
schema?: Record<string, {type: 'TEXT' | 'NUMERIC' | 'TAG', sortable: boolean}>;
|
|
14
|
+
data?: Record<string, string>;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
type WorkflowOptions = {
|
|
@@ -66,6 +67,7 @@ type WorkerConfig = {
|
|
|
66
67
|
taskQueue: string; //`subscribes` in the YAML (e.g, 'hello-world')
|
|
67
68
|
workflow: Function; //target function to run
|
|
68
69
|
options?: WorkerOptions;
|
|
70
|
+
search?: WorkflowSearchOptions;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
type WorkerOptions = {
|