@hotmeshio/hotmesh 0.0.19 → 0.0.20

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 (58) hide show
  1. package/README.md +4 -4
  2. package/build/modules/errors.d.ts +2 -1
  3. package/build/modules/errors.js +2 -1
  4. package/build/package.json +2 -1
  5. package/build/services/activities/activity.d.ts +2 -2
  6. package/build/services/activities/activity.js +10 -8
  7. package/build/services/activities/hook.d.ts +2 -1
  8. package/build/services/activities/hook.js +12 -9
  9. package/build/services/activities/signal.d.ts +4 -0
  10. package/build/services/activities/signal.js +16 -2
  11. package/build/services/durable/client.d.ts +15 -5
  12. package/build/services/durable/client.js +37 -14
  13. package/build/services/durable/factory.d.ts +2 -16
  14. package/build/services/durable/factory.js +276 -46
  15. package/build/services/durable/handle.d.ts +1 -1
  16. package/build/services/durable/handle.js +18 -5
  17. package/build/services/durable/search.d.ts +8 -1
  18. package/build/services/durable/search.js +34 -7
  19. package/build/services/durable/worker.d.ts +7 -9
  20. package/build/services/durable/worker.js +29 -23
  21. package/build/services/durable/workflow.d.ts +18 -1
  22. package/build/services/durable/workflow.js +99 -35
  23. package/build/services/engine/index.d.ts +2 -2
  24. package/build/services/engine/index.js +7 -12
  25. package/build/services/hotmesh/index.d.ts +2 -2
  26. package/build/services/hotmesh/index.js +2 -2
  27. package/build/services/signaler/store.d.ts +2 -2
  28. package/build/services/signaler/store.js +17 -7
  29. package/build/services/signaler/stream.js +1 -0
  30. package/build/services/store/clients/redis.js +1 -1
  31. package/build/services/store/index.js +3 -0
  32. package/build/services/telemetry/index.js +7 -1
  33. package/build/types/activity.d.ts +5 -3
  34. package/build/types/durable.d.ts +12 -1
  35. package/build/types/hook.d.ts +0 -1
  36. package/build/types/index.d.ts +1 -1
  37. package/modules/errors.ts +4 -2
  38. package/package.json +2 -1
  39. package/services/activities/activity.ts +10 -8
  40. package/services/activities/hook.ts +13 -10
  41. package/services/activities/signal.ts +17 -3
  42. package/services/durable/client.ts +40 -15
  43. package/services/durable/factory.ts +274 -46
  44. package/services/durable/handle.ts +18 -5
  45. package/services/durable/search.ts +36 -7
  46. package/services/durable/worker.ts +30 -24
  47. package/services/durable/workflow.ts +111 -38
  48. package/services/engine/index.ts +8 -12
  49. package/services/hotmesh/index.ts +3 -3
  50. package/services/signaler/store.ts +18 -8
  51. package/services/signaler/stream.ts +1 -0
  52. package/services/store/clients/redis.ts +1 -1
  53. package/services/store/index.ts +2 -0
  54. package/services/telemetry/index.ts +6 -1
  55. package/types/activity.ts +10 -8
  56. package/types/durable.ts +13 -0
  57. package/types/hook.ts +0 -1
  58. package/types/index.ts +1 -0
@@ -3,18 +3,18 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.WorkerService = void 0;
5
5
  const errors_1 = require("../../modules/errors");
6
+ const key_1 = require("../../modules/key");
6
7
  const asyncLocalStorage_1 = require("./asyncLocalStorage");
7
8
  const factory_1 = require("./factory");
8
9
  const hotmesh_1 = require("../hotmesh");
9
10
  const stream_1 = require("../../types/stream");
