@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
@@ -43,7 +43,7 @@ class Worker extends Activity {
43
43
  this.mapInputData();
44
44
 
45
45
  const multi = this.store.getMulti();
46
- //await this.registerTimeout();
46
+ //todo: await this.registerTimeout();
47
47
  await CollatorService.authorizeReentry(this, multi);
48
48
  await this.setState(multi);
49
49
  await this.setStatus(0, multi);
@@ -56,13 +56,12 @@ class Worker extends Activity {
56
56
  'app.activity.mid': messageId,
57
57
  'app.job.jss': jobStatus
58
58
  });
59
- //TODO: UPDATE ACTIVITY STATE (LEG 1 EXIT)
59
+
60
60
  return this.context.metadata.aid;
61
61
  } catch (error) {
62
62
  if (error instanceof GetStateError) {
63
63
  this.logger.error('worker-get-state-error', error);
64
64
  } else {
65
- console.error(error);
66
65
  this.logger.error('worker-process-error', error);
67
66
  }
68
67
  telemetry.setActivityError(error.message);
@@ -92,96 +91,6 @@ class Worker extends Activity {
92
91
  }
93
92
  return (await this.engine.streamSignaler?.publishMessage(this.config.subtype, streamData)) as string;
94
93
  }
95
-
96
-
97
- //******** SIGNAL RE-ENTRY POINT (DUPLEX LEG 2 of 2) ********//
98
- async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<void> {
99
- this.setLeg(2);
100
- const jid = this.context.metadata.jid;
101
- const aid = this.metadata.aid;
102
- this.status = status;
103
- this.code = code;
104
- this.logger.debug('worker-process-event', { topic: this.config.subtype, jid, aid, status, code });
105
- let telemetry: TelemetryService;
106
- try {
107
- await this.getState();
108
- const aState = await CollatorService.notarizeReentry(this);
109
- this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
110
-
111
- telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
112
- let isComplete = CollatorService.isActivityComplete(this.context.metadata.js);
113
-
114
- if (isComplete) {
115
- this.logger.warn('worker-process-event-duplicate', { jid, aid });
116
- this.logger.debug('worker-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
117
- return;
118
- }
119
- telemetry.startActivitySpan(this.leg);
120
- if (status === StreamStatus.PENDING) {
121
- await this.processPending(telemetry);
122
- } else if (status === StreamStatus.SUCCESS) {
123
- await this.processSuccess(telemetry);
124
- } else {
125
- await this.processError(telemetry);
126
- }
127
- } catch (error) {
128
- this.logger.error('worker-process-event-error', error);
129
- telemetry.setActivityError(error.message);
130
- throw error;
131
- } finally {
132
- telemetry.endActivitySpan();
133
- this.logger.debug('worker-process-event-end', { jid, aid });
134
- }
135
- }
136
-
137
- async processPending(telemetry: TelemetryService): Promise<void> {
138
- this.bindActivityData('output');
139
- this.adjacencyList = await this.filterAdjacent();
140
- this.mapJobData();
141
- const multi = this.store.getMulti();
142
- await this.setState(multi);
143
- await CollatorService.notarizeContinuation(this, multi);
144
-
145
- await this.setStatus(this.adjacencyList.length, multi);
146
- const multiResponse = await multi.exec() as MultiResponseFlags;
147
- this.transitionAdjacent(multiResponse, telemetry);
148
- }
149
-
150
- async processSuccess(telemetry: TelemetryService): Promise<void> {
151
- this.bindActivityData('output');
152
- this.adjacencyList = await this.filterAdjacent();
153
- this.mapJobData();
154
- const multi = this.store.getMulti();
155
- await this.setState(multi);
156
- await CollatorService.notarizeCompletion(this, multi);
157
-
158
- await this.setStatus(this.adjacencyList.length - 1, multi);
159
- const multiResponse = await multi.exec() as MultiResponseFlags;
160
- this.transitionAdjacent(multiResponse, telemetry);
161
- }
162
-
163
- async processError(telemetry: TelemetryService): Promise<void> {
164
- this.bindActivityError(this.data);
165
- this.adjacencyList = await this.filterAdjacent();
166
- const multi = this.store.getMulti();
167
- await this.setState(multi);
168
- await CollatorService.notarizeCompletion(this, multi);
169
-
170
- await this.setStatus(this.adjacencyList.length - 1, multi);
171
- const multiResponse = await multi.exec() as MultiResponseFlags;
172
- this.transitionAdjacent(multiResponse, telemetry);
173
- }
174
-
175
- async transitionAdjacent(multiResponse: MultiResponseFlags, telemetry: TelemetryService): Promise<void> {
176
- telemetry.mapActivityAttributes();
177
- const jobStatus = this.resolveStatus(multiResponse);
178
- const attrs: StringScalarType = { 'app.job.jss': jobStatus };
179
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
180
- if (messageIds.length) {
181
- attrs['app.activity.mids'] = messageIds.join(',')
182
- }
183
- telemetry.setActivityAttributes(attrs);
184
- }
185
94
  }
