@hotmeshio/hotmesh 0.0.6 → 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 +84 -9
  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 -6
  11. package/build/cjs/services/activities/worker.js +1 -93
  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 +18 -19
  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 +84 -9
  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 -6
  32. package/build/esm/services/activities/worker.js +1 -93
  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 +18 -19
  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 +91 -9
  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 -97
  49. package/services/collator/index.ts +43 -11
  50. package/services/durable/factory.ts +17 -1
  51. package/services/durable/worker.ts +18 -19
  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,100 +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
- if (isComplete) {
114
- this.logger.warn('worker-process-event-duplicate', { jid, aid });
115
- this.logger.debug('worker-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
116
- return;
117
- }
118
- telemetry.startActivitySpan(this.leg);
119
- if (status === StreamStatus.PENDING) {
120
- await this.processPending(telemetry);
121
- } else if (status === StreamStatus.SUCCESS) {
122
- await this.processSuccess(telemetry);
123
- } else {
124
- await this.processError(telemetry);
125
- }
126
- } catch (error) {
127
- this.logger.error('worker-process-event-error', error);
128
- telemetry.setActivityError(error.message);
129
- throw error;
130
- } finally {
131
- telemetry.endActivitySpan();
132
- this.logger.debug('worker-process-event-end', { jid, aid });
133
- }
134
- }
135
-
136
- async processPending(telemetry: TelemetryService): Promise<void> {
137
- this.bindActivityData('output');
138
- this.adjacencyList = await this.filterAdjacent();
139
- this.mapJobData();
140
- const multi = this.store.getMulti();
141
- await this.setState(multi);
142
- await CollatorService.notarizeContinuation(this, multi);
143
-
144
- await this.setStatus(0, multi);
145
- const multiResponse = await multi.exec() as MultiResponseFlags;
146
- telemetry.mapActivityAttributes();
147
- const jobStatus = this.resolveStatus(multiResponse);
148
- telemetry.setActivityAttributes({ 'app.job.jss': jobStatus });
149
- }
150
-
151
- async processSuccess(telemetry: TelemetryService): Promise<void> {
152
- this.bindActivityData('output');
153
- this.adjacencyList = await this.filterAdjacent();
154
- this.mapJobData();
155
- const multi = this.store.getMulti();
156
- await this.setState(multi);
157
- await CollatorService.notarizeCompletion(this, multi);
158
-
159
- await this.setStatus(this.adjacencyList.length - 1, multi);
160
- const multiResponse = await multi.exec() as MultiResponseFlags;
161
- telemetry.mapActivityAttributes();
162
- const jobStatus = this.resolveStatus(multiResponse);
163
- const attrs: StringScalarType = { 'app.job.jss': jobStatus };
164
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
165
- if (messageIds.length) {
166
- attrs['app.activity.mids'] = messageIds.join(',')
167
- }
168
- telemetry.setActivityAttributes(attrs);
169
- }
170
-
171
- async processError(telemetry: TelemetryService): Promise<void> {
172
- this.bindActivityError(this.data);
173
- this.adjacencyList = await this.filterAdjacent();
174
- const multi = this.store.getMulti();
175
- await this.setState(multi);
176
- await CollatorService.notarizeCompletion(this, multi);
177
-
178
- await this.setStatus(this.adjacencyList.length - 1, multi);
179
- const multiResponse = await multi.exec() as MultiResponseFlags;
180
- telemetry.mapActivityAttributes();
181
- const jobStatus = this.resolveStatus(multiResponse);
182
- const attrs: StringScalarType = { 'app.job.jss': jobStatus };
183
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
184
- if (messageIds.length) {
185
- attrs['app.activity.mids'] = messageIds.join(',')
186
- }
187
- telemetry.setActivityAttributes(attrs);
188
- }
189
94
  }
190
95
 
191
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)) {
@@ -92,17 +93,20 @@ export class WorkerService {
92
93
  * allowing proxyActivities to succeed.
93
94
  */
94
95
  static registerActivities<ACT>(activities: ACT): Registry {
95
- Object.keys(activities).forEach(key => {
96
- WorkerService.activityRegistry[key] = (activities as any)[key];
97
- });
96
+ if (typeof activities === 'function') {
97
+ WorkerService.activityRegistry[activities.name] = activities as Function;
98
+ } else {
99
+ Object.keys(activities).forEach(key => {
100
+ WorkerService.activityRegistry[activities[key].name] = (activities as any)[key] as Function;
101
+ });
102
+ }
98
103
  return WorkerService.activityRegistry;
99
104
  }
100
105
 
101
106
  static async create(config: WorkerConfig) {
107
+ //always call `registerActivities` before `import`
102
108
  WorkerService.connection = config.connection;
103
- //pre-cache user activity functions
104
109
  WorkerService.registerActivities<typeof config.activities>(config.activities);
105
- //import the user's workflow file (triggers activity functions to be wrapped)
106
110
  const workflow = await import(config.workflowsPath);
107
111
  const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
108
112
  const baseTopic = `${config.taskQueue}-${workflowFunctionName}`;
@@ -111,9 +115,8 @@ export class WorkerService {
111
115
 
112
116
  //initialize supporting workflows
113
117
  const worker = new WorkerService();
114
-
115
- const activityRunner = await worker.initActivityWorkflow(config, activityTopic);
116
- await WorkerService.activateWorkflow(activityRunner, activityTopic, getActivityYAML);
118
+ worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
119
+ await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
117
120
  worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
118
121
  await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML);
119
122
  return worker;
@@ -132,11 +135,7 @@ export class WorkerService {
132
135
  }
133
136
 
134
137
  async run() {
135
- if (this.workflowRunner) {
136
- this.workflowRunner.engine.logger.info('WorkerService is running');
137
- } else {
138
- console.log('WorkerService is running');
139
- }
138
+ this.workflowRunner.engine.logger.info('WorkerService is running');
140
139
  }
141
140
 
142
141
  async initActivityWorkflow(config: WorkerConfig, activityTopic: string): Promise<HotMesh> {
@@ -173,10 +172,11 @@ export class WorkerService {
173
172
  data: { response: pojoResponse }
174
173
  };
175
174
  } catch (err) {
176
- console.error(err);
177
- //todo (make retry configurable)
175
+ this.activityRunner.engine.logger.error('durable-worker-activity-err', err);
178
176
  return {
179
- status: StreamStatus.PENDING,
177
+ status: StreamStatus.ERROR,
178
+ code: 500,
179
+ message: err.message,
180
180
  metadata: { ...data.metadata },
181
181
  data: { error: err }
182
182
  } as StreamDataResponse;
@@ -193,7 +193,7 @@ export class WorkerService {
193
193
  await hotMesh.deploy(getActivityYAML(activityTopic, version));
194
194
  await hotMesh.activate(version);
195
195
  } catch (err) {
196
- console.log('durable-worker-activity-deploy-activate-error', err);
196
+ hotMesh.engine.logger.error('durable-worker-activity-deploy-activate-error', err);
197
197
  throw err;
198
198
  }
199
199
  } else if(app && !app.active) {
@@ -258,10 +258,9 @@ export class WorkerService {
258
258
  data: { response: workflowResponse }
259
259
  };
260
260
  } catch (err) {
261
- //todo: (retryable error types)
262
261
  return {
262
+ status: StreamStatus.ERROR,
263
263
  code: 500,
264
- status: StreamStatus.PENDING,
265
264
  metadata: { ...data.metadata },
266
265
  data: { error: err }
267
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,