@hotmeshio/hotmesh 0.0.7 → 0.0.8

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 (56) hide show
  1. package/build/cjs/package.json +3 -1
  2. package/build/cjs/services/activities/activity.d.ts +6 -0
  3. package/build/cjs/services/activities/activity.js +83 -7
  4. package/build/cjs/services/activities/await.d.ts +2 -2
  5. package/build/cjs/services/activities/await.js +5 -5
  6. package/build/cjs/services/activities/cycle.d.ts +19 -0
  7. package/build/cjs/services/activities/cycle.js +77 -0
  8. package/build/cjs/services/activities/index.d.ts +4 -2
  9. package/build/cjs/services/activities/index.js +4 -2
  10. package/build/cjs/services/activities/worker.d.ts +0 -8
  11. package/build/cjs/services/activities/worker.js +1 -87
  12. package/build/cjs/services/collator/index.d.ts +18 -1
  13. package/build/cjs/services/collator/index.js +41 -11
  14. package/build/cjs/services/durable/factory.js +17 -1
  15. package/build/cjs/services/durable/worker.d.ts +1 -0
  16. package/build/cjs/services/durable/worker.js +9 -14
  17. package/build/cjs/services/durable/workflow.js +5 -1
  18. package/build/cjs/services/mapper/index.js +3 -0
  19. package/build/cjs/services/signaler/stream.js +0 -1
  20. package/build/cjs/types/activity.d.ts +7 -2
  21. package/build/cjs/types/index.d.ts +1 -1
  22. package/build/esm/package.json +3 -1
  23. package/build/esm/services/activities/activity.d.ts +6 -0
  24. package/build/esm/services/activities/activity.js +83 -7
  25. package/build/esm/services/activities/await.d.ts +2 -2
  26. package/build/esm/services/activities/await.js +5 -5
  27. package/build/esm/services/activities/cycle.d.ts +19 -0
  28. package/build/esm/services/activities/cycle.js +74 -0
  29. package/build/esm/services/activities/index.d.ts +4 -2
  30. package/build/esm/services/activities/index.js +4 -2
  31. package/build/esm/services/activities/worker.d.ts +0 -8
  32. package/build/esm/services/activities/worker.js +1 -87
  33. package/build/esm/services/collator/index.d.ts +18 -1
  34. package/build/esm/services/collator/index.js +41 -11
  35. package/build/esm/services/durable/factory.js +17 -1
  36. package/build/esm/services/durable/worker.d.ts +1 -0
  37. package/build/esm/services/durable/worker.js +9 -14
  38. package/build/esm/services/durable/workflow.js +5 -1
  39. package/build/esm/services/mapper/index.js +3 -0
  40. package/build/esm/services/signaler/stream.js +0 -1
  41. package/build/esm/types/activity.d.ts +7 -2
  42. package/build/esm/types/index.d.ts +1 -1
  43. package/package.json +3 -1
  44. package/services/activities/activity.ts +90 -7
  45. package/services/activities/await.ts +5 -5
  46. package/services/activities/cycle.ts +96 -0
  47. package/services/activities/index.ts +4 -2
  48. package/services/activities/worker.ts +2 -93
  49. package/services/collator/index.ts +43 -11
  50. package/services/durable/factory.ts +17 -1
  51. package/services/durable/worker.ts +10 -13
  52. package/services/durable/workflow.ts +4 -1
  53. package/services/mapper/index.ts +3 -0
  54. package/services/signaler/stream.ts +0 -1
  55. package/types/activity.ts +8 -1
  56. package/types/index.ts +1 -0
@@ -1,15 +1,36 @@
1
1
  import { CollationError } from '../../modules/errors';
2
2
  import { CollationFaultType } from '../../types/collator';
