@hotmeshio/hotmesh 0.0.43 → 0.0.44

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 (46) hide show
  1. package/build/package.json +1 -1
  2. package/build/services/activities/trigger.js +7 -1
  3. package/build/services/durable/exporter.d.ts +105 -0
  4. package/build/services/durable/exporter.js +374 -0
  5. package/build/services/durable/factory.js +6 -63
  6. package/build/services/durable/handle.d.ts +4 -0
  7. package/build/services/durable/handle.js +5 -0
  8. package/build/services/durable/workflow.js +24 -21
  9. package/build/services/engine/index.d.ts +6 -1
  10. package/build/services/engine/index.js +9 -2
  11. package/build/services/exporter/index.d.ts +46 -0
  12. package/build/services/exporter/index.js +126 -0
  13. package/build/services/hotmesh/index.d.ts +4 -1
  14. package/build/services/hotmesh/index.js +6 -0
  15. package/build/services/quorum/index.js +2 -1
  16. package/build/services/router/index.d.ts +3 -0
  17. package/build/services/router/index.js +3 -0
  18. package/build/services/store/index.d.ts +5 -2
  19. package/build/services/store/index.js +54 -6
  20. package/build/services/task/index.js +5 -1
  21. package/build/services/worker/index.js +5 -4
  22. package/build/types/activity.d.ts +6 -1
  23. package/build/types/exporter.d.ts +51 -0
  24. package/build/types/exporter.js +8 -0
  25. package/build/types/index.d.ts +1 -0
  26. package/build/types/quorum.d.ts +1 -0
  27. package/build/types/task.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/services/activities/trigger.ts +14 -0
  30. package/services/durable/exporter.ts +408 -0
  31. package/services/durable/factory.ts +6 -63
  32. package/services/durable/handle.ts +12 -0
  33. package/services/durable/workflow.ts +24 -22
  34. package/services/engine/index.ts +20 -5
  35. package/services/exporter/index.ts +147 -0
  36. package/services/hotmesh/index.ts +8 -1
  37. package/services/quorum/index.ts +2 -1
  38. package/services/router/index.ts +3 -0
  39. package/services/store/index.ts +56 -7
  40. package/services/task/index.ts +4 -1
  41. package/services/worker/index.ts +6 -5
  42. package/types/activity.ts +6 -1
  43. package/types/exporter.ts +61 -0
  44. package/types/index.ts +13 -1
  45. package/types/quorum.ts +1 -0
  46. package/types/task.ts +1 -1
@@ -4,6 +4,7 @@ export { AsyncSignal } from './async';
4
4
  export { CacheMode } from './cache';
5
5
  export { CollationFaultType, CollationStage } from './collator';
6
6
  export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowContext, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
7
+ export { ActivityAction, DependencyExport, DurableJobExport, ExportCycles, ExportItem, ExportOptions, ExportTransitions, JobAction, JobExport, JobActionExport, JobTimeline } from './exporter';
7
8
  export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
8
9
  export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
9
10
  export { ILogger } from './logger';
@@ -6,6 +6,7 @@ export interface QuorumProfile {
6
6
  worker_topic?: string;
7
7
  stream?: string;
8
8
  stream_depth?: number;
9
+ counts?: Record<string, number>;
9
10
  }
