@hotmeshio/hotmesh 0.0.52 → 0.0.53

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 (126) hide show
  1. package/README.md +22 -18
  2. package/build/index.d.ts +1 -2
  3. package/build/index.js +1 -3
  4. package/build/modules/enums.d.ts +8 -3
  5. package/build/modules/enums.js +16 -8
  6. package/build/modules/errors.d.ts +98 -18
  7. package/build/modules/errors.js +90 -33
  8. package/build/package.json +7 -2
  9. package/build/services/activities/activity.d.ts +8 -0
  10. package/build/services/activities/activity.js +63 -14
  11. package/build/services/activities/await.js +6 -6
  12. package/build/services/activities/cycle.d.ts +2 -2
  13. package/build/services/activities/cycle.js +5 -5
  14. package/build/services/activities/hook.js +4 -4
  15. package/build/services/activities/interrupt.d.ts +3 -3
  16. package/build/services/activities/interrupt.js +15 -6
  17. package/build/services/activities/signal.d.ts +2 -2
  18. package/build/services/activities/signal.js +4 -4
  19. package/build/services/activities/trigger.js +12 -3
  20. package/build/services/activities/worker.js +6 -6
  21. package/build/services/compiler/deployer.js +33 -5
  22. package/build/services/compiler/validator.d.ts +2 -0
  23. package/build/services/compiler/validator.js +5 -1
  24. package/build/services/durable/client.d.ts +7 -1
  25. package/build/services/durable/client.js +56 -30
  26. package/build/services/durable/exporter.d.ts +7 -72
  27. package/build/services/durable/exporter.js +105 -295
  28. package/build/services/durable/handle.d.ts +11 -6
  29. package/build/services/durable/handle.js +59 -46
  30. package/build/services/durable/index.d.ts +0 -2
  31. package/build/services/durable/index.js +0 -2
  32. package/build/services/durable/schemas/factory.d.ts +33 -0
  33. package/build/services/durable/schemas/factory.js +2356 -0
  34. package/build/services/durable/search.js +8 -8
  35. package/build/services/durable/worker.js +117 -25
  36. package/build/services/durable/workflow.d.ts +46 -43
  37. package/build/services/durable/workflow.js +273 -277
  38. package/build/services/engine/index.js +3 -0
  39. package/build/services/exporter/index.d.ts +2 -4
  40. package/build/services/exporter/index.js +4 -5
  41. package/build/services/mapper/index.d.ts +6 -2
  42. package/build/services/mapper/index.js +6 -2
  43. package/build/services/pipe/functions/array.d.ts +2 -10
  44. package/build/services/pipe/functions/array.js +30 -28
  45. package/build/services/pipe/functions/conditional.d.ts +1 -0
  46. package/build/services/pipe/functions/conditional.js +3 -0
  47. package/build/services/pipe/functions/date.d.ts +1 -0
  48. package/build/services/pipe/functions/date.js +4 -0
  49. package/build/services/pipe/functions/index.d.ts +2 -0
  50. package/build/services/pipe/functions/index.js +2 -0
  51. package/build/services/pipe/functions/logical.d.ts +5 -0
  52. package/build/services/pipe/functions/logical.js +12 -0
  53. package/build/services/pipe/functions/object.d.ts +3 -0
  54. package/build/services/pipe/functions/object.js +25 -7
  55. package/build/services/pipe/index.d.ts +20 -3
  56. package/build/services/pipe/index.js +82 -16
  57. package/build/services/router/index.js +14 -3
  58. package/build/services/serializer/index.d.ts +3 -2
  59. package/build/services/serializer/index.js +11 -4
  60. package/build/services/store/clients/ioredis.js +6 -6
  61. package/build/services/store/clients/redis.js +7 -7
  62. package/build/services/store/index.d.ts +2 -0
  63. package/build/services/store/index.js +4 -1
  64. package/build/services/stream/clients/ioredis.js +8 -8
  65. package/build/services/stream/clients/redis.js +1 -1
  66. package/build/types/activity.d.ts +60 -5
  67. package/build/types/durable.d.ts +168 -33
  68. package/build/types/exporter.d.ts +26 -4
  69. package/build/types/index.d.ts +2 -2
  70. package/build/types/job.d.ts +69 -5
  71. package/build/types/pipe.d.ts +81 -3
  72. package/build/types/stream.d.ts +61 -1
  73. package/build/types/stream.js +4 -0
  74. package/index.ts +1 -2
  75. package/modules/enums.ts +16 -8
  76. package/modules/errors.ts +174 -32
  77. package/package.json +7 -2
  78. package/services/activities/activity.ts +63 -14
  79. package/services/activities/await.ts +6 -6
  80. package/services/activities/cycle.ts +7 -6
  81. package/services/activities/hook.ts +4 -4
  82. package/services/activities/interrupt.ts +19 -9
  83. package/services/activities/signal.ts +6 -5
  84. package/services/activities/trigger.ts +16 -4
  85. package/services/activities/worker.ts +7 -7
  86. package/services/compiler/deployer.ts +33 -6
  87. package/services/compiler/validator.ts +7 -3
  88. package/services/durable/client.ts +47 -14
  89. package/services/durable/exporter.ts +110 -318
  90. package/services/durable/handle.ts +63 -50
  91. package/services/durable/index.ts +0 -2
  92. package/services/durable/schemas/factory.ts +2358 -0
  93. package/services/durable/search.ts +8 -8
  94. package/services/durable/worker.ts +128 -29
  95. package/services/durable/workflow.ts +304 -288
  96. package/services/engine/index.ts +4 -0
  97. package/services/exporter/index.ts +10 -12
  98. package/services/mapper/index.ts +6 -2
  99. package/services/pipe/functions/array.ts +24 -37
  100. package/services/pipe/functions/conditional.ts +4 -0
  101. package/services/pipe/functions/date.ts +6 -0
  102. package/services/pipe/functions/index.ts +7 -5
  103. package/services/pipe/functions/logical.ts +11 -0
  104. package/services/pipe/functions/object.ts +26 -7
  105. package/services/pipe/index.ts +99 -21
  106. package/services/quorum/index.ts +1 -3
  107. package/services/router/index.ts +14 -3
  108. package/services/serializer/index.ts +12 -5
  109. package/services/store/clients/ioredis.ts +6 -6
  110. package/services/store/clients/redis.ts +7 -7
  111. package/services/store/index.ts +4 -1
  112. package/services/stream/clients/ioredis.ts +8 -8
  113. package/services/stream/clients/redis.ts +1 -1
  114. package/types/activity.ts +87 -15
  115. package/types/durable.ts +246 -73
  116. package/types/exporter.ts +31 -5
  117. package/types/index.ts +6 -7
  118. package/types/job.ts +130 -36
  119. package/types/pipe.ts +84 -3
  120. package/types/stream.ts +82 -23
  121. package/build/services/durable/factory.d.ts +0 -17
  122. package/build/services/durable/factory.js +0 -817
  123. package/build/services/durable/meshos.d.ts +0 -127
  124. package/build/services/durable/meshos.js +0 -380
  125. package/services/durable/factory.ts +0 -818
  126. package/services/durable/meshos.ts +0 -441
