@hotmeshio/hotmesh 0.0.12 → 0.0.14
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 +2 -2
- package/build/modules/errors.d.ts +22 -1
- package/build/modules/errors.js +28 -1
- package/build/modules/utils.d.ts +2 -1
- package/build/modules/utils.js +5 -1
- package/build/package.json +7 -2
- package/build/services/activities/activity.d.ts +2 -0
- package/build/services/activities/activity.js +16 -10
- package/build/services/activities/await.d.ts +2 -6
- package/build/services/activities/await.js +12 -75
- package/build/services/activities/cycle.js +2 -2
- package/build/services/activities/index.d.ts +2 -2
- package/build/services/activities/index.js +2 -2
- package/build/services/activities/signal.d.ts +16 -0
- package/build/services/activities/signal.js +94 -0
- package/build/services/activities/trigger.js +4 -3
- package/build/services/activities/worker.d.ts +2 -1
- package/build/services/activities/worker.js +11 -6
- package/build/services/compiler/deployer.js +3 -1
- package/build/services/durable/client.d.ts +3 -2
- package/build/services/durable/client.js +39 -21
- package/build/services/durable/factory.d.ts +22 -18
- package/build/services/durable/factory.js +722 -50
- package/build/services/durable/handle.d.ts +1 -0
- package/build/services/durable/handle.js +5 -1
- package/build/services/durable/worker.d.ts +3 -8
- package/build/services/durable/worker.js +75 -73
- package/build/services/durable/workflow.d.ts +5 -0
- package/build/services/durable/workflow.js +93 -24
- package/build/services/engine/index.d.ts +6 -6
- package/build/services/engine/index.js +25 -15
- package/build/services/hotmesh/index.d.ts +2 -1
- package/build/services/hotmesh/index.js +3 -1
- package/build/services/mapper/index.js +1 -1
- package/build/services/pipe/functions/array.d.ts +1 -0
- package/build/services/pipe/functions/array.js +3 -0
- package/build/services/reporter/index.js +9 -2
- package/build/services/signaler/store.js +8 -3
- package/build/services/signaler/stream.js +3 -3
- package/build/services/store/clients/ioredis.js +15 -15
- package/build/services/store/clients/redis.js +18 -18
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/index.js +11 -3
- package/build/services/task/index.js +3 -3
- package/build/types/activity.d.ts +15 -6
- package/build/types/durable.d.ts +15 -2
- package/build/types/index.d.ts +2 -2
- package/build/types/stats.d.ts +1 -0
- package/modules/errors.ts +35 -0
- package/modules/utils.ts +5 -1
- package/package.json +7 -2
- package/services/activities/activity.ts +19 -9
- package/services/activities/await.ts +14 -90
- package/services/activities/cycle.ts +2 -2
- package/services/activities/index.ts +2 -2
- package/services/activities/signal.ts +124 -0
- package/services/activities/trigger.ts +4 -3
- package/services/activities/worker.ts +13 -13
- package/services/compiler/deployer.ts +3 -1
- package/services/durable/client.ts +48 -23
- package/services/durable/factory.ts +723 -49
- package/services/durable/handle.ts +6 -1
- package/services/durable/worker.ts +92 -79
- package/services/durable/workflow.ts +95 -25
- package/services/engine/index.ts +33 -24
- package/services/hotmesh/index.ts +7 -4
- package/services/mapper/index.ts +1 -1
- package/services/pipe/functions/array.ts +4 -0
- package/services/reporter/index.ts +10 -2
- package/services/signaler/store.ts +8 -3
- package/services/signaler/stream.ts +3 -3
- package/services/store/clients/ioredis.ts +15 -15
- package/services/store/clients/redis.ts +18 -18
- package/services/store/index.ts +12 -3
- package/services/task/index.ts +3 -3
- package/types/activity.ts +16 -7
- package/types/durable.ts +18 -1
- package/types/index.ts +2 -1
- package/types/stats.ts +1 -0
- package/build/services/activities/emit.d.ts +0 -9
- package/build/services/activities/emit.js +0 -13
- package/services/activities/emit.ts +0 -25
|
@@ -18,6 +18,7 @@ declare class HotMeshService {
|
|
|
18
18
|
verifyAndSetNamespace(namespace?: string): void;
|
|
19
19
|
verifyAndSetAppId(appId: string): void;
|
|
20
20
|
static init(config: HotMeshConfig): Promise<HotMeshService>;
|
|
21
|
+
static guid(): string;
|
|
21
22
|
initEngine(config: HotMeshConfig, logger: ILogger): Promise<void>;
|
|
22
23
|
initQuorum(config: HotMeshConfig, engine: EngineService, logger: ILogger): Promise<void>;
|
|
23
24
|
initWorkers(config: HotMeshConfig, logger: ILogger): Promise<void>;
|
|
@@ -37,7 +38,7 @@ declare class HotMeshService {
|
|
|
37
38
|
getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
|
|
38
39
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
39
40
|
scrub(jobId: string): Promise<void>;
|
|
40
|
-
hook(topic: string, data: JobData, dad?: string): Promise<
|
|
41
|
+
hook(topic: string, data: JobData, dad?: string): Promise<string>;
|
|
41
42
|
hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
42
43
|
stop(): Promise<void>;
|
|
43
44
|
compress(terms: string[]): Promise<boolean>;
|
|
@@ -48,6 +48,9 @@ class HotMeshService {
|
|
|
48
48
|
await instance.initWorkers(config, instance.logger);
|
|
49
49
|
return instance;
|
|
50
50
|
}
|
|
51
|
+
static guid() {
|
|
52
|
+
return (0, nanoid_1.nanoid)();
|
|
53
|
+
}
|
|
51
54
|
async initEngine(config, logger) {
|
|
52
55
|
if (config.engine) {
|
|
53
56
|
await connector_1.ConnectorService.initRedisClients(config.engine.redis?.class, config.engine.redis?.options, config.engine);
|
|
@@ -117,7 +120,6 @@ class HotMeshService {
|
|
|
117
120
|
}
|
|
118
121
|
// ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
|
|
119
122
|
async hook(topic, data, dad) {
|
|
120
|
-
//return collation int
|
|
121
123
|
return await this.engine?.hook(topic, data, dad);
|
|
122
124
|
}
|
|
123
125
|
async hookAll(hookTopic, data, query, queryFacets = []) {
|
|
@@ -49,7 +49,7 @@ class MapperService {
|
|
|
49
49
|
if (typeof transitionRule === 'boolean') {
|
|
50
50
|
return transitionRule;
|
|
51
51
|
}
|
|
52
|
-
if (code.toString() === (transitionRule.code || 200).toString()) {
|
|
52
|
+
if ((Array.isArray(transitionRule.code) && transitionRule.code.includes(code || 200)) || code.toString() === (transitionRule.code || 200).toString()) {
|
|
53
53
|
if (!transitionRule.match) {
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
declare class ArrayHandler {
|
|
2
2
|
get(array: any[], index: number): any;
|
|
3
|
+
length(array: any[]): any;
|
|
3
4
|
concat(array1: any[], array2: any[]): any[];
|
|
4
5
|
every(array: any[], callback: (value: any, index: number, array: any[]) => boolean): boolean;
|
|
5
6
|
filter(array: any[], callback: (value: any, index: number, array: any[]) => boolean): any[];
|
|
@@ -20,12 +20,16 @@ class ReporterService {
|
|
|
20
20
|
return statsResponse;
|
|
21
21
|
}
|
|
22
22
|
validateOptions(options) {
|
|
23
|
-
const { start, end, range } = options;
|
|
24
|
-
if (start && end && range || !start && !end && !range) {
|
|
23
|
+
const { start, end, range, granularity } = options;
|
|
24
|
+
if (granularity !== 'infinity' && (start && end && range || !start && !end && !range)) {
|
|
25
25
|
throw new Error('Invalid combination of start, end, and range values. Provide either start+end, end+range, or start+range.');
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
generateDateTimeSets(granularity, range, end, start) {
|
|
29
|
+
if (granularity === 'infinity') {
|
|
30
|
+
//if granularity is infinity, it means a date/time sequence/slice is not used to further segment the statistics
|
|
31
|
+
return ['0'];
|
|
32
|
+
}
|
|
29
33
|
if (!range) {
|
|
30
34
|
//pluck just a single value when no range provided
|
|
31
35
|
range = '0m';
|
|
@@ -155,6 +159,9 @@ class ReporterService {
|
|
|
155
159
|
return segments;
|
|
156
160
|
}
|
|
157
161
|
isoTimestampFromKeyTimestamp(hashKey) {
|
|
162
|
+
if (hashKey.endsWith(':')) {
|
|
163
|
+
return '0';
|
|
164
|
+
}
|
|
158
165
|
const keyTimestamp = hashKey.slice(-12);
|
|
159
166
|
const year = keyTimestamp.slice(0, 4);
|
|
160
167
|
const month = keyTimestamp.slice(4, 6);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.StoreSignaler = void 0;
|
|
4
|
+
const pipe_1 = require("../pipe");
|
|
4
5
|
class StoreSignaler {
|
|
5
6
|
constructor(store, logger) {
|
|
6
7
|
this.store = store;
|
|
@@ -13,10 +14,12 @@ class StoreSignaler {
|
|
|
13
14
|
async registerWebHook(topic, context, multi) {
|
|
14
15
|
const hookRule = await this.getHookRule(topic);
|
|
15
16
|
if (hookRule) {
|
|
17
|
+
const mapExpression = hookRule.conditions.match[0].expected;
|
|
18
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
16
19
|
const jobId = context.metadata.jid;
|
|
17
20
|
const hook = {
|
|
18
21
|
topic,
|
|
19
|
-
resolved
|
|
22
|
+
resolved,
|
|
20
23
|
jobId,
|
|
21
24
|
};
|
|
22
25
|
await this.store.setHookSignal(hook, multi);
|
|
@@ -29,8 +32,10 @@ class StoreSignaler {
|
|
|
29
32
|
async processWebHookSignal(topic, data) {
|
|
30
33
|
const hookRule = await this.getHookRule(topic);
|
|
31
34
|
if (hookRule) {
|
|
32
|
-
//
|
|
33
|
-
const
|
|
35
|
+
//NOTE: both formats are supported: $self.hook.data OR $hook.data
|
|
36
|
+
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
37
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
38
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
34
39
|
const jobId = await this.store.getHookSignal(topic, resolved);
|
|
35
40
|
return jobId;
|
|
36
41
|
}
|
|
@@ -120,9 +120,9 @@ class StreamSignaler {
|
|
|
120
120
|
try {
|
|
121
121
|
output = await callback(input);
|
|
122
122
|
}
|
|
123
|
-
catch (
|
|
124
|
-
this.logger.error(`stream-call-function-error`, {
|
|
125
|
-
output = this.structureUnhandledError(input,
|
|
123
|
+
catch (error) {
|
|
124
|
+
this.logger.error(`stream-call-function-error`, { error });
|
|
125
|
+
output = this.structureUnhandledError(input, error);
|
|
126
126
|
}
|
|
127
127
|
return output;
|
|
128
128
|
}
|
|
@@ -51,45 +51,45 @@ class IORedisStoreService extends index_1.StoreService {
|
|
|
51
51
|
try {
|
|
52
52
|
return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
|
|
53
53
|
}
|
|
54
|
-
catch (
|
|
55
|
-
this.logger.error(`Error publishing 'xadd'; key: ${key}`,
|
|
56
|
-
throw
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
|
|
56
|
+
throw error;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
async xpending(key, group, start, end, count, consumer) {
|
|
60
60
|
try {
|
|
61
61
|
return await this.redisClient.xpending(key, group, start, end, count, consumer);
|
|
62
62
|
}
|
|
63
|
-
catch (
|
|
64
|
-
this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`,
|
|
65
|
-
throw
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
|
|
65
|
+
throw error;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
async xclaim(key, group, consumer, minIdleTime, id, ...args) {
|
|
69
69
|
try {
|
|
70
70
|
return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args);
|
|
71
71
|
}
|
|
72
|
-
catch (
|
|
73
|
-
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`,
|
|
74
|
-
throw
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
|
|
74
|
+
throw error;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
async xack(key, group, id, multi) {
|
|
78
78
|
try {
|
|
79
79
|
return await (multi || this.redisClient).xack(key, group, id);
|
|
80
80
|
}
|
|
81
|
-
catch (
|
|
82
|
-
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`,
|
|
83
|
-
throw
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
|
|
83
|
+
throw error;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
async xdel(key, id, multi) {
|
|
87
87
|
try {
|
|
88
88
|
return await (multi || this.redisClient).xdel(key, id);
|
|
89
89
|
}
|
|
90
|
-
catch (
|
|
91
|
-
this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`,
|
|
92
|
-
throw
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
|
|
92
|
+
throw error;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -63,10 +63,10 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
63
63
|
try {
|
|
64
64
|
return (await this.redisClient.sendCommand(['XGROUP', 'CREATE', key, groupName, id, ...args])) === 1;
|
|
65
65
|
}
|
|
66
|
-
catch (
|
|
66
|
+
catch (error) {
|
|
67
67
|
const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
|
|
68
|
-
this.logger.warn(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`,
|
|
69
|
-
throw
|
|
68
|
+
this.logger.warn(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
|
|
69
|
+
throw error;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
async xadd(key, id, ...args) {
|
|
@@ -77,9 +77,9 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
77
77
|
try {
|
|
78
78
|
return await (multi || this.redisClient).XADD(key, id, { [args[0]]: args[1] });
|
|
79
79
|
}
|
|
80
|
-
catch (
|
|
81
|
-
this.logger.error(`Error publishing 'xadd'; key: ${key}`,
|
|
82
|
-
throw
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
|
|
82
|
+
throw error;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
async xpending(key, group, start, end, count, consumer) {
|
|
@@ -95,18 +95,18 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
95
95
|
args.push(consumer);
|
|
96
96
|
return await this.redisClient.sendCommand(['XPENDING', ...args]);
|
|
97
97
|
}
|
|
98
|
-
catch (
|
|
99
|
-
this.logger.error(`Error in retrieving pending messages for group: ${group} in key: ${key}`,
|
|
100
|
-
throw
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.logger.error(`Error in retrieving pending messages for group: ${group} in key: ${key}`, { error });
|
|
100
|
+
throw error;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
async xclaim(key, group, consumer, minIdleTime, id, ...args) {
|
|
104
104
|
try {
|
|
105
105
|
return await this.redisClient.sendCommand(['XCLAIM', key, group, consumer, minIdleTime.toString(), id, ...args]);
|
|
106
106
|
}
|
|
107
|
-
catch (
|
|
108
|
-
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`,
|
|
109
|
-
throw
|
|
107
|
+
catch (error) {
|
|
108
|
+
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
|
|
109
|
+
throw error;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
async xack(key, group, id, multi) {
|
|
@@ -119,9 +119,9 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
119
119
|
return await this.redisClient[this.commands.xack](key, group, id);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
catch (
|
|
123
|
-
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`,
|
|
124
|
-
throw
|
|
122
|
+
catch (error) {
|
|
123
|
+
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
|
|
124
|
+
throw error;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
async xdel(key, id, multi) {
|
|
@@ -134,9 +134,9 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
134
134
|
return await this.redisClient[this.commands.xdel](key, id);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
catch (
|
|
138
|
-
this.logger.error(`Error in deleting messages with ids: ${id} for key: ${key}`,
|
|
139
|
-
throw
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.logger.error(`Error in deleting messages with ids: ${id} for key: ${key}`, { error });
|
|
139
|
+
throw error;
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -78,7 +78,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
78
78
|
deleteHookSignal(topic: string, resolved: string): Promise<number | undefined>;
|
|
79
79
|
addTaskQueues(keys: string[]): Promise<void>;
|
|
80
80
|
getActiveTaskQueue(): Promise<string | null>;
|
|
81
|
-
deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string): Promise<void>;
|
|
81
|
+
deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise<void>;
|
|
82
82
|
processTaskQueue(sourceKey: string, destinationKey: string): Promise<any>;
|
|
83
83
|
expireJob(jobId: string, inSeconds: number): Promise<void>;
|
|
84
84
|
registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'cron', deletionTime: number, multi?: U): Promise<void>;
|
|
@@ -355,7 +355,8 @@ class StoreService {
|
|
|
355
355
|
const output = {};
|
|
356
356
|
for (const [index, result] of results.entries()) {
|
|
357
357
|
const key = indexKeys[index];
|
|
358
|
-
|
|
358
|
+
//todo: resolve this discrepancy between redis/ioredis
|
|
359
|
+
const idsList = result[1] || result;
|
|
359
360
|
if (idsList && idsList.length > 0) {
|
|
360
361
|
output[key] = idsList;
|
|
361
362
|
}
|
|
@@ -601,11 +602,18 @@ class StoreService {
|
|
|
601
602
|
}
|
|
602
603
|
return workItemKey;
|
|
603
604
|
}
|
|
604
|
-
async deleteProcessedTaskQueue(workItemKey, key, processedKey) {
|
|
605
|
+
async deleteProcessedTaskQueue(workItemKey, key, processedKey, scrub = false) {
|
|
605
606
|
const zsetKey = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId });
|
|
606
607
|
const didRemove = await this.redisClient[this.commands.zrem](zsetKey, workItemKey);
|
|
607
608
|
if (didRemove) {
|
|
608
|
-
|
|
609
|
+
if (scrub) {
|
|
610
|
+
//indexes can be designed to be self-cleaning; `engine.hookAll` exposes this option
|
|
611
|
+
this.redisClient[this.commands.expire](processedKey, 0);
|
|
612
|
+
this.redisClient[this.commands.expire](key.split(":").slice(0, 5).join(":"), 0);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
await this.redisClient[this.commands.rename](processedKey, key);
|
|
616
|
+
}
|
|
609
617
|
}
|
|
610
618
|
this.cache.removeWorkItem(this.appId);
|
|
611
619
|
}
|
|
@@ -15,16 +15,16 @@ class TaskService {
|
|
|
15
15
|
async processWebHooks(hookEventCallback) {
|
|
16
16
|
const workItemKey = await this.store.getActiveTaskQueue();
|
|
17
17
|
if (workItemKey) {
|
|
18
|
-
const [topic, sourceKey, ...sdata] = workItemKey.split('::');
|
|
18
|
+
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split('::');
|
|
19
19
|
const data = JSON.parse(sdata.join('::'));
|
|
20
20
|
const destinationKey = `${sourceKey}:processed`;
|
|
21
21
|
const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
|
|
22
22
|
if (jobId) {
|
|
23
|
+
//todo: don't use 'id', make configurable using hook rule
|
|
23
24
|
await hookEventCallback(topic, { ...data, id: jobId });
|
|
24
|
-
//todo: do final checksum count (values are tracked in the stats hash)
|
|
25
25
|
}
|
|
26
26
|
else {
|
|
27
|
-
await this.store.deleteProcessedTaskQueue(workItemKey, sourceKey, destinationKey);
|
|
27
|
+
await this.store.deleteProcessedTaskQueue(workItemKey, sourceKey, destinationKey, scrub === 'true');
|
|
28
28
|
}
|
|
29
29
|
setImmediate(() => this.processWebHooks(hookEventCallback));
|
|
30
30
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MetricTypes } from "./stats";
|
|
2
2
|
import { StreamRetryPolicy } from "./stream";
|
|
3
|
-
type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle';
|
|
3
|
+
type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle' | 'signal';
|
|
4
4
|
type Consumes = Record<string, string[]>;
|
|
5
5
|
interface BaseActivity {
|
|
6
6
|
title?: string;
|
|
@@ -12,6 +12,7 @@ interface BaseActivity {
|
|
|
12
12
|
job?: Record<string, any>;
|
|
13
13
|
hook?: Record<string, any>;
|
|
14
14
|
telemetry?: Record<string, any>;
|
|
15
|
+
emit?: boolean;
|
|
15
16
|
sleep?: number;
|
|
16
17
|
expire?: number;
|
|
17
18
|
retry?: StreamRetryPolicy;
|
|
@@ -37,6 +38,7 @@ interface TriggerActivityStats {
|
|
|
37
38
|
key?: {
|
|
38
39
|
[key: string]: unknown;
|
|
39
40
|
} | string;
|
|
41
|
+
granularity?: string;
|
|
40
42
|
measures?: Measure[];
|
|
41
43
|
}
|
|
42
44
|
interface TriggerActivity extends BaseActivity {
|
|
@@ -53,17 +55,24 @@ interface WorkerActivity extends BaseActivity {
|
|
|
53
55
|
topic: string;
|
|
54
56
|
timeout: number;
|
|
55
57
|
}
|
|
56
|
-
interface EmitActivity extends BaseActivity {
|
|
57
|
-
type: 'emit';
|
|
58
|
-
}
|
|
59
58
|
interface CycleActivity extends BaseActivity {
|
|
60
59
|
type: 'cycle';
|
|
61
60
|
ancestor: string;
|
|
62
61
|
}
|
|
62
|
+
interface SignalActivity extends BaseActivity {
|
|
63
|
+
type: 'signal';
|
|
64
|
+
subtype: 'one' | 'all';
|
|
65
|
+
topic: string;
|
|
66
|
+
key_name: string;
|
|
67
|
+
key_value: string;
|
|
68
|
+
scrub: boolean;
|
|
69
|
+
signal?: Record<string, any>;
|
|
70
|
+
resolver?: Record<string, any>;
|
|
71
|
+
}
|
|
63
72
|
interface IterateActivity extends BaseActivity {
|
|
64
73
|
type: 'iterate';
|
|
65
74
|
}
|
|
66
|
-
type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity |
|
|
75
|
+
type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | IterateActivity;
|
|
67
76
|
type ActivityData = Record<string, any>;
|
|
68
77
|
type ActivityMetadata = {
|
|
69
78
|
aid: string;
|
|
@@ -89,4 +98,4 @@ type ActivityDataType = {
|
|
|
89
98
|
hook?: Record<string, unknown>;
|
|
90
99
|
};
|
|
91
100
|
type ActivityLeg = 1 | 2;
|
|
92
|
-
export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity,
|
|
101
|
+
export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, SignalActivity, BaseActivity, IterateActivity, TriggerActivity, WorkerActivity };
|
package/build/types/durable.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { RedisClass, RedisOptions } from './redis';
|
|
2
|
+
type WorkflowConfig = {
|
|
3
|
+
backoffCoefficient?: number;
|
|
4
|
+
maximumAttempts?: number;
|
|
5
|
+
maximumInterval?: string;
|
|
6
|
+
initialInterval?: string;
|
|
7
|
+
};
|
|
2
8
|
type WorkflowOptions = {
|
|
3
9
|
taskQueue: string;
|
|
4
10
|
args: any[];
|
|
@@ -6,6 +12,13 @@ type WorkflowOptions = {
|
|
|
6
12
|
workflowName?: string;
|
|
7
13
|
workflowTrace?: string;
|
|
8
14
|
workflowSpan?: string;
|
|
15
|
+
config?: WorkflowConfig;
|
|
16
|
+
};
|
|
17
|
+
type SignalOptions = {
|
|
18
|
+
taskQueue: string;
|
|
19
|
+
data: Record<string, any>;
|
|
20
|
+
workflowId: string;
|
|
21
|
+
workflowName?: string;
|
|
9
22
|
};
|
|
10
23
|
type ActivityWorkflowDataType = {
|
|
11
24
|
activityName: string;
|
|
@@ -39,7 +52,7 @@ type WorkerConfig = {
|
|
|
39
52
|
};
|
|
40
53
|
type WorkerOptions = {
|
|
41
54
|
maxSystemRetries?: number;
|
|
42
|
-
|
|
55
|
+
backoffCoefficient?: number;
|
|
43
56
|
};
|
|
44
57
|
type ContextType = {
|
|
45
58
|
workflowId: string;
|
|
@@ -59,4 +72,4 @@ type ActivityConfig = {
|
|
|
59
72
|
maximumInterval: string;
|
|
60
73
|
};
|
|
61
74
|
};
|
|
62
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, };
|
|
75
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, SignalOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, };
|
package/build/types/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity,
|
|
1
|
+
export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, WorkerActivity, IterateActivity, SignalActivity, TriggerActivity, TriggerActivityStats } from './activity';
|
|
2
2
|
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, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
6
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkflowConfig, WorkerConfig, WorkerOptions, 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/build/types/stats.d.ts
CHANGED
package/modules/errors.ts
CHANGED
|
@@ -12,6 +12,38 @@ class SetStateError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
//thrown when a signal set is incomplete but already configured
|
|
16
|
+
//if a waitFor set has 'n' items, this can be thrown `n - 1` times
|
|
17
|
+
class DurableIncompleteSignalError extends Error {
|
|
18
|
+
code: number;
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = 593;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//the original waitFor error that is thrown for a new signal set
|
|
26
|
+
class DurableWaitForSignalError extends Error {
|
|
27
|
+
code: number;
|
|
28
|
+
signals: {signal: string, index: number}[]; //signal id and execution order in the workflow
|
|
29
|
+
constructor(message: string, signals: {signal: string, index: number}[]) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.signals = signals;
|
|
32
|
+
this.code = 594;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class DurableSleepError extends Error {
|
|
37
|
+
code: number;
|
|
38
|
+
duration: number; //seconds
|
|
39
|
+
index: number; //execution order in the workflow
|
|
40
|
+
constructor(message: string, duration: number, index: number) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.duration = duration;
|
|
43
|
+
this.index = index;
|
|
44
|
+
this.code = 595;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
15
47
|
class DurableTimeoutError extends Error {
|
|
16
48
|
code: number;
|
|
17
49
|
constructor(message: string) {
|
|
@@ -87,6 +119,9 @@ export {
|
|
|
87
119
|
DurableMaxedError,
|
|
88
120
|
DurableFatalError,
|
|
89
121
|
DurableRetryError,
|
|
122
|
+
DurableWaitForSignalError,
|
|
123
|
+
DurableIncompleteSignalError,
|
|
124
|
+
DurableSleepError,
|
|
90
125
|
DuplicateJobError,
|
|
91
126
|
GetStateError,
|
|
92
127
|
SetStateError,
|
package/modules/utils.ts
CHANGED
|
@@ -86,9 +86,13 @@ export async function getSubscriptionTopic(activityId: string, store: StoreServi
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* returns the 12-digit format of the iso timestamp (e.g, 202101010000)
|
|
89
|
+
* returns the 12-digit format of the iso timestamp (e.g, 202101010000); returns
|
|
90
|
+
* an empty string if overridden by the user to not segment by time (infinity).
|
|
90
91
|
*/
|
|
91
92
|
export function getTimeSeries(granularity: string): string {
|
|
93
|
+
if (granularity.toString() === 'infinity') {
|
|
94
|
+
return '0';
|
|
95
|
+
}
|
|
92
96
|
const now = new Date();
|
|
93
97
|
const granularityUnit = granularity.slice(-1);
|
|
94
98
|
const granularityValue = parseInt(granularity.slice(0, -1), 10);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.14",
|
|
4
|
+
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
7
|
"repository": {
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
27
27
|
"test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
28
28
|
"test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
29
|
+
"test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
30
|
+
"test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
|
+
"test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
29
32
|
"test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
30
33
|
"test:sequence": "NODE_ENV=test jest ./tests/functional/sequence/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
34
|
"test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -44,6 +47,8 @@
|
|
|
44
47
|
"test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
45
48
|
"test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
46
49
|
"test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
50
|
+
"test:durable:sleep": "NODE_ENV=test jest ./tests/durable/sleep/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
51
|
+
"test:durable:signal": "NODE_ENV=test jest ./tests/durable/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
47
52
|
"test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
48
53
|
"test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
|
|
49
54
|
},
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
StreamDataType,
|
|
31
31
|
StreamStatus } from '../../types/stream';
|
|
32
32
|
import { TransitionRule } from '../../types/transition';
|
|
33
|
+
import { HookRule } from '../../types/hook';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* The base class for all activities
|
|
@@ -114,9 +115,9 @@ class Activity {
|
|
|
114
115
|
return this.context.metadata.aid;
|
|
115
116
|
} catch (error) {
|
|
116
117
|
if (error instanceof GetStateError) {
|
|
117
|
-
this.logger.error('activity-get-state-error', error);
|
|
118
|
+
this.logger.error('activity-get-state-error', { error });
|
|
118
119
|
} else {
|
|
119
|
-
this.logger.error('activity-process-error', error);
|
|
120
|
+
this.logger.error('activity-process-error', { error });
|
|
120
121
|
}
|
|
121
122
|
telemetry.setActivityError(error.message);
|
|
122
123
|
throw error;
|
|
@@ -135,6 +136,11 @@ class Activity {
|
|
|
135
136
|
return !!(this.config.hook?.topic || this.config.sleep);
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
async getHookRule(topic: string): Promise<HookRule | undefined> {
|
|
140
|
+
const rules = await this.store.getHookRules();
|
|
141
|
+
return rules?.[topic]?.[0] as HookRule;
|
|
142
|
+
}
|
|
143
|
+
|
|
138
144
|
async registerHook(multi?: RedisMulti): Promise<string | void> {
|
|
139
145
|
if (this.config.hook?.topic) {
|
|
140
146
|
const signaler = new StoreSignaler(this.store, this.logger);
|
|
@@ -203,7 +209,7 @@ class Activity {
|
|
|
203
209
|
telemetry.setActivityAttributes(attrs);
|
|
204
210
|
return jobStatus as number;
|
|
205
211
|
} catch (error) {
|
|
206
|
-
this.logger.error('engine-process-hook-event-error', error);
|
|
212
|
+
this.logger.error('engine-process-hook-event-error', { error });
|
|
207
213
|
telemetry.setActivityError(error.message);
|
|
208
214
|
throw error;
|
|
209
215
|
} finally {
|
|
@@ -245,11 +251,11 @@ class Activity {
|
|
|
245
251
|
}
|
|
246
252
|
this.transitionAdjacent(multiResponse, telemetry);
|
|
247
253
|
} catch (error) {
|
|
248
|
-
this.logger.error('activity-process-event-error', error);
|
|
249
|
-
telemetry.setActivityError(error.message);
|
|
254
|
+
this.logger.error('activity-process-event-error', { error });
|
|
255
|
+
telemetry && telemetry.setActivityError(error.message);
|
|
250
256
|
throw error;
|
|
251
257
|
} finally {
|
|
252
|
-
telemetry.endActivitySpan();
|
|
258
|
+
telemetry && telemetry.endActivitySpan();
|
|
253
259
|
this.logger.debug('activity-process-event-end', { jid, aid });
|
|
254
260
|
}
|
|
255
261
|
}
|
|
@@ -570,14 +576,18 @@ class Activity {
|
|
|
570
576
|
|
|
571
577
|
async transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]> {
|
|
572
578
|
let mIds: string[] = [];
|
|
573
|
-
|
|
579
|
+
if (jobStatus <= 0 || this.config.emit) {
|
|
580
|
+
//activity should not send 'emit' if the job is truly over
|
|
581
|
+
const isTrueEmit = jobStatus > 0;
|
|
582
|
+
await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (adjacencyList.length && jobStatus > 0) {
|
|
574
586
|
const multi = this.store.getMulti();
|
|
575
587
|
for (const execSignal of adjacencyList) {
|
|
576
588
|
await this.engine.streamSignaler?.publishMessage(null, execSignal, multi);
|
|
577
589
|
}
|
|
578
590
|
mIds = (await multi.exec()) as string[];
|
|
579
|
-
} else if (jobStatus <= 0) {
|
|
580
|
-
await this.engine.runJobCompletionTasks(this.context);
|
|
581
591
|
}
|
|
582
592
|
return mIds;
|
|
583
593
|
}
|