@hotmeshio/hotmesh 0.0.19 → 0.0.21

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 +36 -10
  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 +23 -2
  22. package/build/services/durable/workflow.js +143 -37
  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 +13 -2
  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 +38 -10
  46. package/services/durable/worker.ts +30 -24
  47. package/services/durable/workflow.ts +158 -40
  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 +14 -1
  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,12 +1,33 @@
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
- * Spawn a child workflow. await the result.
6
+ * Spawn a child workflow. await and return the result.
6
7
  */
7
8
  static executeChild<T>(options: WorkflowOptions): Promise<T>;
9
+ /**
10
+ * spawn a child workflow. return the childJobId.
11
+ */
12
+ static startChild<T>(options: WorkflowOptions): Promise<string>;
8
13
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
9
14
  static search(): Promise<Search>;
15
+ /**
16
+ * those methods that may only be called once must be protected by flagging
17
+ * their execution with a unique key (the key is stored in the workflow state)
18
+ */
19
+ static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
20
+ /**
21
+ * send signal data into any other paused thread (which is paused and
22
+ * awaiting the signal) from within a hook-thread or the main-thread
23
+ */
24
+ static signal(signalId: string, data: Record<any, any>): Promise<string>;
25
+ /**
26
+ * spawn a hook from either the main thread or a hook thread with
27
+ * the provided options; worflowId/TaskQueue/Name are optional and will
28
+ * default to the current workflowId/WorkflowTopic if not provided
29
+ */
30
+ static hook(options: HookOptions): Promise<string>;
10
31
  static sleep(duration: string): Promise<number>;
11
32
  static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
12
33
  static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
@@ -5,48 +5,90 @@ 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
- * Spawn a child workflow. await the result.
19
+ * Spawn a child workflow. await and return 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
- }
23
+ const namespace = store.get('namespace');
24
24
  const workflowId = store.get('workflowId');
25
+ const workflowDimension = store.get('workflowDimension') ?? '';
25
26
  const workflowTrace = store.get('workflowTrace');
26
27
  const workflowSpan = store.get('workflowSpan');
27
28
  const COUNTER = store.get('counter');
28
29
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
29
- const childJobId = `${workflowId}-$${options.workflowName}-${execIndex}`;
30
+ //this is risky but MUST be allowed. Users MAY set the workflowId,
31
+ //but if there is a naming collision, the data from the target entity will be used
32
+ //as there is know way of knowing if the item was generated via a prior run of the workflow
33
+ const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
30
34
  const parentWorkflowId = `${workflowId}-f`;
31
35
  const client = new client_1.ClientService({
32
36
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
33
37
  });
34
- let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
38
+ let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId, namespace);
35
39
  try {
36
- return await handle.result();
40
+ return await handle.result(true);
37
41
  }
38
42
  catch (error) {
39
43
  handle = await client.workflow.start({
40
44
  ...options,
45
+ namespace,
41
46
  workflowId: childJobId,
42
47
  parentWorkflowId,
43
48
  workflowTrace,
44
49
  workflowSpan,
45
50
  });
51
+ //todo: options.startToCloseTimeout
46
52
  const result = await handle.result();
47
53
  return result;
48
54
  }
49
55
  }
