@hotmeshio/hotmesh 0.0.16 → 0.0.18

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 (53) hide show
  1. package/build/modules/utils.d.ts +3 -0
  2. package/build/modules/utils.js +10 -1
  3. package/build/package.json +1 -1
  4. package/build/services/activities/activity.d.ts +4 -12
  5. package/build/services/activities/activity.js +19 -156
  6. package/build/services/activities/hook.d.ts +20 -0
  7. package/build/services/activities/hook.js +124 -0
  8. package/build/services/activities/index.d.ts +2 -0
  9. package/build/services/activities/index.js +2 -0
  10. package/build/services/activities/trigger.js +1 -1
  11. package/build/services/collator/index.js +0 -1
  12. package/build/services/compiler/deployer.d.ts +2 -0
  13. package/build/services/compiler/deployer.js +29 -2
  14. package/build/services/durable/client.d.ts +8 -1
  15. package/build/services/durable/client.js +47 -0
  16. package/build/services/durable/factory.js +88 -11
  17. package/build/services/durable/search.d.ts +15 -0
  18. package/build/services/durable/search.js +45 -0
  19. package/build/services/durable/workflow.d.ts +1 -0
  20. package/build/services/durable/workflow.js +36 -0
  21. package/build/services/engine/index.d.ts +7 -2
  22. package/build/services/engine/index.js +2 -1
  23. package/build/services/store/clients/ioredis.d.ts +1 -0
  24. package/build/services/store/clients/ioredis.js +12 -0
  25. package/build/services/store/clients/redis.d.ts +1 -0
  26. package/build/services/store/clients/redis.js +3 -0
  27. package/build/services/store/index.d.ts +1 -0
  28. package/build/services/telemetry/index.js +2 -1
  29. package/build/types/activity.d.ts +6 -3
  30. package/build/types/durable.d.ts +11 -1
  31. package/build/types/hook.d.ts +1 -0
  32. package/build/types/index.d.ts +2 -2
  33. package/modules/utils.ts +11 -0
  34. package/package.json +1 -1
  35. package/services/activities/activity.ts +20 -167
  36. package/services/activities/hook.ts +149 -0
  37. package/services/activities/index.ts +2 -0
  38. package/services/activities/trigger.ts +1 -1
  39. package/services/collator/index.ts +0 -1
  40. package/services/compiler/deployer.ts +32 -2
  41. package/services/durable/client.ts +51 -2
  42. package/services/durable/factory.ts +88 -11
  43. package/services/durable/search.ts +54 -0
  44. package/services/durable/workflow.ts +34 -1
  45. package/services/engine/index.ts +8 -4
  46. package/services/store/clients/ioredis.ts +13 -0
  47. package/services/store/clients/redis.ts +4 -0
  48. package/services/store/index.ts +1 -0
  49. package/services/telemetry/index.ts +2 -1
  50. package/types/activity.ts +7 -2
  51. package/types/durable.ts +9 -0
  52. package/types/hook.ts +1 -0
  53. package/types/index.ts +2 -0