3
3
  class CollatorService {
4
- static getDimensionalAddress(activity) {
4
+ /**
5
+ * returns the dimensional address (dad) for the target; due
6
+ * to the nature of the notary system, the dad for leg 2 entry
7
+ * must target the `0` index while leg 2 exit must target the
8
+ * current index (0)
9
+ */
10
+ static getDimensionalAddress(activity, isEntry = false) {
5
11
  let dad = activity.context.metadata.dad || activity.metadata.dad;
6
- //todo: unsure about this reset
7
- // if (dad && activity.leg === 2) {
8
- // console.log('setting dad index back to 0=>', dad);
9
- // dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
10
- // }
12
+ if (isEntry && dad && activity.leg === 2) {
13
+ dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
14
+ }
11
15
  return CollatorService.getDimensionsById([...activity.config.ancestors, activity.metadata.aid], dad);
12
16
  }
17
+ /**
18
+ * resolves the dimensional address for the
19
+ * ancestor in the graph to go back to. this address
20
+ * is determined by trimming the last digits from
21
+ * the `dad` (including the target).
22
+ * the target activity index is then set to `0`, so that
23
+ * the origin node can be queried for approval/entry.
24
+ */
25
+ static resolveReentryDimension(activity) {
26
+ const targetActivityId = activity.config.ancestor;
27
+ const ancestors = activity.config.ancestors;
28
+ const ancestorIndex = ancestors.indexOf(targetActivityId);
29
+ const dimensions = activity.metadata.dad.split(','); //e.g., `,0,0,1,0`
30
+ dimensions.length = ancestorIndex + 1;
31
+ dimensions.push('0');
32
+ return dimensions.join(',');
33
+ }
13
34
  static async notarizeEntry(activity, multi) {
14
35
  //decrement by -100_000_000_000_000
15
36
  const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100000000000000, this.getDimensionalAddress(activity), multi);
@@ -24,14 +45,21 @@ class CollatorService {
24
45
  //this.verifyInteger(amount, 1, 'exit');
25
46
  return amount;
26
47
  }
48
+ static async notarizeEarlyExit(activity, multi) {
49
+ //decrement the 2nd and 3rd digits to fully deactivate (`cycle` activities use this command to fully exit after leg 1) (should result in `888000000000000`)
50
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -11000000000000, this.getDimensionalAddress(activity), multi);
51
+ }
52
+ ;
27
53
  static async notarizeEarlyCompletion(activity, multi) {
28
- //initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd and 3rd digits to deactivate the activity
29
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - 11000000000000, this.getDimensionalAddress(activity), multi);
54
+ //initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd
55
+ //3rd digit is optionally kept open if the activity might be used in a cycle
56
+ const decrement = activity.config.cycle ? 10000000000000 : 11000000000000;
57
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - decrement, this.getDimensionalAddress(activity), multi);
30
58
  }
31
59
  ;
32
60
  static async notarizeReentry(activity, multi) {
33
61
  //increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
34
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity), multi);
62
+ const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity, true), multi);
35
63
  this.verifyInteger(amount, 2, 'enter');
36
64
  return amount;
37
65
  }
@@ -42,8 +70,10 @@ class CollatorService {
42
70
  }
43
71
  ;
44
72
  static async notarizeCompletion(activity, multi) {
45
- //close out; actualize leg2 dimension (+1) and decrement the 3rd digit (-1_000_000_000_000)
46
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - 1000000000000, this.getDimensionalAddress(activity), multi);
73
+ //1) ALWAYS actualize leg2 dimension (+1)
74
+ //2) IF the activity is used in a cycle, don't close leg 2!
75
+ const decrement = activity.config.cycle ? 0 : -1000000000000;
76
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), multi);
47
77
  }
48
78
  ;