@@ -1,184 +1,43 @@
1
+ import { VALSEP } from '../../modules/key';
2
+ import { restoreHierarchy } from '../../modules/utils';
1
3
  import { ILogger } from '../logger';
4
+ import { SerializerService } from '../serializer';
2
5
  import { StoreService } from '../store';
3
- import { StringStringType, Symbols } from "../../types";
4
- import { RedisClient, RedisMulti } from '../../types/redis';
5
6
  import {
6
- ActivityAction,
7
- DependencyExport,
8
7
  ExportItem,
9
8
  ExportOptions,
10
- JobAction,
11
- JobActionExport,
12
- DurableJobExport,
13
- JobTimeline } from '../../types/exporter';
14
- import { SerializerService } from '../serializer';
15
- import { restoreHierarchy } from '../../modules/utils';
16
- import { VALSEP } from '../../modules/key';
9
+ DurableJobExport,
10
+ IdemType,
11
+ TimelineEntry } from '../../types/exporter';
12
+ import { RedisClient, RedisMulti } from '../../types/redis';
13
+ import { StringStringType, Symbols } from "../../types/serializer";
17
14
 
18
- /**
19
- * Downloads job data from Redis (hscan, hmget, hgetall)
20
- * Splits, Inflates, and Sorts the job data for use in durable contexts
21
- */
22
15
  class ExporterService {
23
16
  appId: string;
24
17
  logger: ILogger;
25
- serializer: SerializerService
26
18
  store: StoreService<RedisClient, RedisMulti>;
27
19
  symbols: Promise<Symbols> | Symbols;
28
20
 
29
- /**
30
- * Friendly names for the activity ids
31
- */
32
- activitySymbols: Symbols = {
33
- t1: 'trigger',
34
- a1: 'pivot',
35
- w1: 'worker',
36
- a592: 'sleeper',
37
- a594: 'awaiter',
38
- a599: 'retryer',
39
- c592: 'sleep_cycler',
40
- c594: 'await_cycler',
41
- c599: 'retry_cycler',
42
- s5: 'scrubber',
43
- sig: 'hook',
44
- siga1: 'hook_pivot',
45
- sigw1: 'hook_worker',
46
- siga592: 'hook_sleeper',
47
- siga594: 'hook_awaiter',
48
- siga599: 'hook_retryer',
49
- sigc592: 'hook_sleep_cycler',
50
- sigc594: 'hook_await_cycler',
51
- sigc599: 'hook_retry_cycler',
52
- }
53
-
54
- //adjacent transitions
55
- transitions = {
56
- trigger: ['pivot', 'hook'],
57
- pivot: ['worker'],
58
- worker: ['sleeper', 'awaiter', 'retryer', 'scrubber'],
59
- sleeper: ['sleep_cycler'],
60
- awaiter: ['await_cycler'],
61
- retryer: ['retry_cycler'],
62
- hook: ['hook_pivot'],
63
- hook_pivot: ['hook_worker'],
64
- hook_worker: ['hook_sleeper', 'hook_awaiter', 'hook_retryer'],
65
- hook_sleeper: ['hook_sleep_cycler'],
66
- hook_awaiter: ['hook_await_cycler'],
67
- hook_retryer: ['hook_retry_cycler'],
68
- };
69
-
70
- //goto transitions
71
- cycles = {
72
- sleep_cycler: ['pivot'],
73
- await_cycler: ['pivot'],
74
- retry_cycler: ['pivot'],
75
- hook_sleep_cycler: ['hook_pivot'],
76
- hook_await_cycler: ['hook_pivot'],
77
- hook_retry_cycler: ['hook_pivot'],
78
- }
79
-
80
21
  constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
81
22
  this.appId = appId;
82
23
  this.logger = logger;
83
24
  this.store = store;
84
- this.serializer = new SerializerService();
85
25
  }
