@hotmeshio/hotmesh 0.0.56 → 0.0.58
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 +10 -10
- package/build/modules/enums.js +10 -1
- package/build/modules/key.d.ts +38 -0
- package/build/modules/key.js +46 -4
- package/build/modules/utils.d.ts +9 -0
- package/build/modules/utils.js +19 -1
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +28 -0
- package/build/services/activities/activity.js +46 -1
- package/build/services/activities/await.js +4 -0
- package/build/services/activities/cycle.d.ts +7 -0
- package/build/services/activities/cycle.js +16 -1
- package/build/services/activities/hook.d.ts +6 -0
- package/build/services/activities/hook.js +12 -2
- package/build/services/activities/interrupt.js +8 -0
- package/build/services/activities/signal.d.ts +6 -0
- package/build/services/activities/signal.js +15 -0
- package/build/services/activities/trigger.d.ts +4 -0
- package/build/services/activities/trigger.js +7 -1
- package/build/services/activities/worker.js +4 -0
- package/build/services/collator/index.d.ts +70 -0
- package/build/services/collator/index.js +91 -1
- package/build/services/compiler/deployer.js +38 -6
- package/build/services/compiler/index.d.ts +15 -0
- package/build/services/compiler/index.js +20 -0
- package/build/services/compiler/validator.d.ts +3 -0
- package/build/services/compiler/validator.js +25 -0
- package/build/services/connector/clients/ioredis.js +2 -0
- package/build/services/connector/clients/redis.js +2 -0
- package/build/services/connector/index.js +2 -0
- package/build/services/durable/client.d.ts +20 -0
- package/build/services/durable/client.js +25 -0
- package/build/services/durable/exporter.d.ts +22 -0
- package/build/services/durable/exporter.js +30 -1
- package/build/services/durable/handle.d.ts +36 -0
- package/build/services/durable/handle.js +46 -0
- package/build/services/durable/index.d.ts +4 -0
- package/build/services/durable/index.js +4 -0
- package/build/services/durable/schemas/factory.d.ts +29 -0
- package/build/services/durable/schemas/factory.js +29 -0
- package/build/services/durable/search.d.ts +97 -0
- package/build/services/durable/search.js +108 -10
- package/build/services/durable/worker.js +35 -6
- package/build/services/durable/workflow.d.ts +118 -0
- package/build/services/durable/workflow.js +153 -6
- package/build/services/engine/index.d.ts +5 -0
- package/build/services/engine/index.js +43 -1
- package/build/services/exporter/index.d.ts +27 -0
- package/build/services/exporter/index.js +33 -0
- package/build/services/hotmesh/index.js +8 -0
- package/build/services/logger/index.js +2 -0
- package/build/services/mapper/index.d.ts +14 -0
- package/build/services/mapper/index.js +14 -0
- package/build/services/pipe/functions/date.d.ts +7 -0
- package/build/services/pipe/functions/date.js +7 -0
- package/build/services/pipe/functions/math.js +2 -0
- package/build/services/pipe/index.d.ts +15 -0
- package/build/services/pipe/index.js +23 -2
- package/build/services/quorum/index.d.ts +7 -0
- package/build/services/quorum/index.js +21 -0
- package/build/services/reporter/index.d.ts +5 -0
- package/build/services/reporter/index.js +9 -0
- package/build/services/router/index.d.ts +9 -0
- package/build/services/router/index.js +30 -2
- package/build/services/serializer/index.js +23 -6
- package/build/services/store/cache.d.ts +19 -0
- package/build/services/store/cache.js +19 -0
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +55 -0
- package/build/services/store/index.js +81 -5
- package/build/services/stream/clients/ioredis.js +4 -1
- package/build/services/task/index.d.ts +9 -0
- package/build/services/task/index.js +31 -0
- package/build/services/telemetry/index.d.ts +7 -0
- package/build/services/telemetry/index.js +13 -1
- package/build/services/worker/index.d.ts +4 -0
- package/build/services/worker/index.js +6 -2
- package/build/types/activity.d.ts +81 -0
- package/build/types/durable.d.ts +256 -0
- package/build/types/exporter.d.ts +13 -0
- package/build/types/hotmesh.d.ts +10 -1
- package/build/types/hotmesh.js +3 -0
- package/build/types/index.js +1 -1
- package/build/types/job.d.ts +85 -0
- package/build/types/pipe.d.ts +65 -0
- package/build/types/quorum.d.ts +14 -0
- package/build/types/redis.d.ts +6 -0
- package/build/types/stream.d.ts +58 -0
- package/build/types/stream.js +4 -0
- package/package.json +1 -1
- package/types/durable.ts +10 -1
|
@@ -7,8 +7,8 @@ const collator_1 = require("../collator");
|
|
|
7
7
|
const serializer_1 = require("../serializer");
|
|
8
8
|
const pipe_1 = require("../pipe");
|
|
9
9
|
const validator_1 = require("./validator");
|
|
10
|
-
const DEFAULT_METADATA_RANGE_SIZE = 26;
|
|
11
|
-
const DEFAULT_DATA_RANGE_SIZE = 260;
|
|
10
|
+
const DEFAULT_METADATA_RANGE_SIZE = 26; //metadata is 26 slots ([a-z] * 1)
|
|
11
|
+
const DEFAULT_DATA_RANGE_SIZE = 260; //data is 260 slots ([a-zA-Z] * 5)
|
|
12
12
|
const DEFAULT_RANGE_SIZE = DEFAULT_METADATA_RANGE_SIZE + DEFAULT_DATA_RANGE_SIZE;
|
|
13
13
|
class Deployer {
|
|
14
14
|
constructor(manifest) {
|
|
@@ -41,19 +41,22 @@ class Deployer {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
async generateSymKeys() {
|
|
44
|
+
//note: symbol ranges are additive (per version); path assignments are immutable
|
|
44
45
|
const appId = this.manifest.app.id;
|
|
45
46
|
for (const graph of this.manifest.app.graphs) {
|
|
47
|
+
//generate JOB symbols
|
|
46
48
|
const [, trigger] = this.findTrigger(graph);
|
|
47
49
|
const topic = trigger.subscribes;
|
|
48
50
|
const [lower, upper, symbols] = await this.store.reserveSymbolRange(`$${topic}`, DEFAULT_RANGE_SIZE, 'JOB');
|
|
49
|
-
const prefix = '';
|
|
51
|
+
const prefix = ''; //job meta/data is NOT namespaced
|
|
50
52
|
const newSymbols = this.bindSymbols(lower, upper, symbols, prefix, trigger.PRODUCES);
|
|
51
53
|
if (Object.keys(newSymbols).length) {
|
|
52
54
|
await this.store.addSymbols(`$${topic}`, newSymbols);
|
|
53
55
|
}
|
|
56
|
+
//generate ACTIVITY symbols
|
|
54
57
|
for (const [activityId, activity] of Object.entries(graph.activities)) {
|
|
55
58
|
const [lower, upper, symbols] = await this.store.reserveSymbolRange(activityId, DEFAULT_RANGE_SIZE, 'ACTIVITY');
|
|
56
|
-
const prefix = `${activityId}/`;
|
|
59
|
+
const prefix = `${activityId}/`; //activity meta/data is namespaced
|
|
57
60
|
this.bindSelf(activity.consumes, activity.produces, activityId);
|
|
58
61
|
const newSymbols = this.bindSymbols(lower, upper, symbols, prefix, activity.produces);
|
|
59
62
|
if (Object.keys(newSymbols).length) {
|
|
@@ -63,6 +66,7 @@ class Deployer {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
bindSelf(consumes, produces, activityId) {
|
|
69
|
+
//bind self-referential mappings
|
|
66
70
|
for (const selfId of [activityId, '$self']) {
|
|
67
71
|
const selfConsumes = consumes[selfId];
|
|
68
72
|
if (selfConsumes) {
|
|
@@ -86,7 +90,7 @@ class Deployer {
|
|
|
86
90
|
const symbol = (0, utils_1.getSymKey)(startIndex);
|
|
87
91
|
startIndex++;
|
|
88
92
|
newSymbols[fullPath] = symbol;
|
|
89
|
-
currentSymbols[fullPath] = symbol;
|
|
93
|
+
currentSymbols[fullPath] = symbol; // update the currentSymbols to include this new symbol
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
return newSymbols;
|
|
@@ -99,16 +103,20 @@ class Deployer {
|
|
|
99
103
|
if (!jobSchema && !outputSchema)
|
|
100
104
|
continue;
|
|
101
105
|
const activities = graph.activities;
|
|
106
|
+
// Find the trigger activity and bind the job schema to it
|
|
107
|
+
// at execution time, the trigger is a standin for the job
|
|
102
108
|
for (const activityKey in activities) {
|
|
103
109
|
if (activities[activityKey].type === 'trigger') {
|
|
104
110
|
const trigger = activities[activityKey];
|
|
105
111
|
if (jobSchema) {
|
|
112
|
+
//possible for trigger to have job mappings
|
|
106
113
|
if (!trigger.job) {
|
|
107
114
|
trigger.job = {};
|
|
108
115
|
}
|
|
109
116
|
trigger.job.schema = jobSchema;
|
|
110
117
|
}
|
|
111
118
|
if (outputSchema) {
|
|
119
|
+
//impossible for trigger to have output mappings.
|
|
112
120
|
trigger.output = { schema: outputSchema };
|
|
113
121
|
}
|
|
114
122
|
}
|
|
@@ -129,6 +137,8 @@ class Deployer {
|
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
139
|
}
|
|
140
|
+
//the cycle/goto activity includes and ancestor target;
|
|
141
|
+
//update with the cycle flag, so it can be rerun
|
|
132
142
|
bindCycleTarget() {
|
|
133
143
|
for (const graph of this.manifest.app.graphs) {
|
|
134
144
|
const activities = graph.activities;
|
|
@@ -140,6 +150,8 @@ class Deployer {
|
|
|
140
150
|
}
|
|
141
151
|
}
|
|
142
152
|
}
|
|
153
|
+
//it's more intuitive for SDK users to use 'topic',
|
|
154
|
+
//but the compiler is desiged to be generic and uses the attribute, 'subtypes'
|
|
143
155
|
convertTopicsToTypes() {
|
|
144
156
|
for (const graph of this.manifest.app.graphs) {
|
|
145
157
|
const activities = graph.activities;
|
|
@@ -151,6 +163,7 @@ class Deployer {
|
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
165
|
}
|
|
166
|
+
//legacy; remove at beta (assume no legacy refs to 'activity' at that point)
|
|
154
167
|
convertActivitiesToHooks() {
|
|
155
168
|
for (const graph of this.manifest.app.graphs) {
|
|
156
169
|
const activities = graph.activities;
|
|
@@ -170,8 +183,11 @@ class Deployer {
|
|
|
170
183
|
const toTransitions = graph.transitions[fromActivity];
|
|
171
184
|
for (const transition of toTransitions) {
|
|
172
185
|
const to = transition.to;
|
|
186
|
+
//DAGs have one parent; easy to optimize for
|
|
173
187
|
graph.activities[to].parent = fromActivity;
|
|
174
188
|
}
|
|
189
|
+
//temporarily bind the transitions to the parent activity,
|
|
190
|
+
// so the consumer/producer registrar picks up the bindings
|
|
175
191
|
graph.activities[fromActivity].transitions = toTransitions;
|
|
176
192
|
}
|
|
177
193
|
}
|
|
@@ -229,6 +245,10 @@ class Deployer {
|
|
|
229
245
|
traverse(obj[key], newPath);
|
|
230
246
|
}
|
|
231
247
|
else {
|
|
248
|
+
//wildcard mapping (e.g., 'friends[25]')
|
|
249
|
+
//when this is resolved, it will be expanded to
|
|
250
|
+
//`'friends/0', ..., 'friends/24'`, providing 25 dynamic
|
|
251
|
+
//slots in the flow's output data
|
|
232
252
|
const pathName = [...path, key].join('/');
|
|
233
253
|
if (!pathName.includes('[')) {
|
|
234
254
|
const finalPath = `data/${pathName}`;
|
|
@@ -238,15 +258,17 @@ class Deployer {
|
|
|
238
258
|
}
|
|
239
259
|
else {
|
|
240
260
|
const [left, right] = pathName.split('[');
|
|
261
|
+
//check if this variable isLiteralKeyType (#, -, or _)
|
|
241
262
|
const [amount, _] = right.split(']');
|
|
242
263
|
if (!isNaN(parseInt(amount))) {
|
|
264
|
+
//loop to create all possible paths (0 to amount)
|
|
243
265
|
for (let i = 0; i < parseInt(amount); i++) {
|
|
244
266
|
const finalPath = `data/${left}/${i}`;
|
|
245
267
|
if (!result.includes(finalPath)) {
|
|
246
268
|
result.push(finalPath);
|
|
247
269
|
}
|
|
248
270
|
}
|
|
249
|
-
}
|
|
271
|
+
} //else ignore (amount might be '-' or '_') `-` is marker data; `_` is job data;
|
|
250
272
|
}
|
|
251
273
|
}
|
|
252
274
|
}
|
|
@@ -268,6 +290,7 @@ class Deployer {
|
|
|
268
290
|
}
|
|
269
291
|
resolveMappingDependencies() {
|
|
270
292
|
const dynamicMappingRules = [];
|
|
293
|
+
//recursive function to descend into the object and find all dynamic mapping rules
|
|
271
294
|
function traverse(obj, consumes) {
|
|
272
295
|
for (const key in obj) {
|
|
273
296
|
if (typeof obj[key] === 'string') {
|
|
@@ -296,6 +319,7 @@ class Deployer {
|
|
|
296
319
|
}
|
|
297
320
|
}
|
|
298
321
|
const groupedRules = this.groupMappingRules(dynamicMappingRules);
|
|
322
|
+
// Iterate through the graph and add 'produces' field to each activity
|
|
299
323
|
for (const graph of graphs) {
|
|
300
324
|
const activities = graph.activities;
|
|
301
325
|
for (const activityId in activities) {
|
|
@@ -306,6 +330,7 @@ class Deployer {
|
|
|
306
330
|
}
|
|
307
331
|
groupMappingRules(rules) {
|
|
308
332
|
rules = Array.from(new Set(rules)).sort();
|
|
333
|
+
// Group by the first symbol before the period (this is the activity name)
|
|
309
334
|
const groupedRules = {};
|
|
310
335
|
for (const rule of rules) {
|
|
311
336
|
const [group, resolved] = this.resolveMappableValue(rule);
|
|
@@ -324,6 +349,7 @@ class Deployer {
|
|
|
324
349
|
return [group, path.join('/')];
|
|
325
350
|
}
|
|
326
351
|
else {
|
|
352
|
+
//normalize paths to be relative to the activity
|
|
327
353
|
const [group, type, subtype, ...path] = parts;
|
|
328
354
|
const prefix = {
|
|
329
355
|
hook: 'hook/data',
|
|
@@ -340,6 +366,7 @@ class Deployer {
|
|
|
340
366
|
const activities = graph.activities;
|
|
341
367
|
for (const activityKey in activities) {
|
|
342
368
|
const target = activities[activityKey];
|
|
369
|
+
//remove transitions; no longer necessary for runtime
|
|
343
370
|
delete target.transitions;
|
|
344
371
|
activitySchemas[activityKey] = target;
|
|
345
372
|
}
|
|
@@ -352,6 +379,7 @@ class Deployer {
|
|
|
352
379
|
for (const graph of graphs) {
|
|
353
380
|
const activities = graph.activities;
|
|
354
381
|
const subscribesTopic = graph.subscribes;
|
|
382
|
+
// Find the activity ID associated with the subscribes topic
|
|
355
383
|
for (const activityKey in activities) {
|
|
356
384
|
if (activities[activityKey].type === 'trigger') {
|
|
357
385
|
publicSubscriptions[subscribesTopic] = activityKey;
|
|
@@ -414,6 +442,7 @@ class Deployer {
|
|
|
414
442
|
if (!targetActivity.hook) {
|
|
415
443
|
targetActivity.hook = {};
|
|
416
444
|
}
|
|
445
|
+
//create back-reference to the hook topic
|
|
417
446
|
targetActivity.hook.topic = topic;
|
|
418
447
|
}
|
|
419
448
|
}
|
|
@@ -422,6 +451,7 @@ class Deployer {
|
|
|
422
451
|
await this.store.setHookRules(hookRules);
|
|
423
452
|
}
|
|
424
453
|
async deployConsumerGroups() {
|
|
454
|
+
//create one engine group
|
|
425
455
|
const params = { appId: this.manifest.app.id };
|
|
426
456
|
const key = this.store.mintKey(key_1.KeyType.STREAMS, params);
|
|
427
457
|
await this.deployConsumerGroup(key, 'ENGINE');
|
|
@@ -429,9 +459,11 @@ class Deployer {
|
|
|
429
459
|
const activities = graph.activities;
|
|
430
460
|
for (const activityKey in activities) {
|
|
431
461
|
const activity = activities[activityKey];
|
|
462
|
+
//only precreate if the topic is concrete and not `mappable`
|
|
432
463
|
if (activity.type === 'worker' && pipe_1.Pipe.resolve(activity.subtype, {}) === activity.subtype) {
|
|
433
464
|
params.topic = activity.subtype;
|
|
434
465
|
const key = this.store.mintKey(key_1.KeyType.STREAMS, params);
|
|
466
|
+
//create one worker group per unique activity subtype (the topic)
|
|
435
467
|
await this.deployConsumerGroup(key, 'WORKER');
|
|
436
468
|
}
|
|
437
469
|
}
|
|
@@ -2,13 +2,28 @@ import { ILogger } from '../logger';
|
|
|
2
2
|
import { StoreService } from '../store';
|
|
3
3
|
import { HotMeshManifest } from '../../types/hotmesh';
|
|
4
4
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
5
|
+
/**
|
|
6
|
+
* The compiler service converts a graph into a executable program.
|
|
7
|
+
*/
|
|
5
8
|
declare class CompilerService {
|
|
6
9
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
7
10
|
logger: ILogger;
|
|
8
11
|
constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
12
|
+
/**
|
|
13
|
+
* verifies and plans the deployment of an app to Redis; the app is not deployed yet
|
|
14
|
+
* @param path
|
|
15
|
+
*/
|
|
9
16
|
plan(mySchemaOrPath: string): Promise<HotMeshManifest>;
|
|
10
17
|
isPath(input: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* deploys an app to Redis but does NOT activate it.
|
|
20
|
+
*/
|
|
11
21
|
deploy(mySchemaOrPath: string): Promise<HotMeshManifest>;
|
|
22
|
+
/**
|
|
23
|
+
* activates a deployed version of an app;
|
|
24
|
+
* @param appId
|
|
25
|
+
* @param appVersion
|
|
26
|
+
*/
|
|
12
27
|
activate(appId: string, appVersion: string): Promise<boolean>;
|
|
13
28
|
saveAsJSON(originalPath: string, schema: HotMeshManifest): Promise<void>;
|
|
14
29
|
}
|
|
@@ -33,11 +33,18 @@ const fs = __importStar(require("fs/promises"));
|
|
|
33
33
|
const path = __importStar(require("path"));
|
|
34
34
|
const deployer_1 = require("./deployer");
|
|
35
35
|
const validator_1 = require("./validator");
|
|
36
|
+
/**
|
|
37
|
+
* The compiler service converts a graph into a executable program.
|
|
38
|
+
*/
|
|
36
39
|
class CompilerService {
|
|
37
40
|
constructor(store, logger) {
|
|
38
41
|
this.store = store;
|
|
39
42
|
this.logger = logger;
|
|
40
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* verifies and plans the deployment of an app to Redis; the app is not deployed yet
|
|
46
|
+
* @param path
|
|
47
|
+
*/
|
|
41
48
|
async plan(mySchemaOrPath) {
|
|
42
49
|
try {
|
|
43
50
|
let schema;
|
|
@@ -47,8 +54,10 @@ class CompilerService {
|
|
|
47
54
|
else {
|
|
48
55
|
schema = js_yaml_1.default.load(mySchemaOrPath);
|
|
49
56
|
}
|
|
57
|
+
// 1) validate the manifest file
|
|
50
58
|
const validator = new validator_1.Validator(schema);
|
|
51
59
|
validator.validate(this.store);
|
|
60
|
+
// 2) todo: add a PlannerService module that will plan the deployment (what might break, drift, etc)
|
|
52
61
|
return schema;
|
|
53
62
|
}
|
|
54
63
|
catch (err) {
|
|
@@ -58,6 +67,9 @@ class CompilerService {
|
|
|
58
67
|
isPath(input) {
|
|
59
68
|
return !input.trim().startsWith('app:');
|
|
60
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* deploys an app to Redis but does NOT activate it.
|
|
72
|
+
*/
|
|
61
73
|
async deploy(mySchemaOrPath) {
|
|
62
74
|
try {
|
|
63
75
|
let schema;
|
|
@@ -68,10 +80,13 @@ class CompilerService {
|
|
|
68
80
|
else {
|
|
69
81
|
schema = js_yaml_1.default.load(mySchemaOrPath);
|
|
70
82
|
}
|
|
83
|
+
// 2) validate the manifest file (synchronous operation...no callbacks)
|
|
71
84
|
const validator = new validator_1.Validator(schema);
|
|
72
85
|
validator.validate(this.store);
|
|
86
|
+
// 3) deploy the schema (segment, optimize, etc; save to Redis)
|
|
73
87
|
const deployer = new deployer_1.Deployer(schema);
|
|
74
88
|
await deployer.deploy(this.store);
|
|
89
|
+
// 4) save the app version to Redis (so it can be activated later)
|
|
75
90
|
await this.store.setApp(schema.app.id, schema.app.version);
|
|
76
91
|
return schema;
|
|
77
92
|
}
|
|
@@ -79,6 +94,11 @@ class CompilerService {
|
|
|
79
94
|
this.logger.error('compiler-deploy-error', err);
|
|
80
95
|
}
|
|
81
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* activates a deployed version of an app;
|
|
99
|
+
* @param appId
|
|
100
|
+
* @param appVersion
|
|
101
|
+
*/
|
|
82
102
|
async activate(appId, appVersion) {
|
|
83
103
|
return await this.store.activateAppVersion(appId, appVersion);
|
|
84
104
|
}
|
|
@@ -10,6 +10,9 @@ declare class Validator {
|
|
|
10
10
|
static SYS_VARS: string[];
|
|
11
11
|
static CONTEXT_VARS: string[];
|
|
12
12
|
constructor(manifest: HotMeshManifest);
|
|
13
|
+
/**
|
|
14
|
+
* validate the manifest file
|
|
15
|
+
*/
|
|
13
16
|
validate(store: StoreService<RedisClient, RedisMulti>): Promise<void>;
|
|
14
17
|
validateActivityIds(): void;
|
|
15
18
|
isMappingStatement(value: string): boolean;
|
|
@@ -10,6 +10,9 @@ class Validator {
|
|
|
10
10
|
this.store = null;
|
|
11
11
|
this.manifest = manifest;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* validate the manifest file
|
|
15
|
+
*/
|
|
13
16
|
async validate(store) {
|
|
14
17
|
this.store = store;
|
|
15
18
|
this.getMappingStatements();
|
|
@@ -25,10 +28,12 @@ class Validator {
|
|
|
25
28
|
this.validateHooks();
|
|
26
29
|
this.validateConditionalStatements();
|
|
27
30
|
}
|
|
31
|
+
// 1.1) Validate the manifest file activity ids are unique (no duplicates)
|
|
28
32
|
validateActivityIds() {
|
|
29
33
|
const activityIdsSet = new Set();
|
|
30
34
|
this.manifest.app.graphs.forEach((graph) => {
|
|
31
35
|
const ids = Object.keys(graph.activities);
|
|
36
|
+
// Check for duplicates and add ids to the set
|
|
32
37
|
ids.forEach((id) => {
|
|
33
38
|
if (activityIdsSet.has(id)) {
|
|
34
39
|
throw new Error(`Duplicate activity id found: ${id}`);
|
|
@@ -67,7 +72,9 @@ class Validator {
|
|
|
67
72
|
});
|
|
68
73
|
this.mappingStatements = mappingStatements;
|
|
69
74
|
}
|
|
75
|
+
// 1.2) Validate no activity ids are referenced that don't exist
|
|
70
76
|
validateReferencedActivityIds() {
|
|
77
|
+
// get list of all mapping statements and validate
|
|
71
78
|
const mappingStatements = this.mappingStatements;
|
|
72
79
|
const activityIds = this.activityIds;
|
|
73
80
|
for (const activity in mappingStatements) {
|
|
@@ -89,23 +96,41 @@ class Validator {
|
|
|
89
96
|
isContextVariable(value) {
|
|
90
97
|
return ['{$input}', '{$output}', '{$item}', '{$key}', '{$index}'].includes(value);
|
|
91
98
|
}
|
|
99
|
+
// 1.3) Validate the mapping/@pipe statements are valid
|
|
92
100
|
validateMappingStatements() {
|
|
101
|
+
// Implement the method content
|
|
93
102
|
}
|
|
103
|
+
// 1.4) Validate the transitions are valid
|
|
94
104
|
validateTransitions() {
|
|
105
|
+
// Implement the method content
|
|
95
106
|
}
|
|
107
|
+
// 1.5) Validate the transition conditions are valid
|
|
96
108
|
validateTransitionConditions() {
|
|
109
|
+
// Implement the method content
|
|
97
110
|
}
|
|
111
|
+
// 1.6) Validate the stats
|
|
98
112
|
validateStats() {
|
|
113
|
+
// Implement the method content
|
|
99
114
|
}
|
|
115
|
+
// 1.7) Validate the schemas
|
|
100
116
|
validateSchemas() {
|
|
117
|
+
// Implement the method content
|
|
101
118
|
}
|
|
119
|
+
// 1.8) Validate the topics are unique and handled
|
|
102
120
|
validateUniqueHandledTopics() {
|
|
121
|
+
// Implement the method content
|
|
103
122
|
}
|
|
123
|
+
// 1.9) Validate that every graph has publishes and subscribes
|
|
104
124
|
validateGraphPublishSubscribe() {
|
|
125
|
+
// Implement the method content
|
|
105
126
|
}
|
|
127
|
+
// 1.10) Validate hooks, including mapping statements
|
|
106
128
|
validateHooks() {
|
|
129
|
+
// Implement the method content
|
|
107
130
|
}
|
|
131
|
+
// 1.11) Validate conditional statements
|
|
108
132
|
validateConditionalStatements() {
|
|
133
|
+
// Implement the method content
|
|
109
134
|
}
|
|
110
135
|
}
|
|
111
136
|
exports.Validator = Validator;
|
|
@@ -5,6 +5,8 @@ const utils_1 = require("../../modules/utils");
|
|
|
5
5
|
const ioredis_1 = require("../connector/clients/ioredis");
|
|
6
6
|
const redis_1 = require("../connector/clients/redis");
|
|
7
7
|
class ConnectorService {
|
|
8
|
+
//1) Initialize `store`, `stream`, and `subscription` Redis clients.
|
|
9
|
+
//2) Bind to the target if not already present
|
|
8
10
|
static async initRedisClients(Redis, options, target) {
|
|
9
11
|
if (!target.store || !target.stream || !target.sub) {
|
|
10
12
|
const instances = [];
|
|
@@ -8,12 +8,32 @@ export declare class ClientService {
|
|
|
8
8
|
static instances: Map<string, HotMesh | Promise<HotMesh>>;
|
|
9
9
|
constructor(config: ClientConfig);
|
|
10
10
|
getHotMeshClient: (workflowTopic: string, namespace?: string) => Promise<HotMesh>;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a stream (Redis `XGROUP.CREATE`) where events can be published (XADD).
|
|
13
|
+
* It is possible that the worker that will read from this stream channel
|
|
14
|
+
* has not yet been initialized, so this call ensures that the channel
|
|
15
|
+
* exists and is ready to serve as a container for events.
|
|
16
|
+
*/
|
|
11
17
|
static createStream: (hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* It is possible for a client to invoke a workflow without first
|
|
20
|
+
* creating the stream. This method will verify that the stream
|
|
21
|
+
* exists and if not, create it.
|
|
22
|
+
*/
|
|
12
23
|
static verifyStream: (workflowTopic: string, namespace?: string) => Promise<HotMesh>;
|
|
13
24
|
search: (hotMeshClient: HotMesh, index: string, query: string[]) => Promise<string[]>;
|
|
14
25
|
workflow: {
|
|
15
26
|
start: (options: WorkflowOptions) => Promise<WorkflowHandleService>;
|
|
27
|
+
/**
|
|
28
|
+
* send a message to a running workflow that is paused and awaiting the signal
|
|
29
|
+
*/
|
|
16
30
|
signal: (signalId: string, data: Record<any, any>, namespace?: string) => Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* send a message to spawn an parallel in-process thread of execution
|
|
33
|
+
* with the same job state as the main thread but bound to a different
|
|
34
|
+
* handler function. All job state will be journaled to the same hash
|
|
35
|
+
* as is used by the main thread.
|
|
36
|
+
*/
|
|
17
37
|
hook: (options: HookOptions) => Promise<string>;
|
|
18
38
|
getHandle: (taskQueue: string, workflowName: string, workflowId: string, namespace?: string) => Promise<WorkflowHandleService>;
|
|
19
39
|
search: (taskQueue: string, workflowName: string, namespace: null | string, index: string, ...query: string[]) => Promise<string[]>;
|
|
@@ -27,6 +27,7 @@ class ClientService {
|
|
|
27
27
|
}
|
|
28
28
|
return hotMeshClient;
|
|
29
29
|
}
|
|
30
|
+
//create and cache an instance
|
|
30
31
|
const hotMeshClient = hotmesh_1.HotMeshService.init({
|
|
31
32
|
appId: targetNS,
|
|
32
33
|
logLevel: enums_1.HMSH_LOGLEVEL,
|
|
@@ -55,6 +56,8 @@ class ClientService {
|
|
|
55
56
|
const workflowName = options.entity ?? options.workflowName;
|
|
56
57
|
const trc = options.workflowTrace;
|
|
57
58
|
const spn = options.workflowSpan;
|
|
59
|
+
//NOTE: HotMesh 'workflowTopic' is a created by concatenating
|
|
60
|
+
// the taskQueue and workflowName used by the Durable module
|
|
58
61
|
const workflowTopic = `${taskQueueName}-${workflowName}`;
|
|
59
62
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
60
63
|
const payload = {
|
|
@@ -72,10 +75,19 @@ class ClientService {
|
|
|
72
75
|
const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context, { search: options?.search?.data, marker: options?.marker });
|
|
73
76
|
return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
74
77
|
},
|
|
78
|
+
/**
|
|
79
|
+
* send a message to a running workflow that is paused and awaiting the signal
|
|
80
|
+
*/
|
|
75
81
|
signal: async (signalId, data, namespace) => {
|
|
76
82
|
const topic = `${namespace ?? factory_1.APP_ID}.wfs.signal`;
|
|
77
83
|
return await (await this.getHotMeshClient(topic, namespace)).hook(topic, { id: signalId, data });
|
|
78
84
|
},
|
|
85
|
+
/**
|
|
86
|
+
* send a message to spawn an parallel in-process thread of execution
|
|
87
|
+
* with the same job state as the main thread but bound to a different
|
|
88
|
+
* handler function. All job state will be journaled to the same hash
|
|
89
|
+
* as is used by the main thread.
|
|
90
|
+
*/
|
|
79
91
|
hook: async (options) => {
|
|
80
92
|
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
81
93
|
const payload = {
|
|
@@ -86,6 +98,7 @@ class ClientService {
|
|
|
86
98
|
maximumAttempts: options.config?.maximumAttempts || enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
|
|
87
99
|
maximumInterval: (0, ms_1.default)(options.config?.maximumInterval || enums_1.HMSH_DURABLE_MAX_INTERVAL) / 1000,
|
|
88
100
|
};
|
|
101
|
+
//seed search data if presentthe hook before entering
|
|
89
102
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
90
103
|
const msgId = await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, types_1.StreamStatus.PENDING, 202);
|
|
91
104
|
if (options.search?.data) {
|
|
@@ -159,6 +172,12 @@ class ClientService {
|
|
|
159
172
|
_a = ClientService;
|
|
160
173
|
ClientService.topics = [];
|
|
161
174
|
ClientService.instances = new Map();
|
|
175
|
+
/**
|
|
176
|
+
* Creates a stream (Redis `XGROUP.CREATE`) where events can be published (XADD).
|
|
177
|
+
* It is possible that the worker that will read from this stream channel
|
|
178
|
+
* has not yet been initialized, so this call ensures that the channel
|
|
179
|
+
* exists and is ready to serve as a container for events.
|
|
180
|
+
*/
|
|
162
181
|
ClientService.createStream = async (hotMeshClient, workflowTopic, namespace) => {
|
|
163
182
|
const store = hotMeshClient.engine.store;
|
|
164
183
|
const params = { appId: namespace ?? factory_1.APP_ID, topic: workflowTopic };
|
|
@@ -167,8 +186,14 @@ ClientService.createStream = async (hotMeshClient, workflowTopic, namespace) =>
|
|
|
167
186
|
await store.xgroup('CREATE', streamKey, 'WORKER', '$', 'MKSTREAM');
|
|
168
187
|
}
|
|
169
188
|
catch (err) {
|
|
189
|
+
//ignore if already exists
|
|
170
190
|
}
|
|
171
191
|
};
|
|
192
|
+
/**
|
|
193
|
+
* It is possible for a client to invoke a workflow without first
|
|
194
|
+
* creating the stream. This method will verify that the stream
|
|
195
|
+
* exists and if not, create it.
|
|
196
|
+
*/
|
|
172
197
|
ClientService.verifyStream = async (workflowTopic, namespace) => {
|
|
173
198
|
const targetNS = namespace ?? factory_1.APP_ID;
|
|
174
199
|
if (ClientService.instances.has(targetNS)) {
|
|
@@ -10,20 +10,42 @@ declare class ExporterService {
|
|
|
10
10
|
symbols: Promise<Symbols> | Symbols;
|
|
11
11
|
private static symbols;
|
|
12
12
|
constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
13
|
+
/**
|
|
14
|
+
* Convert the job hash from its compiles format into a DurableJobExport object with
|
|
15
|
+
* facets that describe the workflow in terms relevant to narrative storytelling.
|
|
16
|
+
*/
|
|
13
17
|
export(jobId: string, options?: ExportOptions): Promise<DurableJobExport>;
|
|
18
|
+
/**
|
|
19
|
+
* Inflates the job data from Redis into a DurableJobExport object
|
|
20
|
+
* @param jobHash - the job data from Redis
|
|
21
|
+
* @param dependencyList - the list of dependencies for the job
|
|
22
|
+
* @returns - the inflated job data
|
|
23
|
+
*/
|
|
14
24
|
inflate(jobHash: StringStringType, options: ExportOptions): DurableJobExport;
|
|
15
25
|
resolveValue(raw: string, withValues: boolean): Record<string, any> | string | number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Inflates the key from Redis, 3-character symbol
|
|
28
|
+
* into a human-readable JSON path, reflecting the
|
|
29
|
+
* tree-like structure of the unidimensional Hash
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
16
32
|
inflateKey(key: string): string;
|
|
17
33
|
filterFields(fullObject: DurableJobExport, block?: ExportFields[], allow?: ExportFields[]): Partial<DurableJobExport>;
|
|
18
34
|
inflateTransition(match: RegExpMatchArray, value: string, transitionsObject: Record<string, TransitionType>): void;
|
|
19
35
|
sortEntriesByCreated(obj: {
|
|
20
36
|
[key: string]: TransitionType;
|
|
21
37
|
}): TransitionType[];
|
|
38
|
+
/**
|
|
39
|
+
* marker names are overloaded with details like sequence, type, etc
|
|
40
|
+
*/
|
|
22
41
|
keyToObject(key: string): {
|
|
23
42
|
index: number;
|
|
24
43
|
dimension?: string;
|
|
25
44
|
secondary?: number;
|
|
26
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* idem list has a complicated sort order based on indexes and dimensions
|
|
48
|
+
*/
|
|
27
49
|
sortParts(parts: TimelineType[]): TimelineType[];
|
|
28
50
|
}
|
|
29
51
|
export { ExporterService };
|
|
@@ -9,6 +9,10 @@ class ExporterService {
|
|
|
9
9
|
this.logger = logger;
|
|
10
10
|
this.store = store;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Convert the job hash from its compiles format into a DurableJobExport object with
|
|
14
|
+
* facets that describe the workflow in terms relevant to narrative storytelling.
|
|
15
|
+
*/
|
|
12
16
|
async export(jobId, options = {}) {
|
|
13
17
|
if (!ExporterService.symbols.has(this.appId)) {
|
|
14
18
|
const symbols = this.store.getAllSymbols();
|
|
@@ -18,6 +22,12 @@ class ExporterService {
|
|
|
18
22
|
const jobExport = this.inflate(jobData, options);
|
|
19
23
|
return jobExport;
|
|
20
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Inflates the job data from Redis into a DurableJobExport object
|
|
27
|
+
* @param jobHash - the job data from Redis
|
|
28
|
+
* @param dependencyList - the list of dependencies for the job
|
|
29
|
+
* @returns - the inflated job data
|
|
30
|
+
*/
|
|
21
31
|
inflate(jobHash, options) {
|
|
22
32
|
const timeline = [];
|
|
23
33
|
const state = {};
|
|
@@ -27,16 +37,20 @@ class ExporterService {
|
|
|
27
37
|
Object.entries(jobHash).forEach(([key, value]) => {
|
|
28
38
|
const match = key.match(regex);
|
|
29
39
|
if (match) {
|
|
40
|
+
//transitions
|
|
30
41
|
this.inflateTransition(match, value, transitionsObject);
|
|
31
42
|
}
|
|
32
43
|
else if (key.length === 3) {
|
|
44
|
+
//state
|
|
33
45
|
state[this.inflateKey(key)] = serializer_1.SerializerService.fromString(value);
|
|
34
46
|
}
|
|
35
47
|
else if (key.startsWith('_')) {
|
|
48
|
+
//data
|
|
36
49
|
data[key.substring(1)] = value;
|
|
37
50
|
}
|
|
38
51
|
else if (key.startsWith('-')) {
|
|
39
|
-
|
|
52
|
+
//timeline
|
|
53
|
+
const keyParts = this.keyToObject(key); //key parts have meaning
|
|
40
54
|
timeline.push({
|
|
41
55
|
...keyParts,
|
|
42
56
|
key,
|
|
@@ -67,6 +81,12 @@ class ExporterService {
|
|
|
67
81
|
}
|
|
68
82
|
return resolved;
|
|
69
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Inflates the key from Redis, 3-character symbol
|
|
86
|
+
* into a human-readable JSON path, reflecting the
|
|
87
|
+
* tree-like structure of the unidimensional Hash
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
70
90
|
inflateKey(key) {
|
|
71
91
|
const symbols = ExporterService.symbols.get(this.appId);
|
|
72
92
|
if (key in symbols) {
|
|
@@ -104,6 +124,7 @@ class ExporterService {
|
|
|
104
124
|
const activity = parts[0];
|
|
105
125
|
const isCreate = path.endsWith('/output/metadata/ac');
|
|
106
126
|
const isUpdate = path.endsWith('/output/metadata/au');
|
|
127
|
+
//for now only export activity start/stop; activity data would also be interesting
|
|
107
128
|
if (isCreate || isUpdate) {
|
|
108
129
|
const targetName = `${activity},${dimensions}`;
|
|
109
130
|
let target = transitionsObject[targetName];
|
|
@@ -127,6 +148,9 @@ class ExporterService {
|
|
|
127
148
|
});
|
|
128
149
|
return entriesArray;
|
|
129
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* marker names are overloaded with details like sequence, type, etc
|
|
153
|
+
*/
|
|
130
154
|
keyToObject(key) {
|
|
131
155
|
function extractDimension(label) {
|
|
132
156
|
const parts = label.split(',');
|
|
@@ -137,12 +161,14 @@ class ExporterService {
|
|
|
137
161
|
}
|
|
138
162
|
const parts = key.split('-');
|
|
139
163
|
if (parts.length === 4) {
|
|
164
|
+
//-proxy-5- -search-1-1-
|
|
140
165
|
return {
|
|
141
166
|
index: parseInt(parts[2], 10),
|
|
142
167
|
dimension: extractDimension(parts[1]),
|
|
143
168
|
};
|
|
144
169
|
}
|
|
145
170
|
else {
|
|
171
|
+
//-search,0,0-1-1- -proxy,0,0-1-
|
|
146
172
|
return {
|
|
147
173
|
index: parseInt(parts[2], 10),
|
|
148
174
|
secondary: parseInt(parts[3], 10),
|
|
@@ -150,6 +176,9 @@ class ExporterService {
|
|
|
150
176
|
};
|
|
151
177
|
}
|
|
152
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* idem list has a complicated sort order based on indexes and dimensions
|
|
181
|
+
*/
|
|
153
182
|
sortParts(parts) {
|
|
154
183
|
return parts.sort((a, b) => {
|
|
155
184
|
const { dimension: aDim, index: aIdx, secondary: aSec } = a;
|