@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.
Files changed (91) hide show
  1. package/README.md +10 -10
  2. package/build/modules/enums.js +10 -1
  3. package/build/modules/key.d.ts +38 -0
  4. package/build/modules/key.js +46 -4
  5. package/build/modules/utils.d.ts +9 -0
  6. package/build/modules/utils.js +19 -1
  7. package/build/package.json +1 -1
  8. package/build/services/activities/activity.d.ts +28 -0
  9. package/build/services/activities/activity.js +46 -1
  10. package/build/services/activities/await.js +4 -0
  11. package/build/services/activities/cycle.d.ts +7 -0
  12. package/build/services/activities/cycle.js +16 -1
  13. package/build/services/activities/hook.d.ts +6 -0
  14. package/build/services/activities/hook.js +12 -2
  15. package/build/services/activities/interrupt.js +8 -0
  16. package/build/services/activities/signal.d.ts +6 -0
  17. package/build/services/activities/signal.js +15 -0
  18. package/build/services/activities/trigger.d.ts +4 -0
  19. package/build/services/activities/trigger.js +7 -1
  20. package/build/services/activities/worker.js +4 -0
  21. package/build/services/collator/index.d.ts +70 -0
  22. package/build/services/collator/index.js +91 -1
  23. package/build/services/compiler/deployer.js +38 -6
  24. package/build/services/compiler/index.d.ts +15 -0
  25. package/build/services/compiler/index.js +20 -0
  26. package/build/services/compiler/validator.d.ts +3 -0
  27. package/build/services/compiler/validator.js +25 -0
  28. package/build/services/connector/clients/ioredis.js +2 -0
  29. package/build/services/connector/clients/redis.js +2 -0
  30. package/build/services/connector/index.js +2 -0
  31. package/build/services/durable/client.d.ts +20 -0
  32. package/build/services/durable/client.js +25 -0
  33. package/build/services/durable/exporter.d.ts +22 -0
  34. package/build/services/durable/exporter.js +30 -1
  35. package/build/services/durable/handle.d.ts +36 -0
  36. package/build/services/durable/handle.js +46 -0
  37. package/build/services/durable/index.d.ts +4 -0
  38. package/build/services/durable/index.js +4 -0
  39. package/build/services/durable/schemas/factory.d.ts +29 -0
  40. package/build/services/durable/schemas/factory.js +29 -0
  41. package/build/services/durable/search.d.ts +97 -0
  42. package/build/services/durable/search.js +108 -10
  43. package/build/services/durable/worker.js +35 -6
  44. package/build/services/durable/workflow.d.ts +118 -0
  45. package/build/services/durable/workflow.js +153 -6
  46. package/build/services/engine/index.d.ts +5 -0
  47. package/build/services/engine/index.js +43 -1
  48. package/build/services/exporter/index.d.ts +27 -0
  49. package/build/services/exporter/index.js +33 -0
  50. package/build/services/hotmesh/index.js +8 -0
  51. package/build/services/logger/index.js +2 -0
  52. package/build/services/mapper/index.d.ts +14 -0
  53. package/build/services/mapper/index.js +14 -0
  54. package/build/services/pipe/functions/date.d.ts +7 -0
  55. package/build/services/pipe/functions/date.js +7 -0
  56. package/build/services/pipe/functions/math.js +2 -0
  57. package/build/services/pipe/index.d.ts +15 -0
  58. package/build/services/pipe/index.js +23 -2
  59. package/build/services/quorum/index.d.ts +7 -0
  60. package/build/services/quorum/index.js +21 -0
  61. package/build/services/reporter/index.d.ts +5 -0
  62. package/build/services/reporter/index.js +9 -0
  63. package/build/services/router/index.d.ts +9 -0
  64. package/build/services/router/index.js +30 -2
  65. package/build/services/serializer/index.js +23 -6
  66. package/build/services/store/cache.d.ts +19 -0
  67. package/build/services/store/cache.js +19 -0
  68. package/build/services/store/clients/ioredis.js +1 -0
  69. package/build/services/store/index.d.ts +55 -0
  70. package/build/services/store/index.js +81 -5
  71. package/build/services/stream/clients/ioredis.js +4 -1
  72. package/build/services/task/index.d.ts +9 -0
  73. package/build/services/task/index.js +31 -0
  74. package/build/services/telemetry/index.d.ts +7 -0
  75. package/build/services/telemetry/index.js +13 -1
  76. package/build/services/worker/index.d.ts +4 -0
  77. package/build/services/worker/index.js +6 -2
  78. package/build/types/activity.d.ts +81 -0
  79. package/build/types/durable.d.ts +256 -0
  80. package/build/types/exporter.d.ts +13 -0
  81. package/build/types/hotmesh.d.ts +10 -1
  82. package/build/types/hotmesh.js +3 -0
  83. package/build/types/index.js +1 -1
  84. package/build/types/job.d.ts +85 -0
  85. package/build/types/pipe.d.ts +65 -0
  86. package/build/types/quorum.d.ts +14 -0
  87. package/build/types/redis.d.ts +6 -0
  88. package/build/types/stream.d.ts +58 -0
  89. package/build/types/stream.js +4 -0
  90. package/package.json +1 -1
  91. 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;
@@ -45,4 +45,6 @@ RedisConnection.instances = new Map();
45
45
  RedisConnection.clientOptions = {
46
46
  host: 'localhost',
47
47
  port: 6379,
48
+ //password: config.REDIS_PASSWORD,
49
+ //db: config.REDIS_DATABASE,
48
50
  };
@@ -57,4 +57,6 @@ RedisConnection.clientOptions = {
57
57
  port: 6379,
58
58
  tls: false,
59
59
  },
60
+ //password: config.REDIS_PASSWORD,
61
+ //database: config.REDIS_DATABASE,
60
62
  };
@@ -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
- const keyParts = this.keyToObject(key);
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;