86
26
 
87
27
  /**
88
- * Convert the job hash and dependency list into a DurableJobExport object.
89
- * This object contains various facets that describe the interaction
90
- * in terms relevant to narrative storytelling.
28
+ * Convert the job hash from its compiles format into a DurableJobExport object with
29
+ * facets that describe the workflow in terms relevant to narrative storytelling.
91
30
  */
92
31
  async export(jobId: string, options: ExportOptions = {}): Promise<DurableJobExport> {
93
32
  if (!this.symbols) {
94
33
  this.symbols = this.store.getAllSymbols();
95
34
  this.symbols = await this.symbols;
96
35
  }
97
- const depData = await this.store.getDependencies(jobId);
98
36
  const jobData = await this.store.getRaw(jobId);
99
- const jobExport = this.inflate(jobData, depData);
37
+ const jobExport = this.inflate(jobData/*, depData*/);
100
38
  return jobExport;
101
39
  }
102
40
 
103
- /**
104
- * Interleave actions into the replay timeline to create
105
- * a time-ordered timeline of the entire interaction, beginning
106
- * with the entry trigger and concluding with the scrubber
107
- * activity. Using the returned timeline, it is possible to
108
- * create an animated narrative of the job, highlighting
109
- * activities in the graph according to the timeline's
110
- * activity-created (/ac) and activity-updated (/au) entries.
111
- */
112
- createTimeline(replay: ExportItem[], actions: JobActionExport): JobTimeline[] {
113
- const timeline: JobTimeline[] = [];
114
- replay.forEach((item) => {
115
- const dimensions = item[0];
116
- const parts = dimensions.split('/');
117
- const activityName = item[1].split('/')[0];
118
- const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
119
- const timestamp = item[2];
120
- let event: JobTimeline = {
121
- activity: activityName,
122
- duplex: duplex as 'entry' | 'exit',
123
- dimension: dimensions,
124
- timestamp,
125
- created: timestamp,
126
- updated: timestamp,
127
- };
128
- const prior = timeline[timeline.length - 1];
129
- if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
130
- if (event.duplex === 'exit') {
131
- prior.updated = event.timestamp;
132
- } else {
133
- prior.created = event.timestamp;
134
- }
135
- event = prior;
136
- } else {
137
- timeline.push(event);
138
- }
139
-
140
- if (this.isMainEntry(item[1])) {
141
- event.actions = [] as ActivityAction[];
142
- this.interleaveActions(actions.main, event.actions);
143
- } else if (this.isHookEntry(item[1])) {
144
- const hookDimension = `/${parts[1]}/${parts[2]}`;
145
- const hookActions = actions.hooks[hookDimension];
146
- event.actions = [] as ActivityAction[];
147
- this.interleaveActions(hookActions, event.actions);
148
- }
149
- });
150
- return timeline;
151
- }
152
-
153
- /**
154
- * Interleave actions into the 'worker' and 'hook_worker'
155
- * activities (between their /ac and /au entries)
156
- */
157
- interleaveActions(target: JobAction, actions: ActivityAction[]) {
158
- if (target) {
159
- for (let i = target.cursor + 1; i < target.items.length; i++) {
160
- const [_, actionType, jobOrIndex] = target.items[i];
161
- actions.push({ action: actionType, target: jobOrIndex });
162
- target.cursor = i;
163
- if (this.isPausingAction(actionType)) {
164
- break;
165
- }
166
- }
167
- }
168
- }
169
-
170
- isPausingAction(actionType: string): boolean {
171
- return actionType === 'sleep' || actionType === 'waitForSignal';
172
- }
173
-
174
- isMainEntry(key: string): boolean {
175
- return key.startsWith('worker/') && key.endsWith('/ac');
176
- }
177
-
178
- isHookEntry(key: string): boolean {
179
- return key.startsWith('hook_worker/') && key.endsWith('/ac');
180
- }
181
-
182
41
  /**
183
42
  * Inflates the key from Redis, 3-character symbol
184
43
  * into a human-readable JSON path, reflecting the
@@ -188,9 +47,6 @@ class ExporterService {
188
47
  if (key in this.symbols) {
189
48
  const path = this.symbols[key];
190
49
  const parts = path.split('/');
191
- if (parts[0] in this.activitySymbols) {
192
- parts[0] = this.activitySymbols[parts[0]];
193
- }
194
50
  return parts.join('/');
195
51
  }
196
52
  return key;
@@ -203,89 +59,19 @@ class ExporterService {
203
59
  * @param data - the dependency data from Redis
204
60
  * @returns - the organized dependency data
205
61
  */
