@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.
- package/README.md +22 -18
- package/build/index.d.ts +1 -2
- package/build/index.js +1 -3
- package/build/modules/enums.d.ts +8 -3
- package/build/modules/enums.js +16 -8
- package/build/modules/errors.d.ts +98 -18
- package/build/modules/errors.js +90 -33
- package/build/package.json +7 -2
- package/build/services/activities/activity.d.ts +8 -0
- package/build/services/activities/activity.js +63 -14
- package/build/services/activities/await.js +6 -6
- package/build/services/activities/cycle.d.ts +2 -2
- package/build/services/activities/cycle.js +5 -5
- package/build/services/activities/hook.js +4 -4
- package/build/services/activities/interrupt.d.ts +3 -3
- package/build/services/activities/interrupt.js +15 -6
- package/build/services/activities/signal.d.ts +2 -2
- package/build/services/activities/signal.js +4 -4
- package/build/services/activities/trigger.js +12 -3
- package/build/services/activities/worker.js +6 -6
- package/build/services/compiler/deployer.js +33 -5
- package/build/services/compiler/validator.d.ts +2 -0
- package/build/services/compiler/validator.js +5 -1
- package/build/services/durable/client.d.ts +7 -1
- package/build/services/durable/client.js +56 -30
- package/build/services/durable/exporter.d.ts +7 -72
- package/build/services/durable/exporter.js +105 -295
- package/build/services/durable/handle.d.ts +11 -6
- package/build/services/durable/handle.js +59 -46
- package/build/services/durable/index.d.ts +0 -2
- package/build/services/durable/index.js +0 -2
- package/build/services/durable/schemas/factory.d.ts +33 -0
- package/build/services/durable/schemas/factory.js +2356 -0
- package/build/services/durable/search.js +8 -8
- package/build/services/durable/worker.js +117 -25
- package/build/services/durable/workflow.d.ts +46 -43
- package/build/services/durable/workflow.js +273 -277
- package/build/services/engine/index.js +3 -0
- package/build/services/exporter/index.d.ts +2 -4
- package/build/services/exporter/index.js +4 -5
- package/build/services/mapper/index.d.ts +6 -2
- package/build/services/mapper/index.js +6 -2
- package/build/services/pipe/functions/array.d.ts +2 -10
- package/build/services/pipe/functions/array.js +30 -28
- package/build/services/pipe/functions/conditional.d.ts +1 -0
- package/build/services/pipe/functions/conditional.js +3 -0
- package/build/services/pipe/functions/date.d.ts +1 -0
- package/build/services/pipe/functions/date.js +4 -0
- package/build/services/pipe/functions/index.d.ts +2 -0
- package/build/services/pipe/functions/index.js +2 -0
- package/build/services/pipe/functions/logical.d.ts +5 -0
- package/build/services/pipe/functions/logical.js +12 -0
- package/build/services/pipe/functions/object.d.ts +3 -0
- package/build/services/pipe/functions/object.js +25 -7
- package/build/services/pipe/index.d.ts +20 -3
- package/build/services/pipe/index.js +82 -16
- package/build/services/router/index.js +14 -3
- package/build/services/serializer/index.d.ts +3 -2
- package/build/services/serializer/index.js +11 -4
- package/build/services/store/clients/ioredis.js +6 -6
- package/build/services/store/clients/redis.js +7 -7
- package/build/services/store/index.d.ts +2 -0
- package/build/services/store/index.js +4 -1
- package/build/services/stream/clients/ioredis.js +8 -8
- package/build/services/stream/clients/redis.js +1 -1
- package/build/types/activity.d.ts +60 -5
- package/build/types/durable.d.ts +168 -33
- package/build/types/exporter.d.ts +26 -4
- package/build/types/index.d.ts +2 -2
- package/build/types/job.d.ts +69 -5
- package/build/types/pipe.d.ts +81 -3
- package/build/types/stream.d.ts +61 -1
- package/build/types/stream.js +4 -0
- package/index.ts +1 -2
- package/modules/enums.ts +16 -8
- package/modules/errors.ts +174 -32
- package/package.json +7 -2
- package/services/activities/activity.ts +63 -14
- package/services/activities/await.ts +6 -6
- package/services/activities/cycle.ts +7 -6
- package/services/activities/hook.ts +4 -4
- package/services/activities/interrupt.ts +19 -9
- package/services/activities/signal.ts +6 -5
- package/services/activities/trigger.ts +16 -4
- package/services/activities/worker.ts +7 -7
- package/services/compiler/deployer.ts +33 -6
- package/services/compiler/validator.ts +7 -3
- package/services/durable/client.ts +47 -14
- package/services/durable/exporter.ts +110 -318
- package/services/durable/handle.ts +63 -50
- package/services/durable/index.ts +0 -2
- package/services/durable/schemas/factory.ts +2358 -0
- package/services/durable/search.ts +8 -8
- package/services/durable/worker.ts +128 -29
- package/services/durable/workflow.ts +304 -288
- package/services/engine/index.ts +4 -0
- package/services/exporter/index.ts +10 -12
- package/services/mapper/index.ts +6 -2
- package/services/pipe/functions/array.ts +24 -37
- package/services/pipe/functions/conditional.ts +4 -0
- package/services/pipe/functions/date.ts +6 -0
- package/services/pipe/functions/index.ts +7 -5
- package/services/pipe/functions/logical.ts +11 -0
- package/services/pipe/functions/object.ts +26 -7
- package/services/pipe/index.ts +99 -21
- package/services/quorum/index.ts +1 -3
- package/services/router/index.ts +14 -3
- package/services/serializer/index.ts +12 -5
- package/services/store/clients/ioredis.ts +6 -6
- package/services/store/clients/redis.ts +7 -7
- package/services/store/index.ts +4 -1
- package/services/stream/clients/ioredis.ts +8 -8
- package/services/stream/clients/redis.ts +1 -1
- package/types/activity.ts +87 -15
- package/types/durable.ts +246 -73
- package/types/exporter.ts +31 -5
- package/types/index.ts +6 -7
- package/types/job.ts +130 -36
- package/types/pipe.ts +84 -3
- package/types/stream.ts +82 -23
- package/build/services/durable/factory.d.ts +0 -17
- package/build/services/durable/factory.js +0 -817
- package/build/services/durable/meshos.d.ts +0 -127
- package/build/services/durable/meshos.js +0 -380
- package/services/durable/factory.ts +0 -818
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
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
|
|
89
|
-
*
|
|
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
|
|
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[]
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
67
|
+
index,
|
|
68
|
+
action,
|
|
247
69
|
topic,
|
|
248
70
|
gid,
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
298
|
-
|
|
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:
|
|
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)] =
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
180
|
+
idempotents: sortParts(idempotents),
|
|
339
181
|
state: Object.entries(restoreHierarchy(state))[0][1],
|
|
340
182
|
status: jobHash[':'],
|
|
341
|
-
|
|
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:
|
|
348
|
-
const [_, letters,
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
33
|
-
*
|
|
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
|
-
*
|
|
85
|
-
* the workflow
|
|
86
|
-
*
|
|
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(
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
+
|
|
110
112
|
if (err) {
|
|
111
|
-
return
|
|
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.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|