@hotmeshio/hotmesh 0.0.34 → 0.0.36
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 +30 -18
- package/build/modules/enums.d.ts +22 -0
- package/build/modules/enums.js +29 -0
- package/build/modules/errors.d.ts +10 -2
- package/build/modules/errors.js +14 -3
- package/build/modules/key.d.ts +16 -15
- package/build/modules/key.js +18 -15
- package/build/modules/utils.d.ts +1 -0
- package/build/modules/utils.js +6 -1
- package/build/package.json +3 -1
- package/build/services/activities/activity.d.ts +5 -0
- package/build/services/activities/activity.js +27 -6
- package/build/services/activities/await.js +11 -3
- package/build/services/activities/cycle.js +10 -2
- package/build/services/activities/hook.js +8 -2
- package/build/services/activities/index.d.ts +2 -2
- package/build/services/activities/index.js +2 -2
- package/build/services/activities/interrupt.d.ts +16 -0
- package/build/services/activities/interrupt.js +129 -0
- package/build/services/activities/signal.js +9 -2
- package/build/services/activities/trigger.d.ts +4 -0
- package/build/services/activities/trigger.js +14 -4
- package/build/services/activities/worker.js +10 -2
- package/build/services/collator/index.d.ts +4 -0
- package/build/services/collator/index.js +8 -0
- package/build/services/compiler/deployer.js +1 -3
- package/build/services/connector/index.js +2 -3
- package/build/services/durable/client.js +7 -3
- package/build/services/durable/factory.js +65 -284
- package/build/services/durable/handle.d.ts +37 -0
- package/build/services/durable/handle.js +52 -9
- package/build/services/durable/meshos.js +2 -2
- package/build/services/durable/worker.js +9 -2
- package/build/services/durable/workflow.d.ts +24 -0
- package/build/services/durable/workflow.js +56 -1
- package/build/services/engine/index.d.ts +14 -6
- package/build/services/engine/index.js +52 -27
- package/build/services/hotmesh/index.d.ts +3 -1
- package/build/services/hotmesh/index.js +11 -3
- package/build/services/quorum/index.d.ts +1 -0
- package/build/services/quorum/index.js +10 -0
- package/build/services/signaler/stream.js +25 -29
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +40 -4
- package/build/services/store/index.js +114 -9
- package/build/services/task/index.d.ts +5 -4
- package/build/services/task/index.js +12 -14
- package/build/types/activity.d.ts +35 -5
- package/build/types/durable.d.ts +4 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/job.d.ts +18 -1
- package/build/types/quorum.d.ts +11 -7
- package/build/types/stream.d.ts +4 -1
- package/build/types/stream.js +2 -0
- package/modules/enums.ts +32 -0
- package/modules/errors.ts +24 -9
- package/modules/key.ts +4 -1
- package/modules/utils.ts +5 -0
- package/package.json +3 -1
- package/services/activities/activity.ts +34 -8
- package/services/activities/await.ts +11 -4
- package/services/activities/cycle.ts +10 -3
- package/services/activities/hook.ts +8 -3
- package/services/activities/index.ts +2 -2
- package/services/activities/interrupt.ts +159 -0
- package/services/activities/signal.ts +9 -3
- package/services/activities/trigger.ts +21 -5
- package/services/activities/worker.ts +10 -3
- package/services/collator/index.ts +10 -1
- package/services/compiler/deployer.ts +1 -3
- package/services/connector/index.ts +3 -5
- package/services/durable/client.ts +8 -4
- package/services/durable/factory.ts +65 -284
- package/services/durable/handle.ts +55 -9
- package/services/durable/meshos.ts +2 -3
- package/services/durable/worker.ts +9 -2
- package/services/durable/workflow.ts +66 -2
- package/services/engine/index.ts +74 -26
- package/services/hotmesh/index.ts +14 -4
- package/services/quorum/index.ts +9 -0
- package/services/signaler/stream.ts +27 -24
- package/services/store/clients/ioredis.ts +1 -0
- package/services/store/index.ts +119 -11
- package/services/task/index.ts +18 -18
- package/types/activity.ts +38 -8
- package/types/durable.ts +8 -4
- package/types/index.ts +1 -1
- package/types/job.ts +30 -1
- package/types/quorum.ts +13 -8
- package/types/stream.ts +3 -0
- package/build/services/activities/iterate.d.ts +0 -9
- package/build/services/activities/iterate.js +0 -13
- package/services/activities/iterate.ts +0 -26
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.StreamSignaler = void 0;
|
|
4
|
+
const enums_1 = require("../../modules/enums");
|
|
4
5
|
const key_1 = require("../../modules/key");
|
|
5
6
|
const utils_1 = require("../../modules/utils");
|
|
6
7
|
const telemetry_1 = require("../telemetry");
|
|
7
8
|
const stream_1 = require("../../types/stream");
|
|
8
|
-
const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
|
|
9
|
-
const MAX_TIMEOUT_MS = 60000;
|
|
10
|
-
const GRADUATED_INTERVAL_MS = 5000;
|
|
11
|
-
const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
|
|
12
|
-
const TEST_BLOCK_DURATION = 1000; //Set to `1000` so tests can interrupt quickly
|
|
13
|
-
const BLOCK_TIME_MS = process.env.NODE_ENV === 'test' ? TEST_BLOCK_DURATION : BLOCK_DURATION;
|
|
14
|
-
const SYSTEM_STATUS_CODE = 999;
|
|
15
|
-
const UNKNOWN_STATUS_CODE = 500;
|
|
16
|
-
const UNKNOWN_STATUS_MESSAGE = 'unknown';
|
|
17
|
-
const XCLAIM_DELAY_MS = 1000 * 60; //max time a message can be unacked before it is claimed by another
|
|
18
|
-
const XCLAIM_COUNT = 3; //max number of times a message can be claimed by another before it is dead-lettered
|
|
19
|
-
const XPENDING_COUNT = 10;
|
|
20
9
|
class StreamSignaler {
|
|
21
10
|
constructor(config, stream, store, logger) {
|
|
22
11
|
this.throttle = 0;
|
|
@@ -28,8 +17,8 @@ class StreamSignaler {
|
|
|
28
17
|
this.topic = config.topic;
|
|
29
18
|
this.stream = stream;
|
|
30
19
|
this.store = store;
|
|
31
|
-
this.reclaimDelay = config.reclaimDelay || XCLAIM_DELAY_MS;
|
|
32
|
-
this.reclaimCount = config.reclaimCount || XCLAIM_COUNT;
|
|
20
|
+
this.reclaimDelay = config.reclaimDelay || enums_1.XCLAIM_DELAY_MS;
|
|
21
|
+
this.reclaimCount = config.reclaimCount || enums_1.XCLAIM_COUNT;
|
|
33
22
|
this.logger = logger;
|
|
34
23
|
}
|
|
35
24
|
async createGroup(stream, group) {
|
|
@@ -59,7 +48,7 @@ class StreamSignaler {
|
|
|
59
48
|
return;
|
|
60
49
|
}
|
|
61
50
|
try {
|
|
62
|
-
const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', BLOCK_TIME_MS, 'STREAMS', stream, '>');
|
|
51
|
+
const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', enums_1.BLOCK_TIME_MS, 'STREAMS', stream, '>');
|
|
63
52
|
if (this.isStreamMessage(result)) {
|
|
64
53
|
const [[, messages]] = result;
|
|
65
54
|
for (const [id, message] of messages) {
|
|
@@ -81,7 +70,7 @@ class StreamSignaler {
|
|
|
81
70
|
if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
|
|
82
71
|
this.logger.error(`stream-consume-message-error`, { err, stream, group, consumer });
|
|
83
72
|
this.errorCount++;
|
|
84
|
-
const timeout = Math.min(GRADUATED_INTERVAL_MS * (2 ** this.errorCount), MAX_TIMEOUT_MS);
|
|
73
|
+
const timeout = Math.min(enums_1.GRADUATED_INTERVAL_MS * (2 ** this.errorCount), enums_1.MAX_TIMEOUT_MS);
|
|
85
74
|
setTimeout(consume.bind(this), timeout);
|
|
86
75
|
}
|
|
87
76
|
}
|
|
@@ -101,7 +90,7 @@ class StreamSignaler {
|
|
|
101
90
|
telemetry.startStreamSpan(input, this.role);
|
|
102
91
|
output = await this.execStreamLeg(input, stream, id, callback.bind(this));
|
|
103
92
|
if (output?.status === stream_1.StreamStatus.ERROR) {
|
|
104
|
-
telemetry.setStreamError(`Function Status Code ${output.code ||
|
|
93
|
+
telemetry.setStreamError(`Function Status Code ${output.code || enums_1.STATUS_CODE_UNKNOWN}`);
|
|
105
94
|
}
|
|
106
95
|
this.errorCount = 0;
|
|
107
96
|
}
|
|
@@ -140,6 +129,7 @@ class StreamSignaler {
|
|
|
140
129
|
await (0, utils_1.sleepFor)(timeout);
|
|
141
130
|
return await this.publishMessage(input.metadata.topic, {
|
|
142
131
|
data: input.data,
|
|
132
|
+
//note: retain guid (this is a retry attempt)
|
|
143
133
|
metadata: { ...input.metadata, try: (input.metadata.try || 0) + 1 },
|
|
144
134
|
policies: input.policies,
|
|
145
135
|
});
|
|
@@ -148,6 +138,12 @@ class StreamSignaler {
|
|
|
148
138
|
output = this.structureError(input, output);
|
|
149
139
|
}
|
|
150
140
|
}
|
|
141
|
+
else if (typeof output.metadata !== 'object') {
|
|
142
|
+
output.metadata = { ...input.metadata, guid: (0, utils_1.guid)() };
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
output.metadata.guid = (0, utils_1.guid)();
|
|
146
|
+
}
|
|
151
147
|
output.type = stream_1.StreamDataType.RESPONSE;
|
|
152
148
|
return await this.publishMessage(null, output);
|
|
153
149
|
}
|
|
@@ -157,7 +153,7 @@ class StreamSignaler {
|
|
|
157
153
|
const errorCode = output.code.toString();
|
|
158
154
|
const policy = policies?.[errorCode];
|
|
159
155
|
const maxRetries = policy?.[0];
|
|
160
|
-
const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
|
|
156
|
+
const tryCount = Math.min(input.metadata.try || 0, enums_1.MAX_RETRIES);
|
|
161
157
|
//only possible values for maxRetries are 1, 2, 3
|
|
162
158
|
//only possible values for tryCount are 0, 1, 2
|
|
163
159
|
if (maxRetries > tryCount) {
|
|
@@ -172,7 +168,7 @@ class StreamSignaler {
|
|
|
172
168
|
error.message = err.message;
|
|
173
169
|
}
|
|
174
170
|
else {
|
|
175
|
-
error.message =
|
|
171
|
+
error.message = enums_1.STATUS_MESSAGE_UNKNOWN;
|
|
176
172
|
}
|
|
177
173
|
if (typeof err.stack === 'string') {
|
|
178
174
|
error.stack = err.stack;
|
|
@@ -182,17 +178,17 @@ class StreamSignaler {
|
|
|
182
178
|
}
|
|
183
179
|
return {
|
|
184
180
|
status: 'error',
|
|
185
|
-
code:
|
|
186
|
-
metadata: { ...input.metadata },
|
|
181
|
+
code: enums_1.STATUS_CODE_UNKNOWN,
|
|
182
|
+
metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
|
|
187
183
|
data: error
|
|
188
184
|
};
|
|
189
185
|
}
|
|
190
186
|
structureUnacknowledgedError(input) {
|
|
191
187
|
const message = 'stream message max delivery count exceeded';
|
|
192
|
-
const code =
|
|
188
|
+
const code = enums_1.STATUS_CODE_UNACKED;
|
|
193
189
|
const data = { message, code };
|
|
194
190
|
const output = {
|
|
195
|
-
metadata: { ...input.metadata },
|
|
191
|
+
metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
|
|
196
192
|
status: stream_1.StreamStatus.ERROR,
|
|
197
193
|
code,
|
|
198
194
|
data,
|
|
@@ -202,9 +198,9 @@ class StreamSignaler {
|
|
|
202
198
|
return output;
|
|
203
199
|
}
|
|
204
200
|
structureError(input, output) {
|
|
205
|
-
const message = output.data?.message ? output.data?.message.toString() :
|
|
201
|
+
const message = output.data?.message ? output.data?.message.toString() : enums_1.STATUS_MESSAGE_UNKNOWN;
|
|
206
202
|
const statusCode = output.code || output.data?.code;
|
|
207
|
-
const code = isNaN(statusCode) ?
|
|
203
|
+
const code = isNaN(statusCode) ? enums_1.STATUS_CODE_UNKNOWN : parseInt(statusCode.toString());
|
|
208
204
|
const data = { message, code };
|
|
209
205
|
if (typeof output.data?.error === 'object') {
|
|
210
206
|
data.error = { ...output.data.error };
|
|
@@ -212,7 +208,7 @@ class StreamSignaler {
|
|
|
212
208
|
return {
|
|
213
209
|
status: stream_1.StreamStatus.ERROR,
|
|
214
210
|
code,
|
|
215
|
-
metadata: { ...input.metadata },
|
|
211
|
+
metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
|
|
216
212
|
data
|
|
217
213
|
};
|
|
218
214
|
}
|
|
@@ -220,7 +216,7 @@ class StreamSignaler {
|
|
|
220
216
|
for (const instance of [...StreamSignaler.signalers]) {
|
|
221
217
|
instance.stopConsuming();
|
|
222
218
|
}
|
|
223
|
-
await (0, utils_1.sleepFor)(BLOCK_TIME_MS);
|
|
219
|
+
await (0, utils_1.sleepFor)(enums_1.BLOCK_TIME_MS);
|
|
224
220
|
}
|
|
225
221
|
async stopConsuming() {
|
|
226
222
|
this.shouldConsume = false;
|
|
@@ -240,7 +236,7 @@ class StreamSignaler {
|
|
|
240
236
|
this.throttle = delayInMillis;
|
|
241
237
|
this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
|
|
242
238
|
}
|
|
243
|
-
async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = XPENDING_COUNT) {
|
|
239
|
+
async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = enums_1.XPENDING_COUNT) {
|
|
244
240
|
let pendingMessages = [];
|
|
245
241
|
const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
|
|
246
242
|
for (const pendingMessageInfo of pendingMessagesInfo) {
|
|
@@ -269,7 +265,7 @@ class StreamSignaler {
|
|
|
269
265
|
// ii) corrupt hardware/network/transport/etc
|
|
270
266
|
// 3b) system error: Redis unable to accept `xadd` request
|
|
271
267
|
// 4c) system error: Redis unable to accept `xdel`/`xack` request
|
|
272
|
-
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code:
|
|
268
|
+
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: enums_1.STATUS_CODE_UNACKED, count });
|
|
273
269
|
const streamData = reclaimedMessage[0]?.[1]?.[1];
|
|
274
270
|
//fatal risk point 1 of 3): json is corrupt
|
|
275
271
|
const [err, input] = this.parseStreamData(streamData);
|
|
@@ -10,6 +10,7 @@ import { SymbolSets, StringStringType, StringAnyType, Symbols } from '../../type
|
|
|
10
10
|
import { IdsData, JobStatsRange, StatsType } from '../../types/stats';
|
|
11
11
|
import { Transitions } from '../../types/transition';
|
|
12
12
|
import { ReclaimedMessageType } from '../../types/stream';
|
|
13
|
+
import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
|
|
13
14
|
interface AbstractRedisClient {
|
|
14
15
|
exec(): any;
|
|
15
16
|
}
|
|
@@ -54,6 +55,13 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
54
55
|
setApp(id: string, version: string): Promise<HotMeshApp>;
|
|
55
56
|
activateAppVersion(id: string, version: string): Promise<boolean>;
|
|
56
57
|
registerAppVersion(appId: string, version: string): Promise<any>;
|
|
58
|
+
/**
|
|
59
|
+
* Registers jobId with the originJobId that spawned it. In the future,
|
|
60
|
+
* when originJobId is interrupted or expired, the items in the
|
|
61
|
+
* list (added via RPUSH) are LPOPed. If origin was expired, then
|
|
62
|
+
* LPOPed items from the list are likewise expired;
|
|
63
|
+
*/
|
|
64
|
+
setDependency(originJobId: string, topic: string, jobId: string, multi?: U): Promise<any>;
|
|
57
65
|
setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi?: U): Promise<any>;
|
|
58
66
|
hGetAllResult(result: any): any;
|
|
59
67
|
getJobStats(jobKeys: string[]): Promise<JobStatsRange>;
|
|
@@ -62,8 +70,8 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
62
70
|
getStatus(jobId: string, appId: string): Promise<number>;
|
|
63
71
|
setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi?: U): Promise<string>;
|
|
64
72
|
/**
|
|
65
|
-
*
|
|
66
|
-
* should
|
|
73
|
+
* Returns custom search fields and values.
|
|
74
|
+
* NOTE: The `fields` param should NOT prefix items with an underscore.
|
|
67
75
|
*/
|
|
68
76
|
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
69
77
|
getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
|
|
@@ -87,8 +95,36 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
87
95
|
deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise<void>;
|
|
88
96
|
processTaskQueue(sourceKey: string, destinationKey: string): Promise<any>;
|
|
89
97
|
expireJob(jobId: string, inSeconds: number): Promise<void>;
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
/**
|
|
99
|
+
* register the descendants of an expired origin flow to be
|
|
100
|
+
* expired at a future date; options indicate whether this
|
|
101
|
+
* is a standard `expire` or an `interrupt`
|
|
102
|
+
*/
|
|
103
|
+
registerExpireJob(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* registers a hook activity to be awakened (uses ZSET to
|
|
106
|
+
* store the 'sleep group' and LIST to store the events
|
|
107
|
+
* for the given sleep group. Sleep groups are
|
|
108
|
+
* organized into 'n'-second blocks (LISTS))
|
|
109
|
+
*/
|
|
110
|
+
registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt', deletionTime: number, multi?: U): Promise<void>;
|
|
111
|
+
getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt'] | void>;
|
|
112
|
+
/**
|
|
113
|
+
* when processing time jobs, the target LIST ID returned
|
|
114
|
+
* from the ZSET query can be prefixed to denote what to
|
|
115
|
+
* do with the work list. (not everything is known in advance,
|
|
116
|
+
* so the ZSET key defines HOW to approach the work in the
|
|
117
|
+
* generic LIST (lists typically contain target job ids)
|
|
118
|
+
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
119
|
+
*/
|
|
120
|
+
resolveKeyContext(listKey: string): [('sleep' | 'expire' | 'interrupt'), string];
|
|
121
|
+
/**
|
|
122
|
+
* Interrupts a job and sets sets a job error (410), if 'throw'!=false.
|
|
123
|
+
* This method is called by the engine and not by an activity and is
|
|
124
|
+
* followed by a call to execute job completion/cleanup tasks
|
|
125
|
+
* associated with a job completion event.
|
|
126
|
+
*/
|
|
127
|
+
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<void>;
|
|
92
128
|
scrub(jobId: string): Promise<void>;
|
|
93
129
|
}
|
|
94
130
|
export { StoreService };
|
|
@@ -28,6 +28,8 @@ const key_1 = require("../../modules/key");
|
|
|
28
28
|
const serializer_1 = require("../serializer");
|
|
29
29
|
const cache_1 = require("./cache");
|
|
30
30
|
const utils_1 = require("../../modules/utils");
|
|
31
|
+
const enums_1 = require("../../modules/enums");
|
|
32
|
+
const errors_1 = require("../../modules/errors");
|
|
31
33
|
class StoreService {
|
|
32
34
|
constructor(redisClient) {
|
|
33
35
|
this.commands = {
|
|
@@ -296,6 +298,21 @@ class StoreService {
|
|
|
296
298
|
};
|
|
297
299
|
return await this.redisClient[this.commands.hset](key, payload);
|
|
298
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Registers jobId with the originJobId that spawned it. In the future,
|
|
303
|
+
* when originJobId is interrupted or expired, the items in the
|
|
304
|
+
* list (added via RPUSH) are LPOPed. If origin was expired, then
|
|
305
|
+
* LPOPed items from the list are likewise expired;
|
|
306
|
+
*/
|
|
307
|
+
async setDependency(originJobId, topic, jobId, multi) {
|
|
308
|
+
const privateMulti = multi || this.getMulti();
|
|
309
|
+
const depParams = { appId: this.appId, jobId: originJobId };
|
|
310
|
+
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
|
|
311
|
+
privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
|
|
312
|
+
if (!multi) {
|
|
313
|
+
return await privateMulti.exec();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
299
316
|
async setStats(jobKey, jobId, dateTime, stats, appVersion, multi) {
|
|
300
317
|
const params = { appId: appVersion.id, jobId, jobKey, dateTime };
|
|
301
318
|
const privateMulti = multi || this.getMulti();
|
|
@@ -395,8 +412,8 @@ class StoreService {
|
|
|
395
412
|
return jobId;
|
|
396
413
|
}
|
|
397
414
|
/**
|
|
398
|
-
*
|
|
399
|
-
* should
|
|
415
|
+
* Returns custom search fields and values.
|
|
416
|
+
* NOTE: The `fields` param should NOT prefix items with an underscore.
|
|
400
417
|
*/
|
|
401
418
|
async getQueryState(jobId, fields) {
|
|
402
419
|
const key = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
@@ -437,7 +454,7 @@ class StoreService {
|
|
|
437
454
|
return [state, status];
|
|
438
455
|
}
|
|
439
456
|
else {
|
|
440
|
-
throw new
|
|
457
|
+
throw new errors_1.GetStateError(jobId);
|
|
441
458
|
}
|
|
442
459
|
}
|
|
443
460
|
async collate(jobId, activityId, amount, dIds, multi) {
|
|
@@ -646,6 +663,25 @@ class StoreService {
|
|
|
646
663
|
await this.redisClient[this.commands.expire](jobKey, inSeconds);
|
|
647
664
|
}
|
|
648
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* register the descendants of an expired origin flow to be
|
|
668
|
+
* expired at a future date; options indicate whether this
|
|
669
|
+
* is a standard `expire` or an `interrupt`
|
|
670
|
+
*/
|
|
671
|
+
async registerExpireJob(jobId, deletionTime, options) {
|
|
672
|
+
const depParams = { appId: this.appId, jobId };
|
|
673
|
+
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
|
|
674
|
+
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
675
|
+
const depKeyContext = `::${context}::${depKey}`;
|
|
676
|
+
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
677
|
+
await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* registers a hook activity to be awakened (uses ZSET to
|
|
681
|
+
* store the 'sleep group' and LIST to store the events
|
|
682
|
+
* for the given sleep group. Sleep groups are
|
|
683
|
+
* organized into 'n'-second blocks (LISTS))
|
|
684
|
+
*/
|
|
649
685
|
async registerTimeHook(jobId, activityId, type, deletionTime, multi) {
|
|
650
686
|
const listKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
651
687
|
const timeEvent = `${type}::${activityId}::${jobId}`;
|
|
@@ -657,18 +693,87 @@ class StoreService {
|
|
|
657
693
|
}
|
|
658
694
|
async getNextTimeJob(listKey) {
|
|
659
695
|
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
660
|
-
|
|
661
|
-
listKey = listKey || await this.zRangeByScore(zsetKey, 0, now);
|
|
696
|
+
listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
|
|
662
697
|
if (listKey) {
|
|
663
|
-
const
|
|
698
|
+
const [pType, pKey] = this.resolveKeyContext(listKey);
|
|
699
|
+
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
664
700
|
if (timeEvent) {
|
|
665
|
-
//
|
|
666
|
-
const [
|
|
667
|
-
return [listKey, jobId.join('::'), activityId];
|
|
701
|
+
//there are 3 time-related event triggers: sleep, expire, interrupt
|
|
702
|
+
const [_type, activityId, ...jobId] = timeEvent.split('::');
|
|
703
|
+
return [listKey, jobId.join('::'), activityId, pType];
|
|
668
704
|
}
|
|
669
705
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
670
706
|
}
|
|
671
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* when processing time jobs, the target LIST ID returned
|
|
710
|
+
* from the ZSET query can be prefixed to denote what to
|
|
711
|
+
* do with the work list. (not everything is known in advance,
|
|
712
|
+
* so the ZSET key defines HOW to approach the work in the
|
|
713
|
+
* generic LIST (lists typically contain target job ids)
|
|
714
|
+
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
715
|
+
*/
|
|
716
|
+
resolveKeyContext(listKey) {
|
|
717
|
+
if (listKey.startsWith('::INTERRUPT')) {
|
|
718
|
+
return ['interrupt', listKey.split('::')[2]];
|
|
719
|
+
}
|
|
720
|
+
else if (listKey.startsWith('::EXPIRE')) {
|
|
721
|
+
return ['expire', listKey.split('::')[2]];
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
return ['sleep', listKey];
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Interrupts a job and sets sets a job error (410), if 'throw'!=false.
|
|
729
|
+
* This method is called by the engine and not by an activity and is
|
|
730
|
+
* followed by a call to execute job completion/cleanup tasks
|
|
731
|
+
* associated with a job completion event.
|
|
732
|
+
*/
|
|
733
|
+
async interrupt(topic, jobId, options = {}) {
|
|
734
|
+
try {
|
|
735
|
+
//verify job exists
|
|
736
|
+
const status = await this.getStatus(jobId, this.appId);
|
|
737
|
+
if (status <= 0) {
|
|
738
|
+
//verify still active; job already completed
|
|
739
|
+
throw new Error(`Job ${jobId} already completed`);
|
|
740
|
+
}
|
|
741
|
+
//decrement job status (:) by 1bil
|
|
742
|
+
const amount = -1000000000;
|
|
743
|
+
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
744
|
+
const result = await this.redisClient[this.commands.hincrbyfloat](jobKey, ':', amount);
|
|
745
|
+
if (result <= amount) {
|
|
746
|
+
//verify active state; job already interrupted
|
|
747
|
+
throw new Error(`Job ${jobId} already completed`);
|
|
748
|
+
}
|
|
749
|
+
//persist the error unless specifically told not to
|
|
750
|
+
if (options.throw !== false) {
|
|
751
|
+
const errKey = `metadata/err`; //job errors are stored at the path `metadata/err`
|
|
752
|
+
const symbolNames = [`$${topic}`]; //the symbol for `metadata/err` is in redis and stored using the job topic
|
|
753
|
+
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
754
|
+
const symVals = await this.getSymbolValues();
|
|
755
|
+
this.serializer.resetSymbols(symKeys, symVals, {});
|
|
756
|
+
//persists the standard 410 error (job is `gone`)
|
|
757
|
+
const err = JSON.stringify({
|
|
758
|
+
code: enums_1.STATUS_CODE_INTERRUPT,
|
|
759
|
+
message: options.reason ?? `job [${jobId}] interrupted`,
|
|
760
|
+
job_id: jobId
|
|
761
|
+
});
|
|
762
|
+
const payload = { [errKey]: amount.toString() };
|
|
763
|
+
const hashData = this.serializer.package(payload, symbolNames);
|
|
764
|
+
const errSymbol = Object.keys(hashData)[0];
|
|
765
|
+
await this.redisClient[this.commands.hset](jobKey, errSymbol, err);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (e) {
|
|
769
|
+
if (!options.suppress) {
|
|
770
|
+
throw e;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
this.logger.debug('suppressed-interrupt', { message: e.message });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
672
777
|
async scrub(jobId) {
|
|
673
778
|
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
674
779
|
await this.redisClient[this.commands.del](jobKey);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { ILogger } from '../logger';
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
|
-
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
5
4
|
import { HookInterface } from '../../types/hook';
|
|
5
|
+
import { JobCompletionOptions } from '../../types/job';
|
|
6
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
6
7
|
declare class TaskService {
|
|
7
8
|
store: StoreService<RedisClient, RedisMulti>;
|
|
8
9
|
logger: ILogger;
|
|
@@ -10,9 +11,9 @@ declare class TaskService {
|
|
|
10
11
|
constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
11
12
|
processWebHooks(hookEventCallback: HookInterface): Promise<void>;
|
|
12
13
|
enqueueWorkItems(keys: string[]): Promise<void>;
|
|
13
|
-
registerJobForCleanup(jobId: string, inSeconds
|
|
14
|
-
registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | '
|
|
15
|
-
processTimeHooks(timeEventCallback: (jobId: string, activityId: string) => Promise<void>, listKey?: string): Promise<void>;
|
|
14
|
+
registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions): Promise<void>;
|
|
15
|
+
registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt', inSeconds?: number, multi?: RedisMulti): Promise<void>;
|
|
16
|
+
processTimeHooks(timeEventCallback: (jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt') => Promise<void>, listKey?: string): Promise<void>;
|
|
16
17
|
cancelCleanup(): void;
|
|
17
18
|
}
|
|
18
19
|
export { TaskService };
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskService = void 0;
|
|
4
|
+
const enums_1 = require("../../modules/enums");
|
|
4
5
|
const utils_1 = require("../../modules/utils");
|
|
5
|
-
//system timer granularity limit (task queues organize)
|
|
6
|
-
const FIDELITY_SECONDS = 15; //note: this can be reduced using 'watch' or scout role
|
|
7
|
-
//default resolution/fidelity when expiring
|
|
8
|
-
const EXPIRATION_FIDELITY_SECONDS = 60;
|
|
9
6
|
class TaskService {
|
|
10
7
|
constructor(store, logger) {
|
|
11
8
|
this.cleanupTimeout = null;
|
|
@@ -32,27 +29,28 @@ class TaskService {
|
|
|
32
29
|
async enqueueWorkItems(keys) {
|
|
33
30
|
await this.store.addTaskQueues(keys);
|
|
34
31
|
}
|
|
35
|
-
async registerJobForCleanup(jobId, inSeconds =
|
|
36
|
-
if (inSeconds >
|
|
32
|
+
async registerJobForCleanup(jobId, inSeconds = enums_1.EXPIRE_DURATION, options) {
|
|
33
|
+
if (inSeconds > 0) {
|
|
37
34
|
await this.store.expireJob(jobId, inSeconds);
|
|
35
|
+
const expireTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
|
|
36
|
+
await this.store.registerExpireJob(jobId, expireTimeSlot, options);
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
|
-
async registerTimeHook(jobId, activityId, type, inSeconds = FIDELITY_SECONDS, multi) {
|
|
41
|
-
const awakenTimeSlot = Math.floor((Date.now() + inSeconds * 1000) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
|
|
39
|
+
async registerTimeHook(jobId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
|
|
40
|
+
const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
|
|
42
41
|
await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
|
|
43
42
|
}
|
|
44
|
-
//todo: need 'scout' role in quorum to check for this and then alert the quorum to get to work
|
|
45
43
|
async processTimeHooks(timeEventCallback, listKey) {
|
|
46
44
|
try {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
const [listKey, jobId, activityId] =
|
|
50
|
-
await timeEventCallback(jobId, activityId);
|
|
45
|
+
const timeJob = await this.store.getNextTimeJob(listKey);
|
|
46
|
+
if (timeJob) {
|
|
47
|
+
const [listKey, jobId, activityId, type] = timeJob;
|
|
48
|
+
await timeEventCallback(jobId, activityId, type);
|
|
51
49
|
await (0, utils_1.sleepFor)(0);
|
|
52
50
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
53
51
|
}
|
|
54
52
|
else {
|
|
55
|
-
let sleep = (0, utils_1.XSleepFor)(FIDELITY_SECONDS * 1000);
|
|
53
|
+
let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
|
|
56
54
|
this.cleanupTimeout = sleep.timerId;
|
|
57
55
|
await sleep.promise;
|
|
58
56
|
this.processTimeHooks(timeEventCallback);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MetricTypes } from "./stats";
|
|
2
2
|
import { StreamRetryPolicy } from "./stream";
|
|
3
|
-
type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | '
|
|
3
|
+
type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'interrupt' | 'cycle' | 'signal' | 'hook';
|
|
4
4
|
type Consumes = Record<string, string[]>;
|
|
5
5
|
interface BaseActivity {
|
|
6
6
|
title?: string;
|
|
@@ -32,13 +32,33 @@ interface Measure {
|
|
|
32
32
|
target: string;
|
|
33
33
|
}
|
|
34
34
|
interface TriggerActivityStats {
|
|
35
|
+
/**
|
|
36
|
+
* parent job; including this allows the parent's
|
|
37
|
+
* expiration/interruption events to cascade; set
|
|
38
|
+
* `expire` in the YAML for the dependent graph
|
|
39
|
+
* to 0 and provide the parent for dependent,
|
|
40
|
+
* cascading interruption and cleanup
|
|
41
|
+
*/
|
|
42
|
+
parent?: string;
|
|
35
43
|
id?: {
|
|
36
44
|
[key: string]: unknown;
|
|
37
45
|
} | string;
|
|
38
46
|
key?: {
|
|
39
47
|
[key: string]: unknown;
|
|
40
48
|
} | string;
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated
|
|
51
|
+
* return 'infinity' to disable; default behavior
|
|
52
|
+
* is to always segment keys by time to ensure
|
|
53
|
+
* indexes (Redis LIST) never grow unbounded
|
|
54
|
+
* as a default behavior; for now, 5m is default
|
|
55
|
+
* and infinity can be set to override
|
|
56
|
+
*/
|
|
41
57
|
granularity?: string;
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated
|
|
60
|
+
* what to capture
|
|
61
|
+
*/
|
|
42
62
|
measures?: Measure[];
|
|
43
63
|
}
|
|
44
64
|
interface TriggerActivity extends BaseActivity {
|
|
@@ -74,10 +94,20 @@ interface SignalActivity extends BaseActivity {
|
|
|
74
94
|
status?: string;
|
|
75
95
|
code?: number;
|
|
76
96
|
}
|
|
77
|
-
interface
|
|
78
|
-
type: '
|
|
97
|
+
interface InterruptActivity extends BaseActivity {
|
|
98
|
+
type: 'interrupt';
|
|
99
|
+
/** Optional Reason; will be used as the error `message` when thrown; NOTE: 410 is the error `code` */
|
|
100
|
+
reason?: string;
|
|
101
|
+
/** default is `true` (throw JobInterrupted error upon interrupting) */
|
|
102
|
+
throw?: boolean;
|
|
103
|
+
/** TODO: // default is `false` (do not interrupt child jobs) */
|
|
104
|
+
descend?: boolean;
|
|
105
|
+
/** target job id (if not present the current job will be targeted) */
|
|
106
|
+
target?: string;
|
|
107
|
+
/** topic to publish the interrupt message (if not present the current job topic will be used) */
|
|
108
|
+
topic?: string;
|
|
79
109
|
}
|
|
80
|
-
type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity |
|
|
110
|
+
type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | InterruptActivity | HookActivity | SignalActivity | CycleActivity;
|
|
81
111
|
type ActivityData = Record<string, any>;
|
|
82
112
|
type ActivityMetadata = {
|
|
83
113
|
aid: string;
|
|
@@ -103,4 +133,4 @@ type ActivityDataType = {
|
|
|
103
133
|
hook?: Record<string, unknown>;
|
|
104
134
|
};
|
|
105
135
|
type ActivityLeg = 1 | 2;
|
|
106
|
-
export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, HookActivity, SignalActivity, BaseActivity,
|
|
136
|
+
export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, HookActivity, SignalActivity, BaseActivity, InterruptActivity, TriggerActivity, WorkerActivity };
|
package/build/types/durable.d.ts
CHANGED
|
@@ -52,10 +52,12 @@ type WorkflowOptions = {
|
|
|
52
52
|
entity?: string;
|
|
53
53
|
workflowName?: string;
|
|
54
54
|
parentWorkflowId?: string;
|
|
55
|
+
originJobId?: string;
|
|
55
56
|
workflowTrace?: string;
|
|
56
57
|
workflowSpan?: string;
|
|
57
58
|
search?: WorkflowSearchOptions;
|
|
58
59
|
config?: WorkflowConfig;
|
|
60
|
+
expire?: number;
|
|
59
61
|
};
|
|
60
62
|
type HookOptions = {
|
|
61
63
|
namespace?: string;
|
|
@@ -83,6 +85,8 @@ type WorkflowDataType = {
|
|
|
83
85
|
arguments: any[];
|
|
84
86
|
workflowId: string;
|
|
85
87
|
workflowTopic: string;
|
|
88
|
+
workflowDimension?: string;
|
|
89
|
+
originJobId?: string;
|
|
86
90
|
};
|
|
87
91
|
type MeshOSClassConfig = {
|
|
88
92
|
namespace: string;
|
package/build/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, HookActivity, WorkerActivity,
|
|
1
|
+
export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, HookActivity, WorkerActivity, InterruptActivity, 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';
|
package/build/types/job.d.ts
CHANGED
|
@@ -39,6 +39,18 @@ type JobState = {
|
|
|
39
39
|
errors: ActivityData;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
type JobInterruptOptions = {
|
|
43
|
+
/** Optional reason when throwing the error */
|
|
44
|
+
reason?: string;
|
|
45
|
+
/** default is `true` when `undefined` (throw JobInterrupted/410 error) */
|
|
46
|
+
throw?: boolean;
|
|
47
|
+
/** default behavior is `false` when `undefined` (do NOT interrupt child jobs) */
|
|
48
|
+
descend?: boolean;
|
|
49
|
+
/** default is false; if true, errors related to inactivation (like overage...already inactive) are suppressed/ignored */
|
|
50
|
+
suppress?: boolean;
|
|
51
|
+
/** how long to wait in seconds before fully expiring/removing the hash from Redis; the job is inactive, but can remain in the cache indefinitely. minimum 1 second.*/
|
|
52
|
+
expire?: number;
|
|
53
|
+
};
|
|
42
54
|
type JobOutput = {
|
|
43
55
|
metadata: JobMetadata;
|
|
44
56
|
data: JobData;
|
|
@@ -47,4 +59,9 @@ type PartialJobState = {
|
|
|
47
59
|
metadata: JobMetadata | Pick<JobMetadata, 'jid' | 'dad' | 'aid'>;
|
|
48
60
|
data: JobData;
|
|
49
61
|
};
|
|
50
|
-
|
|
62
|
+
type JobCompletionOptions = {
|
|
63
|
+
emit?: boolean;
|
|
64
|
+
interrupt?: boolean;
|
|
65
|
+
expire?: number;
|
|
66
|
+
};
|
|
67
|
+
export { JobCompletionOptions, JobInterruptOptions, JobData, JobsData, JobMetadata, JobOutput, JobState, JobStatus, PartialJobState, };
|
package/build/types/quorum.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { JobOutput } from "./job";
|
|
2
|
-
/**
|
|
3
|
-
* The types in this file are used to define those messages that are sent
|
|
4
|
-
* to hotmesh client instances when a new version is about to be activated.
|
|
5
|
-
* These messages serve to coordinate the cache invalidation and switch-over
|
|
6
|
-
* to the new version without any downtime and a coordinating parent server.
|
|
7
|
-
*/
|
|
8
|
-
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage;
|
|
9
2
|
export interface PingMessage {
|
|
10
3
|
type: 'ping';
|
|
11
4
|
originator: string;
|
|
@@ -14,6 +7,10 @@ export interface WorkMessage {
|
|
|
14
7
|
type: 'work';
|
|
15
8
|
originator: string;
|
|
16
9
|
}
|
|
10
|
+
export interface CronMessage {
|
|
11
|
+
type: 'cron';
|
|
12
|
+
originator: string;
|
|
13
|
+
}
|
|
17
14
|
export interface PongMessage {
|
|
18
15
|
type: 'pong';
|
|
19
16
|
originator: string;
|
|
@@ -44,3 +41,10 @@ export interface SubscriptionCallback {
|
|
|
44
41
|
export interface QuorumMessageCallback {
|
|
45
42
|
(topic: string, message: QuorumMessage): void;
|
|
46
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The types in this file are used to define those messages that are sent
|
|
46
|
+
* to hotmesh client instances when a new version is about to be activated.
|
|
47
|
+
* These messages serve to coordinate the cache invalidation and switch-over
|
|
48
|
+
* to the new version without any downtime and a coordinating parent server.
|
|
49
|
+
*/
|
|
50
|
+
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
|