56
+ /**
57
+ * spawn a child workflow. return the childJobId.
58
+ */
59
+ static async startChild(options) {
60
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
61
+ const namespace = store.get('namespace');
62
+ const workflowId = store.get('workflowId');
63
+ const workflowDimension = store.get('workflowDimension') ?? '';
64
+ const workflowTrace = store.get('workflowTrace');
65
+ const workflowSpan = store.get('workflowSpan');
66
+ const COUNTER = store.get('counter');
67
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
68
+ const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
69
+ const parentWorkflowId = `${workflowId}-f`;
70
+ const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
71
+ try {
72
+ //get the status; if there is no error, return childJobId (what was spawned)
73
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
74
+ await hotMeshClient.getStatus(childJobId);
75
+ return childJobId;
76
+ }
77
+ catch (error) {
78
+ const client = new client_1.ClientService({
79
+ connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
80
+ });
81
+ await client.workflow.start({
82
+ ...options,
83
+ namespace,
84
+ workflowId: childJobId,
85
+ parentWorkflowId,
86
+ workflowTrace,
87
+ workflowSpan,
88
+ });
89
+ return childJobId;
90
+ }
91
+ }
50
92
  static proxyActivities(options) {
51
93
  if (options.activities) {
52
94
  worker_1.WorkerService.registerActivities(options.activities);
@@ -63,28 +105,94 @@ class WorkflowService {
63
105
  }
64
106
  static async search() {
65
107
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
66
- if (!store) {
67
- throw new Error('durable-store-not-found');
68
- }
69
108
  const workflowId = store.get('workflowId');
109
+ const workflowDimension = store.get('workflowDimension') ?? '';
70
110
  const workflowTopic = store.get('workflowTopic');
71
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
72
- return new search_1.Search(workflowId, hotMeshClient);
111
+ const namespace = store.get('namespace');
112
+ const COUNTER = store.get('counter');
113
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
114
+ //this ID is used as a item key with a hash (dash prefix ensures no collision)
115
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
116
+ const searchSessionId = `-search${workflowDimension}-${execIndex}`;
117
+ return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
118
+ }
119
+ /**
120
+ * those methods that may only be called once must be protected by flagging
121
+ * their execution with a unique key (the key is stored in the workflow state)
122
+ */
123
+ static async isSideEffectAllowed(hotMeshClient, prefix) {
124
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
125
+ const workflowId = store.get('workflowId');
126
+ const workflowDimension = store.get('workflowDimension') ?? '';
127
+ const COUNTER = store.get('counter');
128
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
129
+ //this ID is used as a item key with a hash (dash prefix ensures no collision)
130
+ const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
131
+ //this ID is used as a item key with a hash (dash prefix ensures no collision)
132
+ const keyParams = {
133
+ appId: hotMeshClient.appId,
134
+ jobId: ''
135
+ };
136
+ const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
137
+ const workflowGuid = `${hotMeshPrefix}${workflowId}`;
138
+ const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
139
+ return guidValue === 1;
140
+ }
141
+ /**
142
+ * send signal data into any other paused thread (which is paused and
143
+ * awaiting the signal) from within a hook-thread or the main-thread
144
+ */
145
+ static async signal(signalId, data) {
146
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
147
+ const namespace = store.get('namespace');
148
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.wfs.signal`, { namespace });
149
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
150
+ return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
151
+ }
152
+ }
153
+ /**
154
+ * spawn a hook from either the main thread or a hook thread with
155
+ * the provided options; worflowId/TaskQueue/Name are optional and will
156
+ * default to the current workflowId/WorkflowTopic if not provided
157
+ */
158
+ static async hook(options) {
159
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
160
+ const namespace = store.get('namespace');
161
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
162
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
163
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
164
+ let workflowId;
165
+ let workflowTopic;
166
+ if (options.workflowId && options.taskQueue && options.workflowName) {
167
+ workflowId = options.workflowId;
168
+ workflowTopic = `${options.taskQueue}-${options.workflowName}`;
169
+ }
170
+ else {
171
+ workflowId = store.get('workflowId');
172
+ workflowTopic = store.get('workflowTopic');
173
+ }
174
+ const payload = {
175
+ arguments: [...options.args],
176
+ id: workflowId,
177
+ workflowTopic,
178
+ backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
179
+ };
180
+ return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
181
+ }
73
182
  }
74
183
  static async sleep(duration) {
75
184
  const seconds = (0, ms_1.default)(duration) / 1000;
76
185
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
77
- if (!store) {
78
- throw new Error('durable-store-not-found');
79
- }
80
186
  const COUNTER = store.get('counter');
81
187
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
82
188
  const workflowId = store.get('workflowId');
83
189
  const workflowTopic = store.get('workflowTopic');
84
- const sleepJobId = `${workflowId}-$sleep-${execIndex}`;
190
+ const workflowDimension = store.get('workflowDimension') ?? '';
191
+ const namespace = store.get('namespace');
192
+ const sleepJobId = `${workflowId}-$sleep${workflowDimension}-${execIndex}`;
85
193
  try {
86
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
87
- await hotMeshClient.getState(factory_1.SLEEP_SUBSCRIBES_TOPIC, sleepJobId);
194
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
195
+ await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
88
196
  //if no error is thrown, we've already slept, return the delay
89
197
  return seconds;
90
198
  }
@@ -92,28 +200,27 @@ class WorkflowService {
92
200
  //if an error, the sleep job was not found...rethrow error; sleep job
93
201
  // will be automatically created according to the DAG rules (they
94
202
  // spawn a new sleep job if error code 595 is thrown by the worker)
95
- throw new errors_1.DurableSleepError(workflowId, seconds, execIndex);
203
+ throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
96
204
  }
97
205
  }
98
206
  static async waitForSignal(signals, options) {
99
207
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
100
- if (!store) {
101
- throw new Error('durable-store-not-found');
102
- }
103
208
  const COUNTER = store.get('counter');
104
209
  const workflowId = store.get('workflowId');
105
210
  const workflowTopic = store.get('workflowTopic');
106
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
211
+ const workflowDimension = store.get('workflowDimension') ?? '';
212
+ const namespace = store.get('namespace');
213
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
107
214
  //iterate the list of signals and check for done
108
215
  let allAreComplete = true;
109
216
  let noneAreComplete = false;
110
217
  const signalResults = [];
111
218
  for (const signal of signals) {
112
219
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
113
- const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
220
+ const wfsJobId = `${workflowId}-$wfs${workflowDimension}-${execIndex}`;
114
221
  try {
115
222
  if (allAreComplete) {
116
- const state = await hotMeshClient.getState(factory_1.WFS_SUBSCRIBES_TOPIC, wfsJobId);
223
+ const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
117
224
  if (state.data?.signalData) {
118
225
  //user data is nested to isolate from the signal id; unpackage it
119
226
  const signalData = state.data.signalData;
@@ -151,22 +258,21 @@ class WorkflowService {
151
258
  static wrapActivity(activityName, options) {
152
259
  return async function () {
153
260
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
154
- if (!store) {
155
- throw new Error('durable-store-not-found');
156
- }
157
261
  const COUNTER = store.get('counter');
158
262
  //increment by state (not value) to avoid race conditions
159
263
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
160
264
  const workflowId = store.get('workflowId');
265
+ const workflowDimension = store.get('workflowDimension') ?? '';
161
266
  const workflowTopic = store.get('workflowTopic');
162
267
  const trc = store.get('workflowTrace');
163
268
  const spn = store.get('workflowSpan');
269
+ const namespace = store.get('namespace');
164
270
  const activityTopic = `${workflowTopic}-activity`;
165
- const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
271
+ const activityJobId = `${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
166
272
  let activityState;
167
273
  try {
168
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
169
- activityState = await hotMeshClient.getState(factory_1.ACTIVITY_SUBSCRIBES_TOPIC, activityJobId);
274
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
275
+ activityState = await hotMeshClient.getState(`${hotMeshClient.appId}.activity.execute`, activityJobId);
170
276
  if (activityState.metadata.err) {
171
277
  await hotMeshClient.scrub(activityJobId);
172
278
  throw new Error(activityState.metadata.err);
@@ -176,9 +282,9 @@ class WorkflowService {
176
282
  }
177
283
  //one time subscription
178
284
  return await new Promise((resolve, reject) => {
179
- hotMeshClient.sub(`${factory_1.ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`, async (topic, message) => {
285
+ hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
180
286
  const response = message.data?.response;
181
- hotMeshClient.unsub(`${factory_1.ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`);
287
+ hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
182
288
  // Resolve the Promise when the callback is triggered with a message
183
289
  resolve(response);
184
290
  });
@@ -196,9 +302,9 @@ class WorkflowService {
196
302
  activityName,
197
303
  };
198
304
  //start the job
199
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
305
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
200
306
  const context = { metadata: { trc, spn }, data: {} };
201
- const jobOutput = await hotMeshClient.pubsub(factory_1.ACTIVITY_SUBSCRIBES_TOPIC, payload, context, duration);
307
+ const jobOutput = await hotMeshClient.pubsub(`${hotMeshClient.appId}.activity.execute`, payload, context, duration);
202
308
  return jobOutput.data.response;
203
309
  }
204
310
  };
@@ -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
  }