@hotmeshio/hotmesh 0.0.12 → 0.0.14

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 (82) hide show
  1. package/README.md +2 -2
  2. package/build/modules/errors.d.ts +22 -1
  3. package/build/modules/errors.js +28 -1
  4. package/build/modules/utils.d.ts +2 -1
  5. package/build/modules/utils.js +5 -1
  6. package/build/package.json +7 -2
  7. package/build/services/activities/activity.d.ts +2 -0
  8. package/build/services/activities/activity.js +16 -10
  9. package/build/services/activities/await.d.ts +2 -6
  10. package/build/services/activities/await.js +12 -75
  11. package/build/services/activities/cycle.js +2 -2
  12. package/build/services/activities/index.d.ts +2 -2
  13. package/build/services/activities/index.js +2 -2
  14. package/build/services/activities/signal.d.ts +16 -0
  15. package/build/services/activities/signal.js +94 -0
  16. package/build/services/activities/trigger.js +4 -3
  17. package/build/services/activities/worker.d.ts +2 -1
  18. package/build/services/activities/worker.js +11 -6
  19. package/build/services/compiler/deployer.js +3 -1
  20. package/build/services/durable/client.d.ts +3 -2
  21. package/build/services/durable/client.js +39 -21
  22. package/build/services/durable/factory.d.ts +22 -18
  23. package/build/services/durable/factory.js +722 -50
  24. package/build/services/durable/handle.d.ts +1 -0
  25. package/build/services/durable/handle.js +5 -1
  26. package/build/services/durable/worker.d.ts +3 -8
  27. package/build/services/durable/worker.js +75 -73
  28. package/build/services/durable/workflow.d.ts +5 -0
  29. package/build/services/durable/workflow.js +93 -24
  30. package/build/services/engine/index.d.ts +6 -6
  31. package/build/services/engine/index.js +25 -15
  32. package/build/services/hotmesh/index.d.ts +2 -1
  33. package/build/services/hotmesh/index.js +3 -1
  34. package/build/services/mapper/index.js +1 -1
  35. package/build/services/pipe/functions/array.d.ts +1 -0
  36. package/build/services/pipe/functions/array.js +3 -0
  37. package/build/services/reporter/index.js +9 -2
  38. package/build/services/signaler/store.js +8 -3
  39. package/build/services/signaler/stream.js +3 -3
  40. package/build/services/store/clients/ioredis.js +15 -15
  41. package/build/services/store/clients/redis.js +18 -18
  42. package/build/services/store/index.d.ts +1 -1
  43. package/build/services/store/index.js +11 -3
  44. package/build/services/task/index.js +3 -3
  45. package/build/types/activity.d.ts +15 -6
  46. package/build/types/durable.d.ts +15 -2
  47. package/build/types/index.d.ts +2 -2
  48. package/build/types/stats.d.ts +1 -0
  49. package/modules/errors.ts +35 -0
  50. package/modules/utils.ts +5 -1
  51. package/package.json +7 -2
  52. package/services/activities/activity.ts +19 -9
  53. package/services/activities/await.ts +14 -90
  54. package/services/activities/cycle.ts +2 -2
  55. package/services/activities/index.ts +2 -2
  56. package/services/activities/signal.ts +124 -0
  57. package/services/activities/trigger.ts +4 -3
  58. package/services/activities/worker.ts +13 -13
  59. package/services/compiler/deployer.ts +3 -1
  60. package/services/durable/client.ts +48 -23
  61. package/services/durable/factory.ts +723 -49
  62. package/services/durable/handle.ts +6 -1
  63. package/services/durable/worker.ts +92 -79
  64. package/services/durable/workflow.ts +95 -25
  65. package/services/engine/index.ts +33 -24
  66. package/services/hotmesh/index.ts +7 -4
  67. package/services/mapper/index.ts +1 -1
  68. package/services/pipe/functions/array.ts +4 -0
  69. package/services/reporter/index.ts +10 -2
  70. package/services/signaler/store.ts +8 -3
  71. package/services/signaler/stream.ts +3 -3
  72. package/services/store/clients/ioredis.ts +15 -15
  73. package/services/store/clients/redis.ts +18 -18
  74. package/services/store/index.ts +12 -3
  75. package/services/task/index.ts +3 -3
  76. package/types/activity.ts +16 -7
  77. package/types/durable.ts +18 -1
  78. package/types/index.ts +2 -1
  79. package/types/stats.ts +1 -0
  80. package/build/services/activities/emit.d.ts +0 -9
  81. package/build/services/activities/emit.js +0 -13
  82. package/services/activities/emit.ts +0 -25