10
11
  export interface PingMessage {
11
12
  type: 'ping';
@@ -1 +1 @@
1
- export type WorkListTaskType = 'sleep' | 'expire' | 'interrupt' | 'delist';
1
+ export type WorkListTaskType = 'sleep' | 'expire' | 'expire-child' | 'interrupt' | 'delist' | 'child';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.43",
3
+ "version": "0.0.44",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -186,11 +186,15 @@ class Trigger extends Activity {
186
186
  async registerJobDependency(multi?: RedisMulti): Promise<void> {
187
187
  const depKey = this.config.stats?.parent ?? this.context.metadata.pj;
188
188
  let resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
189
+ const adjKey = this.config.stats?.adjacent;
190
+ let resolvedAdjKey = depKey ? Pipe.resolve(adjKey, this.context) : '';
189
191
  if (!resolvedDepKey) {
190
192
  resolvedDepKey = this.context.metadata.pj;
191
193
  }
192
194
  if (resolvedDepKey) {
195
+ const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
193
196
  await this.store.registerJobDependency(
197
+ isParentOrigin ? 'expire-child' : 'expire',
194
198
  resolvedDepKey,
195
199
  this.context.metadata.tpc,
196
200
  this.context.metadata.jid,
@@ -198,6 +202,16 @@ class Trigger extends Activity {
198
202
  multi,
199
203
  );
200
204
  }
205
+ if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
206
+ await this.store.registerJobDependency(
207
+ 'child',
208
+ resolvedAdjKey,
209
+ this.context.metadata.tpc,
210
+ this.context.metadata.jid,
211
+ this.context.metadata.gid,
212
+ multi,
213
+ );
214
+ }
201
215
  }
202
216
 
203
217
  async setStats(multi?: RedisMulti): Promise<void> {
@@ -0,0 +1,408 @@
1
+ import { ILogger } from '../logger';
2
+ import { StoreService } from '../store';
3
+ import { StringStringType, Symbols } from "../../types";
4
+ import { RedisClient, RedisMulti } from '../../types/redis';
5
+ import {
6
+ ActivityAction,
7
+ DependencyExport,
8
+ ExportItem,
9
+ ExportOptions,
10
+ JobAction,
11
+ JobActionExport,
12
+ DurableJobExport,
13
+ JobTimeline } from '../../types/exporter';
14
+ import { SerializerService } from '../serializer';
15
+ import { restoreHierarchy } from '../../modules/utils';
16
+
17
+ /**
18
+ * Downloads job data from Redis (hscan, hmget, hgetall)
19
+ * Splits, Inflates, and Sorts the job data for use in durable contexts
20
+ */
21
+ class ExporterService {
22
+ appId: string;
23
+ logger: ILogger;
24
+ serializer: SerializerService
25
+ store: StoreService<RedisClient, RedisMulti>;
26
+ symbols: Promise<Symbols> | Symbols;
27
+
28
+ /**
29
+ * Friendly names for the activity ids
30
+ */
31
+ activitySymbols: Symbols = {
32
+ t1: 'trigger',
33
+ a1: 'pivot',
34
+ w1: 'worker',
35
+ a592: 'sleeper',
36
+ a594: 'awaiter',
37
+ a599: 'retryer',
38
+ c592: 'sleep_cycler',
39
+ c594: 'await_cycler',
40
+ c599: 'retry_cycler',
41
+ s5: 'scrubber',
42
+ sig: 'hook',
43
+ siga1: 'hook_pivot',
44
+ sigw1: 'hook_worker',
45
+ siga592: 'hook_sleeper',
46
+ siga594: 'hook_awaiter',
47
+ siga599: 'hook_retryer',
48
+ sigc592: 'hook_sleep_cycler',
49
+ sigc594: 'hook_await_cycler',
50
+ sigc599: 'hook_retry_cycler',
51
+ }
52
+
53
+ //adjacent transitions
54
+ transitions = {
55
+ trigger: ['pivot', 'hook'],
56
+ pivot: ['worker'],
57
+ worker: ['sleeper', 'awaiter', 'retryer', 'scrubber'],
58
+ sleeper: ['sleep_cycler'],
59
+ awaiter: ['await_cycler'],
60
+ retryer: ['retry_cycler'],
61
+ hook: ['hook_pivot'],
62
+ hook_pivot: ['hook_worker'],
63
+ hook_worker: ['hook_sleeper', 'hook_awaiter', 'hook_retryer'],
64
+ hook_sleeper: ['hook_sleep_cycler'],
65
+ hook_awaiter: ['hook_await_cycler'],
66
+ hook_retryer: ['hook_retry_cycler'],
67
+ };
68
+
69
+ //goto transitions
70
+ cycles = {
71
+ sleep_cycler: ['pivot'],
72
+ await_cycler: ['pivot'],
73
+ retry_cycler: ['pivot'],
74
+ hook_sleep_cycler: ['hook_pivot'],
75
+ hook_await_cycler: ['hook_pivot'],
76
+ hook_retry_cycler: ['hook_pivot'],
77
+ }
78
+
79
+ constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
80
+ this.appId = appId;
81
+ this.logger = logger;
82
+ this.store = store;
83
+ this.serializer = new SerializerService();
84
+ }
85
+
86
+ /**
87
+ * Convert the job hash and dependency list into a DurableJobExport object.
88
+ * This object contains various facets that describe the interaction
89
+ * in terms relevant to narrative storytelling.
90
+ */
91
+ async export(jobId: string, options: ExportOptions = {}): Promise<DurableJobExport> {
92
+ if (!this.symbols) {
93
+ this.symbols = this.store.getAllSymbols();
94
+ this.symbols = await this.symbols;
95
+ }
96
+ const depData = await this.store.getDependencies(jobId);
97
+ const jobData = await this.store.getRaw(jobId);
98
+ const jobExport = this.inflate(jobData, depData);
99
+ return jobExport;
100
+ }
101
+
102
+ /**
103
+ * Interleave actions into the replay timeline to create
104
+ * a time-ordered timeline of the entire interaction, beginning
105
+ * with the entry trigger and concluding with the scrubber
106
+ * activity. Using the returned timeline, it is possible to
107
+ * create an animated narrative of the job, highlighting
108
+ * activities in the graph according to the timeline's
109
+ * activity-created (/ac) and activity-updated (/au) entries.
110
+ */
111
+ createTimeline(replay: ExportItem[], actions: JobActionExport): JobTimeline[] {
112
+ const timeline: JobTimeline[] = [];
113
+ replay.forEach((item) => {
114
+ const dimensions = item[0];
115
+ const parts = dimensions.split('/');
116
+ const activityName = item[1].split('/')[0];
117
+ const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
118
+ const timestamp = item[2];
119
+ const event: JobTimeline = {
120
+ activity: activityName,
121
+ duplex: duplex as 'entry' | 'exit',
122
+ dimension: dimensions,
123
+ timestamp,
124
+ };
125
+ timeline.push(event);
126
+
127
+ if (this.isMainEntry(item[1])) {
128
+ event.actions = [] as ActivityAction[];
129
+ this.interleaveActions(actions.main, event.actions);
130
+ } else if (this.isHookEntry(item[1])) {
131
+ const hookDimension = `/${parts[1]}/${parts[2]}`;
132
+ const hookActions = actions.hooks[hookDimension];
133
+ event.actions = [] as ActivityAction[];
134
+ this.interleaveActions(hookActions, event.actions);
135
+ }
136
+ });
137
+ return timeline;
138
+ }
139
+
140
+ /**
141
+ * Interleave actions into the 'worker' and 'hook_worker'
142
+ * activities (between their /ac and /au entries)
143
+ */
144
+ interleaveActions(target: JobAction, actions: ActivityAction[]) {
145
+ if (target) {
146
+ for (let i = target.cursor + 1; i < target.items.length; i++) {
147
+ const [_, actionType, jobOrIndex] = target.items[i];
148
+ actions.push({ action: actionType, target: jobOrIndex });
149
+ target.cursor = i;
150
+ if (this.isPausingAction(actionType)) {
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ isPausingAction(actionType: string): boolean {
158
+ return actionType === 'sleep' || actionType === 'waitForSignal';
159
+ }
160
+
161
+ isMainEntry(key: string): boolean {
162
+ return key.startsWith('worker/') && key.endsWith('/ac');
163
+ }
164
+
165
+ isHookEntry(key: string): boolean {
166
+ return key.startsWith('hook_worker/') && key.endsWith('/ac');
167
+ }
168
+
169
+ /**
170
+ * Inflates the key from Redis, 3-character symbol
171
+ * into a human-readable JSON path, reflecting the
172
+ * tree-like structure of the unidimensional Hash
173
+ */
174
+ inflateKey(key: string): string {
175
+ if (key in this.symbols) {
176
+ const path = this.symbols[key];
177
+ const parts = path.split('/');
178
+ if (parts[0] in this.activitySymbols) {
179
+ parts[0] = this.activitySymbols[parts[0]];
180
+ }
181
+ return parts.join('/');
182
+ }
183
+ return key;
184
+ }
185
+
186
+ /**
187
+ * Inflates the dependency data from Redis into a DurableJobExport object by
188
+ * organizing the dimensional isolate in sch a way asto interleave
189
+ * into a story
190
+ * @param data - the dependency data from Redis
191
+ * @returns - the organized dependency data
192
+ */
193
+ inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
194
+ //console.log('dependency data>', data);
195
+ const hookReg = /([0-9,]+)-(\d+)$/;
196
+ const flowReg = /-(\d+)$/;
197
+ return data.map((dependency, index: number): DependencyExport => {
198
+ const [action, topic, gid, ...jid] = dependency.split('::');
199
+ const jobId = jid.join('::');
200
+ const match = jobId.match(hookReg);
201
+ let prefix: string;
202
+ let type: 'hook' | 'flow' | 'other';
203
+ let dimensionKey: string = '';
204
+
205
+ if (match) {
206
+ //hook-originating dependency
207
+ const [_, dimension, counter] = match;
208
+ dimensionKey = dimension.split(',').join('/');
209
+ prefix = `${dimensionKey}[${counter}]`;
210
+ type = 'hook';
211
+ } else {
212
+ const match = jobId.match(flowReg);
213
+ if (match) {
214
+ //main workflow-originating dependency
215
+ const [_, counter] = match;
216
+ prefix = `[${counter}]`;
217
+ type = 'flow';
218
+ } else {
219
+ //'other' types like signal cleanup
220
+ prefix = '/';
221
+ type = 'other';
222
+ }
223
+ }
224
+ this.seedActions(
225
+ type,
226
+ action,
227
+ topic,
228
+ dependency,
229
+ prefix,
230
+ dimensionKey,
231
+ actions,
232
+ jobId,
233
+ );
234
+ return {
235
+ type: action,
236
+ topic,
237
+ gid,
238
+ jid: jobId,
239
+ } as unknown as DependencyExport;
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Adds historical actions (proxyActivity, executeChild)
245
+ * using the `dependency list` to determine
246
+ * after-the-fact what happened within the 'black-box'
247
+ * worker function. This is necessary to interleave the
248
+ * actions into the replay timeline, given that it isn't
249
+ * really possible to know the inner-workings of the user's
250
+ * function
251
+ *
252
+ */
253
+ seedActions(type: 'flow'|'hook'|'other', action: string, topic: string, dep: string, prefix: string, dimensionKey: string, actions: JobActionExport, jobId: string) {
254
+ if (type !== 'other' && action === 'expire-child') {
255
+ let depType: string;
256
+ if (topic == `${this.appId}.activity.execute`) {
257
+ depType = 'proxyActivity';
258
+ } else if (topic == `${this.appId}.execute`) {
259
+ depType = 'executeChild';
260
+ } else if (topic == `${this.appId}.wfsc.execute`) {
261
+ depType = 'waitForSignal';
262
+ }
263
+
264
+ if (depType) {
265
+ if (type === 'flow') {
266
+ actions.main.items.push([prefix, depType, jobId]);
267
+ } else if (type === 'hook') {
268
+ if (!actions.hooks[dimensionKey]) {
269
+ actions.hooks[dimensionKey] = {
270
+ cursor: -1,
271
+ items: [],
272
+ };
273
+ }
274
+ actions.hooks[dimensionKey].items.push([prefix, depType, jobId]);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Inflates the job data from Redis into a DurableJobExport object
282
+ * @param jobHash - the job data from Redis
283
+ * @param dependencyList - the list of dependencies for the job
284
+ * @returns - the inflated job data
285
+ */
286
+ inflate(jobHash: StringStringType, dependencyList: string[]): DurableJobExport {
287
+ //the list of actions taken in the workflow and hook functions
288
+ const actions: JobActionExport = {
289
+ hooks: {},
290
+ main: { cursor: -1, items: [] },
291
+ };
292
+ const dependencies = this.inflateDependencyData(dependencyList, actions);
293
+ const state: StringStringType = {};
294
+ const data: StringStringType = {};
295
+ const other: ExportItem[] = [];
296
+ const replay: ExportItem[] = [];
297
+ const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
298
+
299
+ Object.entries(jobHash).forEach(([key, value]) => {
300
+ const match = key.match(regex);
301
+ if (match) {
302
+ //activity process state
303
+ this.inflateProcess(match, value, replay);
304
+ } else if (key.length === 3) {
305
+ //job state
306
+ state[this.inflateKey(key)] = this.serializer.fromString(value);
307
+ } else if (key.startsWith('_')) {
308
+ //job data
309
+ data[key.substring(1)] = value;
310
+ } else if (key.startsWith('-')) {
311
+ //actions with side effect (replayable)
312
+ this.inflateActions(key, value, actions);
313
+ } else {
314
+ //collator guids, etc
315
+ other.push([null, key, value]);
316
+ }
317
+ });
318
+
319
+ replay.sort(this.dateSort)
320
+ actions.main.items.sort(this.reverseSort);
321
+ Object.entries(actions.hooks).forEach(([key, value]) => {
322
+ value.items.sort(this.reverseSort);
323
+ });
324
+
325
+ return {
326
+ data: restoreHierarchy(data),
327
+ dependencies,
328
+ state: Object.entries(restoreHierarchy(state))[0][1],
329
+ status: jobHash[':'],
330
+ timeline: this.createTimeline(replay, actions),
331
+ transitions: { ...this.transitions },
332
+ cycles: { ...this.cycles },
333
+ };
334
+ }
335
+
336
+ inflateProcess(match: RegExpMatchArray, value: string, replay: ExportItem[]) {
337
+ const [_, letters, numbers] = match;
338
+ const path = this.inflateKey(letters);
339
+ if (path.endsWith('/output/metadata/ac') ||
340
+ path.endsWith('/output/metadata/au')) {
341
+ const dimensions = `/${numbers.replace(/,/g, '/')}`;
342
+ const resolved = this.serializer.fromString(value);
343
+ replay.push([
344
+ dimensions,
345
+ path,
346
+ resolved,
347
+ ]);
348
+ }
349
+ }
350
+
351
+ inflateActions(key: string, value: string, actions: JobActionExport) {
352
+ let [_, dimensionalType, counter, subcounter] = key.split('-');
353
+ if (subcounter) {
354
+ counter = `${counter}.${subcounter}`;
355
+ }
356
+ const [type, ...dimensions] = dimensionalType.split(',');
357
+ let dimensionKey = '';
358
+ let isHook = false;
359
+ if (dimensions.length > 0) {
360
+ dimensionKey = `/${dimensions.join('/')}`;
361
+ isHook = true;
362
+ }
363
+ let targetList: ExportItem[];
364
+ if (isHook) {
365
+ if (!actions.hooks[dimensionKey]) {
366
+ actions.hooks[dimensionKey] = {
367
+ cursor: -1,
368
+ items: [],
369
+ };
370
+ }
371
+ targetList = actions.hooks[dimensionKey].items;
372
+ } else {
373
+ targetList = actions.main.items;
374
+ }
375
+ targetList.push([
376
+ `${dimensionKey}[${counter}]`,
377
+ type,
378
+ value,
379
+ ]);
380
+ }
381
+
382
+ reverseSort(aKey: ExportItem, bKey: ExportItem) {
383
+ if (aKey[0] > bKey[0]) {
384
+ return 1;
385
+ } else if (aKey[0] < bKey[0]) {
386
+ return -1;
387
+ } else {
388
+ if (aKey[1] > bKey[1]) {
389
+ return 1;
390
+ } else if (aKey[1] < bKey[1]) {
391
+ return -1;
392
+ }
393
+ return 0;
394
+ }
395
+ }
396
+
397
+ dateSort(aKey: ExportItem, bKey: ExportItem) {
398
+ if (aKey[2] > bKey[2]) {
399
+ return 1;
400
+ } else if (aKey[2] < bKey[2]) {
401
+ return -1;
402
+ } else {
403
+ return 0;
404
+ }
405
+ }
406
+ }
407
+
408
+ export { ExporterService };
@@ -57,6 +57,7 @@ const getWorkflowYAML = (app: string, version: string) => {
57
57
  id: '{$self.input.data.workflowId}'
58
58
  key: '{$self.input.data.parentWorkflowId}'
59
59
  parent: '{$self.input.data.originJobId}'
60
+ adjacent: '{$self.input.data.parentWorkflowId}'
60
61
  job:
61
62
  maps:
62
63
  done: false
@@ -260,10 +261,7 @@ const getWorkflowYAML = (app: string, version: string) => {
260
261
  description: index will be appended later
261
262
  maps:
262
263
  signals: '{sigw1.output.data.signals}'
263
- parentWorkflowId:
264
- '@pipe':
265
- - ['{$job.metadata.jid}', '-w']
266
- - ['{@string.concat}']
264
+ parentWorkflowId: '{$job.metadata.jid}'
267
265
  originJobId:
268
266
  '@pipe':
269
267
  - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
@@ -353,10 +351,7 @@ const getWorkflowYAML = (app: string, version: string) => {
353
351
  description: index will be appended later
354
352
  maps:
355
353
  signals: '{w1.output.data.signals}'
356
- parentWorkflowId:
357
- '@pipe':
358
- - ['{$job.metadata.jid}', '-w']
359
- - ['{@string.concat}']
354
+ parentWorkflowId: '{$job.metadata.jid}'
360
355
  originJobId:
361
356
  '@pipe':
362
357
  - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
@@ -523,6 +518,7 @@ const getWorkflowYAML = (app: string, version: string) => {
523
518
  id: '{$self.input.data.workflowId}'
524
519
  key: '{$self.input.data.parentWorkflowId}'
525
520
  parent: '{$self.input.data.originJobId}'
521
+ adjacent: '{$self.input.data.parentWorkflowId}'
526
522
 
527
523
  w1a:
528
524
  title: Activity Worker - Calls Activity Functions
@@ -563,61 +559,6 @@ const getWorkflowYAML = (app: string, version: string) => {
563
559
  t1a:
564
560
  - to: w1a
565
561
 
566
- - subscribes: ${app}.sleep.execute
567
- publishes: ${app}.sleep.executed
568
-
569
- expire: 0
570
-
571
- input:
572
- schema:
573
- type: object
574
- properties:
575
- parentWorkflowId:
576
- type: string
577
- originJobId:
578
- type: string
579
- workflowId:
580
- type: string
581
- duration:
582
- type: number
583
- description: in seconds
584
- index:
585
- type: number
586
- output:
587
- schema:
588
- type: object
589
- properties:
590
- done:
591
- type: boolean
592
- duration:
593
- type: number
594
- index:
595
- type: number
596
-
597
- activities:
598
- t1s:
599
- title: Sleep Flow Trigger
600
- type: trigger
601
- stats:
602
- id: '{$self.input.data.workflowId}'
603
- key: '{$self.input.data.parentWorkflowId}'
604
- parent: '{$self.input.data.originJobId}'
605
-
606
- a1s:
607
- title: Sleep for a duration
608
- type: hook
609
- sleep: '{t1s.output.data.duration}'
610
- job:
611
- maps:
612
- done: true
613
- duration: '{t1s.output.data.duration}'
614
- index: '{t1s.output.data.index}'
615
- workflowId: '{t1s.output.data.workflowId}'
616
-
617
- transitions:
618
- t1s:
619
- - to: a1s
620
-
621
562
  - subscribes: ${app}.wfsc.execute
622
563
  publishes: ${app}.wfsc.executed
623
564
 
@@ -662,6 +603,7 @@ const getWorkflowYAML = (app: string, version: string) => {
662
603
  stats:
663
604
  id: '{$self.input.data.cycleWorkflowId}'
664
605
  parent: '{$self.input.data.originJobId}'
606
+ adjacent: '{$self.input.data.parentWorkflowId}'
665
607
 
666
608
  a1wc:
667
609
  title: Pivot - All Cycling Descendants Point Here
@@ -833,6 +775,7 @@ const getWorkflowYAML = (app: string, version: string) => {
833
775
  id: '{$self.input.data.workflowId}'
834
776
  key: '{$self.input.data.parentWorkflowId}'
835
777
  parent: '{$self.input.data.originJobId}'
778
+ adjacent: '{$self.input.data.parentWorkflowId}'
836
779
 
837
780
  a1ww:
838
781
  title: WFS - signal entry point
@@ -1,9 +1,12 @@
1
1
  import { HMSH_CODE_INTERRUPT } from '../../modules/enums';
2
+ import { ExporterService } from './exporter';
2
3
  import { HotMeshService as HotMesh } from '../hotmesh';
4
+ import { DurableJobExport } from '../../types/exporter';
3
5
  import { JobInterruptOptions, JobOutput } from '../../types/job';
4
6
  import { StreamError } from '../../types/stream';
5
7
 
6
8
  export class WorkflowHandleService {
9
+ exporter: ExporterService
7
10
  hotMesh: HotMesh;
8
11
  workflowTopic: string;
9
12
  workflowId: string;
@@ -12,6 +15,15 @@ export class WorkflowHandleService {
12
15
  this.workflowTopic = workflowTopic;
13
16
  this.workflowId = workflowId;
14
17
  this.hotMesh = hotMesh;
18
+ this.exporter = new ExporterService(
19
+ this.hotMesh.appId,
20
+ this.hotMesh.engine.store,
21
+ this.hotMesh.engine.logger,
22
+ );
23
+ }
24
+
25
+ async export(): Promise<DurableJobExport> {
26
+ return this.exporter.export(this.workflowId);
15
27
  }
16
28
 
17
29
  /**
@@ -45,7 +45,7 @@ export class WorkflowService {
45
45
  const entityOrEmptyString = options.entity ?? '';
46
46
  //If the workflowId is not provided, it is generated from the entity and the workflow name
47
47
  const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
48
- const parentWorkflowId = `${workflowId}-f`;
48
+ const parentWorkflowId = workflowId;
49
49
 
50
50
  const client = new Client({
51
51
  connection: await Connection.connect(WorkerService.connection),
@@ -91,33 +91,35 @@ export class WorkflowService {
91
91
  const workflowSpan = store.get('workflowSpan');
92
92
  const COUNTER = store.get('counter');
93
93
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
94
+ const sessionId = `-start${workflowDimension}-${execIndex}-`;
94
95
  //NOTE: this is the hash prefix; necessary for the search index to locate the entity
95
96
  const entityOrEmptyString = options.entity ?? '';
96
97
  //If the workflowId is not provided, it is generated from the entity and the workflow name
97
- const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
98
- const parentWorkflowId = `${workflowId}-f`;
98
+ const parentWorkflowId = workflowId;
99
99
  const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
100
100
 
101
- try {
102
- //get the status; if there is no error, return childJobId (what was spawned)
103
- const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
104
- await hotMeshClient.getStatus(childJobId);
105
- return childJobId;
106
- } catch (error) {
107
- const client = new Client({
108
- connection: await Connection.connect(WorkerService.connection),
109
- });
110
-
111
- await client.workflow.start({
112
- ...options,
113
- namespace,
114
- workflowId: childJobId,
115
- parentWorkflowId,
116
- workflowTrace,
117
- workflowSpan,
118
- });
101
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
102
+ const keyParams = { appId: hotMeshClient.appId, jobId: workflowId }
103
+ const workflowGuid = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
104
+ let childJobId = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId) as string;
105
+ if (childJobId) {
119
106
  return childJobId;
107
+ } else {
108
+ childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
120
109
  }
110
+ const client = new Client({
111
+ connection: await Connection.connect(WorkerService.connection),
112
+ });
113
+ await client.workflow.start({
114
+ ...options,
115
+ namespace,
116
+ workflowId: childJobId,
117
+ parentWorkflowId,
118
+ workflowTrace,
119
+ workflowSpan,
120
+ });
121
+ await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, childJobId);
122
+ return childJobId;
121
123
  }
122
124
 
123
125
  /**
@@ -472,7 +474,7 @@ export class WorkflowService {
472
474
  arguments: Array.from(arguments),
473
475
  //when the origin job is removed
474
476
  originJobId: originJobId ?? workflowId,
475
- parentWorkflowId: `${workflowId}-a`,
477
+ parentWorkflowId: workflowId,
476
478
  workflowId: activityJobId,
477
479
  workflowTopic: activityTopic,
478
480
  activityName,