186
95
 
187
96
  export { Worker };
@@ -4,22 +4,45 @@ import { CollationFaultType, CollationStage } from '../../types/collator';
4
4
  import { ActivityDuplex } from '../../types/activity';
5
5
  import { HotMeshGraph } from '../../types/hotmesh';
6
6
  import { Activity } from '../activities/activity';
7
+ import { Cycle } from '../activities/cycle';
7
8
 
8
9
  class CollatorService {
9
10
 
10
11
  //max int digit count that supports `hincrby`
11
12
  static targetLength = 15;
12
13
 
13
- static getDimensionalAddress(activity: Activity): Record<string, string> {
14
+ /**
15
+ * returns the dimensional address (dad) for the target; due
16
+ * to the nature of the notary system, the dad for leg 2 entry
17
+ * must target the `0` index while leg 2 exit must target the
18
+ * current index (0)
19
+ */
20
+ static getDimensionalAddress(activity: Activity, isEntry = false): Record<string, string> {
14
21
  let dad = activity.context.metadata.dad || activity.metadata.dad;
15
- //todo: unsure about this reset
16
- // if (dad && activity.leg === 2) {
17
- // console.log('setting dad index back to 0=>', dad);
18
- // dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
19
- // }
22
+ if (isEntry && dad && activity.leg === 2) {
23
+ dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
24
+ }
20
25
  return CollatorService.getDimensionsById([...activity.config.ancestors, activity.metadata.aid], dad);
21
26
  }
22
27
 
28
+ /**
29
+ * resolves the dimensional address for the
30
+ * ancestor in the graph to go back to. this address
31
+ * is determined by trimming the last digits from
32
+ * the `dad` (including the target).
33
+ * the target activity index is then set to `0`, so that
34
+ * the origin node can be queried for approval/entry.
35
+ */
36
+ static resolveReentryDimension(activity: Cycle) {
37
+ const targetActivityId = activity.config.ancestor;
38
+ const ancestors = activity.config.ancestors;
39
+ const ancestorIndex = ancestors.indexOf(targetActivityId);
40
+ const dimensions = activity.metadata.dad.split(','); //e.g., `,0,0,1,0`
41
+ dimensions.length = ancestorIndex + 1;
42
+ dimensions.push('0');
43
+ return dimensions.join(',');
44
+ }
45
+
23
46
  static async notarizeEntry(activity: Activity, multi?: RedisMulti): Promise<number> {
24
47
  //decrement by -100_000_000_000_000
25
48
  const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100_000_000_000_000, this.getDimensionalAddress(activity), multi);
@@ -35,14 +58,21 @@ class CollatorService {
35
58
  return amount;
36
59
  }
37
60
 
61
+ static async notarizeEarlyExit(activity: Activity, multi?: RedisMulti): Promise<number> {
62
+ //decrement the 2nd and 3rd digits to fully deactivate (`cycle` activities use this command to fully exit after leg 1) (should result in `888000000000000`)
63
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -11_000_000_000_000, this.getDimensionalAddress(activity), multi);
64
+ };
65
+
38
66
  static async notarizeEarlyCompletion(activity: Activity, multi?: RedisMulti): Promise<number> {
39
- //initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd and 3rd digits to deactivate the activity
40
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - 11_000_000_000_000, this.getDimensionalAddress(activity), multi);
67
+ //initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd
68
+ //3rd digit is optionally kept open if the activity might be used in a cycle
69
+ const decrement = activity.config.cycle ? 10_000_000_000_000 : 11_000_000_000_000;
70
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - decrement, this.getDimensionalAddress(activity), multi);
41
71
  };
