@hotmeshio/hotmesh 0.0.23 → 0.0.25

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 (41) hide show
  1. package/README.md +58 -66
  2. package/build/index.d.ts +2 -1
  3. package/build/index.js +3 -1
  4. package/build/package.json +2 -2
  5. package/build/services/durable/factory.js +6 -6
  6. package/build/services/durable/handle.js +2 -4
  7. package/build/services/durable/index.d.ts +2 -2
  8. package/build/services/durable/index.js +2 -2
  9. package/build/services/durable/meshos.d.ts +112 -0
  10. package/build/services/durable/meshos.js +306 -0
  11. package/build/services/durable/search.js +0 -1
  12. package/build/services/durable/worker.d.ts +1 -1
  13. package/build/services/durable/worker.js +8 -4
  14. package/build/services/durable/workflow.d.ts +4 -0
  15. package/build/services/durable/workflow.js +21 -9
  16. package/build/services/signaler/stream.js +1 -2
  17. package/build/services/store/clients/ioredis.js +2 -2
  18. package/build/services/store/clients/redis.js +2 -3
  19. package/build/services/stream/clients/redis.js +1 -1
  20. package/build/services/sub/clients/redis.js +1 -1
  21. package/build/types/durable.d.ts +28 -13
  22. package/build/types/index.d.ts +1 -1
  23. package/index.ts +2 -1
  24. package/package.json +2 -2
  25. package/services/durable/factory.ts +6 -6
  26. package/services/durable/handle.ts +2 -4
  27. package/services/durable/index.ts +2 -2
  28. package/services/durable/meshos.ts +366 -0
  29. package/services/durable/search.ts +0 -1
  30. package/services/durable/worker.ts +8 -5
  31. package/services/durable/workflow.ts +23 -10
  32. package/services/signaler/stream.ts +1 -2
  33. package/services/store/clients/ioredis.ts +2 -3
  34. package/services/store/clients/redis.ts +2 -3
  35. package/services/stream/clients/redis.ts +1 -1
  36. package/services/sub/clients/redis.ts +1 -1
  37. package/types/durable.ts +38 -12
  38. package/types/index.ts +8 -3
  39. package/build/services/durable/meshdb.d.ts +0 -113
  40. package/build/services/durable/meshdb.js +0 -211
  41. package/services/durable/meshdb.ts +0 -254