206
- inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
207
- const hookReg = /([0-9,]+)-(\d+)$/;
208
- const flowReg = /-(\d+)$/;
209
- return data.map((dependency, index: number): DependencyExport => {
210
- const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
211
- const jobId = jid.join(VALSEP);
212
- const match = jobId.match(hookReg);
213
- let prefix: string;
214
- let type: 'hook' | 'flow' | 'other';
215
- let dimensionKey: string = '';
216
- if (match) {
217
- //hook-originating dependency
218
- const [_, dimension, counter] = match;
219
- dimensionKey = dimension.split(',').join('/');
220
- prefix = `${dimensionKey}[${counter}]`;
221
- type = 'hook';
222
- } else {
223
- const match = jobId.match(flowReg);
224
- if (match) {
225
- //main workflow-originating dependency
226
- const [_, counter] = match;
227
- prefix = `[${counter}]`;
228
- type = 'flow';
229
- } else {
230
- //'other' types like signal cleanup
231
- prefix = '/';
232
- type = 'other';
233
- }
234
- }
235
- this.seedActions(
236
- type,
237
- action,
238
- topic,
239
- dependency,
240
- prefix,
241
- dimensionKey,
242
- actions,
243
- jobId,
244
- );
62
+ inflateDependencyData(data: string[]): Record<string, any>[] {
63
+ return data.map((dependency, index: number): Record<string, any> => {
64
+ const [action, topic, gid, dimension, ...jid] = dependency.split(VALSEP);
65
+ const job_id = jid.join(VALSEP);
245
66
  return {
246
- type: action,
67
+ index,
68
+ action,
247
69
  topic,
248
70
  gid,
249
- jid: jobId,
250
- } as unknown as DependencyExport;
251
- });
252
- }
253
-
254
- /**
255
- * Adds historical actions (proxyActivity, executeChild)
256
- * using the `dependency list` to determine
257
- * after-the-fact what happened within the 'black-box'
258
- * worker function. This is necessary to interleave the
259
- * actions into the replay timeline, given that it isn't
260
- * really possible to know the inner-workings of the user's
261
- * function
262
- *
263
- */
264
- seedActions(type: 'flow'|'hook'|'other', action: string, topic: string, dep: string, prefix: string, dimensionKey: string, actions: JobActionExport, jobId: string) {
265
- if (type !== 'other' && action === 'expire-child') {
266
- let depType: string;
267
- if (topic == `${this.appId}.activity.execute`) {
268
- depType = 'proxyActivity';
269
- } else if (topic == `${this.appId}.execute`) {
270
- depType = 'executeChild';
271
- } else if (topic == `${this.appId}.wfsc.execute`) {
272
- depType = 'waitForSignal';
273
- }
274
-
275
- if (depType) {
276
- if (type === 'flow') {
277
- actions.main.items.push([prefix, depType, jobId]);
278
- } else if (type === 'hook') {
279
- if (!actions.hooks[dimensionKey]) {
280
- actions.hooks[dimensionKey] = {
281
- cursor: -1,
282
- items: [],
283
- };
284
- }
285
- actions.hooks[dimensionKey].items.push([prefix, depType, jobId]);
286
- }
71
+ dimension,
72
+ job_id,
287
73
  }
288
- }
74
+ });
289
75
  }