10
- const key_1 = require("../../modules/key");
11
11
  class WorkerService {
12
12
  static async activateWorkflow(hotMesh) {
13
- const app = await hotMesh.engine.store.getApp(factory_1.APP_ID);
13
+ const app = await hotMesh.engine.store.getApp(hotMesh.engine.appId);
14
14
  const appVersion = app?.version;
15
15
  if (!appVersion) {
16
16
  try {
17
- await hotMesh.deploy((0, factory_1.getWorkflowYAML)(factory_1.APP_ID, factory_1.APP_VERSION));
17
+ await hotMesh.deploy((0, factory_1.getWorkflowYAML)(hotMesh.engine.appId, factory_1.APP_VERSION));
18
18
  await hotMesh.activate(factory_1.APP_VERSION);
19
19
  }
20
20
  catch (err) {
@@ -32,11 +32,6 @@ class WorkerService {
32
32
  }
33
33
  }
34
34
  }
35
- /**
36
- * NOTE: Because the worker imports the workflows dynamically AFTER
37
- * the activities are loaded, there will be items in the registry,
38
- * allowing proxyActivities to succeed.
39
- */
40
35
  static registerActivities(activities) {
41
36
  if (typeof activities === 'function' && typeof WorkerService.activityRegistry[activities.name] !== 'function') {
42
37
  WorkerService.activityRegistry[activities.name] = activities;
@@ -52,10 +47,11 @@ class WorkerService {
52
47
  }
53
48
  /**
54
49
  * For those deployments with a redis stack backend (with the FT module),
55
- * this method will configure the search index for the workflow.
50
+ * this method will configure the search index for the workflow. For all
51
+ * others, this method will fail gracefully. In all cases, the values
52
+ * will be stored in the workflow's central HASH data structure, allowing
53
+ * for manual traversal and inspection as well.
56
54
  */
57
- //todo: bind this to the Search service; update constructor to expect hotMeshClient as first param (id is optional
58
- //refactor and delete other one as well)
59
55
  static async configureSearchIndex(hotMeshClient, search) {
60
56
  if (search?.schema) {
61
57
  const store = hotMeshClient.engine.store;
@@ -83,7 +79,6 @@ class WorkerService {
83
79
  }
84
80
  }
85
81
  static async create(config) {
86
- //always call `registerActivities` before `import`
87
82
  WorkerService.connection = config.connection;
88
83
  const workflow = config.workflow;
89
84
  const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
@@ -119,7 +114,8 @@ class WorkerService {
119
114
  options: config.connection.options
120
115
  };
121
116
  const hotMeshWorker = await hotmesh_1.HotMeshService.init({
122
- appId: factory_1.APP_ID,
117
+ logLevel: config.options?.logLevel ?? 'info',
118
+ appId: config.namespace ?? factory_1.APP_ID,
123
119
  engine: { redis: redisConfig },
124
120
  workers: [
125
121
  { topic: activityTopic,
@@ -167,26 +163,34 @@ class WorkerService {
167
163
  options: config.connection.options
168
164
  };
169
165
  const hotMeshWorker = await hotmesh_1.HotMeshService.init({
170
- appId: factory_1.APP_ID,
166
+ logLevel: config.options?.logLevel ?? 'info',
167
+ appId: config.namespace ?? factory_1.APP_ID,
171
168
  engine: { redis: redisConfig },
172
169
  workers: [{
173
170
  topic: workflowTopic,
174
171
  redis: redisConfig,
175
- callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
172
+ callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic, config).bind(this)
176
173
  }]
177
174
  });
178
175
  WorkerService.instances.set(workflowTopic, hotMeshWorker);
179
176
  return hotMeshWorker;
180
177
  }
181
- wrapWorkflowFunction(workflowFunction, workflowTopic) {
178
+ wrapWorkflowFunction(workflowFunction, workflowTopic, config) {
182
179
  return async (data) => {
183
180
  const counter = { counter: 0 };
184
181
  try {
185
182
  //incoming data payload has arguments and workflowId
186
183
  const workflowInput = data.data;
187
184
  const context = new Map();
185
+ context.set('namespace', config.namespace ?? factory_1.APP_ID);
188
186
  context.set('counter', counter);
189
187
  context.set('workflowId', workflowInput.workflowId);
188
+ if (data.data.workflowDimension) {
189
+ //every hook function runs in an isolated dimension controlled
190
+ //by the index assigned when the signal was received; even if the
191
+ //hook function re-runs, its scope will always remain constant
192
+ context.set('workflowDimension', data.data.workflowDimension);
193
+ }
190
194
  context.set('workflowTopic', workflowTopic);
191
195
  context.set('workflowName', workflowTopic.split('-').pop());
192
196
  context.set('workflowTrace', data.metadata.trc);
@@ -210,9 +214,10 @@ class WorkerService {
210
214
  metadata: { ...data.metadata },
211
215
  data: {
212
216
  code: err.code,
213
- message: JSON.stringify({ duration: err.duration, index: err.index }),
217
+ message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
214
218
  duration: err.duration,
215
- index: err.index
219
+ index: err.index,
220
+ dimension: err.dimension
216
221
  }
217
222
  };
218
223
  //not an error...just a trigger to wait for a signal
@@ -258,15 +263,16 @@ class WorkerService {
258
263
  _a = WorkerService;
259
264
  WorkerService.activityRegistry = {}; //user's activities
260
265
  WorkerService.instances = new Map();
261
- WorkerService.getHotMesh = async (worflowTopic, options) => {
262
- if (WorkerService.instances.has(worflowTopic)) {
263
- return await WorkerService.instances.get(worflowTopic);
266
+ WorkerService.getHotMesh = async (workflowTopic, config, options) => {
267
+ if (WorkerService.instances.has(workflowTopic)) {
268
+ return await WorkerService.instances.get(workflowTopic);
264
269
  }
265
270
  const hotMeshClient = hotmesh_1.HotMeshService.init({
266
- appId: factory_1.APP_ID,
271
+ logLevel: options?.logLevel ?? 'info',
272
+ appId: config.namespace ?? factory_1.APP_ID,
267
273
  engine: { redis: { ...WorkerService.connection } }
268
274
  });
269
- WorkerService.instances.set(worflowTopic, hotMeshClient);
275
+ WorkerService.instances.set(workflowTopic, hotMeshClient);
270
276
  await WorkerService.activateWorkflow(await hotMeshClient);
271
277
  return hotMeshClient;
272
278
  };
@@ -1,5 +1,6 @@
1
- import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
2
1
  import { Search } from './search';
2
+ import { HotMeshService as HotMesh } from '../hotmesh';
3
+ import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
3
4
  export declare class WorkflowService {
4
5
  /**
5
6
  * Spawn a child workflow. await the result.
@@ -7,6 +8,22 @@ export declare class WorkflowService {
7
8
  static executeChild<T>(options: WorkflowOptions): Promise<T>;
8
9
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
9
10
  static search(): Promise<Search>;
11
+ /**
12
+ * those methods that may only be called once must be protected by flagging
13
+ * their execution with a unique key (the key is stored in the workflow state)
14
+ */
15
+ static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
16
+ /**
17
+ * send signal data into any other paused thread (which is paused and
18
+ * awaiting the signal) from within a hook-thread or the main-thread
19
+ */
20
+ static signal(signalId: string, data: Record<any, any>): Promise<string>;
21
+ /**
22
+ * spawn a hook from either the main thread or a hook thread with
23
+ * the provided options; worflowId/TaskQueue/Name are optional and will
24
+ * default to the current workflowId/WorkflowTopic if not provided
25
+ */
26
+ static hook(options: HookOptions): Promise<string>;
10
27
  static sleep(duration: string): Promise<number>;
11
28
  static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
12
29
  static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
@@ -5,35 +5,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WorkflowService = void 0;
7
7
  const ms_1 = __importDefault(require("ms"));
8
+ const errors_1 = require("../../modules/errors");
9
+ const key_1 = require("../../modules/key");
8
10
  const asyncLocalStorage_1 = require("./asyncLocalStorage");
9
- const worker_1 = require("./worker");
10
11
  const client_1 = require("./client");
11
12
  const connection_1 = require("./connection");
12
13
  const factory_1 = require("./factory");
13
- const errors_1 = require("../../modules/errors");
14
14
  const search_1 = require("./search");
15
+ const worker_1 = require("./worker");
16
+ const stream_1 = require("../../types/stream");
15
17
  class WorkflowService {
16
18
  /**
17
19
  * Spawn a child workflow. await the result.
18
20
  */
19
21
  static async executeChild(options) {
20
22
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
21
- if (!store) {
22
- throw new Error('durable-store-not-found');
23
- }
24
23
  const workflowId = store.get('workflowId');
24
+ const workflowDimension = store.get('workflowDimension') ?? '';
25
25
  const workflowTrace = store.get('workflowTrace');
26
26
  const workflowSpan = store.get('workflowSpan');
27
27
  const COUNTER = store.get('counter');
28
28
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
29
- const childJobId = `${workflowId}-$${options.workflowName}-${execIndex}`;
29
+ const childJobId = `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
30
30
  const parentWorkflowId = `${workflowId}-f`;
31
31
  const client = new client_1.ClientService({
32
32
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
33
33
  });
34
34
  let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
35
35
  try {
36
- return await handle.result();
36
+ return await handle.result(true);
37
37
  }
38
38
  catch (error) {
39
39
  handle = await client.workflow.start({
@@ -63,28 +63,94 @@ class WorkflowService {
63
63
  }
64
64
  static async search() {
65
65
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
66
- if (!store) {
67
- throw new Error('durable-store-not-found');
68
- }
69
66
  const workflowId = store.get('workflowId');
67
+ const workflowDimension = store.get('workflowDimension') ?? '';
70
68
  const workflowTopic = store.get('workflowTopic');
71
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
72
- return new search_1.Search(workflowId, hotMeshClient);
69
+ const namespace = store.get('namespace');
70
+ const COUNTER = store.get('counter');
71
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
72
+ //this ID is used as a item key with a hash (dash prefix ensures no collision)
73
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
74
+ const searchSessionId = `-search${workflowDimension}-${execIndex}`;
75
+ return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
76
+ }
77
+ /**
78
+ * those methods that may only be called once must be protected by flagging
79
+ * their execution with a unique key (the key is stored in the workflow state)
80
+ */
81
+ static async isSideEffectAllowed(hotMeshClient, prefix) {
82
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
83
+ const workflowId = store.get('workflowId');
84
+ const workflowDimension = store.get('workflowDimension') ?? '';
85
+ const COUNTER = store.get('counter');
86
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
87
+ //this ID is used as a item key with a hash (dash prefix ensures no collision)
88
+ const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
89
+ //this ID is used as a item key with a hash (dash prefix ensures no collision)
90
+ const keyParams = {
91
+ appId: hotMeshClient.appId,
92
+ jobId: ''
93
+ };
94
+ const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
95
+ const workflowGuid = `${hotMeshPrefix}${workflowId}`;
96
+ const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
97
+ return guidValue === 1;
98
+ }
99
+ /**
100
+ * send signal data into any other paused thread (which is paused and
101
+ * awaiting the signal) from within a hook-thread or the main-thread
102
+ */
103
+ static async signal(signalId, data) {
104
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
105
+ const namespace = store.get('namespace');
106
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.wfs.signal`, { namespace });
107
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
108
+ return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
109
+ }
110
+ }
111
+ /**
112
+ * spawn a hook from either the main thread or a hook thread with
113
+ * the provided options; worflowId/TaskQueue/Name are optional and will
114
+ * default to the current workflowId/WorkflowTopic if not provided
115
+ */
116
+ static async hook(options) {
117
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
118
+ const namespace = store.get('namespace');
119
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
120
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
121
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
122
+ let workflowId;
123
+ let workflowTopic;
124
+ if (options.workflowId && options.taskQueue && options.workflowName) {
125
+ workflowId = options.workflowId;
126
+ workflowTopic = `${options.taskQueue}-${options.workflowName}`;
127
+ }
128
+ else {
129
+ workflowId = store.get('workflowId');
130
+ workflowTopic = store.get('workflowTopic');
131
+ }
132
+ const payload = {
133
+ arguments: [...options.args],
134
+ id: workflowId,
135
+ workflowTopic,
136
+ backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
137
+ };
138
+ return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
139
+ }
73
140
  }
74
141
  static async sleep(duration) {
75
142
  const seconds = (0, ms_1.default)(duration) / 1000;
76
143
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
77
- if (!store) {
78
- throw new Error('durable-store-not-found');
79
- }
80
144
  const COUNTER = store.get('counter');
81
145
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
82
146
  const workflowId = store.get('workflowId');
83
147
  const workflowTopic = store.get('workflowTopic');
84
- const sleepJobId = `${workflowId}-$sleep-${execIndex}`;
148
+ const workflowDimension = store.get('workflowDimension') ?? '';
149
+ const namespace = store.get('namespace');
150
+ const sleepJobId = `${workflowId}-$sleep${workflowDimension}-${execIndex}`;
85
151
  try {
86
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
87
- await hotMeshClient.getState(factory_1.SLEEP_SUBSCRIBES_TOPIC, sleepJobId);
152
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
153
+ await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
88
154
  //if no error is thrown, we've already slept, return the delay
89
155
  return seconds;
90
156
  }
@@ -92,28 +158,27 @@ class WorkflowService {
92
158
  //if an error, the sleep job was not found...rethrow error; sleep job
93
159
  // will be automatically created according to the DAG rules (they
94
160
  // spawn a new sleep job if error code 595 is thrown by the worker)
95
- throw new errors_1.DurableSleepError(workflowId, seconds, execIndex);
161
+ throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
96
162
  }
97
163
  }
98
164
  static async waitForSignal(signals, options) {
99
165
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
100
- if (!store) {
101
- throw new Error('durable-store-not-found');
102
- }
103
166
  const COUNTER = store.get('counter');
104
167
  const workflowId = store.get('workflowId');
105
168
  const workflowTopic = store.get('workflowTopic');
106
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
169
+ const workflowDimension = store.get('workflowDimension') ?? '';
170
+ const namespace = store.get('namespace');
171
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
107
172
  //iterate the list of signals and check for done
108
173
  let allAreComplete = true;
109
174
  let noneAreComplete = false;
110
175
  const signalResults = [];
111
176
  for (const signal of signals) {
112
177
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
113
- const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
178
+ const wfsJobId = `${workflowId}-$wfs${workflowDimension}-${execIndex}`;
114
179
  try {
115
180
  if (allAreComplete) {
116
- const state = await hotMeshClient.getState(factory_1.WFS_SUBSCRIBES_TOPIC, wfsJobId);
181
+ const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
117
182
  if (state.data?.signalData) {
118
183
  //user data is nested to isolate from the signal id; unpackage it
119
184
  const signalData = state.data.signalData;
@@ -151,22 +216,21 @@ class WorkflowService {
151
216
  static wrapActivity(activityName, options) {
152
217
  return async function () {
153
218
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
154
- if (!store) {
155
- throw new Error('durable-store-not-found');
156
- }
157
219
  const COUNTER = store.get('counter');
158
220
  //increment by state (not value) to avoid race conditions
159
221
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
160
222
  const workflowId = store.get('workflowId');
223
+ const workflowDimension = store.get('workflowDimension') ?? '';
161
224
  const workflowTopic = store.get('workflowTopic');
162
225
  const trc = store.get('workflowTrace');
163
226
  const spn = store.get('workflowSpan');
227
+ const namespace = store.get('namespace');
164
228
  const activityTopic = `${workflowTopic}-activity`;
165
- const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
229
+ const activityJobId = `${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
166
230
  let activityState;
167
231
  try {
168
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
169
- activityState = await hotMeshClient.getState(factory_1.ACTIVITY_SUBSCRIBES_TOPIC, activityJobId);
232
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
233
+ activityState = await hotMeshClient.getState(`${hotMeshClient.appId}.activity.execute`, activityJobId);
170
234
  if (activityState.metadata.err) {
171
235
  await hotMeshClient.scrub(activityJobId);
172
236
  throw new Error(activityState.metadata.err);
@@ -176,9 +240,9 @@ class WorkflowService {
176
240
  }
177
241
  //one time subscription
178
242
  return await new Promise((resolve, reject) => {
179
- hotMeshClient.sub(`${factory_1.ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`, async (topic, message) => {
243
+ hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
180
244
  const response = message.data?.response;
181
- hotMeshClient.unsub(`${factory_1.ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`);
245
+ hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
182
246
  // Resolve the Promise when the callback is triggered with a message
183
247
  resolve(response);
184
248
  });
@@ -196,9 +260,9 @@ class WorkflowService {
196
260
  activityName,
197
261
  };
198
262
  //start the job
199
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
263
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
200
264
  const context = { metadata: { trc, spn }, data: {} };
201
- const jobOutput = await hotMeshClient.pubsub(factory_1.ACTIVITY_SUBSCRIBES_TOPIC, payload, context, duration);
265
+ const jobOutput = await hotMeshClient.pubsub(`${hotMeshClient.appId}.activity.execute`, payload, context, duration);
202
266
  return jobOutput.data.response;
203
267
  }
204
268
  };
@@ -19,7 +19,7 @@ import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '..
19
19
  import { JobMessageCallback } from '../../types/quorum';
20
20
  import { RedisClient, RedisMulti } from '../../types/redis';
21
21
  import { GetStatsOptions, IdsResponse, JobStatsInput, StatsResponse } from '../../types/stats';
22
- import { StreamData, StreamDataResponse, StreamError } from '../../types/stream';
22
+ import { StreamCode, StreamData, StreamDataResponse, StreamError, StreamStatus } from '../../types/stream';
23
23
  declare class EngineService {
24
24
  namespace: string;
25
25
  apps: HotMeshApps | null;
@@ -63,7 +63,7 @@ declare class EngineService {
63
63
  hasParentJob(context: JobState): boolean;
64
64
  resolveError(metadata: JobMetadata): StreamError | undefined;
65
65
  scrub(jobId: string): Promise<void>;
66
- hook(topic: string, data: JobData, dad?: string): Promise<string>;
66
+ hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
67
67
  hookTime(jobId: string, activityId: string): Promise<JobStatus | void>;
68
68
  hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
69
69
  pub(topic: string, data: JobData, context?: JobState): Promise<string>;
@@ -248,7 +248,8 @@ class EngineService {
248
248
  await activityHandler.process();
249
249
  }
250
250
  else {
251
- await activityHandler.processWebHookEvent();
251
+ //a 202 code keeps the hook alive (hooks are single-use by default)
252
+ await activityHandler.processWebHookEvent(streamData.status, streamData.code);
252
253
  }
253
254
  }
254
255
  else if (streamData.type === stream_2.StreamDataType.AWAIT) {
@@ -269,7 +270,7 @@ class EngineService {
269
270
  }
270
271
  else {
271
272
  const activityHandler = await this.initActivity(`.${streamData.metadata.aid}`, streamData.data, context);
272
- await activityHandler.processEvent(streamData.status, streamData.code);
273
+ await activityHandler.processEvent(streamData.status, streamData.code, 'output');
273
274
  }
274
275
  this.logger.debug('engine-process-stream-message-end', {
275
276
  jid: streamData.metadata.jid,
@@ -323,21 +324,15 @@ class EngineService {
323
324
  await this.store.scrub(jobId);
324
325
  }
325
326
  // ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
326
- async hook(topic, data, dad) {
327
+ async hook(topic, data, status = stream_2.StreamStatus.SUCCESS, code = 200) {
327
328
  const hookRule = await this.storeSignaler.getHookRule(topic);
328
- const [aid, schema] = await this.getSchema(`.${hookRule.to}`);
329
- if (!dad) {
330
- //assume dimensional address is singular (0)
331
- // for ancestors and self if not provided
332
- // todo: register
333
- dad = ',0'.repeat(schema.ancestors.length + 1);
334
- }
329
+ const [aid] = await this.getSchema(`.${hookRule.to}`);
335
330
  const streamData = {
336
331
  type: stream_2.StreamDataType.WEBHOOK,
332
+ status,
333
+ code,
337
334
  metadata: {
338
- //jid is unknown at this point; will be resolved using the data
339
335
  aid,
340
- dad,
341
336
  topic
342
337
  },
343
338
  data,
@@ -6,7 +6,7 @@ import { JobState, JobData, JobOutput, JobStatus } from '../../types/job';
6
6
  import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
7
7
  import { JobMessageCallback } from '../../types/quorum';
8
8
  import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
9
- import { StreamData, StreamDataResponse } from '../../types/stream';
9
+ import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
10
10
  declare class HotMeshService {
11
11
  namespace: string;
12
12
  appId: string;
@@ -38,7 +38,7 @@ declare class HotMeshService {
38
38
  getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
39
39
  resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
40
40
  scrub(jobId: string): Promise<void>;
41
- hook(topic: string, data: JobData, dad?: string): Promise<string>;
41
+ hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
42
42
  hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
43
43
  stop(): Promise<void>;
44
44
  compress(terms: string[]): Promise<boolean>;
@@ -119,8 +119,8 @@ class HotMeshService {
119
119
  await this.engine?.scrub(jobId);
120
120
  }
121
121
  // ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
122
- async hook(topic, data, dad) {
123
- return await this.engine?.hook(topic, data, dad);
122
+ async hook(topic, data, status, code) {
123
+ return await this.engine?.hook(topic, data, status, code);
124
124
  }
125
125
  async hookAll(hookTopic, data, query, queryFacets = []) {
126
126
  return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
@@ -8,8 +8,8 @@ declare class StoreSignaler {
8
8
  logger: ILogger;
9
9
  constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
10
10
  getHookRule(topic: string): Promise<HookRule | undefined>;
11
- registerWebHook(topic: string, context: JobState, multi?: RedisMulti): Promise<string>;
12
- processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<string>;
11
+ registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string>;
12
+ processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string] | undefined>;
13
13
  deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
14
14
  }
15
15
  export { StoreSignaler };
@@ -11,7 +11,7 @@ class StoreSignaler {
11
11
  const rules = await this.store.getHookRules();
12
12
  return rules?.[topic]?.[0];
13
13
  }
14
- async registerWebHook(topic, context, multi) {
14
+ async registerWebHook(topic, context, dad, multi) {
15
15
  const hookRule = await this.getHookRule(topic);
16
16
  if (hookRule) {
17
17
  const mapExpression = hookRule.conditions.match[0].expected;
@@ -20,7 +20,8 @@ class StoreSignaler {
20
20
  const hook = {
21
21
  topic,
22
22
  resolved,
23
- jobId,
23
+ //hookSignalId is composed of `<dad>::<jid>`
24
+ jobId: `${dad}::${jobId}`,
24
25
  };
25
26
  await this.store.setHookSignal(hook, multi);
26
27
  return jobId;
@@ -36,18 +37,27 @@ class StoreSignaler {
36
37
  const context = { $self: { hook: { data } }, $hook: { data } };
37
38
  const mapExpression = hookRule.conditions.match[0].actual;
38
39
  const resolved = pipe_1.Pipe.resolve(mapExpression, context);
39
- const jobId = await this.store.getHookSignal(topic, resolved);
40
- return jobId;
40
+ const hookSignalId = await this.store.getHookSignal(topic, resolved);
41
+ if (!hookSignalId) {
42
+ //messages can be double-processed; not an issue; return undefined
43
+ //users can also provide a bogus topic; not an issue; return undefined
44
+ return undefined;
45
+ }
46
+ const [dad, jid] = hookSignalId.split('::');
47
+ //return [jid, aid, dad]
48
+ return [jid, hookRule.to, dad];
41
49
  }
42
50
  else {
43
- throw new Error('signaler.process:error: hook rule not found');
51
+ throw new Error('signal-not-found');
44
52
  }
45
53
  }
46
54
  async deleteWebHookSignal(topic, data) {
47
55
  const hookRule = await this.getHookRule(topic);
48
56
  if (hookRule) {
49
- //todo: use the rule to generate `resolved`
50
- const resolved = data.id;
57
+ //NOTE: both formats are supported: $self.hook.data OR $hook.data
58
+ const context = { $self: { hook: { data } }, $hook: { data } };
59
+ const mapExpression = hookRule.conditions.match[0].actual;
60
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
51
61
  return await this.store.deleteHookSignal(topic, resolved);
52
62
  }
53
63
  else {
@@ -121,6 +121,7 @@ class StreamSignaler {
121
121
  output = await callback(input);
122
122
  }
123
123
  catch (error) {
124
+ console.error(error);
124
125
  this.logger.error(`stream-call-function-error`, { error });
125
126
  output = this.structureUnhandledError(input, error);
126
127
  }
@@ -68,7 +68,7 @@ class RedisStoreService extends index_1.StoreService {
68
68
  }
69
69
  catch (error) {
70
70
  const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
71
- this.logger.warn(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
71
+ this.logger.info(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
72
72
  throw error;
73
73
  }
74
74
  }
@@ -422,6 +422,9 @@ class StoreService {
422
422
  }
423
423
  return [state, status];
424
424
  }
425
+ else {
426
+ throw new Error(`Job ${jobId} not found`);
427
+ }
425
428
  }
426
429
  async collate(jobId, activityId, amount, dIds, multi) {
427
430
  const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
@@ -207,11 +207,17 @@ class TelemetryService {
207
207
  }
208
208
  static bindActivityTelemetryToState(state, config, metadata, context, leg) {
209
209
  if (config.type === 'trigger') {
210
+ //trigger activities run non-duplexed and only have a single leg (2)
210
211
  state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
211
212
  state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l2s;
212
213
  }
213
214
  else if (utils_1.polyfill.resolveActivityType(config.type) === 'hook' && leg === 1) {
214
- //activities run non-duplexed and only have a single leg
215
+ //hook activities run non-duplexed and only have a single leg (1)
216
+ state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
217
+ state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l1s;
218
+ }
219
+ else if (config.type === 'signal' && leg === 1) {
220
+ //signal activities run non-duplexed and only have a single leg (1)
215
221
  state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
216
222
  state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l1s;
217
223
  }
@@ -66,11 +66,13 @@ interface SignalActivity extends BaseActivity {
66
66
  type: 'signal';
67
67
  subtype: 'one' | 'all';
68
68
  topic: string;
69
- key_name: string;
70
- key_value: string;
71
- scrub: boolean;
69
+ key_name?: string;
70
+ key_value?: string;
71
+ scrub?: boolean;
72
72
  signal?: Record<string, any>;
73
73
  resolver?: Record<string, any>;
74
+ status?: string;
75
+ code?: number;
74
76
  }
75
77
  interface IterateActivity extends BaseActivity {
76
78
  type: 'iterate';