@@ -264,11 +264,11 @@ const getWorkflowYAML = (app: string, version: string) => {
264
264
  - ['{@string.concat}']
265
265
  cycleWorkflowId:
266
266
  '@pipe':
267
- - ['{$job.metadata.jid}', '-$wfc', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
267
+ - ['-', '{$job.metadata.jid}', '-$wfc', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
268
268
  - ['{@string.concat}']
269
269
  baseWorkflowId:
270
270
  '@pipe':
271
- - ['{$job.metadata.jid}', '-$wfs', '{sig.output.metadata.dad}', '-']
271
+ - ['-', '{$job.metadata.jid}', '-$wfs', '{sig.output.metadata.dad}', '-']
272
272
  - ['{@string.concat}']
273
273
  output:
274
274
  schema:
@@ -312,7 +312,7 @@ const getWorkflowYAML = (app: string, version: string) => {
312
312
  - ['{@string.concat}']
313
313
  workflowId:
314
314
  '@pipe':
315
- - ['{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
315
+ - ['-', '{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
316
316
  - ['{@string.concat}']
317
317
  output:
318
318
  schema:
@@ -382,11 +382,11 @@ const getWorkflowYAML = (app: string, version: string) => {
382
382
  - ['{@string.concat}']
383
383
  cycleWorkflowId:
384
384
  '@pipe':
385
- - ['{$job.metadata.jid}', '-$wfc-', '{w1.output.data.index}']
385
+ - ['-', '{$job.metadata.jid}', '-$wfc-', '{w1.output.data.index}']
386
386
  - ['{@string.concat}']
387
387
  baseWorkflowId:
388
388
  '@pipe':
389
- - ['{$job.metadata.jid}', '-$wfs-']
389
+ - ['-', '{$job.metadata.jid}', '-$wfs-']
390
390
  - ['{@string.concat}']
391
391
  output:
392
392
  schema:
@@ -430,7 +430,7 @@ const getWorkflowYAML = (app: string, version: string) => {
430
430
  - ['{@string.concat}']
431
431
  workflowId:
432
432
  '@pipe':
433
- - ['{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
433
+ - ['-', '{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
434
434
  - ['{@string.concat}']
435
435
  output:
436
436
  schema:
@@ -39,10 +39,8 @@ export class WorkflowHandleService {
39
39
  throw new Error(JSON.parse(state.metadata.err));
40
40
  }
41
41
  if (state?.data?.done) {
42
- //child flows are never technically 'done' as they have an open hook
43
- //that is tied to the parent flow's completion. so, we need to check
44
- //the 'done' flag on the child flow's payload (not the 'js' metadata field
45
- //which is typically used); the `loadState` parameter triggers this
42
+ //child flows are never 'done'; they use a hook
43
+ //that only closes upon parent flow completion.
46
44
  return state.data.response;
47
45
  }
48
46
  }
@@ -1,6 +1,6 @@
1
1
  import { ClientService } from './client';
2
2
  import { ConnectionService } from './connection';
3
- import { MeshDBService } from './meshdb';
3
+ import { MeshOSService } from './meshos';
4
4
  import { WorkerService } from './worker';
5
5
  import { WorkflowService } from './workflow';
6
6
  import { ContextType } from '../../types/durable';
@@ -8,7 +8,7 @@ import { ContextType } from '../../types/durable';
8
8
  export const Durable = {
9
9
  Client: ClientService,
10
10
  Connection: ConnectionService,
11
- MeshDB: MeshDBService,
11
+ MeshOS: MeshOSService,
12
12
  Worker: WorkerService,
13
13
  workflow: WorkflowService,
14
14
  };
@@ -0,0 +1,366 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ import { Durable } from '.';
4
+ import { asyncLocalStorage } from './asyncLocalStorage';
5
+ import { ClientService as Client } from './client';
6
+ import { WorkflowHandleService } from './handle';
7
+ import { Search } from './search';
8
+ import { WorkerService as Worker } from './worker';
9
+ import { WorkflowService } from './workflow';
10
+ import { StreamSignaler } from '../signaler/stream';
11
+ import {
12
+ FindOptions,
13
+ MeshOSActivityOptions,
14
+ MeshOSConfig,
15
+ MeshOSOptions,
16
+ MeshOSWorkerOptions,
17
+ WorkflowSearchOptions } from '../../types/durable';
18
+ import { RedisOptions, RedisClass } from '../../types/redis';
19
+ import { StringAnyType } from '../../types/serializer';
20
+
21
+ /**
22
+ * The base class for running MeshOS workflows.
23
+ * Extend this class, add your Redis config, and add functions to
24
+ * execute as durable `hooks`, `workflows`, and `activities`.
25
+ */
26
+
27
+ export class MeshOSService {
28
+
29
+ /**
30
+ * The top-level Redis isolation. All workflow data is
31
+ * isolated within this namespace. Values should be
32
+ * lower-case with no spaces (e.g, 'staging', 'prod', 'test',
33
+ * 'routing-stagig', 'reporting-prod', etc.).
34
+ * 1) only url-safe values are allowed;
35
+ * 2) the 'a' symbol is reserved by HotMesh for indexing apps
36
+ */
37
+ namespace = 'durable';
38
+
39
+ /**
40
+ * Data is routed to workers that specify this task queue.
41
+ * Setting the task queue when the worker is created will
42
+ * ensure that the worker only receives messages destined
43
+ * for the queue. Callers can specify the taskQue to when
44
+ * starting a job to call those workers.
45
+ */
46
+ taskQueue = 'default';
47
+
48
+ /**
49
+ * These methods run as durable workflows
50
+ */
51
+ workflowFunctions: Array<MeshOSOptions | string> = [];
52
+
53
+ /**
54
+ * These methods run as hooks (hook into a running workflow)
55
+ */
56
+ hookFunctions: Array<MeshOSOptions | string> = [];
57
+
58
+ /**
59
+ * These methods run as proxied activities (and are safely memoized)
60
+ */
61
+ proxyFunctions: Array<MeshOSActivityOptions | string> = [];
62
+
63
+ /**
64
+ * The workflow GUID. Workflows will be persisted to
65
+ * Redis using the pattern hmsh:<namespace>:j:<id>.
66
+ */
67
+ id: string;
68
+
69
+ /**
70
+ * The Redis connection options. NOTE: Redis and IORedis
71
+ * use different formats for their connection config.
72
+ */
73
+ redisOptions: RedisOptions = {
74
+ host: 'localhost',
75
+ port: 6379,
76
+ password: '',
77
+ db: 0,
78
+ };
79
+
80
+ /**
81
+ * The Redis connection class.
82
+ *
83
+ * @example
84
+ * import Redis from 'ioredis';
85
+ * import * as Redis from 'redis';
86
+ */
87
+ redisClass: RedisClass;
88
+
89
+ /**
90
+ * Optional model declaration (custom workflow state)
91
+ */
92
+ model: StringAnyType;
93
+
94
+ /**
95
+ * Optional configuration for Redis FT search
96
+ */
97
+ search: WorkflowSearchOptions;
98
+
99
+ static MeshOS = WorkflowService
100
+
101
+ static async getHotMeshClient (redisClass: RedisClass, redisOptions: RedisOptions, namespace: string, taskQueue: string) {
102
+ const client = new Client({
103
+ connection: {
104
+ class: redisClass,
105
+ options: redisOptions,
106
+ }
107
+ });
108
+ return await client.getHotMeshClient(taskQueue, namespace);
109
+ }
110
+
111
+ /**
112
+ * mints a workflow ID, using the search prefix.
113
+ * NOTE: The prefix is necesary when indexing
114
+ * HASHes when FT search is enabled.
115
+ * @returns {string}
116
+ */
117
+ static mintGuid(): string {
118
+ const my = new this();
119
+ return `${my.search?.prefix?.[0]}${nanoid()}}`;
120
+ }
121
+
122
+ /**
123
+ * Creates an FT search index
124
+ */
125
+ static async createIndex() {
126
+ const my = new this();
127
+ const hmClient = await MeshOSService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
128
+ Search.configureSearchIndex(hmClient, my.search)
129
+ }
130
+
131
+ /**
132
+ * stop the workers
133
+ * @returns {Promise<void>}
134
+ */
135
+ static async stopWorkers(): Promise<void> {
136
+ await Durable.Client.shutdown();
137
+ await Durable.Worker.shutdown();
138
+ await StreamSignaler.stopConsuming();
139
+ }
140
+
141
+ /**
142
+ * Initializes the worker(s). This is a static
143
+ * method that allows for optional task Queue targeting.
144
+ * An `allowList` may be optionally provided to start
145
+ * specific `worker` and `hook` methods.
146
+ * @param {MeshOSWorkerOptions} [options]
147
+ */
148
+ static async startWorkers(options?: MeshOSWorkerOptions) {
149
+ const taskQueue = options && options.taskQueue;
150
+ const allowList = options && options.allowList || [];
151
+ const my = new this();
152
+
153
+ //helper functions
154
+ const resolveFunctionNames = (arr: any[]) => arr.map(item => typeof item === 'string' ? item : item.name);
155
+ const belongsTo = (name: string, target: Array<MeshOSOptions | MeshOSActivityOptions | string>): boolean => {
156
+ const isWorkflow = target.find((item: MeshOSOptions | string) => {
157
+ return typeof item === 'string' ? item === name : item.name === name;
158
+ });
159
+ return isWorkflow !== undefined;
160
+ };
161
+
162
+ // proxy registered activities
163
+ const proxyFunctionNames = resolveFunctionNames([...my.proxyFunctions]);
164
+ if (proxyFunctionNames.length) {
165
+ const proxyActivities = proxyFunctionNames.reduce((acc, funcName) => {
166
+ let originalMethod = my[funcName];
167
+ if (typeof originalMethod === 'function') {
168
+ acc[funcName] = async (...args: any[]) => {
169
+ return await originalMethod.apply(my, args);
170
+ }
171
+ }
172
+ return acc;
173
+ }, {});
174
+ const proxiedActivities = Durable.workflow.proxyActivities({
175
+ activities: proxyActivities
176
+ });
177
+ Object.assign(my, proxiedActivities);
178
+ }
179
+
180
+ const functionsToIterate = allowList.length ? resolveFunctionNames(allowList) : resolveFunctionNames([...my.workflowFunctions, ...my.hookFunctions]);
181
+
182
+ // Iterating through the functions sequentially
183
+ for (const funcName of functionsToIterate) {
184
+ const originalMethod = my[funcName];
185
+ if (typeof originalMethod === 'function') {
186
+
187
+ //wrap the function to return
188
+ const wrappedFunction = {
189
+ [funcName]: async (...args: any[]) => {
190
+ const store = asyncLocalStorage.getStore();
191
+ const workflowId = store.get('workflowId');
192
+
193
+ //use a Proxy to wrap hook methods
194
+ const context = new Proxy(my, {
195
+ get: (target, prop, receiver) => {
196
+ if (prop === 'id') {
197
+ return workflowId;
198
+ } else if (typeof target[prop] === 'function') {
199
+ return (...args: any[]) => {
200
+ return new Promise(async (resolve, reject) => {
201
+ if (belongsTo(prop as string, my.hookFunctions)) {
202
+ return WorkflowService.hook({
203
+ namespace: my.namespace,
204
+ taskQueue: my.taskQueue,
205
+ workflowName: prop as string,
206
+ workflowId,
207
+ args,
208
+ }).then(resolve).catch(reject);
209
+ }
210
+ //otherwise, call the method as a standard instance method.
211
+ target[prop].apply(this, args).then(resolve).catch(reject);
212
+ });
213
+ }
214
+ }
215
+ return Reflect.get(target, prop, receiver);
216
+ },
217
+ });
218
+ return await originalMethod.apply(context, args);
219
+ }
220
+ };
221
+
222
+ //start the worker
223
+ await Worker.create({
224
+ namespace: my.namespace,
225
+ connection: {
226
+ class: my.redisClass,
227
+ options: my.redisOptions,
228
+ },
229
+ taskQueue: taskQueue ?? my.taskQueue,
230
+ workflow: wrappedFunction,
231
+ });
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * executes the redis FT search query
238
+ * @example '@_quantity:[89 89]'
239
+ * @param {any[]} args
240
+ * @returns {string}
241
+ */
242
+ static async find(options: FindOptions, ...args: string[]): Promise<string[] | [number]> {
243
+ const my = new this();
244
+ const client = new Client({ connection: {
245
+ class: my.redisClass,
246
+ options: my.redisOptions
247
+ }});
248
+ //workflow name is the function name driving the workflow
249
+ let workflowName: string;
250
+ if (options?.workflowName) {
251
+ workflowName = options?.workflowName
252
+ } else if(my.workflowFunctions?.length) {
253
+ let target = my.workflowFunctions[0];
254
+ if (typeof target === 'string') {
255
+ workflowName = target;
256
+ } else {
257
+ workflowName = target.name;
258
+ }
259
+ }
260
+ return await client.workflow.search(
261
+ options?.taskQueue ?? my.taskQueue,
262
+ workflowName,
263
+ my.namespace,
264
+ my.search.index,
265
+ ...args,
266
+ ); //[count, [id, fields[], id, fields[], id, fields[], ...]]
267
+ }
268
+
269
+ /**
270
+ * returns the workflow handle. The handle can then be
271
+ * used to query for status, state, custom state, etc.
272
+ * @param {string} id
273
+ * @returns {Promise<WorkflowHandleService>}
274
+ */
275
+ static async get(id: string): Promise<WorkflowHandleService> {
276
+ const my = new this();
277
+ const client = new Client({ connection: {
278
+ class: my.redisClass,
279
+ options: my.redisOptions
280
+ }});
281
+ let workflowName: string;
282
+ let target = my.workflowFunctions[0];
283
+ if (typeof target === 'string') {
284
+ workflowName = target;
285
+ } else {
286
+ workflowName = target.name;
287
+ }
288
+ return await client.workflow.getHandle(
289
+ my.taskQueue,
290
+ workflowName,
291
+ id,
292
+ my.namespace,
293
+ );
294
+ }
295
+
296
+ /**
297
+ * Optionally include a target taskQueue to exec the
298
+ * workflow's call on a specific worker queue.
299
+ */
300
+ constructor(id?: string | MeshOSConfig, options?: MeshOSConfig) {
301
+ if (typeof(id) === 'string') {
302
+ this.id = id;
303
+ } else if (id?.id) {
304
+ this.id = id.id;
305
+ options = id;
306
+ id = undefined;
307
+ };
308
+ if (options?.taskQueue) {
309
+ this.taskQueue = options.taskQueue;
310
+ } else if (!id && !options?.taskQueue) {
311
+ return this;
312
+ }
313
+
314
+ function belongsTo(name: string, target: Array<MeshOSOptions | MeshOSActivityOptions | string>): boolean {
315
+ const isWorkflow = target.find((item: MeshOSOptions | string) => {
316
+ return typeof item === 'string' ? item === name : item.name === name;
317
+ });
318
+ return isWorkflow !== undefined;
319
+ }
320
+
321
+ return new Proxy(this, {
322
+ get: (target, prop, receiver) => {
323
+ if (typeof target[prop] === 'function') {
324
+ return (...args: any[]) => {
325
+ return new Promise(async (resolve, reject) => {
326
+ const client = new Client({ connection: {
327
+ class: this.redisClass,
328
+ options: this.redisOptions
329
+ }});
330
+ if (belongsTo(prop as string, this.workflowFunctions)) {
331
+ //start a new workflow
332
+ const handle = await client.workflow.start({
333
+ namespace: this.namespace,
334
+ args,
335
+ taskQueue: this.taskQueue,
336
+ workflowName: prop as string,
337
+ workflowId: this.id,
338
+ });
339
+ if (options?.await) {
340
+ //wait for the workflow to complete
341
+ const result = await handle.result();
342
+ return resolve(result);
343
+ } else {
344
+ //return the workflow handle
345
+ return resolve(handle);
346
+ }
347
+ } else if (belongsTo(prop as string, this.hookFunctions)) {
348
+ //hook into a running workflow
349
+ return client.workflow.hook({
350
+ namespace: this.namespace,
351
+ taskQueue: this.taskQueue,
352
+ workflowName: prop as string,
353
+ workflowId: this.id,
354
+ args,
355
+ }).then(resolve).catch(reject);
356
+ }
357
+ //otherwise, call the method as a standard instance method.
358
+ target[prop].apply(this, args).then(resolve).catch(reject);
359
+ });
360
+ };
361
+ }
362
+ return Reflect.get(target, prop, receiver);
363
+ },
364
+ });
365
+ }
366
+ }
@@ -44,7 +44,6 @@ export class Search {
44
44
  const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
45
45
  await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length.toString(), ...prefixes, 'SCHEMA', ...schema);
46
46
  } catch (err) {
47
- console.error(err);
48
47
  hotMeshClient.engine.logger.info('durable-client-search-err', { err });
49
48
  }
50
49
  }
@@ -72,13 +72,15 @@ export class WorkerService {
72
72
  Object.keys(activities).forEach(key => {
73
73
  if (activities[key].name && typeof WorkerService.activityRegistry[activities[key].name] !== 'function') {
74
74
  WorkerService.activityRegistry[activities[key].name] = (activities as any)[key] as Function;
75
+ } else if (typeof (activities as any)[key] === 'function') {
76
+ WorkerService.activityRegistry[key] = (activities as any)[key] as Function;
75
77
  }
76
78
  });
77
79
  }
78
80
  return WorkerService.activityRegistry;
79
81
  }
80
82
 
81
- static async create(config: WorkerConfig) {
83
+ static async create(config: WorkerConfig): Promise<WorkerService> {
82
84
  WorkerService.connection = config.connection;
83
85
  const workflow = config.workflow;
84
86
  const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
@@ -95,16 +97,17 @@ export class WorkerService {
95
97
  return worker;
96
98
  }
97
99
 
98
- static resolveWorkflowTarget(workflow: object | Function): [string, Function] {
100
+ static resolveWorkflowTarget(workflow: object | Function, name?: string): [string, Function] {
99
101
  let workflowFunction: Function;
100
102
  if (typeof workflow === 'function') {
101
103
  workflowFunction = workflow;
104
+ return [workflowFunction.name ?? name, workflowFunction];
102
105
  } else {
103
106
  const workflowFunctionNames = Object.keys(workflow);
104
- workflowFunction = workflow[workflowFunctionNames[workflowFunctionNames.length - 1]];
105
- return WorkerService.resolveWorkflowTarget(workflowFunction);
107
+ const lastFunctionName = workflowFunctionNames[workflowFunctionNames.length - 1];
108
+ workflowFunction = workflow[lastFunctionName];
109
+ return WorkerService.resolveWorkflowTarget(workflowFunction, lastFunctionName);
106
110
  }
107
- return [workflowFunction.name, workflowFunction];
108
111
  }
109
112
 
110
113
  async run() {
@@ -37,7 +37,7 @@ export class WorkflowService {
37
37
  //this is risky but MUST be allowed. Users MAY set the workflowId,
38
38
  //but if there is a naming collision, the data from the target entity will be used
39
39
  //as there is know way of knowing if the item was generated via a prior run of the workflow
40
- const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
40
+ const childJobId = options.workflowId ?? `-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
41
41
  const parentWorkflowId = `${workflowId}-f`;
42
42
 
43
43
  const client = new Client({
@@ -80,7 +80,7 @@ export class WorkflowService {
80
80
  const workflowSpan = store.get('workflowSpan');
81
81
  const COUNTER = store.get('counter');
82
82
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
83
- const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
83
+ const childJobId = options.workflowId ?? `-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
84
84
  const parentWorkflowId = `${workflowId}-f`;
85
85
  const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
86
86
 
@@ -111,7 +111,7 @@ export class WorkflowService {
111
111
  */
112
112
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
113
113
  if (options.activities) {
114
- WorkerService.registerActivities(options.activities)
114
+ WorkerService.registerActivities(options.activities);
115
115
  }
116
116
 
117
117
  const proxy: any = {};
@@ -142,6 +142,16 @@ export class WorkflowService {
142
142
  return new Search(workflowId, hotMeshClient, searchSessionId);
143
143
  }
144
144
 
145
+ /**
146
+ * return a handle to the hotmesh client currently running the workflow
147
+ */
148
+ static async getHotMesh(): Promise<HotMesh> {
149
+ const store = asyncLocalStorage.getStore();
150
+ const workflowTopic = store.get('workflowTopic');
151
+ const namespace = store.get('namespace');
152
+ return await WorkerService.getHotMesh(workflowTopic, { namespace });
153
+ }
154
+
145
155
  /**
146
156
  * those methods that may only be called once must be protected by flagging
147
157
  * their execution with a unique key (the key is stored in the HASH alongside
@@ -169,8 +179,11 @@ export class WorkflowService {
169
179
  */
170
180
  static async signal(signalId: string, data: Record<any, any>): Promise<string> {
171
181
  const store = asyncLocalStorage.getStore();
182
+ const workflowTopic = store.get('workflowTopic');
172
183
  const namespace = store.get('namespace');
173
- const hotMeshClient = await WorkerService.getHotMesh(`${namespace}.wfs.signal`, { namespace });
184
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
185
+ //todo: this particular one is better patterned as a get/set,
186
+ //since the receipt is a meaningful string (the stream id)
174
187
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
175
188
  return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
176
189
  }
@@ -183,8 +196,9 @@ export class WorkflowService {
183
196
  */
184
197
  static async hook(options: HookOptions): Promise<string> {
185
198
  const store = asyncLocalStorage.getStore();
199
+ const workflowTopic = store.get('workflowTopic');
186
200
  const namespace = store.get('namespace');
187
- const hotMeshClient = await WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
201
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
188
202
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
189
203
  const store = asyncLocalStorage.getStore();
190
204
  const workflowId = options.workflowId ?? store.get('workflowId');
@@ -212,7 +226,7 @@ export class WorkflowService {
212
226
  const workflowTopic = store.get('workflowTopic');
213
227
  const workflowDimension = store.get('workflowDimension') ?? '';
214
228
  const namespace = store.get('namespace');
215
- const sleepJobId = `${workflowId}-$sleep${workflowDimension}-${execIndex}`;
229
+ const sleepJobId = `-${workflowId}-$sleep${workflowDimension}-${execIndex}`;
216
230
 
217
231
  try {
218
232
  const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
@@ -220,9 +234,8 @@ export class WorkflowService {
220
234
  //if no error is thrown, we've already slept, return the delay
221
235
  return seconds;
222
236
  } catch (e) {
223
- //if an error, the sleep job was not found...rethrow error; sleep job
224
- // will be automatically created according to the DAG rules (they
225
237
  // spawn a new sleep job if error code 595 is thrown by the worker)
238
+ // NOTE: If this message shows up in your stack trace, you forgot to await `Durable.workflow.sleep()` in your workflow code.
226
239
  throw new DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
227
240
  }
228
241
  }
@@ -242,7 +255,7 @@ export class WorkflowService {
242
255
  const signalResults: any[] = [];
243
256
  for (const signal of signals) {
244
257
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
245
- const wfsJobId = `${workflowId}-$wfs${workflowDimension}-${execIndex}`;
258
+ const wfsJobId = `-${workflowId}-$wfs${workflowDimension}-${execIndex}`;
246
259
  try {
247
260
  if (allAreComplete) {
248
261
  const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
@@ -289,7 +302,7 @@ export class WorkflowService {
289
302
  const spn = store.get('workflowSpan');
290
303
  const namespace = store.get('namespace');
291
304
  const activityTopic = `${workflowTopic}-activity`;
292
- const activityJobId = `${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
305
+ const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
293
306
 
294
307
  let activityState: JobOutput
295
308
  try {
@@ -61,7 +61,7 @@ class StreamSignaler {
61
61
  try {
62
62
  await this.store.xgroup('CREATE', stream, group, '$', 'MKSTREAM');
63
63
  } catch (err) {
64
- this.logger.info('consumer-group-exists', { stream, group });
64
+ this.logger.debug('consumer-group-exists', { stream, group });
65
65
  }
66
66
  }
67
67
 
@@ -150,7 +150,6 @@ class StreamSignaler {
150
150
  try {
151
151
  output = await callback(input);
152
152
  } catch (error) {
153
- console.error(error);
154
153
  this.logger.error(`stream-call-function-error`, { error });
155
154
  output = this.structureUnhandledError(input, error);
156
155
  }
@@ -5,7 +5,6 @@ import { Cache } from '../cache';
5
5
  import { StoreService } from '../index';
6
6
  import { RedisClientType, RedisMultiType } from '../../../types/ioredisclient';
7
7
  import { ReclaimedMessageType } from '../../../types/stream';
8
- import { type } from 'os';
9
8
 
10
9
  class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
11
10
  redisClient: RedisClientType;
@@ -60,14 +59,14 @@ class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType>
60
59
  try {
61
60
  return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
62
61
  } catch (err) {
63
- this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
62
+ this.logger.debug(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
64
63
  throw err;
65
64
  }
66
65
  } else {
67
66
  try {
68
67
  return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
69
68
  } catch (err) {
70
- this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
69
+ this.logger.debug(`Consumer group not created for key: ${key} and group: ${groupName}`);
71
70
  throw err;
72
71
  }
73
72
  }
@@ -46,8 +46,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
46
46
  }
47
47
 
48
48
  getMulti(): RedisMultiType {
49
- const multi = this.redisClient.MULTI();
50
- return multi as unknown as RedisMultiType;
49
+ return this.redisClient.multi() as unknown as RedisMultiType;
51
50
  }
52
51
 
53
52
  async exec(...args: any[]): Promise<string|string[]|string[][]> {
@@ -86,7 +85,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
86
85
  return (await this.redisClient.sendCommand(['XGROUP', 'CREATE', key, groupName, id, ...args])) === 1;
87
86
  } catch (error) {
88
87
  const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
89
- this.logger.info(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
88
+ this.logger.debug(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
90
89
  throw error;
91
90
  }
92
91
  }
@@ -21,7 +21,7 @@ class RedisStreamService extends StreamService<RedisClientType, RedisMultiType>
21
21
  }
22
22
 
23
23
  getMulti(): RedisMultiType {
24
- return this.redisClient.MULTI() as unknown as RedisMultiType;
24
+ return this.redisClient.multi() as unknown as RedisMultiType;
25
25
  }
26
26
 
27
27
  mintKey(type: KeyType, params: KeyStoreParams): string {
@@ -21,7 +21,7 @@ class RedisSubService extends SubService<RedisClientType, RedisMultiType> {
21
21
  }
22
22
 
23
23
  getMulti(): RedisMultiType {
24
- const multi = this.redisClient.MULTI();
24
+ const multi = this.redisClient.multi();
25
25
  return multi as unknown as RedisMultiType;
26
26
  }
27
27