@hotmeshio/hotmesh 0.0.18 → 0.0.20
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 +4 -4
- package/build/modules/errors.d.ts +2 -1
- package/build/modules/errors.js +2 -1
- package/build/modules/utils.js +7 -0
- package/build/package.json +2 -1
- package/build/services/activities/activity.d.ts +2 -2
- package/build/services/activities/activity.js +10 -8
- package/build/services/activities/hook.d.ts +4 -3
- package/build/services/activities/hook.js +15 -12
- package/build/services/activities/signal.d.ts +4 -0
- package/build/services/activities/signal.js +16 -2
- package/build/services/durable/client.d.ts +15 -5
- package/build/services/durable/client.js +45 -54
- package/build/services/durable/factory.d.ts +2 -16
- package/build/services/durable/factory.js +276 -46
- package/build/services/durable/handle.d.ts +1 -1
- package/build/services/durable/handle.js +18 -5
- package/build/services/durable/search.d.ts +8 -1
- package/build/services/durable/search.js +34 -7
- package/build/services/durable/worker.d.ts +10 -7
- package/build/services/durable/worker.js +59 -49
- package/build/services/durable/workflow.d.ts +20 -2
- package/build/services/durable/workflow.js +97 -84
- package/build/services/engine/index.d.ts +2 -2
- package/build/services/engine/index.js +7 -12
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +2 -2
- package/build/services/signaler/store.d.ts +2 -2
- package/build/services/signaler/store.js +17 -7
- package/build/services/signaler/stream.js +1 -0
- package/build/services/store/clients/redis.js +1 -1
- package/build/services/store/index.js +3 -0
- package/build/services/telemetry/index.js +7 -1
- package/build/types/activity.d.ts +5 -3
- package/build/types/durable.d.ts +17 -4
- package/build/types/hook.d.ts +0 -1
- package/build/types/index.d.ts +1 -1
- package/modules/errors.ts +4 -2
- package/modules/utils.ts +6 -0
- package/package.json +2 -1
- package/services/activities/activity.ts +10 -8
- package/services/activities/hook.ts +17 -14
- package/services/activities/signal.ts +17 -3
- package/services/durable/client.ts +48 -56
- package/services/durable/factory.ts +274 -46
- package/services/durable/handle.ts +18 -5
- package/services/durable/search.ts +36 -7
- package/services/durable/worker.ts +61 -51
- package/services/durable/workflow.ts +110 -84
- package/services/engine/index.ts +8 -12
- package/services/hotmesh/index.ts +3 -3
- package/services/signaler/store.ts +18 -8
- package/services/signaler/stream.ts +1 -0
- package/services/store/clients/redis.ts +1 -1
- package/services/store/index.ts +2 -0
- package/services/telemetry/index.ts +6 -1
- package/types/activity.ts +10 -8
- package/types/durable.ts +18 -3
- package/types/hook.ts +0 -1
- package/types/index.ts +1 -0
|
@@ -11,7 +11,7 @@ class StoreSignaler {
|
|
|
11
11
|
const rules = await this.store.getHookRules();
|
|
12
12
|
return rules?.[topic]?.[0];
|
|
13
13
|
}
|
|
14
|
-
async registerWebHook(topic, context, multi) {
|
|
14
|
+
async registerWebHook(topic, context, dad, multi) {
|
|
15
15
|
const hookRule = await this.getHookRule(topic);
|
|
16
16
|
if (hookRule) {
|
|
17
17
|
const mapExpression = hookRule.conditions.match[0].expected;
|
|
@@ -20,7 +20,8 @@ class StoreSignaler {
|
|
|
20
20
|
const hook = {
|
|
21
21
|
topic,
|
|
22
22
|
resolved,
|
|
23
|
-
|
|
23
|
+
//hookSignalId is composed of `<dad>::<jid>`
|
|
24
|
+
jobId: `${dad}::${jobId}`,
|
|
24
25
|
};
|
|
25
26
|
await this.store.setHookSignal(hook, multi);
|
|
26
27
|
return jobId;
|
|
@@ -36,18 +37,27 @@ class StoreSignaler {
|
|
|
36
37
|
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
37
38
|
const mapExpression = hookRule.conditions.match[0].actual;
|
|
38
39
|
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
39
|
-
const
|
|
40
|
-
|
|
40
|
+
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
41
|
+
if (!hookSignalId) {
|
|
42
|
+
//messages can be double-processed; not an issue; return undefined
|
|
43
|
+
//users can also provide a bogus topic; not an issue; return undefined
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const [dad, jid] = hookSignalId.split('::');
|
|
47
|
+
//return [jid, aid, dad]
|
|
48
|
+
return [jid, hookRule.to, dad];
|
|
41
49
|
}
|
|
42
50
|
else {
|
|
43
|
-
throw new Error('
|
|
51
|
+
throw new Error('signal-not-found');
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
async deleteWebHookSignal(topic, data) {
|
|
47
55
|
const hookRule = await this.getHookRule(topic);
|
|
48
56
|
if (hookRule) {
|
|
49
|
-
//
|
|
50
|
-
const
|
|
57
|
+
//NOTE: both formats are supported: $self.hook.data OR $hook.data
|
|
58
|
+
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
59
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
60
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
51
61
|
return await this.store.deleteHookSignal(topic, resolved);
|
|
52
62
|
}
|
|
53
63
|
else {
|
|
@@ -68,7 +68,7 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
|
|
71
|
-
this.logger.
|
|
71
|
+
this.logger.info(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
|
|
72
72
|
throw error;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -422,6 +422,9 @@ class StoreService {
|
|
|
422
422
|
}
|
|
423
423
|
return [state, status];
|
|
424
424
|
}
|
|
425
|
+
else {
|
|
426
|
+
throw new Error(`Job ${jobId} not found`);
|
|
427
|
+
}
|
|
425
428
|
}
|
|
426
429
|
async collate(jobId, activityId, amount, dIds, multi) {
|
|
427
430
|
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
@@ -207,11 +207,17 @@ class TelemetryService {
|
|
|
207
207
|
}
|
|
208
208
|
static bindActivityTelemetryToState(state, config, metadata, context, leg) {
|
|
209
209
|
if (config.type === 'trigger') {
|
|
210
|
+
//trigger activities run non-duplexed and only have a single leg (2)
|
|
210
211
|
state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
|
|
211
212
|
state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l2s;
|
|
212
213
|
}
|
|
213
214
|
else if (utils_1.polyfill.resolveActivityType(config.type) === 'hook' && leg === 1) {
|
|
214
|
-
//activities run non-duplexed and only have a single leg
|
|
215
|
+
//hook activities run non-duplexed and only have a single leg (1)
|
|
216
|
+
state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
|
|
217
|
+
state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l1s;
|
|
218
|
+
}
|
|
219
|
+
else if (config.type === 'signal' && leg === 1) {
|
|
220
|
+
//signal activities run non-duplexed and only have a single leg (1)
|
|
215
221
|
state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
|
|
216
222
|
state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l1s;
|
|
217
223
|
}
|
|
@@ -66,11 +66,13 @@ interface SignalActivity extends BaseActivity {
|
|
|
66
66
|
type: 'signal';
|
|
67
67
|
subtype: 'one' | 'all';
|
|
68
68
|
topic: string;
|
|
69
|
-
key_name
|
|
70
|
-
key_value
|
|
71
|
-
scrub
|
|
69
|
+
key_name?: string;
|
|
70
|
+
key_value?: string;
|
|
71
|
+
scrub?: boolean;
|
|
72
72
|
signal?: Record<string, any>;
|
|
73
73
|
resolver?: Record<string, any>;
|
|
74
|
+
status?: string;
|
|
75
|
+
code?: number;
|
|
74
76
|
}
|
|
75
77
|
interface IterateActivity extends BaseActivity {
|
|
76
78
|
type: 'iterate';
|
package/build/types/durable.d.ts
CHANGED
|
@@ -6,14 +6,16 @@ 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 = {
|
|
18
|
+
namespace?: string;
|
|
17
19
|
taskQueue: string;
|
|
18
20
|
args: any[];
|
|
19
21
|
workflowId: string;
|
|
@@ -24,6 +26,15 @@ type WorkflowOptions = {
|
|
|
24
26
|
search?: WorkflowSearchOptions;
|
|
25
27
|
config?: WorkflowConfig;
|
|
26
28
|
};
|
|
29
|
+
type HookOptions = {
|
|
30
|
+
namespace?: string;
|
|
31
|
+
taskQueue: string;
|
|
32
|
+
args: any[];
|
|
33
|
+
workflowId: string;
|
|
34
|
+
workflowName?: string;
|
|
35
|
+
search?: WorkflowSearchOptions;
|
|
36
|
+
config?: WorkflowConfig;
|
|
37
|
+
};
|
|
27
38
|
type SignalOptions = {
|
|
28
39
|
taskQueue: string;
|
|
29
40
|
data: Record<string, any>;
|
|
@@ -59,8 +70,10 @@ type WorkerConfig = {
|
|
|
59
70
|
taskQueue: string;
|
|
60
71
|
workflow: Function;
|
|
61
72
|
options?: WorkerOptions;
|
|
73
|
+
search?: WorkflowSearchOptions;
|
|
62
74
|
};
|
|
63
75
|
type WorkerOptions = {
|
|
76
|
+
logLevel?: string;
|
|
64
77
|
maxSystemRetries?: number;
|
|
65
78
|
backoffCoefficient?: number;
|
|
66
79
|
};
|
|
@@ -82,4 +95,4 @@ type ActivityConfig = {
|
|
|
82
95
|
maximumInterval: string;
|
|
83
96
|
};
|
|
84
97
|
};
|
|
85
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, SignalOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
|
|
98
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, SignalOptions, HookOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
|
package/build/types/hook.d.ts
CHANGED
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, NativeConnection, ProxyType, Registry, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
6
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, HookOptions, 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/modules/errors.ts
CHANGED
|
@@ -36,11 +36,13 @@ class DurableWaitForSignalError extends Error {
|
|
|
36
36
|
class DurableSleepError extends Error {
|
|
37
37
|
code: number;
|
|
38
38
|
duration: number; //seconds
|
|
39
|
-
index: number; //execution order in the workflow
|
|
40
|
-
|
|
39
|
+
index: number; //execution order in the workflow
|
|
40
|
+
dimension: string; //hook dimension (e.g., ',0,1,0') (uses empty string for `null`)
|
|
41
|
+
constructor(message: string, duration: number, index: number, dimension: string) {
|
|
41
42
|
super(message);
|
|
42
43
|
this.duration = duration;
|
|
43
44
|
this.index = index;
|
|
45
|
+
this.dimension = dimension;
|
|
44
46
|
this.code = 595;
|
|
45
47
|
}
|
|
46
48
|
}
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
46
46
|
"test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
47
47
|
"test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
48
|
+
"test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
48
49
|
"test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
49
50
|
"test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
50
51
|
"test:durable:sleep": "NODE_ENV=test jest ./tests/durable/sleep/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -70,16 +70,20 @@ class Activity {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
//******** DUPLEX RE-ENTRY POINT ********//
|
|
73
|
-
async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'
|
|
73
|
+
async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'): Promise<void> {
|
|
74
74
|
this.setLeg(2);
|
|
75
|
-
const jid = this.context.metadata.jid
|
|
75
|
+
const jid = this.context.metadata.jid;
|
|
76
|
+
if (!jid) {
|
|
77
|
+
this.logger.error('activity-process-event-error', { message: 'job id is undefined' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
76
80
|
const aid = this.metadata.aid;
|
|
77
81
|
this.status = status;
|
|
78
82
|
this.code = code;
|
|
79
83
|
this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
|
|
80
84
|
let telemetry: TelemetryService;
|
|
81
85
|
try {
|
|
82
|
-
await this.getState(
|
|
86
|
+
await this.getState();
|
|
83
87
|
const aState = await CollatorService.notarizeReentry(this);
|
|
84
88
|
this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
|
|
85
89
|
|
|
@@ -107,7 +111,6 @@ class Activity {
|
|
|
107
111
|
this.logger.info('process-event-inactive-error', { error });
|
|
108
112
|
return;
|
|
109
113
|
}
|
|
110
|
-
console.error(error);
|
|
111
114
|
this.logger.error('activity-process-event-error', { error });
|
|
112
115
|
telemetry && telemetry.setActivityError(error.message);
|
|
113
116
|
throw error;
|
|
@@ -323,7 +326,7 @@ class Activity {
|
|
|
323
326
|
return MDATA_SYMBOLS[keys_to_save].KEYS.map((key) => `output/metadata/${key}`);
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
async getState(
|
|
329
|
+
async getState() {
|
|
327
330
|
//assemble list of paths necessary to create 'job state' from the 'symbol hash'
|
|
328
331
|
const jobSymbolHashName = `$${this.config.subscribes}`;
|
|
329
332
|
const consumes: Consumes = {
|
|
@@ -348,10 +351,9 @@ class Activity {
|
|
|
348
351
|
}
|
|
349
352
|
TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
|
|
350
353
|
let { dad, jid } = this.context.metadata;
|
|
351
|
-
|
|
352
|
-
const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad);
|
|
354
|
+
const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad || '');
|
|
353
355
|
//`state` is a flat hash; context is a tree
|
|
354
|
-
const [state, status] = await this.store.getState(
|
|
356
|
+
const [state, status] = await this.store.getState(jid, consumes, dIds);
|
|
355
357
|
this.context = restoreHierarchy(state) as JobState;
|
|
356
358
|
this.initDimensionalAddress(dad);
|
|
357
359
|
this.initSelf(this.context);
|
|
@@ -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 { StreamCode, StreamStatus } from '../../types/stream';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Listens for `webhook`, `timehook`, and `cycle` (repeat) signals
|
|
@@ -108,7 +108,7 @@ class Hook extends Activity {
|
|
|
108
108
|
async registerHook(multi?: RedisMulti): Promise<string | void> {
|
|
109
109
|
if (this.config.hook?.topic) {
|
|
110
110
|
const signaler = new StoreSignaler(this.store, this.logger);
|
|
111
|
-
return await signaler.registerWebHook(this.config.hook.topic, this.context, multi);
|
|
111
|
+
return await signaler.registerWebHook(this.config.hook.topic, this.context, this.resolveDad(), multi);
|
|
112
112
|
} else if (this.config.sleep) {
|
|
113
113
|
const durationInSeconds = Pipe.resolve(this.config.sleep, this.context);
|
|
114
114
|
const jobId = this.context.metadata.jid;
|
|
@@ -119,19 +119,22 @@ class Hook extends Activity {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
async processWebHookEvent(): Promise<JobStatus | void> {
|
|
122
|
+
async processWebHookEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<JobStatus | void> {
|
|
123
123
|
this.logger.debug('hook-process-web-hook-event', {
|
|
124
124
|
topic: this.config.hook.topic,
|
|
125
|
-
aid: this.metadata.aid
|
|
125
|
+
aid: this.metadata.aid,
|
|
126
|
+
status,
|
|
127
|
+
code,
|
|
126
128
|
});
|
|
127
129
|
const signaler = new StoreSignaler(this.store, this.logger);
|
|
128
130
|
const data = { ...this.data };
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
const signal = await signaler.processWebHookSignal(this.config.hook.topic, data);
|
|
132
|
+
if (signal) {
|
|
133
|
+
const [jobId, aid, dad] = signal;
|
|
134
|
+
this.context.metadata.jid = jobId;
|
|
135
|
+
this.context.metadata.dad = dad;
|
|
136
|
+
await this.processEvent(status, code, 'hook');
|
|
137
|
+
if (code === 200) { //otherwise 202 for pending/keepalive
|
|
135
138
|
await signaler.deleteWebHookSignal(this.config.hook.topic, data);
|
|
136
139
|
}
|
|
137
140
|
} //else => already resolved
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
ActivityMetadata,
|
|
11
11
|
SignalActivity } from '../../types/activity';
|
|
12
12
|
import { JobState } from '../../types/job';
|
|
13
|
-
import { MultiResponseFlags
|
|
13
|
+
import { MultiResponseFlags } from '../../types/redis';
|
|
14
14
|
import { StringScalarType } from '../../types/serializer';
|
|
15
15
|
import { JobStatsInput } from '../../types/stats';
|
|
16
16
|
|
|
@@ -50,8 +50,11 @@ class Signal extends Activity {
|
|
|
50
50
|
await this.setStatus(this.adjacencyList.length - 1, multi);
|
|
51
51
|
const multiResponse = await multi.exec() as MultiResponseFlags;
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
if (this.config.subtype === 'all') {
|
|
54
|
+
await this.hookAll();
|
|
55
|
+
} else {
|
|
56
|
+
await this.hookOne();
|
|
57
|
+
}
|
|
55
58
|
|
|
56
59
|
//transition to adjacent activities
|
|
57
60
|
const jobStatus = this.resolveStatus(multiResponse);
|
|
@@ -92,6 +95,17 @@ class Signal extends Activity {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The signal activity will hook one
|
|
100
|
+
*/
|
|
101
|
+
async hookOne(): Promise<string> {
|
|
102
|
+
const topic = Pipe.resolve(this.config.topic, this.context);
|
|
103
|
+
const signalInputData = this.mapSignalData();
|
|
104
|
+
const status = Pipe.resolve(this.config.status, this.context);
|
|
105
|
+
const code = Pipe.resolve(this.config.code, this.context);
|
|
106
|
+
return await this.engine.hook(topic, signalInputData, status, code);
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
/**
|
|
96
110
|
* The signal activity will hook all paused jobs that share the same job key.
|
|
97
111
|
*/
|
|
@@ -1,55 +1,17 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
|
-
import { APP_ID, APP_VERSION, DEFAULT_COEFFICIENT,
|
|
2
|
+
import { APP_ID, APP_VERSION, DEFAULT_COEFFICIENT, getWorkflowYAML } from './factory';
|
|
3
3
|
import { WorkflowHandleService } from './handle';
|
|
4
4
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
5
5
|
import {
|
|
6
6
|
ClientConfig,
|
|
7
7
|
Connection,
|
|
8
|
+
HookOptions,
|
|
8
9
|
WorkflowOptions,
|
|
9
10
|
WorkflowSearchOptions} from '../../types/durable';
|
|
10
11
|
import { JobState } from '../../types/job';
|
|
11
12
|
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
|
-
*/
|
|
13
|
+
import { Search } from './search';
|
|
14
|
+
import { StreamStatus } from '../../types';
|
|
53
15
|
|
|
54
16
|
export class ClientService {
|
|
55
17
|
|
|
@@ -61,14 +23,14 @@ export class ClientService {
|
|
|
61
23
|
this.connection = config.connection;
|
|
62
24
|
}
|
|
63
25
|
|
|
64
|
-
getHotMeshClient = async (worflowTopic: string) => {
|
|
26
|
+
getHotMeshClient = async (worflowTopic: string, namespace?: string) => {
|
|
65
27
|
//NOTE: every unique topic inits a new engine
|
|
66
28
|
if (ClientService.instances.has(worflowTopic)) {
|
|
67
29
|
return await ClientService.instances.get(worflowTopic);
|
|
68
30
|
}
|
|
69
31
|
|
|
70
32
|
const hotMeshClient = HotMesh.init({
|
|
71
|
-
appId: APP_ID,
|
|
33
|
+
appId: namespace ?? APP_ID,
|
|
72
34
|
engine: {
|
|
73
35
|
redis: {
|
|
74
36
|
class: this.connection.class,
|
|
@@ -80,14 +42,14 @@ export class ClientService {
|
|
|
80
42
|
|
|
81
43
|
//since the YAML topic is dynamic, it MUST be manually created before use
|
|
82
44
|
const store = (await hotMeshClient).engine.store;
|
|
83
|
-
const params = { appId: APP_ID, topic: worflowTopic };
|
|
45
|
+
const params = { appId: namespace ?? APP_ID, topic: worflowTopic };
|
|
84
46
|
const streamKey = store.mintKey(KeyType.STREAMS, params);
|
|
85
47
|
try {
|
|
86
48
|
await store.xgroup('CREATE', streamKey, 'WORKER', '$', 'MKSTREAM');
|
|
87
49
|
} catch (err) {
|
|
88
50
|
//ignore if already exists
|
|
89
51
|
}
|
|
90
|
-
await this.activateWorkflow(await hotMeshClient);
|
|
52
|
+
await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
|
|
91
53
|
return hotMeshClient;
|
|
92
54
|
}
|
|
93
55
|
|
|
@@ -96,11 +58,11 @@ export class ClientService {
|
|
|
96
58
|
* this method will configure the search index for the workflow.
|
|
97
59
|
*/
|
|
98
60
|
configureSearchIndex = async (hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void> => {
|
|
99
|
-
if (search) {
|
|
61
|
+
if (search?.schema) {
|
|
100
62
|
const store = hotMeshClient.engine.store;
|
|
101
63
|
const schema: string[] = [];
|
|
102
64
|
for (const [key, value] of Object.entries(search.schema)) {
|
|
103
|
-
//prefix with
|
|
65
|
+
//prefix with an underscore (avoids collisions with hotmesh reserved symbols)
|
|
104
66
|
schema.push(`_${key}`);
|
|
105
67
|
schema.push(value.type);
|
|
106
68
|
if (value.sortable) {
|
|
@@ -134,7 +96,7 @@ export class ClientService {
|
|
|
134
96
|
const spn = options.workflowSpan;
|
|
135
97
|
//topic is concat of taskQueue and workflowName
|
|
136
98
|
const workflowTopic = `${taskQueueName}-${workflowName}`;
|
|
137
|
-
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
99
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
138
100
|
this.configureSearchIndex(hotMeshClient, options.search)
|
|
139
101
|
const payload = {
|
|
140
102
|
arguments: [...options.args],
|
|
@@ -145,25 +107,55 @@ export class ClientService {
|
|
|
145
107
|
}
|
|
146
108
|
const context = { metadata: { trc, spn }, data: {}};
|
|
147
109
|
const jobId = await hotMeshClient.pub(
|
|
148
|
-
|
|
110
|
+
`${options.namespace ?? APP_ID}.execute`,
|
|
149
111
|
payload,
|
|
150
112
|
context as JobState);
|
|
113
|
+
if (jobId && options.search?.data) {
|
|
114
|
+
//job successfully kicked off; there is default job data to persist
|
|
115
|
+
const searchSessionId = `-search-0`;
|
|
116
|
+
const search = new Search(jobId, hotMeshClient, searchSessionId);
|
|
117
|
+
for (const [key, value] of Object.entries(options.search.data)) {
|
|
118
|
+
search.set(key, value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
151
121
|
return new WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
152
122
|
},
|
|
153
123
|
|
|
154
|
-
|
|
155
|
-
|
|
124
|
+
/**
|
|
125
|
+
* send a message to a running workflow that is paused and awaiting the signal
|
|
126
|
+
*/
|
|
127
|
+
signal: async (signalId: string, data: Record<any, any>, namespace?: string): Promise<string> => {
|
|
128
|
+
const topic = `${namespace ?? APP_ID}.wfs.signal`;
|
|
129
|
+
return await (await this.getHotMeshClient(topic, namespace)).hook(topic, { id: signalId, data });
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* send a message to spawn an parallel in-process thread of execution
|
|
134
|
+
* with the same job state as the main thread but bound to a different
|
|
135
|
+
* handler function. All job state will be journaled to the same hash
|
|
136
|
+
* as is used by the main thread.
|
|
137
|
+
*/
|
|
138
|
+
hook: async (options: HookOptions): Promise<string> => {
|
|
139
|
+
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
140
|
+
const payload = {
|
|
141
|
+
arguments: [...options.args],
|
|
142
|
+
id: options.workflowId,
|
|
143
|
+
workflowTopic,
|
|
144
|
+
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
145
|
+
}
|
|
146
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
147
|
+
return await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, StreamStatus.PENDING, 202);
|
|
156
148
|
},
|
|
157
149
|
|
|
158
|
-
getHandle: async (taskQueue: string, workflowName: string, workflowId: string): Promise<WorkflowHandleService> => {
|
|
150
|
+
getHandle: async (taskQueue: string, workflowName: string, workflowId: string, namespace?: string): Promise<WorkflowHandleService> => {
|
|
159
151
|
const workflowTopic = `${taskQueue}-${workflowName}`;
|
|
160
|
-
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
152
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic, namespace);
|
|
161
153
|
return new WorkflowHandleService(hotMeshClient, workflowTopic, workflowId);
|
|
162
154
|
},
|
|
163
155
|
|
|
164
|
-
search: async (taskQueue: string, workflowName: string, index: string, ...query: string[]): Promise<string[]> => {
|
|
156
|
+
search: async (taskQueue: string, workflowName: string, namespace: null | string, index: string, ...query: string[]): Promise<string[]> => {
|
|
165
157
|
const workflowTopic = `${taskQueue}-${workflowName}`;
|
|
166
|
-
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
158
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic, namespace);
|
|
167
159
|
try {
|
|
168
160
|
return await this.search(hotMeshClient, index, query);
|
|
169
161
|
} catch (err) {
|