290
76
 
291
77
  /**
@@ -294,17 +80,12 @@ class ExporterService {
294
80
  * @param dependencyList - the list of dependencies for the job
295
81
  * @returns - the inflated job data
296
82
  */
297
- inflate(jobHash: StringStringType, dependencyList: string[]): DurableJobExport {
298
- //the list of actions taken in the workflow and hook functions
299
- const actions: JobActionExport = {
300
- hooks: {},
301
- main: { cursor: -1, items: [] },
302
- };
303
- const dependencies = this.inflateDependencyData(dependencyList, actions);
83
+ inflate(jobHash: StringStringType): DurableJobExport {
84
+ const idempotents: IdemType[] = [];
304
85
  const state: StringStringType = {};
305
86
  const data: StringStringType = {};
306
87
  const other: ExportItem[] = [];
307
- const replay: ExportItem[] = [];
88
+ const replay: Record<string, TimelineEntry> = {};
308
89
  const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
309
90
 
310
91
  Object.entries(jobHash).forEach(([key, value]) => {
@@ -314,104 +95,115 @@ class ExporterService {
314
95
  this.inflateProcess(match, value, replay);
315
96
  } else if (key.length === 3) {
316
97
  //job state
317
- state[this.inflateKey(key)] = this.serializer.fromString(value);
98
+ state[this.inflateKey(key)] = SerializerService.fromString(value);
318
99
  } else if (key.startsWith('_')) {
319
100
  //job data
320
101
  data[key.substring(1)] = value;
321
102
  } else if (key.startsWith('-')) {
322
103
  //actions with side effect (replayable)
323
- this.inflateActions(key, value, actions);
104
+ idempotents.push({
105
+ key,
106
+ value: SerializerService.fromString(value),
107
+ parts: extractParts(key),
108
+ });
324
109
  } else {
325
110
  //collator guids, etc
326
111
  other.push([null, key, value]);
327
112
  }
328
113
  });
329
114
 
330
- replay.sort(this.dateSort)
331
- actions.main.items.sort(this.reverseSort);
332
- Object.entries(actions.hooks).forEach(([key, value]) => {
333
- value.items.sort(this.reverseSort);
334
- });
115
+ const sortEntriesByCreated = (obj: { [key: string]: TimelineEntry }): TimelineEntry[] => {
116
+ const entriesArray: TimelineEntry[] = Object.values(obj);
117
+ entriesArray.sort((a, b) => {
118
+ return (a.created || a.updated).localeCompare(b.created || b.updated);
119
+ });
120
+ return entriesArray;
121
+ }
122
+
123
+ /**
124
+ * idem list has a complicated sort order based on indexes and dimensions
125
+ */
126
+ const sortParts = (parts: IdemType[]): IdemType[]=> {
127
+ return parts.sort((a, b) => {
128
+ const { dimension: aDim, index: aIdx, secondary: aSec } = a.parts;
129
+ const { dimension: bDim, index: bIdx, secondary: bSec } = b.parts;
130
+
131
+ if (aDim === undefined && bDim !== undefined) return -1;
132
+ if (aDim !== undefined && bDim === undefined) return 1;
133
+ if (aDim !== undefined && bDim !== undefined) {
134
+ if (aDim < bDim) return -1;
135
+ if (aDim > bDim) return 1;
136
+ }
137
+
138
+ if (aIdx < bIdx) return -1;
139
+ if (aIdx > bIdx) return 1;
140
+
141
+ if (aSec === undefined && bSec !== undefined) return -1;
142
+ if (aSec !== undefined && bSec === undefined) return 1;
143
+ if (aSec !== undefined && bSec !== undefined) {
144
+ if (aSec < bSec) return -1;
145
+ if (aSec > bSec) return 1;
146
+ }
147
+
148
+ return 0;
149
+ });
150
+ };
335
151
 
152
+ function extractParts(key: string): {index: number, dimension?: string, secondary?: number} {
153
+ function extractDimension(label: string): string {
154
+ const parts = label.split(',');
155
+ if (parts.length > 1) {
156
+ parts.shift();
157
+ return parts.join(',');
158
+ }
159
+ }
160
+
161
+ const parts = key.split('-');
162
+ if (parts.length === 4) {
163
+ //-proxy-5- -search-1-1-
164
+ return {
165
+ index: parseInt(parts[2], 10),
166
+ dimension: extractDimension(parts[1]),
167
+ }
168
+ } else {
169
+ //-search,0,0-1-1- -proxy,0,0-1-
170
+ return {
171
+ index: parseInt(parts[2], 10),
172
+ secondary: parseInt(parts[3], 10),
173
+ dimension: extractDimension(parts[1]),
174
+ }
175
+ }
176
+ }
177
+
336
178
  return {
337
179
  data: restoreHierarchy(data),
338
- dependencies,
180
+ idempotents: sortParts(idempotents),
339
181
  state: Object.entries(restoreHierarchy(state))[0][1],
340
182
  status: jobHash[':'],
341
- timeline: this.createTimeline(replay, actions),
342
- transitions: { ...this.transitions },
343
- cycles: { ...this.cycles },
183
+ replay: sortEntriesByCreated(replay),
344
184
  };
345
185
  }
346
186
 
347
- inflateProcess(match: RegExpMatchArray, value: string, replay: ExportItem[]) {
348
- const [_, letters, numbers] = match;
187
+ inflateProcess(match: RegExpMatchArray, value: string, replay: Record<string, Record<string, any>>) {
188
+ const [_, letters, dimensions] = match;
349
189
  const path = this.inflateKey(letters);
350
- if (path.endsWith('/output/metadata/ac') ||
351
- path.endsWith('/output/metadata/au')) {
352
- const dimensions = `/${numbers.replace(/,/g, '/')}`;
353
- const resolved = this.serializer.fromString(value);
354
- replay.push([
355
- dimensions,
356
- path,
357
- resolved,
358
- ]);
359
- }
360
- }
361
-
362
- inflateActions(key: string, value: string, actions: JobActionExport) {
363
- let [_, dimensionalType, counter, subcounter] = key.split('-');
364
- if (subcounter) {
365
- counter = `${counter}.${subcounter}`;
366
- }
367
- const [type, ...dimensions] = dimensionalType.split(',');
368
- let dimensionKey = '';
369
- let isHook = false;
370
- if (dimensions.length > 0) {
371
- dimensionKey = `/${dimensions.join('/')}`;
372
- isHook = true;
373
- }
374
- let targetList: ExportItem[];
375
- if (isHook) {
376
- if (!actions.hooks[dimensionKey]) {
377
- actions.hooks[dimensionKey] = {
378
- cursor: -1,
379
- items: [],
190
+ const parts = path.split('/');
191
+ const activity = parts[0];
192
+ const isCreate = path.endsWith('/output/metadata/ac');
193
+ const isUpdate = path.endsWith('/output/metadata/au');
194
+ if (isCreate || isUpdate) {
195
+ const targetName = `${activity},${dimensions}`;
196
+ let target = replay[targetName];
197
+ if (!target) {
198
+ replay[targetName] = {
199
+ activity,
200
+ dimensions,
201
+ created: isCreate ? value : null,
202
+ updated: isUpdate ? value : null,
380
203
  };
204
+ } else {
205
+ target[isCreate ? 'created' : 'updated'] = value;
381
206
  }
382
- targetList = actions.hooks[dimensionKey].items;
383
- } else {
384
- targetList = actions.main.items;
385
- }
386
- targetList.push([
387
- `${dimensionKey}[${counter}]`,
388
- type,
389
- value,
390
- ]);
391
- }
392
-
393
- reverseSort(aKey: ExportItem, bKey: ExportItem) {
394
- if (aKey[0] > bKey[0]) {
395
- return 1;
396
- } else if (aKey[0] < bKey[0]) {
397
- return -1;
398
- } else {
399
- if (aKey[1] > bKey[1]) {
400
- return 1;
401
- } else if (aKey[1] < bKey[1]) {
402
- return -1;
403
- }
404
- return 0;
405
- }
406
- }
407
-
408
- dateSort(aKey: ExportItem, bKey: ExportItem) {
409
- if (aKey[2] > bKey[2]) {
410
- return 1;
411
- } else if (aKey[2] < bKey[2]) {
412
- return -1;
413
- } else {
414
- return 0;
415
207
  }
416
208
  }
417
209
  }
@@ -1,4 +1,3 @@
1
- import { HMSH_CODE_INTERRUPT } from '../../modules/enums';
2
1
  import { ExporterService } from './exporter';
3
2
  import { HotMeshService as HotMesh } from '../hotmesh';
4
3
  import { DurableJobExport } from '../../types/exporter';
@@ -29,8 +28,8 @@ export class WorkflowHandleService {
29
28
  /**
30
29
  * Sends a signal to the workflow. This is a way to send
31
30
  * a message to a workflow that is paused due to having
32
- * executed a `waitForSignal` workflow extension. Awakens
33
- * the workflow if no other signals are pending.
31
+ * executed `Durable.workflow.waitFor`. The workflow
32
+ * will awaken if no other signals are pending.
34
33
  */
35
34
  async signal(signalId: string, data: Record<any, any>): Promise<void> {
36
35
  await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
@@ -81,67 +80,81 @@ export class WorkflowHandleService {
81
80
  }
82
81
 
83
82
  /**
84
- * Awaits for the workflow to complete and returns the result. If
85
- * the workflow thows and error, this method will likewise throw
86
- * an error.
83
+ * Waits for the workflow to complete and returns the result. If
84
+ * the workflow response includes an error, this method will rethrow
85
+ * the error, including the stack trace if available.
86
+ * Wrap calls in a try/catch as necessary to avoid unhandled exceptions.
87
87
  */
88
- async result(loadState?: boolean): Promise<any> {
89
- if (loadState) {
90
- const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
91
- if (!state.data && state.metadata.err) {
92
- throw new Error(JSON.parse(state.metadata.err));
93
- }
94
- if (state?.data?.done) {
95
- //child flows are never 'done'; they use a hook
96
- //that only closes upon parent flow completion.
97
- return state.data.response;
98
- }
99
- }
100
- let status = await this.hotMesh.getStatus(this.workflowId);
88
+ async result<T>(config?: {state?: boolean, throwOnError?: boolean}): Promise<T | StreamError> {
101
89
  const topic = `${this.hotMesh.appId}.executed.${this.workflowId}`;
102
-
103
- return new Promise((resolve, reject) => {
104
- let isResolved = false;
105
- //common fulfill/unsubscribe
106
- const complete = async (response?: any, err?: string) => {
90
+ let isResolved = false;
91
+
92
+ return new Promise(async (resolve, reject) => {
93
+
94
+ /**
95
+ * rejects/resolves the promise based on the `throwOnError`
96
+ * default behavior is to throw if error
97
+ */
98
+ const safeReject = (err: StreamError) => {
99
+ if (config?.throwOnError === false) {
100
+ return resolve(err);
101
+ }
102
+ reject(err);
103
+ };
104
+
105
+ /**
106
+ * Common completion function that unsubscribes from the topic/returns
107
+ */
108
+ const complete = async (response?: T, err?: StreamError) => {
107
109
  if (isResolved) return;
108
110
  isResolved = true;
109
- this.hotMesh.unsub(topic);
111
+
110
112
  if (err) {
111
- return reject(JSON.parse(err));
113
+ return safeReject(err as StreamError);
112
114
  } else if (!response) {
113
115
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
114
- if (state.metadata.err) {
115
- const error = JSON.parse(state.metadata.err) as StreamError;
116
- if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
117
- return reject({ ...error, job_id: this.workflowId });
118
- }
116
+ if (state.data?.done && !state.data?.$error) {
117
+ return resolve(state.data.response as T);
118
+ } else if (state.data?.$error) {
119
+ return safeReject(state.data.$error as StreamError)
120
+ } else if (state.metadata.err) {
121
+ return safeReject(JSON.parse(state.metadata.err) as StreamError);
119
122
  }
120
- response = state.data?.response;
123
+ response = state.data?.response as T;
121
124
  }
122
- resolve(response);
125
+ resolve(response as T);
123
126
  };
124
- //check for done
125
- if (status <= 0) {
126
- return complete();
127
+
128
+ //more expensive; fetches the entire job, not just the `status`
129
+ if (config?.state) {
130
+ const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
131
+ if (state?.data?.done && !state.data?.$error) {
132
+ return complete(state.data.response as T);
133
+ } else if (state.data?.$error) {
134
+ return complete(null, state.data.$error as StreamError);
135
+ } else if (state.metadata.err) {
136
+ return complete(null, JSON.parse(state.metadata.err) as StreamError);
137
+ }
127
138
  }
128
- //subscribe to topic
129
- this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
130
- if (state.metadata.err) {
139
+
140
+ //subscribe to 'done' topic
141
+ this.hotMesh.sub(topic, async (_topic: string, state: JobOutput) => {
142
+ this.hotMesh.unsub(topic);
143
+ if (state.data.done && !state.data?.$error) {
144
+ await complete(state.data?.response as T);
145
+ } else if (state.data?.$error) {
146
+ return complete(null, state.data.$error as StreamError)
147
+ } else if (state.metadata.err) {
131
148
  const error = JSON.parse(state.metadata.err) as StreamError;
132
- if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
133
- return await complete(null, state.metadata.err);
134
- }
149
+ return await complete(null, error);
135
150
  }
136
- await complete(state.data?.response);
137
151
  });
138
- //resolve for race condition
139
- setTimeout(async () => {
140
- status = await this.hotMesh.getStatus(this.workflowId);
141
- if (status <= 0) {
142
- await complete();
143
- }
144
- }, 0);
152
+
153
+ //check state in case completed during wiring
154
+ const status = await this.hotMesh.getStatus(this.workflowId);
155
+ if (status <= 0) {
156
+ await complete();
157
+ }
145
158
  });
146
159
  }
147
160
  }
@@ -1,6 +1,5 @@
1
1
  import { ClientService } from './client';
2
2
  import { ConnectionService } from './connection';
3
- import { MeshOSService } from './meshos';
4
3
  import { Search } from './search';
5
4
  import { WorkerService } from './worker';
6
5
  import { WorkflowService } from './workflow';
@@ -11,7 +10,6 @@ export const Durable = {
11
10
  Client: ClientService,
12
11
  Connection: ConnectionService,
13
12
  Search,
14
- MeshOS: MeshOSService,
15
13
  Worker: WorkerService,
16
14
  workflow: WorkflowService,
17
15