@@ -1,5 +1,6 @@
1
1
  import { JobOutput } from '../../types/job';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
+ import { PUBLISHES_TOPIC } from './factory';
3
4
 
4
5
  export class WorkflowHandleService {
5
6
  hotMesh: HotMesh;
@@ -12,9 +13,13 @@ export class WorkflowHandleService {
12
13
  this.hotMesh = hotMesh;
13
14
  }
14
15
 
16
+ async signal(signalId: string, data: Record<any, any>): Promise<void> {
17
+ await this.hotMesh.hook('durable.wfs.signal', { id: signalId, data });
18
+ }
19
+
15
20
  async result(): Promise<any> {
16
21
  let status = await this.hotMesh.getStatus(this.workflowId);
17
- const topic = `${this.workflowTopic}.${this.workflowId}`;
22
+ const topic = `${PUBLISHES_TOPIC}.${this.workflowId}`;
18
23
 
19
24
  return new Promise((resolve, reject) => {
20
25
  let isResolved = false;
@@ -1,43 +1,48 @@
1
+ import {
2
+ DurableFatalError,
3
+ DurableIncompleteSignalError,
4
+ DurableMaxedError,
5
+ DurableRetryError,
6
+ DurableSleepError,
7
+ DurableTimeoutError,
8
+ DurableWaitForSignalError} from '../../modules/errors';
1
9
  import { asyncLocalStorage } from './asyncLocalStorage';
10
+ import { APP_ID, APP_VERSION, getWorkflowYAML } from './factory';
2
11
  import { HotMeshService as HotMesh } from '../hotmesh';
3
- import { RedisClass, RedisOptions } from '../../types/redis';
4
- import { StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
5
- import { ActivityWorkflowDataType,
12
+ import {
13
+ ActivityWorkflowDataType,
6
14
  Connection,
7
15
  Registry,
8
16
  WorkerConfig,
9
17
  WorkerOptions,
10
18
  WorkflowDataType } from "../../types/durable";
11
- import { getWorkflowYAML, getActivityYAML } from './factory';
19
+ import { RedisClass, RedisOptions } from '../../types/redis';
12
20
  import {
13
- DurableFatalError,
14
- DurableMaxedError,
15
- DurableRetryError,
16
- DurableTimeoutError } from '../../modules/errors';
21
+ StreamData,
22
+ StreamDataResponse,
23
+ StreamStatus } from '../../types/stream';
17
24
 
18
25
  /*
19
26
  Here is an example of how the methods in this file are used:
20
27
 
21
28
  ./worker.ts
22
29
 
23
- import { Durable: { NativeConnection, Worker } } from '@hotmeshio/hotmesh';
30
+ import { Durable } from '@hotmeshio/hotmesh';
24
31
  import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
25
32
 
26
33
  import * as workflows from './workflows';
27
34
 
28
35
  async function run() {
29
- const connection = await NativeConnection.connect({
30
- class: Redis,
31
- options: {
32
- host: 'localhost',
33
- port: 6379,
36
+ const worker = await Durable.Worker.create({
37
+ connection: {
38
+ class: Redis,
39
+ options: {
40
+ host: 'localhost',
41
+ port: 6379,
42
+ },
34
43
  },
35
- });
36
- const worker = await Worker.create({
37
- connection,
38
44
  taskQueue: 'hello-world',
39
45
  workflow: workflows.example,
40
- activities,
41
46
  });
42
47
  await worker.run();
43
48
  }
@@ -59,30 +64,29 @@ export class WorkerService {
59
64
  if (WorkerService.instances.has(worflowTopic)) {
60
65
  return await WorkerService.instances.get(worflowTopic);
61
66
  }
62
- const hotMesh = HotMesh.init({
63
- appId: worflowTopic,
67
+ const hotMeshClient = HotMesh.init({
68
+ appId: APP_ID,
64
69
  engine: { redis: { ...WorkerService.connection } }
65
70
  });
66
- WorkerService.instances.set(worflowTopic, hotMesh);
67
- await WorkerService.activateWorkflow(await hotMesh, worflowTopic, getWorkflowYAML, options);
68
- return hotMesh;
71
+ WorkerService.instances.set(worflowTopic, hotMeshClient);
72
+ await WorkerService.activateWorkflow(await hotMeshClient);
73
+ return hotMeshClient;
69
74
  }
70
75
 
71
- static async activateWorkflow(hotMesh: HotMesh, topic: string, dagFactory: Function, options: WorkerOptions = {}) {
72
- const version = '1';
73
- const app = await hotMesh.engine.store.getApp(topic);
76
+ static async activateWorkflow(hotMesh: HotMesh) {
77
+ const app = await hotMesh.engine.store.getApp(APP_ID);
74
78
  const appVersion = app?.version;
75
79
  if(!appVersion) {
76
80
  try {
77
- await hotMesh.deploy(dagFactory(topic, version, options.maxSystemRetries, options.backoffExponent));
78
- await hotMesh.activate(version);
81
+ await hotMesh.deploy(getWorkflowYAML(APP_ID, APP_VERSION));
82
+ await hotMesh.activate(APP_VERSION);
79
83
  } catch (err) {
80
84
  hotMesh.engine.logger.error('durable-worker-deploy-activate-err', err);
81
85
  throw err;
82
86
  }
83
87
  } else if(app && !app.active) {
84
88
  try {
85
- await hotMesh.activate(version);
89
+ await hotMesh.activate(APP_VERSION);
86
90
  } catch (err) {
87
91
  hotMesh.engine.logger.error('durable-worker-activate-err', err);
88
92
  throw err;
@@ -91,10 +95,6 @@ export class WorkerService {
91
95
  }
92
96
 
93
97
  /**
94
- * The `worker` calls `registerActivities` immediately BEFORE
95
- * dynamically importing the user's workflow module. That file
96
- * contains a call, `proxyActivities`, which needs this info.
97
- *
98
98
  * NOTE: Because the worker imports the workflows dynamically AFTER
99
99
  * the activities are loaded, there will be items in the registry,
100
100
  * allowing proxyActivities to succeed.
@@ -115,7 +115,6 @@ export class WorkerService {
115
115
  static async create(config: WorkerConfig) {
116
116
  //always call `registerActivities` before `import`
117
117
  WorkerService.connection = config.connection;
118
- //user can provide the workflow file directly
119
118
  const workflow = config.workflow;
120
119
  const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
121
120
  const baseTopic = `${config.taskQueue}-${workflowFunctionName}`;
@@ -124,10 +123,9 @@ export class WorkerService {
124
123
 
125
124
  //initialize supporting workflows
126
125
  const worker = new WorkerService();
127
- worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
128
- await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
129
- worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
130
- await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML, config.options);
126
+ worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
127
+ worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
128
+ await WorkerService.activateWorkflow(worker.workflowRunner);
131
129
  return worker;
132
130
  }
133
131
 
@@ -147,13 +145,13 @@ export class WorkerService {
147
145
  this.workflowRunner.engine.logger.info('WorkerService is running');
148
146
  }
149
147
 
150
- async initActivityWorkflow(config: WorkerConfig, activityTopic: string): Promise<HotMesh> {
148
+ async initActivityWorker(config: WorkerConfig, activityTopic: string): Promise<HotMesh> {
151
149
  const redisConfig = {
152
150
  class: config.connection.class as RedisClass,
153
151
  options: config.connection.options as RedisOptions
154
152
  };
155
- const hmshInstance = await HotMesh.init({
156
- appId: activityTopic,
153
+ const hotMeshWorker = await HotMesh.init({
154
+ appId: APP_ID,
157
155
  engine: { redis: redisConfig },
158
156
  workers: [
159
157
  { topic: activityTopic,
@@ -162,8 +160,8 @@ export class WorkerService {
162
160
  }
163
161
  ]
164
162
  });
165
- WorkerService.instances.set(activityTopic, hmshInstance);
166
- return hmshInstance;
163
+ WorkerService.instances.set(activityTopic, hotMeshWorker);
164
+ return hotMeshWorker;
167
165
  }
168
166
 
169
167
  wrapActivityFunctions(): Function {
@@ -197,45 +195,22 @@ export class WorkerService {
197
195
  }
198
196
  }
199
197
 
200
- async activateActivityWorkflow(hotMesh: HotMesh, activityTopic: string) {
201
- const version = '1';
202
- const app = await hotMesh.engine.store.getApp(activityTopic);
203
- const appVersion = app?.version as unknown as number;
204
- if(isNaN(appVersion)) {
205
- try {
206
- await hotMesh.deploy(getActivityYAML(activityTopic, version));
207
- await hotMesh.activate(version);
208
- } catch (err) {
209
- hotMesh.engine.logger.error('durable-worker-activity-deploy-activate-error', err);
210
- throw err;
211
- }
212
- } else if(app && !app.active) {
213
- try {
214
- await hotMesh.activate(version);
215
- } catch (err) {
216
- hotMesh.engine.logger.error('durable-worker-activity-activate-err', err);
217
- throw err;
218
- }
219
- }
220
- }
221
-
222
- async initWorkerWorkflow(config: WorkerConfig, workflowTopic: string, workflowFunction: Function): Promise<HotMesh> {
198
+ async initWorkflowWorker(config: WorkerConfig, workflowTopic: string, workflowFunction: Function): Promise<HotMesh> {
223
199
  const redisConfig = {
224
200
  class: config.connection.class as RedisClass,
225
201
  options: config.connection.options as RedisOptions
226
202
  };
227
- const hmshInstance = await HotMesh.init({
228
- appId: workflowTopic,
203
+ const hotMeshWorker = await HotMesh.init({
204
+ appId: APP_ID,
229
205
  engine: { redis: redisConfig },
230
- workers: [
231
- { topic: workflowTopic,
232
- redis: redisConfig,
233
- callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
234
- }
235
- ]
206
+ workers: [{
207
+ topic: workflowTopic,
208
+ redis: redisConfig,
209
+ callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
210
+ }]
236
211
  });
237
- WorkerService.instances.set(workflowTopic, hmshInstance);
238
- return hmshInstance;
212
+ WorkerService.instances.set(workflowTopic, hotMeshWorker);
213
+ return hotMeshWorker;
239
214
  }
240
215
 
241
216
  static Context = {
@@ -249,11 +224,11 @@ export class WorkerService {
249
224
 
250
225
  wrapWorkflowFunction(workflowFunction: Function, workflowTopic: string): Function {
251
226
  return async (data: StreamData): Promise<StreamDataResponse> => {
227
+ const counter = { counter: 0 };
252
228
  try {
253
229
  //incoming data payload has arguments and workflowId
254
230
  const workflowInput = data.data as unknown as WorkflowDataType;
255
231
  const context = new Map();
256
- const counter = { counter: 0 };
257
232
  context.set('counter', counter);
258
233
  context.set('workflowId', workflowInput.workflowId);
259
234
  context.set('workflowTopic', workflowTopic);
@@ -268,10 +243,48 @@ export class WorkerService {
268
243
  code: 200,
269
244
  status: StreamStatus.SUCCESS,
270
245
  metadata: { ...data.metadata },
271
- data: { response: workflowResponse }
246
+ data: { response: workflowResponse, done: true }
272
247
  };
273
248
  } catch (err) {
274
- // 59* - Durable*Error
249
+
250
+ //not an error...just a trigger to sleep
251
+ if (err instanceof DurableSleepError) {
252
+ return {
253
+ status: StreamStatus.SUCCESS,
254
+ code: err.code,
255
+ metadata: { ...data.metadata },
256
+ data: {
257
+ code: err.code,
258
+ message: JSON.stringify({ duration: err.duration, index: err.index }),
259
+ duration: err.duration,
260
+ index: err.index
261
+ }
262
+ } as StreamDataResponse;
263
+
264
+ //not an error...just a trigger to wait for a signal
265
+ } else if (err instanceof DurableWaitForSignalError) {
266
+ return {
267
+ status: StreamStatus.SUCCESS,
268
+ code: err.code,
269
+ metadata: { ...data.metadata },
270
+ data: {
271
+ code: err.code,
272
+ signals: err.signals,
273
+ index: err.signals[0].index
274
+ }
275
+ } as StreamDataResponse;
276
+
277
+ //not an error...still waiting for all the signals to arrive
278
+ } else if (err instanceof DurableIncompleteSignalError) {
279
+ return {
280
+ status: StreamStatus.SUCCESS,
281
+ code: err.code,
282
+ metadata: { ...data.metadata },
283
+ data: { code: err.code }
284
+ } as StreamDataResponse;
285
+ }
286
+
287
+ // all other errors are fatal (598, 597, 596) or will be retried (599)
275
288
  return {
276
289
  status: StreamStatus.ERROR,
277
290
  code: err.code || new DurableRetryError(err.message).code,
@@ -6,6 +6,9 @@ import { ClientService as Client } from './client';
6
6
  import { ConnectionService as Connection } from './connection';
7
7
  import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
8
8
  import { JobOutput, JobState } from '../../types';
9
+ import { ACTIVITY_PUBLISHES_TOPIC, ACTIVITY_SUBSCRIBES_TOPIC, SLEEP_SUBSCRIBES_TOPIC, WFS_SUBSCRIBES_TOPIC } from './factory';
10
+ import { DurableIncompleteSignalError, DurableSleepError, DurableWaitForSignalError } from '../../modules/errors';
11
+ import { stringify } from 'querystring';
9
12
 
10
13
  /*
11
14
  `proxyActivities` returns a wrapped instance of the
@@ -13,19 +16,6 @@ target activity, so that when the workflow calls a
13
16
  proxied activity, it is actually calling the proxy
14
17
  function, which in turn calls the activity function.
15
18
 
16
- `proxyActivities` must be called AFTER the activities
17
- have been registered in order to work properly.
18
- If the activities are not already registered,
19
- `proxyActivities` will throw an error. This is OK.
20
-
21
- The `client` (client.ts) is equivalent to the
22
- HotMesh `engine`. The jobs it creates will be
23
- put in the taskQueue. When the `worker` (worker.ts)
24
- is eventually initialized (if it happens to be inited later),
25
- it will see the items in the queue and process them. If it happens
26
- to already be inited, the jobs will immediately be dequeued and
27
- processed. In either case, the jobs will be processed.
28
-
29
19
  Here is an example of how the methods in this file are used:
30
20
 
31
21
  ./workflows.ts
@@ -50,6 +40,10 @@ export async function example(name: string): Promise<string> {
50
40
  */
51
41
 
52
42
  export class WorkflowService {
43
+
44
+ /**
45
+ * Spawn a child workflow. await the result.
46
+ */
53
47
  static async executeChild<T>(options: WorkflowOptions): Promise<T> {
54
48
  const store = asyncLocalStorage.getStore();
55
49
  if (!store) {
@@ -62,10 +56,9 @@ export class WorkflowService {
62
56
  const client = new Client({
63
57
  connection: await Connection.connect(WorkerService.connection),
64
58
  });
65
- //todo: allow cross/app callback (pj:'@DURABLE@hello-world@<pjid>'/pa: <paid>/pd: <pdad>)
66
59
  const handle = await client.workflow.start({
67
60
  ...options,
68
- workflowId: `${workflowId}${options.workflowId}`, //concat
61
+ workflowId: `${workflowId}${options.workflowId}`, //concat (caller MUST PROVIDE)
69
62
  workflowTrace,
70
63
  workflowSpan,
71
64
  });
@@ -89,6 +82,82 @@ export class WorkflowService {
89
82
  return proxy;
90
83
  }
91
84
 
85
+ static async sleep(duration: string): Promise<number> {
86
+ const seconds = ms(duration) / 1000;
87
+
88
+ const store = asyncLocalStorage.getStore();
89
+ if (!store) {
90
+ throw new Error('durable-store-not-found');
91
+ }
92
+ const COUNTER = store.get('counter');
93
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
94
+ const workflowId = store.get('workflowId');
95
+ const workflowTopic = store.get('workflowTopic');
96
+ const sleepJobId = `${workflowId}-$sleep-${execIndex}`;
97
+
98
+ try {
99
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic);
100
+ await hotMeshClient.getState(SLEEP_SUBSCRIBES_TOPIC, sleepJobId);
101
+ //if no error is thrown, we've already slept, return the delay
102
+ return seconds;
103
+ } catch (e) {
104
+ //if an error, the sleep job was not found...rethrow error; sleep job
105
+ // will be automatically created according to the DAG rules (they
106
+ // spawn a new sleep job if error code 595 is thrown by the worker)
107
+ throw new DurableSleepError(workflowId, seconds, execIndex);
108
+ }
109
+ }
110
+
111
+ static async waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]> {
112
+ const store = asyncLocalStorage.getStore();
113
+ if (!store) {
114
+ throw new Error('durable-store-not-found');
115
+ }
116
+ const COUNTER = store.get('counter');
117
+ const workflowId = store.get('workflowId');
118
+ const workflowTopic = store.get('workflowTopic');
119
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic);
120
+
121
+ //iterate the list of signals and check for done
122
+ let allAreComplete = true;
123
+ let noneAreComplete = false;
124
+ const signalResults: any[] = [];
125
+ for (const signal of signals) {
126
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
127
+ const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
128
+ try {
129
+ if (allAreComplete) {
130
+ const state = await hotMeshClient.getState(WFS_SUBSCRIBES_TOPIC, wfsJobId);
131
+ if (state.data?.signalData) {
132
+ //user data is nested to isolate from the signal id; unpackage it
133
+ const signalData = state.data.signalData as { id: string, data: Record<any, any> };
134
+ signalResults.push(signalData.data);
135
+ } else {
136
+ allAreComplete = false;
137
+ }
138
+ } else {
139
+ signalResults.push({ signal, index: execIndex });
140
+ }
141
+ } catch (err) {
142
+ //todo: options.startToCloseTimeout
143
+ allAreComplete = false;
144
+ noneAreComplete = true;
145
+ signalResults.push({ signal, index: execIndex });
146
+ }
147
+ };
148
+
149
+ if(allAreComplete) {
150
+ return signalResults;
151
+ } else if(noneAreComplete) {
152
+ //this error is caught by the workflow runner
153
+ //it is then returned as the workflow result (594)
154
+ throw new DurableWaitForSignalError(workflowId, signalResults);
155
+ } else {
156
+ //this error happens when a signal is received but others are still open
157
+ throw new DurableIncompleteSignalError(workflowId);
158
+ }
159
+ }
160
+
92
161
  static wrapActivity<T>(activityName: string, options?: ActivityConfig): T {
93
162
  return async function() {
94
163
  const store = asyncLocalStorage.getStore();
@@ -107,20 +176,19 @@ export class WorkflowService {
107
176
 
108
177
  let activityState: JobOutput
109
178
  try {
110
- const hmshInstance = await WorkerService.getHotMesh(activityTopic);
111
- activityState = await hmshInstance.getState(activityTopic, activityJobId);
179
+ const hotMeshClient = await WorkerService.getHotMesh(activityTopic);
180
+ activityState = await hotMeshClient.getState(ACTIVITY_SUBSCRIBES_TOPIC, activityJobId);
112
181
  if (activityState.metadata.err) {
113
- await hmshInstance.scrub(activityJobId);
182
+ await hotMeshClient.scrub(activityJobId);
114
183
  throw new Error(activityState.metadata.err);
115
- } else if (activityState.metadata.js === 0) {
116
- //return immediately
184
+ } else if (activityState.metadata.js === 0 || activityState.data?.done) {
117
185
  return activityState.data?.response as T;
118
186
  }
119
187
  //one time subscription
120
188
  return await new Promise((resolve, reject) => {
121
- hmshInstance.sub(activityTopic, async (topic, message) => {
189
+ hotMeshClient.sub(`${ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`, async (topic, message) => {
122
190
  const response = message.data?.response;
123
- hmshInstance.unsub(activityTopic);
191
+ hotMeshClient.unsub(`${ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`);
124
192
  // Resolve the Promise when the callback is triggered with a message
125
193
  resolve(response);
126
194
  });
@@ -130,14 +198,16 @@ export class WorkflowService {
130
198
  const duration = ms(options?.startToCloseTimeout || '1 minute');
131
199
  const payload = {
132
200
  arguments: Array.from(arguments),
201
+ //the parent id is provided to categorize this activity for later cleanup
202
+ parentWorkflowId: `${workflowId}-a`,
133
203
  workflowId: activityJobId,
134
- workflowTopic,
204
+ workflowTopic: activityTopic,
135
205
  activityName,
136
206
  };
137
207
  //start the job
138
- const hmshInstance = await WorkerService.getHotMesh(activityTopic);
208
+ const hotMeshClient = await WorkerService.getHotMesh(activityTopic);
139
209
  const context = { metadata: { trc, spn }, data: {}};
140
- const jobOutput = await hmshInstance.pubsub(activityTopic, payload, context as JobState, duration);
210
+ const jobOutput = await hotMeshClient.pubsub(ACTIVITY_SUBSCRIBES_TOPIC, payload, context as JobState, duration);
141
211
  return jobOutput.data.response as T;
142
212
  }
143
213
  } as T;
@@ -68,6 +68,7 @@ import { StringStringType } from '../../types';
68
68
  //wait time to see if a job is complete
69
69
  const OTT_WAIT_TIME = 1000;
70
70
  const STATUS_CODE_SUCCESS = 200;
71
+ const STATUS_CODE_PENDING = 202;
71
72
  const STATUS_CODE_TIMEOUT = 504;
72
73
 
73
74
  class EngineService {
@@ -366,7 +367,7 @@ class EngineService {
366
367
  }
367
368
 
368
369
  // ***************** `AWAIT` ACTIVITY RETURN RESPONSE ****************
369
- async execAdjacentParent(context: JobState, jobOutput: JobOutput): Promise<string> {
370
+ async execAdjacentParent(context: JobState, jobOutput: JobOutput, emit = false): Promise<string> {
370
371
  if (this.hasParentJob(context)) {
371
372
  //errors are stringified `StreamError` objects
372
373
  const error = this.resolveError(jobOutput.metadata);
@@ -386,6 +387,9 @@ class EngineService {
386
387
  streamData.status = StreamStatus.ERROR;
387
388
  streamData.data = error;
388
389
  streamData.code = error.code;
390
+ } else if (emit) {
391
+ streamData.status = StreamStatus.PENDING;
392
+ streamData.code = STATUS_CODE_PENDING;
389
393
  } else {
390
394
  streamData.status = StreamStatus.SUCCESS;
391
395
  streamData.code = STATUS_CODE_SUCCESS;
@@ -409,7 +413,7 @@ class EngineService {
409
413
  }
410
414
 
411
415
  // ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
412
- async hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void> {
416
+ async hook(topic: string, data: JobData, dad?: string): Promise<string> {
413
417
  const hookRule = await this.storeSignaler.getHookRule(topic);
414
418
  const [aid, schema] = await this.getSchema(`.${hookRule.to}`);
415
419
  if (!dad) {
@@ -428,7 +432,7 @@ class EngineService {
428
432
  },
429
433
  data,
430
434
  };
431
- await this.streamSignaler.publishMessage(null, streamData);
435
+ return await this.streamSignaler.publishMessage(null, streamData) as string;
432
436
  }
433
437
  async hookTime(jobId: string, activityId: string): Promise<JobStatus | void> {
434
438
  //the activityid is concatenated with its dimensional address (dad); split to resolve
@@ -445,24 +449,26 @@ class EngineService {
445
449
  };
446
450
  await this.streamSignaler.publishMessage(null, streamData);
447
451
  }
448
- async hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
452
+ async hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
449
453
  const config = await this.getVID();
450
454
  const hookRule = await this.storeSignaler.getHookRule(hookTopic);
451
455
  if (hookRule) {
452
456
  const subscriptionTopic = await getSubscriptionTopic(hookRule.to, this.store, config)
453
- const resolvedQuery = await this.resolveQuery(subscriptionTopic, query);
457
+ const resolvedQuery = await this.resolveQuery(subscriptionTopic, keyResolver);
454
458
  const reporter = new ReporterService(config, this.store, this.logger);
455
459
  const workItems = await reporter.getWorkItems(resolvedQuery, queryFacets);
456
- const taskService = new TaskService(this.store, this.logger);
457
- await taskService.enqueueWorkItems(
458
- workItems.map(
459
- workItem => `${hookTopic}::${workItem}::${JSON.stringify(data)}`
460
- ));
461
- this.store.publish(
462
- KeyType.QUORUM,
463
- { type: 'work', originator: this.guid },
464
- this.appId
465
- );
460
+ if (workItems.length) {
461
+ const taskService = new TaskService(this.store, this.logger);
462
+ await taskService.enqueueWorkItems(
463
+ workItems.map(
464
+ workItem => `${hookTopic}::${workItem}::${keyResolver.scrub || false}::${JSON.stringify(data)}`
465
+ ));
466
+ this.store.publish(
467
+ KeyType.QUORUM,
468
+ { type: 'work', originator: this.guid },
469
+ this.appId
470
+ );
471
+ }
466
472
  return workItems;
467
473
  } else {
468
474
  throw new Error(`unable to find hook rule for topic ${hookTopic}`);
@@ -472,7 +478,7 @@ class EngineService {
472
478
 
473
479
  // ********************** PUB/SUB ENTRY POINT **********************
474
480
  //publish (returns just the job id)
475
- async pub(topic: string, data: JobData, context?: JobState) {
481
+ async pub(topic: string, data: JobData, context?: JobState): Promise<string> {
476
482
  const activityHandler = await this.initActivity(topic, data, context);
477
483
  if (activityHandler) {
478
484
  return await activityHandler.process();
@@ -534,7 +540,7 @@ class EngineService {
534
540
  }, timeout);
535
541
  });
536
542
  }
537
- async resolveOneTimeSubscription(context: JobState, jobOutput: JobOutput) {
543
+ async resolveOneTimeSubscription(context: JobState, jobOutput: JobOutput, emit = false) {
538
544
  //todo: subscriber should query for the job...only publish minimum context needed
539
545
  if (this.hasOneTimeSubscription(context)) {
540
546
  const message: JobMessage = {
@@ -551,7 +557,7 @@ class EngineService {
551
557
  const schema = await this.store.getSchema(activityId, config);
552
558
  return schema.publishes;
553
559
  }
554
- async resolvePersistentSubscriptions(context: JobState, jobOutput: JobOutput) {
560
+ async resolvePersistentSubscriptions(context: JobState, jobOutput: JobOutput, emit = false) {
555
561
  const topic = await this.getPublishesTopic(context);
556
562
  if (topic) {
557
563
  const message: JobMessage = {
@@ -577,20 +583,23 @@ class EngineService {
577
583
  }
578
584
 
579
585
 
580
- // ***************** JOB COMPLETION/CLEANUP *****************
581
- async runJobCompletionTasks(context: JobState) {
586
+ // ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
587
+ async runJobCompletionTasks(context: JobState, emit = false) {
588
+ //if 'emit' is true, the job isn't done. it's just emitting
582
589
  const isAwait = this.hasParentJob(context);
583
590
  const isOneTimeSubscription = this.hasOneTimeSubscription(context);
584
591
  const topic = await this.getPublishesTopic(context);
585
592
  if (isAwait || isOneTimeSubscription || topic) {
586
593
  const jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
587
594
  //always wait for stream pub/sub
588
- await this.execAdjacentParent(context, jobOutput);
595
+ await this.execAdjacentParent(context, jobOutput, emit);
589
596
  //no need to wait for standard pub/sub
590
- this.resolveOneTimeSubscription(context, jobOutput);
591
- this.resolvePersistentSubscriptions(context, jobOutput);
597
+ this.resolveOneTimeSubscription(context, jobOutput, emit);
598
+ this.resolvePersistentSubscriptions(context, jobOutput, emit);
599
+ }
600
+ if (!emit) {
601
+ this.task.registerJobForCleanup(context.metadata.jid, context.metadata.expire);
592
602
  }
593
- this.task.registerJobForCleanup(context.metadata.jid, context.metadata.expire);
594
603
  }
595
604
 
596
605
 
@@ -63,6 +63,10 @@ class HotMeshService {
63
63
  return instance;
64
64
  }
65
65
 
66
+ static guid(): string {
67
+ return nanoid();
68
+ }
69
+
66
70
  async initEngine(config: HotMeshConfig, logger: ILogger): Promise<void> {
67
71
  if (config.engine) {
68
72
  await ConnectorService.initRedisClients(
@@ -104,7 +108,7 @@ class HotMeshService {
104
108
  }
105
109
 
106
110
  // ************* PUB/SUB METHODS *************
107
- async pub(topic: string, data: JobData = {}, context?: JobState) {
111
+ async pub(topic: string, data: JobData = {}, context?: JobState): Promise<string> {
108
112
  return await this.engine?.pub(topic, data, context);
109
113
  }
110
114
  async sub(topic: string, callback: JobMessageCallback): Promise<void> {
@@ -145,7 +149,7 @@ class HotMeshService {
145
149
  async getStatus(jobId: string): Promise<JobStatus> {
146
150
  return this.engine?.getStatus(jobId);
147
151
  }
148
- async getState(topic: string, jobId: string) {
152
+ async getState(topic: string, jobId: string): Promise<JobOutput> {
149
153
  return this.engine?.getState(topic, jobId);
150
154
  }
151
155
  async getIds(topic: string, query: JobStatsInput, queryFacets = []): Promise<IdsResponse> {
@@ -161,8 +165,7 @@ class HotMeshService {
161
165
  }
162
166
 
163
167
  // ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
164
- async hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void> {
165
- //return collation int
168
+ async hook(topic: string, data: JobData, dad?: string): Promise<string> {
166
169
  return await this.engine?.hook(topic, data, dad);
167
170
  }
168
171
  async hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
@@ -59,7 +59,7 @@ class MapperService {
59
59
  if (typeof transitionRule === 'boolean') {
60
60
  return transitionRule;
61
61
  }
62
- if (code.toString() === (transitionRule.code || 200).toString()) {
62
+ if ((Array.isArray(transitionRule.code) && transitionRule.code.includes(code || 200)) || code.toString() === (transitionRule.code || 200).toString()) {
63
63
  if (!transitionRule.match) {
64
64
  return true;
65
65
  }