42
72
 
43
73
  static async notarizeReentry(activity: Activity, multi?: RedisMulti): Promise<number> {
44
74
  //increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
45
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity), multi);
75
+ const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity, true), multi);
46
76
  this.verifyInteger(amount, 2, 'enter');
47
77
  return amount;
48
78
  };
@@ -53,8 +83,10 @@ class CollatorService {
53
83
  };
54
84
 
55
85
  static async notarizeCompletion(activity: Activity, multi?: RedisMulti): Promise<number> {
56
- //close out; actualize leg2 dimension (+1) and decrement the 3rd digit (-1_000_000_000_000)
57
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - 1_000_000_000_000, this.getDimensionalAddress(activity), multi);
86
+ //1) ALWAYS actualize leg2 dimension (+1)
87
+ //2) IF the activity is used in a cycle, don't close leg 2!
88
+ const decrement = activity.config.cycle ? 0 : -1_000_000_000_000;
89
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), multi);
58
90
  };
59
91
 
60
92
  static getDigitAtIndex(num: number, targetDigitIndex: number): number | null {
@@ -26,7 +26,12 @@ const getWorkflowYAML = (topic: string, 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: string, 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
 
57
73
  const getActivityYAML = (topic: string, version = '1') => {
@@ -44,6 +44,7 @@ export class WorkerService {
44
44
  static connection: Connection;
45
45
  static instances = new Map<string, HotMesh | Promise<HotMesh>>();
46
46
  workflowRunner: HotMesh;
47
+ activityRunner: HotMesh;
47
48
 
48
49
  static getHotMesh = async (worflowTopic: string) => {
49
50
  if (WorkerService.instances.has(worflowTopic)) {
@@ -114,8 +115,8 @@ export class WorkerService {
114
115
 
115
116
  //initialize supporting workflows
116
117
  const worker = new WorkerService();
117
- const activityRunner = await worker.initActivityWorkflow(config, activityTopic);
118
- await WorkerService.activateWorkflow(activityRunner, activityTopic, getActivityYAML);
118
+ worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
119
+ await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
119
120
  worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
120
121
  await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML);
121
122
  return worker;
@@ -134,11 +135,7 @@ export class WorkerService {
134
135
  }
135
136
 
136
137
  async run() {
137
- if (this.workflowRunner) {
138
- this.workflowRunner.engine.logger.info('WorkerService is running');
139
- } else {
140
- console.log('WorkerService is running');
141
- }
138
+ this.workflowRunner.engine.logger.info('WorkerService is running');
142
139
  }
143
140
 
144
141
  async initActivityWorkflow(config: WorkerConfig, activityTopic: string): Promise<HotMesh> {
@@ -175,10 +172,11 @@ export class WorkerService {
175
172
  data: { response: pojoResponse }
176
173
  };
177
174
  } catch (err) {
178
- console.error(err);
179
- //todo (make retry configurable)
175
+ this.activityRunner.engine.logger.error('durable-worker-activity-err', err);
180
176
  return {
181
- status: StreamStatus.PENDING,
177
+ status: StreamStatus.ERROR,
178
+ code: 500,
179
+ message: err.message,
182
180
  metadata: { ...data.metadata },
183
181
  data: { error: err }
184
182
  } as StreamDataResponse;
@@ -195,7 +193,7 @@ export class WorkerService {
195
193
  await hotMesh.deploy(getActivityYAML(activityTopic, version));
196
194
  await hotMesh.activate(version);
197
195
  } catch (err) {
198
- console.log('durable-worker-activity-deploy-activate-error', err);
196
+ hotMesh.engine.logger.error('durable-worker-activity-deploy-activate-error', err);
199
197
  throw err;
200
198
  }
201
199
  } else if(app && !app.active) {
@@ -260,10 +258,9 @@ export class WorkerService {
260
258
  data: { response: workflowResponse }
261
259
  };
262
260
  } catch (err) {
263
- //todo: (retryable error types)
264
261
  return {
262
+ status: StreamStatus.ERROR,
265
263
  code: 500,
266
- status: StreamStatus.PENDING,
267
264
  metadata: { ...data.metadata },
268
265
  data: { error: err }
269
266
  } as StreamDataResponse;
@@ -103,7 +103,10 @@ export class WorkflowService {
103
103
  try {
104
104
  const hmshInstance = await WorkerService.getHotMesh(activityTopic);
105
105
  activityState = await hmshInstance.getState(activityTopic, activityJobId);
106
- if (activityState.metadata.js == 1) {
106
+ if (activityState.metadata.err) {
107
+ await hmshInstance.scrub(activityJobId);
108
+ throw new Error(activityState.metadata.err);
109
+ } else if (activityState.metadata.js === 0) {
107
110
  //return immediately
108
111
  return activityState.data?.response as T;
109
112
  }
@@ -60,6 +60,9 @@ class MapperService {
60
60
  return transitionRule;
61
61
  }
62
62
  if (code.toString() === (transitionRule.code || 200).toString()) {
63
+ if (!transitionRule.match) {
64
+ return true;
65
+ }
63
66
  const orGate = transitionRule.gate === 'or';
64
67
  let allAreTrue = true;
65
68
  let someAreTrue = false;
@@ -150,7 +150,6 @@ class StreamSignaler {
150
150
  try {
151
151
  output = await callback(input);
152
152
  } catch (err) {
153
- console.error(err);
154
153
  this.logger.error(`stream-call-function-error`, { stream, id, err });
155
154
  output = this.structureUnhandledError(input, err);
156
155
  }
package/types/activity.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { MetricTypes } from "./stats";
2
2
  import { StreamRetryPolicy } from "./stream";
3
3
 
4
- type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate';
4
+ type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle';
5
5
 
6
6
  type Consumes = Record<string, string[]>;
7
7
 
@@ -18,6 +18,7 @@ interface BaseActivity {
18
18
  sleep?: number; //@pipe /in seconds
19
19
  expire?: number; //-1 forever (15 seconds default); todo: make globally configurable
20
20
  retry?: StreamRetryPolicy
21
+ cycle?: boolean; //if true, the `notary` will leave leg 2 open, so it can be re/cycled
21
22
  collationInt?: number; //compiler
22
23
  consumes?: Consumes; //compiler
23
24
  PRODUCES?: string[]; //compiler
@@ -61,6 +62,11 @@ interface EmitActivity extends BaseActivity {
61
62
  type: 'emit';
62
63
  }
63
64
 
65
+ interface CycleActivity extends BaseActivity {
66
+ type: 'cycle';
67
+ ancestor: string; //ancestor activity id
68
+ }
69
+
64
70
  interface IterateActivity extends BaseActivity {
65
71
  type: 'iterate';
66
72
  }
@@ -108,6 +114,7 @@ export {
108
114
  Consumes,
109
115
  TriggerActivityStats,
110
116
  AwaitActivity,
117
+ CycleActivity,
111
118
  BaseActivity,
112
119
  EmitActivity,
113
120
  IterateActivity,
package/types/index.ts CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  Consumes,
10
10
  AwaitActivity,
11
11
  BaseActivity,
12
+ CycleActivity,
12
13
  EmitActivity,
13
14
  WorkerActivity,
14
15
  IterateActivity,