@hotmeshio/hotmesh 0.0.18 → 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 (60) 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/modules/utils.js +7 -0
  5. package/build/package.json +2 -1
  6. package/build/services/activities/activity.d.ts +2 -2
  7. package/build/services/activities/activity.js +10 -8
  8. package/build/services/activities/hook.d.ts +4 -3
  9. package/build/services/activities/hook.js +15 -12
  10. package/build/services/activities/signal.d.ts +4 -0
  11. package/build/services/activities/signal.js +16 -2
  12. package/build/services/durable/client.d.ts +15 -5
  13. package/build/services/durable/client.js +45 -54
  14. package/build/services/durable/factory.d.ts +2 -16
  15. package/build/services/durable/factory.js +276 -46
  16. package/build/services/durable/handle.d.ts +1 -1
  17. package/build/services/durable/handle.js +18 -5
  18. package/build/services/durable/search.d.ts +8 -1
  19. package/build/services/durable/search.js +34 -7
  20. package/build/services/durable/worker.d.ts +10 -7
  21. package/build/services/durable/worker.js +59 -49
  22. package/build/services/durable/workflow.d.ts +20 -2
  23. package/build/services/durable/workflow.js +97 -84
  24. package/build/services/engine/index.d.ts +2 -2
  25. package/build/services/engine/index.js +7 -12
  26. package/build/services/hotmesh/index.d.ts +2 -2
  27. package/build/services/hotmesh/index.js +2 -2
  28. package/build/services/signaler/store.d.ts +2 -2
  29. package/build/services/signaler/store.js +17 -7
  30. package/build/services/signaler/stream.js +1 -0
  31. package/build/services/store/clients/redis.js +1 -1
  32. package/build/services/store/index.js +3 -0
  33. package/build/services/telemetry/index.js +7 -1
  34. package/build/types/activity.d.ts +5 -3
  35. package/build/types/durable.d.ts +17 -4
  36. package/build/types/hook.d.ts +0 -1
  37. package/build/types/index.d.ts +1 -1
  38. package/modules/errors.ts +4 -2
  39. package/modules/utils.ts +6 -0
  40. package/package.json +2 -1
  41. package/services/activities/activity.ts +10 -8
  42. package/services/activities/hook.ts +17 -14
  43. package/services/activities/signal.ts +17 -3
  44. package/services/durable/client.ts +48 -56
  45. package/services/durable/factory.ts +274 -46
  46. package/services/durable/handle.ts +18 -5
  47. package/services/durable/search.ts +36 -7
  48. package/services/durable/worker.ts +61 -51
  49. package/services/durable/workflow.ts +110 -84
  50. package/services/engine/index.ts +8 -12
  51. package/services/hotmesh/index.ts +3 -3
  52. package/services/signaler/store.ts +18 -8
  53. package/services/signaler/stream.ts +1 -0
  54. package/services/store/clients/redis.ts +1 -1
  55. package/services/store/index.ts +2 -0
  56. package/services/telemetry/index.ts +6 -1
  57. package/types/activity.ts +10 -8
  58. package/types/durable.ts +18 -3
  59. package/types/hook.ts +0 -1
  60. package/types/index.ts +1 -0
@@ -3,47 +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
- /*
11
- Here is an example of how the methods in this file are used:
12
-
13
- ./worker.ts
14
-
15
- import { Durable } from '@hotmeshio/hotmesh';
16
- import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
17
-
18
- import * as workflows from './workflows';
19
-
20
- async function run() {
21
- const worker = await Durable.Worker.create({
22
- connection: {
23
- class: Redis,
24
- options: {
25
- host: 'localhost',
26
- port: 6379,
27
- },
28
- },
29
- taskQueue: 'hello-world',
30
- workflow: workflows.example,
31
- });
32
- await worker.run();
33
- }
34
-
35
- run().catch((err) => {
36
- console.error(err);
37
- process.exit(1);
38
- });
39
- */
40
11
  class WorkerService {
41
12
  static async activateWorkflow(hotMesh) {
42
- const app = await hotMesh.engine.store.getApp(factory_1.APP_ID);
13
+ const app = await hotMesh.engine.store.getApp(hotMesh.engine.appId);
43
14
  const appVersion = app?.version;
44
15
  if (!appVersion) {
45
16
  try {
46
- 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));
47
18
  await hotMesh.activate(factory_1.APP_VERSION);
48
19
  }
49
20
  catch (err) {
@@ -61,11 +32,6 @@ class WorkerService {
61
32
  }
62
33
  }
63
34
  }