49
79
  static getDigitAtIndex(num, targetDigitIndex) {
@@ -26,7 +26,12 @@ const getWorkflowYAML = (topic, version = '1') => {
26
26
  type: trigger
27
27
  stats:
28
28
  id: '{$self.input.data.workflowId}'
29
+
29
30
  a1:
31
+ type: activity
32
+ cycle: true
33
+
34
+ w1:
30
35
  type: worker
31
36
  topic: ${topic}
32
37
  input:
@@ -49,9 +54,20 @@ const getWorkflowYAML = (topic, version = '1') => {
49
54
  job:
50
55
  maps:
51
56
  response: '{$self.output.data.response}'
57
+
58
+ c1:
59
+ type: cycle
60
+ ancestor: a1
52
61
  transitions:
53
62
  t1:
54
- - to: a1`;
63
+ - to: a1
64
+ a1:
65
+ - to: w1
66
+ w1:
67
+ - to: c1
68
+ conditions:
69
+ code: 500
70
+ `;
55
71
  };
56
72
  const getActivityYAML = (topic, version = '1') => {
57
73
  return `app:
@@ -5,6 +5,7 @@ export declare class WorkerService {
5
5
  static connection: Connection;
6
6
  static instances: Map<string, HotMesh | Promise<HotMesh>>;
7
7
  workflowRunner: HotMesh;
8
+ activityRunner: HotMesh;
8
9
  static getHotMesh: (worflowTopic: string) => Promise<HotMesh>;
9
10
  static activateWorkflow(hotMesh: HotMesh, topic: string, factory: Function): Promise<void>;
10
11
  /**
@@ -94,8 +94,8 @@ class WorkerService {
94
94
  const workflowTopic = `${baseTopic}`;
95
95
  //initialize supporting workflows
96
96
  const worker = new WorkerService();
97
- const activityRunner = await worker.initActivityWorkflow(config, activityTopic);
98
- await WorkerService.activateWorkflow(activityRunner, activityTopic, getActivityYAML);
97
+ worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
98
+ await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
99
99
  worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
100
100
  await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML);
101
101
  return worker;
@@ -113,12 +113,7 @@ class WorkerService {
113
113
  return [workflowFunction.name, workflowFunction];
114
114
  }
115
115
  async run() {
116
- if (this.workflowRunner) {
117
- this.workflowRunner.engine.logger.info('WorkerService is running');
118
- }
119
- else {
120
- console.log('WorkerService is running');
121
- }
116
+ this.workflowRunner.engine.logger.info('WorkerService is running');
122
117
  }
123
118
  async initActivityWorkflow(config, activityTopic) {
124
119
  const redisConfig = {
@@ -153,10 +148,11 @@ class WorkerService {
153
148
  };
154
149
  }
155
150
  catch (err) {
156
- console.error(err);
157
- //todo (make retry configurable)
151
+ this.activityRunner.engine.logger.error('durable-worker-activity-err', err);
158
152
  return {
159
- status: StreamStatus.PENDING,
153
+ status: StreamStatus.ERROR,
154
+ code: 500,
155
+ message: err.message,
160
156
  metadata: { ...data.metadata },
161
157
  data: { error: err }
162
158
  };
@@ -173,7 +169,7 @@ class WorkerService {
173
169
  await hotMesh.activate(version);
174
170
  }
175
171
  catch (err) {
176
- console.log('durable-worker-activity-deploy-activate-error', err);
172
+ hotMesh.engine.logger.error('durable-worker-activity-deploy-activate-error', err);
177
173
  throw err;
178
174
  }
179
175
  }
@@ -229,10 +225,9 @@ class WorkerService {
229
225
  };
230
226
  }
231
227
  catch (err) {
232
- //todo: (retryable error types)
233
228
  return {
229
+ status: StreamStatus.ERROR,
234
230
  code: 500,
235
- status: StreamStatus.PENDING,
236
231
  metadata: { ...data.metadata },
237
232
  data: { error: err }
238
233
  };
@@ -94,7 +94,11 @@ export class WorkflowService {
94
94
  try {
95
95
  const hmshInstance = await WorkerService.getHotMesh(activityTopic);
96
96
  activityState = await hmshInstance.getState(activityTopic, activityJobId);
97
- if (activityState.metadata.js == 1) {
97
+ if (activityState.metadata.err) {
98
+ await hmshInstance.scrub(activityJobId);
99
+ throw new Error(activityState.metadata.err);
100
+ }
101
+ else if (activityState.metadata.js === 0) {
98
102
  //return immediately
99
103
  return activityState.data?.response;
100
104
  }
@@ -47,6 +47,9 @@ class MapperService {
47
47
  return transitionRule;
48
48
  }
49
49
  if (code.toString() === (transitionRule.code || 200).toString()) {
50
+ if (!transitionRule.match) {
51
+ return true;
52
+ }
50
53
  const orGate = transitionRule.gate === 'or';
51
54
  let allAreTrue = true;
52
55
  let someAreTrue = false;
@@ -118,7 +118,6 @@ class StreamSignaler {
118
118
  output = await callback(input);
119
119
  }
120
120
  catch (err) {
121
- console.error(err);
122
121
  this.logger.error(`stream-call-function-error`, { stream, id, err });
123
122
  output = this.structureUnhandledError(input, err);
124
123
  }
@@ -1,6 +1,6 @@
1
1
  import { MetricTypes } from "./stats";
2
2
  import { StreamRetryPolicy } from "./stream";
3
- type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate';
3
+ type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle';
4
4
  type Consumes = Record<string, string[]>;
5
5
  interface BaseActivity {
6
6
  title?: string;
@@ -15,6 +15,7 @@ interface BaseActivity {
15
15
  sleep?: number;
16
16
  expire?: number;
17
17
  retry?: StreamRetryPolicy;
18
+ cycle?: boolean;
18
19
  collationInt?: number;
19
20
  consumes?: Consumes;
20
21
  PRODUCES?: string[];
@@ -55,6 +56,10 @@ interface WorkerActivity extends BaseActivity {
55
56
  interface EmitActivity extends BaseActivity {
56
57
  type: 'emit';
57
58
  }
59
+ interface CycleActivity extends BaseActivity {
60
+ type: 'cycle';
61
+ ancestor: string;
62
+ }
58
63
  interface IterateActivity extends BaseActivity {
59
64
  type: 'iterate';
60
65
  }
@@ -84,4 +89,4 @@ type ActivityDataType = {
84
89
  hook?: Record<string, unknown>;
85
90
  };
86
91
  type ActivityLeg = 1 | 2;
87
- export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, BaseActivity, EmitActivity, IterateActivity, TriggerActivity, WorkerActivity };
92
+ export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, BaseActivity, EmitActivity, IterateActivity, TriggerActivity, WorkerActivity };
@@ -1,4 +1,4 @@
1
- export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, EmitActivity, WorkerActivity, IterateActivity, TriggerActivity, TriggerActivityStats } from './activity';
1
+ export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, EmitActivity, WorkerActivity, IterateActivity, 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Durable Workflows",
5
5
  "main": "build/cjs/index.js",
6
6
  "module": "build/esm/index.js",
@@ -29,6 +29,7 @@
29
29
  "test": "NODE_ENV=test jest --detectOpenHandles --forceExit --verbose",
30
30
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
31
31
  "test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
32
+ "test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/index.test.ts --detectOpenHandles --forceExit --verbose",
32
33
  "test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
33
34
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
34
35
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
@@ -48,6 +49,7 @@
48
49
  "test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
49
50
  "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
50
51
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
52
+ "test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
51
53
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
52
54
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
53
55
  },
@@ -129,7 +129,7 @@ class Activity {
129
129
  this.leg = leg;
130
130
  }
131
131
 
132
- //******** SIGNALER RE-ENTRY POINT (B) ********//
132
+ //******** SIGNAL RE-ENTRY POINT ********//
133
133
  doesHook(): boolean {
134
134
  return !!(this.config.hook?.topic || this.config.sleep);
135
135
  }
@@ -170,11 +170,6 @@ class Activity {
170
170
  return await this.processHookEvent(jobId);
171
171
  }
172
172
 
173
- //todo: hooks are currently singletons. but they can support
174
- // dimensional threads like `await` and `worker` do.
175
- // Copy code from those activities to support cyclical
176
- // timehook and eventhook inputs by adding a 'pending'
177
- // flag to hooks that allows for repeated signals
178
173
  async processHookEvent(jobId: string): Promise<JobStatus | void> {
179
174
  this.logger.debug('activity-process-hook-event', { jobId });
180
175
  let telemetry: TelemetryService;
@@ -207,7 +202,6 @@ class Activity {
207
202
  telemetry.setActivityAttributes(attrs);
208
203
  return jobStatus as number;
209
204
  } catch (error) {
210
- console.error('this error?', error);
211
205
  this.logger.error('engine-process-hook-event-error', error);
212
206
  telemetry.setActivityError(error.message);
213
207
  throw error;
@@ -216,6 +210,95 @@ class Activity {
216
210
  }
217
211
  }
218
212
 
213
+ //******** DUPLEX RE-ENTRY POINT ********//
214
+ async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<void> {
215
+ this.setLeg(2);
216
+ const jid = this.context.metadata.jid;
217
+ const aid = this.metadata.aid;
218
+ this.status = status;
219
+ this.code = code;
220
+ this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
221
+ let telemetry: TelemetryService;
222
+ try {
223
+ await this.getState();
224
+ const aState = await CollatorService.notarizeReentry(this);
225
+ this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
226
+
227
+ telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
228
+ let isComplete = CollatorService.isActivityComplete(this.context.metadata.js);
229
+
230
+ if (isComplete) {
231
+ this.logger.warn('activity-process-event-duplicate', { jid, aid });
232
+ this.logger.debug('activity-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
233
+ return;
234
+ }
235
+
236
+ telemetry.startActivitySpan(this.leg);
237
+ let multiResponse: MultiResponseFlags;
238
+ if (status === StreamStatus.PENDING) {
239
+ multiResponse = await this.processPending(telemetry);
240
+ } else if (status === StreamStatus.SUCCESS) {
241
+ multiResponse = await this.processSuccess(telemetry);
242
+ } else {
243
+ multiResponse = await this.processError(telemetry);
244
+ }
245
+ this.transitionAdjacent(multiResponse, telemetry);
246
+ } catch (error) {
247
+ this.logger.error('activity-process-event-error', error);
248
+ telemetry.setActivityError(error.message);
249
+ throw error;
250
+ } finally {
251
+ telemetry.endActivitySpan();
252
+ this.logger.debug('activity-process-event-end', { jid, aid });
253
+ }
254
+ }
255
+
256
+ async processPending(telemetry: TelemetryService): Promise<MultiResponseFlags> {
257
+ this.bindActivityData('output');
258
+ this.adjacencyList = await this.filterAdjacent();
259
+ this.mapJobData();
260
+ const multi = this.store.getMulti();
261
+ await this.setState(multi);
262
+ await CollatorService.notarizeContinuation(this, multi);
263
+
264
+ await this.setStatus(this.adjacencyList.length, multi);
265
+ return await multi.exec() as MultiResponseFlags;
266
+ }
267
+
268
+ async processSuccess(telemetry: TelemetryService): Promise<MultiResponseFlags> {
269
+ this.bindActivityData('output');
270
+ this.adjacencyList = await this.filterAdjacent();
271
+ this.mapJobData();
272
+ const multi = this.store.getMulti();
273
+ await this.setState(multi);
274
+ await CollatorService.notarizeCompletion(this, multi);
275
+
276
+ await this.setStatus(this.adjacencyList.length - 1, multi);
277
+ return await multi.exec() as MultiResponseFlags;
278
+ }
279
+
280
+ async processError(telemetry: TelemetryService): Promise<MultiResponseFlags> {
281
+ this.bindActivityError(this.data);
282
+ this.adjacencyList = await this.filterAdjacent();
283
+ const multi = this.store.getMulti();
284
+ await this.setState(multi);
285
+ await CollatorService.notarizeCompletion(this, multi);
286
+
287
+ await this.setStatus(this.adjacencyList.length - 1, multi);
288
+ return await multi.exec() as MultiResponseFlags;
289
+ }
290
+
291
+ async transitionAdjacent(multiResponse: MultiResponseFlags, telemetry: TelemetryService): Promise<void> {
292
+ telemetry.mapActivityAttributes();
293
+ const jobStatus = this.resolveStatus(multiResponse);
294
+ const attrs: StringScalarType = { 'app.job.jss': jobStatus };
295
+ const messageIds = await this.transition(this.adjacencyList, jobStatus);
296
+ if (messageIds.length) {
297
+ attrs['app.activity.mids'] = messageIds.join(',')
298
+ }
299
+ telemetry.setActivityAttributes(attrs);
300
+ }
301
+
219
302
  resolveStatus(multiResponse: MultiResponseFlags): number {
220
303
  const activityStatus = multiResponse[multiResponse.length - 1];
221
304
  if (Array.isArray(activityStatus)) {
@@ -44,7 +44,7 @@ class Await extends Activity {
44
44
  this.mapInputData();
45
45
 
46
46
  const multi = this.store.getMulti();
47
- //await this.registerTimeout();
47
+ //todo: await this.registerTimeout();
48
48
  await CollatorService.authorizeReentry(this, multi);
49
49
  await this.setState(multi);
50
50
  await this.setStatus(0, multi);
@@ -121,11 +121,11 @@ class Await extends Activity {
121
121
  if (status === StreamStatus.SUCCESS) {
122
122
  this.bindActivityData('output');
123
123
  this.adjacencyList = await this.filterAdjacent();
124
- multiResponse = await this.processSuccess(this.adjacencyList);
124
+ multiResponse = await this.processSuccessResponse(this.adjacencyList);
125
125
  } else {
126
126
  this.bindActivityError(this.data);
127
127
  this.adjacencyList = await this.filterAdjacent();
128
- multiResponse = await this.processError(this.adjacencyList);
128
+ multiResponse = await this.processErrorResponse(this.adjacencyList);
129
129
  }
130
130
 
131
131
  telemetry.mapActivityAttributes();
@@ -146,7 +146,7 @@ class Await extends Activity {
146
146
  }
147
147
  }
148
148
 
149
- async processSuccess(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
149
+ async processSuccessResponse(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
150
150
  this.mapJobData();
151
151
  const multi = this.store.getMulti();
152
152
  await this.setState(multi);
@@ -156,7 +156,7 @@ class Await extends Activity {
156
156
  return await multi.exec() as MultiResponseFlags;
157
157
  }
158
158
 
159
- async processError(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
159
+ async processErrorResponse(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
160
160
  //todo: if adjacencyList.length == 0, then map to the job output
161
161
  // this method would be added to Base activity class
162
162
  //this.mapJobData();
@@ -0,0 +1,96 @@
1
+ import { GetStateError } from '../../modules/errors';
2
+ import { CollatorService } from '../collator';
3
+ import { EngineService } from '../engine';
4
+ import { Activity, ActivityType } from './activity';
5
+ import {
6
+ ActivityData,
7
+ ActivityMetadata,
8
+ CycleActivity } from '../../types/activity';
9
+ import { JobState } from '../../types/job';
10
+ import { MultiResponseFlags, RedisMulti } from '../../types/redis';
11
+ import { StreamData } from '../../types/stream';
12
+ import { TelemetryService } from '../telemetry';
13
+
14
+ class Cycle extends Activity {
15
+ config: CycleActivity;
16
+
17
+ constructor(
18
+ config: ActivityType,
19
+ data: ActivityData,
20
+ metadata: ActivityMetadata,
21
+ hook: ActivityData | null,
22
+ engine: EngineService,
23
+ context?: JobState) {
24
+ super(config, data, metadata, hook, engine, context);
25
+ }
26
+
27
+
28
+ //******** LEG 1 ENTRY ********//
29
+ async process(): Promise<string> {
30
+ this.logger.debug('cycle-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
31
+ let telemetry: TelemetryService;
32
+ try {
33
+ //verify entry is allowed
34
+ this.setLeg(1);
35
+ await CollatorService.notarizeEntry(this);
36
+ await this.getState();
37
+ telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
38
+ telemetry.startActivitySpan(this.leg);
39
+ this.mapInputData();
40
+
41
+ //set state/status
42
+ let multi = this.store.getMulti();
43
+ await this.setState(multi);
44
+ await this.setStatus(0, multi); //leg 1 never changes job status
45
+ const multiResponse = await multi.exec() as MultiResponseFlags;
46
+ telemetry.mapActivityAttributes();
47
+ const jobStatus = this.resolveStatus(multiResponse);
48
+
49
+ //cycle the target ancestor
50
+ multi = this.store.getMulti();
51
+ const messageId = await this.cycleAncestorActivity(multi);
52
+ telemetry.setActivityAttributes({
53
+ 'app.activity.mid': messageId,
54
+ 'app.job.jss': jobStatus
55
+ });
56
+
57
+ //exit early (`Cycle` activities only execute Leg 1)
58
+ await CollatorService.notarizeEarlyExit(this, multi);
59
+ await multi.exec() as MultiResponseFlags;
60
+
61
+ return this.context.metadata.aid;
62
+ } catch (error) {
63
+ if (error instanceof GetStateError) {
64
+ this.logger.error('cycle-get-state-error', error);
65
+ } else {
66
+ this.logger.error('cycle-process-error', error);
67
+ }
68
+ telemetry.setActivityError(error.message);
69
+ throw error;
70
+ } finally {
71
+ telemetry.endActivitySpan();
72
+ this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Trigger the target ancestor to execute in a cycle,
78
+ * without violating the constraints of the DAG. Immutable
79
+ * `individual activity state` will execute in a new dimensional
80
+ * thread while `shared job state` can change. This
81
+ * pattern allows for retries without violating the DAG.
82
+ */
83
+ async cycleAncestorActivity(multi: RedisMulti): Promise<string> {
84
+ const streamData: StreamData = {
85
+ metadata: {
86
+ dad: CollatorService.resolveReentryDimension(this),
87
+ jid: this.context.metadata.jid,
88
+ aid: this.config.ancestor,
89
+ },
90
+ data: {} //todo: verify immutability, before enabling: `this.context.data`
91
+ };
92
+ return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
93
+ }
94
+ }
95
+
96
+ export { Cycle };
@@ -1,13 +1,15 @@
1
1
  import { Activity } from './activity';
2
2
  import { Await } from './await';
3
- import { Worker } from './worker';
4
- import { Iterate } from './iterate';
3
+ import { Cycle } from './cycle';
5
4
  import { Emit } from './emit';
5
+ import { Iterate } from './iterate';
6
6
  import { Trigger } from './trigger';
7
+ import { Worker } from './worker';
7
8
 
8
9
  export default {
9
10
  activity: Activity,
10
11
  await: Await,
12
+ cycle: Cycle,
11
13
  iterate: Iterate,
12
14
  emit: Emit,
13
15
  trigger: Trigger,