@@ -1,4 +1,4 @@
1
- import { CollationError, GetStateError } from '../../modules/errors';
1
+ import { CollationError } from '../../modules/errors';
2
2
  import {
3
3
  formatISODate,
4
4
  getValueByPath,
@@ -9,7 +9,6 @@ import { ILogger } from '../logger';
9
9
  import { MapperService } from '../mapper';
10
10
  import { Pipe } from '../pipe';
11
11
  import { MDATA_SYMBOLS } from '../serializer';
12
- import { StoreSignaler } from '../signaler/store';
13
12
  import { StoreService } from '../store';
14
13
  import { TelemetryService } from '../telemetry';
15
14
  import {
@@ -30,7 +29,6 @@ import {
30
29
  StreamDataType,
31
30
  StreamStatus } from '../../types/stream';
32
31
  import { TransitionRule } from '../../types/transition';
33
- import { HookRule } from '../../types/hook';
34
32
 
35
33
  /**
36
34
  * The base class for all activities
@@ -48,7 +46,7 @@ class Activity {
48
46
  code: StreamCode = 200;
49
47
  leg: ActivityLeg;
50
48
  adjacencyList: StreamData[];
51
- adjacentIndex = 0; //can be updated by leg2 using 'as' metadata hincrby output
49
+ adjacentIndex = 0;
52
50
 
53
51
  constructor(
54
52
  config: ActivityType,
@@ -67,171 +65,21 @@ class Activity {
67
65
  this.store = engine.store;
68
66
  }
69
67
 
70
- //******** INITIAL ENTRY POINT (A) ********//
71
- async process(): Promise<string> {
72
- this.logger.debug('activity-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
73
- let telemetry: TelemetryService;
74
- try {
75
- this.setLeg(1);
76
- await CollatorService.notarizeEntry(this);
77
-
78
- await this.getState();
79
- telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
80
- telemetry.startActivitySpan(this.leg);
81
- let multiResponse: MultiResponseFlags;
82
-
83
- const multi = this.store.getMulti();
84
- if (this.doesHook()) {
85
- //sleep and wait to awaken upon a signal
86
- await this.registerHook(multi);
87
- this.mapOutputData();
88
- this.mapJobData();
89
- await this.setState(multi);
90
- await CollatorService.authorizeReentry(this, multi);
91
-
92
- await this.setStatus(0, multi);
93
- await multi.exec();
94
- telemetry.mapActivityAttributes();
95
- } else {
96
- //end the activity and transition to its children
97
- this.adjacencyList = await this.filterAdjacent();
98
- this.mapOutputData();
99
- this.mapJobData();
100
- await this.setState(multi);
101
- await CollatorService.notarizeEarlyCompletion(this, multi);
102
-
103
- await this.setStatus(this.adjacencyList.length - 1, multi);
104
- multiResponse = await multi.exec() as MultiResponseFlags;
105
- telemetry.mapActivityAttributes();
106
- const jobStatus = this.resolveStatus(multiResponse);
107
- const attrs: StringScalarType = { 'app.job.jss': jobStatus };
108
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
109
- if (messageIds.length) {
110
- attrs['app.activity.mids'] = messageIds.join(',')
111
- }
112
- telemetry.setActivityAttributes(attrs);
113
- }
114
-
115
- return this.context.metadata.aid;
116
- } catch (error) {
117
- if (error instanceof GetStateError) {
118
- this.logger.error('activity-get-state-error', { error });
119
- } else {
120
- this.logger.error('activity-process-error', { error });
121
- }
122
- telemetry.setActivityError(error.message);
123
- throw error;
124
- } finally {
125
- telemetry.endActivitySpan();
126
- this.logger.debug('activity-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
127
- }
128
- }
129
-
130
68
  setLeg(leg: ActivityLeg): void {
131
69
  this.leg = leg;
132
70
  }
133
71
 
134
- //******** SIGNAL RE-ENTRY POINT ********//
135
- doesHook(): boolean {
136
- return !!(this.config.hook?.topic || this.config.sleep);
137
- }
138
-
139
- async getHookRule(topic: string): Promise<HookRule | undefined> {
140
- const rules = await this.store.getHookRules();
141
- return rules?.[topic]?.[0] as HookRule;
142
- }
143
-
144
- async registerHook(multi?: RedisMulti): Promise<string | void> {
145
- if (this.config.hook?.topic) {
146
- const signaler = new StoreSignaler(this.store, this.logger);
147
- return await signaler.registerWebHook(this.config.hook.topic, this.context, multi);
148
- } else if (this.config.sleep) {
149
- const durationInSeconds = Pipe.resolve(this.config.sleep, this.context);
150
- const jobId = this.context.metadata.jid;
151
- const activityId = this.metadata.aid;
152
- const dId = this.metadata.dad;
153
- await this.engine.task.registerTimeHook(jobId, `${activityId}${dId||''}`, 'sleep', durationInSeconds);
154
- return jobId;
155
- }
156
- }
157
-
158
- async processWebHookEvent(): Promise<JobStatus | void> {
159
- this.logger.debug('engine-process-web-hook-event', {
160
- topic: this.config.hook.topic,
161
- aid: this.metadata.aid
162
- });
163
- const signaler = new StoreSignaler(this.store, this.logger);
164
- const data = { ...this.data };
165
- const jobId = await signaler.processWebHookSignal(this.config.hook.topic, data);
166
- if (jobId) {
167
- await this.processHookEvent(jobId);
168
- await signaler.deleteWebHookSignal(this.config.hook.topic, data);
169
- } //else => already resolved
170
- }
171
-
172
- async processTimeHookEvent(jobId: string): Promise<JobStatus | void> {
173
- this.logger.debug('engine-process-time-hook-event', {
174
- jid: jobId,
175
- aid: this.metadata.aid
176
- });
177
- return await this.processHookEvent(jobId);
178
- }
179
-
180
- async processHookEvent(jobId: string): Promise<JobStatus | void> {
181
- this.logger.debug('activity-process-hook-event', { jobId });
182
- let telemetry: TelemetryService;
183
- try {
184
- this.setLeg(2);
185
- await this.getState(jobId);
186
- telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
187
- telemetry.startActivitySpan(this.leg);
188
- const aState = await CollatorService.notarizeReentry(this);
189
- this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
190
-
191
- this.bindActivityData('hook');
192
- this.mapJobData();
193
- this.adjacencyList = await this.filterAdjacent();
194
-
195
- const multi = this.engine.store.getMulti();
196
- await this.setState(multi);
197
- await CollatorService.notarizeCompletion(this, multi);
198
-
199
- await this.setStatus(this.adjacencyList.length - 1, multi);
200
- const multiResponse = await multi.exec() as MultiResponseFlags;
201
-
202
- telemetry.mapActivityAttributes();
203
- const jobStatus = this.resolveStatus(multiResponse);
204
- const attrs: StringScalarType = { 'app.job.jss': jobStatus };
205
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
206
- if (messageIds.length) {
207
- attrs['app.activity.mids'] = messageIds.join(',')
208
- }
209
- telemetry.setActivityAttributes(attrs);
210
- return jobStatus as number;
211
- } catch (error) {
212
- if (error instanceof CollationError && error.fault === 'inactive') {
213
- this.logger.info('process-hook-event-inactive-error', { error });
214
- return;
215
- }
216
- this.logger.error('engine-process-hook-event-error', { error });
217
- telemetry.setActivityError(error.message);
218
- throw error;
219
- } finally {
220
- telemetry.endActivitySpan();
221
- }
222
- }
223
-
224
72
  //******** DUPLEX RE-ENTRY POINT ********//
225
- async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<void> {
73
+ async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output', jobId?: string): Promise<void> {
226
74
  this.setLeg(2);
227
- const jid = this.context.metadata.jid;
75
+ const jid = this.context.metadata.jid || jobId;
228
76
  const aid = this.metadata.aid;
229
77
  this.status = status;
230
78
  this.code = code;
231
79
  this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
232
80
  let telemetry: TelemetryService;
233
81
  try {
234
- await this.getState();
82
+ await this.getState(jobId);
235
83
  const aState = await CollatorService.notarizeReentry(this);
236
84
  this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
237
85
 
@@ -247,18 +95,19 @@ class Activity {
247
95
  telemetry.startActivitySpan(this.leg);
248
96
  let multiResponse: MultiResponseFlags;
249
97
  if (status === StreamStatus.PENDING) {
250
- multiResponse = await this.processPending(telemetry);
98
+ multiResponse = await this.processPending(telemetry, type);
251
99
  } else if (status === StreamStatus.SUCCESS) {
252
- multiResponse = await this.processSuccess(telemetry);
100
+ multiResponse = await this.processSuccess(telemetry, type);
253
101
  } else {
254
- multiResponse = await this.processError(telemetry);
102
+ multiResponse = await this.processError(telemetry, type);
255
103
  }
256
104
  this.transitionAdjacent(multiResponse, telemetry);
257
105
  } catch (error) {
258
- if (error instanceof CollationError && error.fault === 'inactive') {
106
+ if (error instanceof CollationError) {
259
107
  this.logger.info('process-event-inactive-error', { error });
260
108
  return;
261
109
  }
110
+ console.error(error);
262
111
  this.logger.error('activity-process-event-error', { error });
263
112
  telemetry && telemetry.setActivityError(error.message);
264
113
  throw error;
@@ -268,8 +117,8 @@ class Activity {
268
117
  }
269
118
  }
270
119
 
271
- async processPending(telemetry: TelemetryService): Promise<MultiResponseFlags> {
272
- this.bindActivityData('output');
120
+ async processPending(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags> {
121
+ this.bindActivityData(type);
273
122
  this.adjacencyList = await this.filterAdjacent();
274
123
  this.mapJobData();
275
124
  const multi = this.store.getMulti();
@@ -280,8 +129,8 @@ class Activity {
280
129
  return await multi.exec() as MultiResponseFlags;
281
130
  }
282
131
 
283
- async processSuccess(telemetry: TelemetryService): Promise<MultiResponseFlags> {
284
- this.bindActivityData('output');
132
+ async processSuccess(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags> {
133
+ this.bindActivityData(type);
285
134
  this.adjacencyList = await this.filterAdjacent();
286
135
  this.mapJobData();
287
136
  const multi = this.store.getMulti();
@@ -292,7 +141,7 @@ class Activity {
292
141
  return await multi.exec() as MultiResponseFlags;
293
142
  }
294
143
 
295
- async processError(telemetry: TelemetryService): Promise<MultiResponseFlags> {
144
+ async processError(telemetry: TelemetryService, type: string): Promise<MultiResponseFlags> {
296
145
  this.bindActivityError(this.data);
297
146
  this.adjacencyList = await this.filterAdjacent();
298
147
  const multi = this.store.getMulti();
@@ -584,7 +433,11 @@ class Activity {
584
433
 
585
434
  async transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]> {
586
435
  let mIds: string[] = [];
587
- if (jobStatus <= 0 || this.config.emit) {
436
+ let emit: boolean = false;
437
+ if (this.config.emit) {
438
+ emit = Pipe.resolve(this.config.emit, this.context);
439
+ }
440
+ if (jobStatus <= 0 || emit) {
588
441
  //activity should not send 'emit' if the job is truly over
589
442
  const isTrueEmit = jobStatus > 0;
590
443
  await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
@@ -0,0 +1,149 @@
1
+ import { CollationError, GetStateError } from '../../modules/errors';
2
+ import { CollatorService } from '../collator';
3
+ import { EngineService } from '../engine';
4
+ import { Pipe } from '../pipe';
5
+ import { StoreSignaler } from '../signaler/store';
6
+ import { TelemetryService } from '../telemetry';
7
+ import {
8
+ ActivityData,
9
+ ActivityMetadata,
10
+ ActivityType,
11
+ HookActivity} from '../../types/activity';
12
+ import { JobState, JobStatus } from '../../types/job';
13
+ import {
14
+ MultiResponseFlags,
15
+ RedisMulti } from '../../types/redis';
16
+ import { StringScalarType } from '../../types/serializer';
17
+ import { HookRule } from '../../types/hook';
18
+ import { Activity } from './activity';
19
+ import { StreamStatus } from '../../types';
20
+
21
+ /**
22
+ * Listens for `webhook`, `timehook`, and `cycle` (repeat) signals
23
+ */
24
+ class Hook extends Activity {
25
+ config: HookActivity;
26
+
27
+ constructor(
28
+ config: ActivityType,
29
+ data: ActivityData,
30
+ metadata: ActivityMetadata,
31
+ hook: ActivityData | null,
32
+ engine: EngineService,
33
+ context?: JobState) {
34
+ super(config, data, metadata, hook, engine, context);
35
+ }
36
+
37
+ //******** INITIAL ENTRY POINT (A) ********//
38
+ async process(): Promise<string> {
39
+ this.logger.debug('hook-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
40
+ let telemetry: TelemetryService;
41
+ try {
42
+ this.setLeg(1);
43
+ await CollatorService.notarizeEntry(this);
44
+
45
+ await this.getState();
46
+ telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
47
+ telemetry.startActivitySpan(this.leg);
48
+ let multiResponse: MultiResponseFlags;
49
+
50
+ const multi = this.store.getMulti();
51
+ if (this.doesHook()) {
52
+ //sleep and wait to awaken upon a signal
53
+ await this.registerHook(multi);
54
+ this.mapOutputData();
55
+ this.mapJobData();
56
+ await this.setState(multi);
57
+ await CollatorService.authorizeReentry(this, multi);
58
+
59
+ await this.setStatus(0, multi);
60
+ await multi.exec();
61
+ telemetry.mapActivityAttributes();
62
+ } else {
63
+ //end the activity and transition to its children
64
+ this.adjacencyList = await this.filterAdjacent();
65
+ this.mapOutputData();
66
+ this.mapJobData();
67
+ await this.setState(multi);
68
+ await CollatorService.notarizeEarlyCompletion(this, multi);
69
+
70
+ await this.setStatus(this.adjacencyList.length - 1, multi);
71
+ multiResponse = await multi.exec() as MultiResponseFlags;
72
+ telemetry.mapActivityAttributes();
73
+ const jobStatus = this.resolveStatus(multiResponse);
74
+ const attrs: StringScalarType = { 'app.job.jss': jobStatus };
75
+ const messageIds = await this.transition(this.adjacencyList, jobStatus);
76
+ if (messageIds.length) {
77
+ attrs['app.activity.mids'] = messageIds.join(',')
78
+ }
79
+ telemetry.setActivityAttributes(attrs);
80
+ }
81
+
82
+ return this.context.metadata.aid;
83
+ } catch (error) {
84
+ if (error instanceof GetStateError) {
85
+ this.logger.error('hook-get-state-error', { error });
86
+ } else {
87
+ this.logger.error('hook-process-error', { error });
88
+ }
89
+ telemetry.setActivityError(error.message);
90
+ throw error;
91
+ } finally {
92
+ telemetry.endActivitySpan();
93
+ this.logger.debug('hook-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
94
+ }
95
+ }
96
+
97
+ //******** SIGNAL RE-ENTRY POINT ********//
98
+ doesHook(): boolean {
99
+ //does this activity use a time-hook or web-hook
100
+ return !!(this.config.hook?.topic || this.config.sleep);
101
+ }
102
+
103
+ async getHookRule(topic: string): Promise<HookRule | undefined> {
104
+ const rules = await this.store.getHookRules();
105
+ return rules?.[topic]?.[0] as HookRule;
106
+ }
107
+
108
+ async registerHook(multi?: RedisMulti): Promise<string | void> {
109
+ if (this.config.hook?.topic) {
110
+ const signaler = new StoreSignaler(this.store, this.logger);
111
+ return await signaler.registerWebHook(this.config.hook.topic, this.context, multi);
112
+ } else if (this.config.sleep) {
113
+ const durationInSeconds = Pipe.resolve(this.config.sleep, this.context);
114
+ const jobId = this.context.metadata.jid;
115
+ const activityId = this.metadata.aid;
116
+ const dId = this.metadata.dad;
117
+ await this.engine.task.registerTimeHook(jobId, `${activityId}${dId||''}`, 'sleep', durationInSeconds);
118
+ return jobId;
119
+ }
120
+ }
121
+
122
+ async processWebHookEvent(): Promise<JobStatus | void> {
123
+ this.logger.debug('hook-process-web-hook-event', {
124
+ topic: this.config.hook.topic,
125
+ aid: this.metadata.aid
126
+ });
127
+ const signaler = new StoreSignaler(this.store, this.logger);
128
+ const data = { ...this.data };
129
+ const jobId = await signaler.processWebHookSignal(this.config.hook.topic, data);
130
+ if (jobId) {
131
+ //if a webhook signal is sent that includes 'keep_alive' the hook will remain open
132
+ const code = data.keep_alive ? 202 : 200;
133
+ await this.processEvent(StreamStatus.SUCCESS, code, 'hook', jobId);
134
+ if (code === 200) {
135
+ await signaler.deleteWebHookSignal(this.config.hook.topic, data);
136
+ }
137
+ } //else => already resolved
138
+ }
139
+
140
+ async processTimeHookEvent(jobId: string): Promise<JobStatus | void> {
141
+ this.logger.debug('hook-process-time-hook-event', {
142
+ jid: jobId,
143
+ aid: this.metadata.aid
144
+ });
145
+ await this.processEvent(StreamStatus.SUCCESS, 200, 'hook');
146
+ }
147
+ }
148
+
149
+ export { Hook };
@@ -1,6 +1,7 @@
1
1
  import { Activity } from './activity';
2
2
  import { Await } from './await';
3
3
  import { Cycle } from './cycle';
4
+ import { Hook } from './hook';
4
5
  import { Iterate } from './iterate';
5
6
  import { Signal } from './signal';
6
7
  import { Trigger } from './trigger';
@@ -10,6 +11,7 @@ export default {
10
11
  activity: Activity,
11
12
  await: Await,
12
13
  cycle: Cycle,
14
+ hook: Hook,
13
15
  iterate: Iterate,
14
16
  signal: Signal,
15
17
  trigger: Trigger,
@@ -178,7 +178,7 @@ class Trigger extends Activity {
178
178
 
179
179
  async setStats(multi?: RedisMulti): Promise<void> {
180
180
  const md = this.context.metadata;
181
- if (this.config.stats?.measures) {
181
+ if (md.key && this.config.stats?.measures) {
182
182
  const config = await this.engine.getVID();
183
183
  const reporter = new ReporterService(config, this.store, this.logger);
184
184
  await this.store.setStats(
@@ -54,7 +54,6 @@ class CollatorService {
54
54
  //set second digit to 8, allowing for re-entry
55
55
  //decrement by -10_000_000_000_000
56
56
  const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10_000_000_000_000, this.getDimensionalAddress(activity), multi);
57
- //this.verifyInteger(amount, 1, 'exit');
58
57
  return amount;
59
58
  }
60
59
 
@@ -25,10 +25,12 @@ class Deployer {
25
25
  async deploy(store: StoreService<RedisClient, RedisMulti>) {
26
26
  this.store = store;
27
27
  CollatorService.compile(this.manifest.app.graphs);
28
+ this.convertActivitiesToHooks();
28
29
  this.convertTopicsToTypes();
29
30
  this.copyJobSchemas();
30
31
  this.bindBackRefs();
31
32
  this.bindParents();
33
+ this.bindCycleTarget();
32
34
  this.resolveMappingDependencies();
33
35
  this.resolveJobMapsPaths();
34
36
  await this.generateSymKeys();
@@ -148,8 +150,22 @@ class Deployer {
148
150
  }
149
151
  }
150
152
 
151
- //more intuitive for SDK users to use 'topic',
152
- //but the compiler is desiged to be generic and uses 'subtypes'
153
+ //the cycle/goto activity includes and ancestor target;
154
+ //update with the cycle flag, so it can be rerun
155
+ bindCycleTarget() {
156
+ for (const graph of this.manifest!.app.graphs) {
157
+ const activities = graph.activities;
158
+ for (const activityKey in activities) {
159
+ const activity = activities[activityKey];
160
+ if (activity.type === 'cycle') {
161
+ activities[activity.ancestor].cycle = true;
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ //it's more intuitive for SDK users to use 'topic',
168
+ //but the compiler is desiged to be generic and uses the attribute, 'subtypes'
153
169
  convertTopicsToTypes() {
154
170
  for (const graph of this.manifest!.app.graphs) {
155
171
  const activities = graph.activities;
@@ -162,6 +178,20 @@ class Deployer {
162
178
  }
163
179
  }
164
180
 
181
+ //legacy; remove at beta (assume no legacy refs to 'activity' at that point)
182
+ convertActivitiesToHooks() {
183
+ for (const graph of this.manifest!.app.graphs) {
184
+ const activities = graph.activities;
185
+ for (const activityKey in activities) {
186
+ const activity = activities[activityKey];
187
+ if (['activity'].includes(activity.type)) {
188
+ activity.type = 'hook';
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+
165
195
  async bindParents() {
166
196
  const graphs = this.manifest.app.graphs;
167
197
  for (const graph of graphs) {
@@ -5,9 +5,10 @@ import { HotMeshService as HotMesh } from '../hotmesh';
5
5
  import {
6
6
  ClientConfig,
7
7
  Connection,
8
- WorkflowOptions } from '../../types/durable';
8
+ WorkflowOptions,
9
+ WorkflowSearchOptions} from '../../types/durable';
9
10
  import { JobState } from '../../types/job';
10
- import { KeyType } from '../../modules/key';
11
+ import { KeyService, KeyType } from '../../modules/key';
11
12
 
12
13
  /*
13
14
  Here is an example of how the methods in this file are used:
@@ -90,6 +91,41 @@ export class ClientService {
90
91
  return hotMeshClient;
91
92
  }
92
93
 
94
+ /**
95
+ * For those deployments with a redis stack backend (with the FT module),
96
+ * this method will configure the search index for the workflow.
97
+ */
98
+ configureSearchIndex = async (hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void> => {
99
+ if (search) {
100
+ const store = hotMeshClient.engine.store;
101
+ const schema: string[] = [];
102
+ for (const [key, value] of Object.entries(search.schema)) {
103
+ //prefix with a comma (avoids collisions with hotmesh reserved words)
104
+ schema.push(`_${key}`);
105
+ schema.push(value.type);
106
+ if (value.sortable) {
107
+ schema.push('SORTABLE');
108
+ }
109
+ }
110
+ try {
111
+ const keyParams = {
112
+ appId: hotMeshClient.appId,
113
+ jobId: ''
114
+ }
115
+ const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
116
+ const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
117
+ await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
118
+ } catch (err) {
119
+ hotMeshClient.engine.logger.info('durable-client-search-err', { err });
120
+ }
121
+ }
122
+ }
123
+
124
+ search = async (hotMeshClient: HotMesh, index: string, query: string[]): Promise<string[]> => {
125
+ const store = hotMeshClient.engine.store;
126
+ return await store.exec('FT.SEARCH', index, ...query) as string[];
127
+ }
128
+
93
129
  workflow = {
94
130
  start: async (options: WorkflowOptions): Promise<WorkflowHandleService> => {
95
131
  const taskQueueName = options.taskQueue;
@@ -99,8 +135,10 @@ export class ClientService {
99
135
  //topic is concat of taskQueue and workflowName
100
136
  const workflowTopic = `${taskQueueName}-${workflowName}`;
101
137
  const hotMeshClient = await this.getHotMeshClient(workflowTopic);
138
+ this.configureSearchIndex(hotMeshClient, options.search)
102
139
  const payload = {
103
140
  arguments: [...options.args],
141
+ parentWorkflowId: options.parentWorkflowId,
104
142
  workflowId: options.workflowId || nanoid(),
105
143
  workflowTopic: workflowTopic,
106
144
  backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
@@ -121,6 +159,17 @@ export class ClientService {
121
159
  const workflowTopic = `${taskQueue}-${workflowName}`;
122
160
  const hotMeshClient = await this.getHotMeshClient(workflowTopic);
123
161
  return new WorkflowHandleService(hotMeshClient, workflowTopic, workflowId);
162
+ },
163
+
164
+ search: async (taskQueue: string, workflowName: string, index: string, ...query: string[]): Promise<string[]> => {
165
+ const workflowTopic = `${taskQueue}-${workflowName}`;
166
+ const hotMeshClient = await this.getHotMeshClient(workflowTopic);
167
+ try {
168
+ return await this.search(hotMeshClient, index, query);
169
+ } catch (err) {
170
+ hotMeshClient.engine.logger.error('durable-client-search-err', { err });
171
+ throw err;
172
+ }
124
173
  }
125
174
  }
126
175