64
- /**
65
- * NOTE: Because the worker imports the workflows dynamically AFTER
66
- * the activities are loaded, there will be items in the registry,
67
- * allowing proxyActivities to succeed.
68
- */
69
35
  static registerActivities(activities) {
70
36
  if (typeof activities === 'function' && typeof WorkerService.activityRegistry[activities.name] !== 'function') {
71
37
  WorkerService.activityRegistry[activities.name] = activities;
@@ -79,8 +45,40 @@ class WorkerService {
79
45
  }
80
46
  return WorkerService.activityRegistry;
81
47
  }
48
+ /**
49
+ * For those deployments with a redis stack backend (with the FT module),
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.
54
+ */
55
+ static async configureSearchIndex(hotMeshClient, search) {
56
+ if (search?.schema) {
57
+ const store = hotMeshClient.engine.store;
58
+ const schema = [];
59
+ for (const [key, value] of Object.entries(search.schema)) {
60
+ //prefix with a comma (avoids collisions with hotmesh reserved words)
61
+ schema.push(`_${key}`);
62
+ schema.push(value.type);
63
+ if (value.sortable) {
64
+ schema.push('SORTABLE');
65
+ }
66
+ }
67
+ try {
68
+ const keyParams = {
69
+ appId: hotMeshClient.appId,
70
+ jobId: ''
71
+ };
72
+ const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
73
+ const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
74
+ await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
75
+ }
76
+ catch (err) {
77
+ hotMeshClient.engine.logger.info('durable-client-search-err', { err });
78
+ }
79
+ }
80
+ }
82
81
  static async create(config) {
83
- //always call `registerActivities` before `import`
84
82
  WorkerService.connection = config.connection;
85
83
  const workflow = config.workflow;
86
84
  const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
@@ -91,6 +89,7 @@ class WorkerService {
91
89
  const worker = new WorkerService();
92
90
  worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
93
91
  worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
92
+ WorkerService.configureSearchIndex(worker.workflowRunner, config.search);
94
93
  await WorkerService.activateWorkflow(worker.workflowRunner);
95
94
  return worker;
96
95
  }
@@ -115,7 +114,8 @@ class WorkerService {
115
114
  options: config.connection.options
116
115
  };
117
116
  const hotMeshWorker = await hotmesh_1.HotMeshService.init({
118
- appId: factory_1.APP_ID,
117
+ logLevel: config.options?.logLevel ?? 'info',
118
+ appId: config.namespace ?? factory_1.APP_ID,
119
119
  engine: { redis: redisConfig },
120
120
  workers: [
121
121
  { topic: activityTopic,
@@ -163,26 +163,34 @@ class WorkerService {
163
163
  options: config.connection.options
164
164
  };
165
165
  const hotMeshWorker = await hotmesh_1.HotMeshService.init({
166
- appId: factory_1.APP_ID,
166
+ logLevel: config.options?.logLevel ?? 'info',
167
+ appId: config.namespace ?? factory_1.APP_ID,
167
168
  engine: { redis: redisConfig },
168
169
  workers: [{
169
170
  topic: workflowTopic,
170
171
  redis: redisConfig,
171
- callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
172
+ callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic, config).bind(this)
172
173
  }]
173
174
  });
174
175
  WorkerService.instances.set(workflowTopic, hotMeshWorker);
175
176
  return hotMeshWorker;
176
177
  }
177
- wrapWorkflowFunction(workflowFunction, workflowTopic) {
178
+ wrapWorkflowFunction(workflowFunction, workflowTopic, config) {
178
179
  return async (data) => {
179
180
  const counter = { counter: 0 };
180
181
  try {
181
182
  //incoming data payload has arguments and workflowId
182
183
  const workflowInput = data.data;
183
184
  const context = new Map();
185
+ context.set('namespace', config.namespace ?? factory_1.APP_ID);
184
186
  context.set('counter', counter);
185
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
+ }
186
194
  context.set('workflowTopic', workflowTopic);
187
195
  context.set('workflowName', workflowTopic.split('-').pop());
188
196
  context.set('workflowTrace', data.metadata.trc);
@@ -206,9 +214,10 @@ class WorkerService {
206
214
  metadata: { ...data.metadata },
207
215
  data: {
208
216
  code: err.code,
209
- message: JSON.stringify({ duration: err.duration, index: err.index }),
217
+ message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
210
218
  duration: err.duration,
211
- index: err.index
219
+ index: err.index,
220
+ dimension: err.dimension
212
221
  }
213
222
  };
214
223
  //not an error...just a trigger to wait for a signal
@@ -254,15 +263,16 @@ class WorkerService {
254
263
  _a = WorkerService;
255
264
  WorkerService.activityRegistry = {}; //user's activities
256
265
  WorkerService.instances = new Map();
257
- WorkerService.getHotMesh = async (worflowTopic, options) => {
258
- if (WorkerService.instances.has(worflowTopic)) {
259
- 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);
260
269
  }
261
270
  const hotMeshClient = hotmesh_1.HotMeshService.init({
262
- appId: factory_1.APP_ID,
271
+ logLevel: options?.logLevel ?? 'info',
272
+ appId: config.namespace ?? factory_1.APP_ID,
263
273
  engine: { redis: { ...WorkerService.connection } }
264
274
  });
265
- WorkerService.instances.set(worflowTopic, hotMeshClient);
275
+ WorkerService.instances.set(workflowTopic, hotMeshClient);
266
276
  await WorkerService.activateWorkflow(await hotMeshClient);
267
277
  return hotMeshClient;
268
278
  };
@@ -1,11 +1,29 @@
1
- import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
1
+ import { Search } from './search';
2
+ import { HotMeshService as HotMesh } from '../hotmesh';
3
+ import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
2
4
  export declare class WorkflowService {
3
5
  /**
4
6
  * Spawn a child workflow. await the result.
5
7
  */
6
8
  static executeChild<T>(options: WorkflowOptions): Promise<T>;
7
9
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
8
- static data(command: 'del' | 'get' | 'set' | 'incr' | 'mult', ...args: string[]): Promise<number | boolean | string>;
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>;
9
27
  static sleep(duration: string): Promise<number>;
10
28
  static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
11
29
  static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
@@ -5,63 +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
- /*
16
- `proxyActivities` returns a wrapped instance of the
17
- target activity, so that when the workflow calls a
18
- proxied activity, it is actually calling the proxy
19
- function, which in turn calls the activity function.
20
-
21
- Here is an example of how the methods in this file are used:
22
-
23
- ./workflows.ts
24
-
25
- import { Durable } from '@hotmeshio/hotmesh';
26
- import * as activities from './activities';
27
-
28
- const { greet } = Durable.workflow.proxyActivities<typeof activities>({
29
- activities: activities,
30
- startToCloseTimeout: '1 minute',
31
- retryPolicy: {
32
- initialInterval: '5 seconds', // Initial delay between retries
33
- maximumAttempts: 3, // Max number of retry attempts
34
- backoffCoefficient: 2.0, // Backoff factor for delay between retries: delay = initialInterval * (backoffCoefficient ^ retry_attempt)
35
- maximumInterval: '30 seconds', // Max delay between retries
36
- },
37
- });
38
-
39
- export async function example(name: string): Promise<string> {
40
- return await greet(name);
41
- }
42
- */
15
+ const worker_1 = require("./worker");
16
+ const stream_1 = require("../../types/stream");
43
17
  class WorkflowService {
44
18
  /**
45
19
  * Spawn a child workflow. await the result.
46
20
  */
47
21
  static async executeChild(options) {
48
22
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
49
- if (!store) {
50
- throw new Error('durable-store-not-found');
51
- }
52
23
  const workflowId = store.get('workflowId');
24
+ const workflowDimension = store.get('workflowDimension') ?? '';
53
25
  const workflowTrace = store.get('workflowTrace');
54
26
  const workflowSpan = store.get('workflowSpan');
55
27
  const COUNTER = store.get('counter');
56
28
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
57
- const childJobId = `${workflowId}-$${options.workflowName}-${execIndex}`;
29
+ const childJobId = `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
58
30
  const parentWorkflowId = `${workflowId}-f`;
59
31
  const client = new client_1.ClientService({
60
32
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
61
33
  });
62
34
  let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
63
35
  try {
64
- return await handle.result();
36
+ return await handle.result(true);
65
37
  }
66
38
  catch (error) {
67
39
  handle = await client.workflow.start({
@@ -89,53 +61,96 @@ class WorkflowService {
89
61
  }
90
62
  return proxy;
91
63
  }
92
- static async data(command, ...args) {
64
+ static async search() {
93
65
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
94
- if (!store) {
95
- throw new Error('durable-store-not-found');
96
- }
97
66
  const workflowId = store.get('workflowId');
67
+ const workflowDimension = store.get('workflowDimension') ?? '';
98
68
  const workflowTopic = store.get('workflowTopic');
99
- try {
100
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
101
- const search = new search_1.Search(workflowId, hotMeshClient);
102
- if (command === 'get') {
103
- return await search.get(args[0]);
104
- }
105
- else if (command === 'set') {
106
- await search.set(args[0], args[1]);
107
- return true;
108
- }
109
- else if (command === 'del') {
110
- await search.del(args[0]);
111
- return true;
112
- }
113
- else if (command === 'incr') {
114
- return await search.incr(args[0], Number(args[1]));
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}`;
115
127
  }
116
- else if (command === 'mult') {
117
- return await search.mult(args[0], Number(args[1]));
128
+ else {
129
+ workflowId = store.get('workflowId');
130
+ workflowTopic = store.get('workflowTopic');
118
131
  }
119
- }
120
- catch (e) {
121
- console.error(e);
122
- return '';
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);
123
139
  }
124
140
  }
125
141
  static async sleep(duration) {
126
142
  const seconds = (0, ms_1.default)(duration) / 1000;
127
143
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
128
- if (!store) {
129
- throw new Error('durable-store-not-found');
130
- }
131
144
  const COUNTER = store.get('counter');
132
145
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
133
146
  const workflowId = store.get('workflowId');
134
147
  const workflowTopic = store.get('workflowTopic');
135
- const sleepJobId = `${workflowId}-$sleep-${execIndex}`;
148
+ const workflowDimension = store.get('workflowDimension') ?? '';
149
+ const namespace = store.get('namespace');
150
+ const sleepJobId = `${workflowId}-$sleep${workflowDimension}-${execIndex}`;
136
151
  try {
137
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
138
- 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);
139
154
  //if no error is thrown, we've already slept, return the delay
140
155
  return seconds;
141
156
  }
@@ -143,28 +158,27 @@ class WorkflowService {
143
158
  //if an error, the sleep job was not found...rethrow error; sleep job
144
159
  // will be automatically created according to the DAG rules (they
145
160
  // spawn a new sleep job if error code 595 is thrown by the worker)
146
- throw new errors_1.DurableSleepError(workflowId, seconds, execIndex);
161
+ throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
147
162
  }
148
163
  }
149
164
  static async waitForSignal(signals, options) {
150
165
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
151
- if (!store) {
152
- throw new Error('durable-store-not-found');
153
- }
154
166
  const COUNTER = store.get('counter');
155
167
  const workflowId = store.get('workflowId');
156
168
  const workflowTopic = store.get('workflowTopic');
157
- 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 });
158
172
  //iterate the list of signals and check for done
159
173
  let allAreComplete = true;
160
174
  let noneAreComplete = false;
161
175
  const signalResults = [];
162
176
  for (const signal of signals) {
163
177
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
164
- const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
178
+ const wfsJobId = `${workflowId}-$wfs${workflowDimension}-${execIndex}`;
165
179
  try {
166
180
  if (allAreComplete) {
167
- const state = await hotMeshClient.getState(factory_1.WFS_SUBSCRIBES_TOPIC, wfsJobId);
181
+ const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
168
182
  if (state.data?.signalData) {
169
183
  //user data is nested to isolate from the signal id; unpackage it
170
184
  const signalData = state.data.signalData;
@@ -202,22 +216,21 @@ class WorkflowService {
202
216
  static wrapActivity(activityName, options) {
203
217
  return async function () {
204
218
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
205
- if (!store) {
206
- throw new Error('durable-store-not-found');
207
- }
208
219
  const COUNTER = store.get('counter');
209
220
  //increment by state (not value) to avoid race conditions
210
221
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
211
222
  const workflowId = store.get('workflowId');
223
+ const workflowDimension = store.get('workflowDimension') ?? '';
212
224
  const workflowTopic = store.get('workflowTopic');
213
225
  const trc = store.get('workflowTrace');
214
226
  const spn = store.get('workflowSpan');
227
+ const namespace = store.get('namespace');
215
228
  const activityTopic = `${workflowTopic}-activity`;
216
- const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
229
+ const activityJobId = `${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
217
230
  let activityState;
218
231
  try {
219
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
220
- 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);
221
234
  if (activityState.metadata.err) {
222
235
  await hotMeshClient.scrub(activityJobId);
223
236
  throw new Error(activityState.metadata.err);
@@ -227,9 +240,9 @@ class WorkflowService {
227
240
  }
228
241
  //one time subscription
229
242
  return await new Promise((resolve, reject) => {
230
- hotMeshClient.sub(`${factory_1.ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`, async (topic, message) => {
243
+ hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
231
244
  const response = message.data?.response;
232
- hotMeshClient.unsub(`${factory_1.ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`);
245
+ hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
233
246
  // Resolve the Promise when the callback is triggered with a message
234
247
  resolve(response);
235
248
  });
@@ -247,9 +260,9 @@ class WorkflowService {
247
260
  activityName,
248
261
  };
249
262
  //start the job
250
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
263
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
251
264
  const context = { metadata: { trc, spn }, data: {} };
252
- 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);
253
266
  return jobOutput.data.response;
254
267
  }
255
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 };