@hotmeshio/hotmesh 0.0.51 → 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 +13 -9
- 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 +65 -16
- 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 +67 -18
- 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
|
@@ -8,117 +8,153 @@ const ms_1 = __importDefault(require("ms"));
|
|
|
8
8
|
const errors_1 = require("../../modules/errors");
|
|
9
9
|
const key_1 = require("../../modules/key");
|
|
10
10
|
const storage_1 = require("../../modules/storage");
|
|
11
|
-
const
|
|
12
|
-
const connection_1 = require("./connection");
|
|
13
|
-
const factory_1 = require("./factory");
|
|
11
|
+
const utils_1 = require("../../modules/utils");
|
|
14
12
|
const search_1 = require("./search");
|
|
15
13
|
const worker_1 = require("./worker");
|
|
16
14
|
const stream_1 = require("../../types/stream");
|
|
17
|
-
const
|
|
15
|
+
const enums_1 = require("../../modules/enums");
|
|
16
|
+
const serializer_1 = require("../serializer");
|
|
18
17
|
class WorkflowService {
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
* @
|
|
22
|
-
* @param {WorkflowOptions} options - the workflow options
|
|
23
|
-
* @returns {Promise<T>} - the result of the child workflow
|
|
19
|
+
* Return a handle to the hotmesh client currently running the workflow
|
|
20
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
24
21
|
*/
|
|
25
|
-
static async
|
|
22
|
+
static async getHotMesh() {
|
|
26
23
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
24
|
+
const workflowTopic = store.get('workflowTopic');
|
|
27
25
|
const namespace = store.get('namespace');
|
|
26
|
+
return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the current workflow context restored
|
|
30
|
+
* from Redis
|
|
31
|
+
*/
|
|
32
|
+
static getContext() {
|
|
33
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
28
34
|
const workflowId = store.get('workflowId');
|
|
29
|
-
const
|
|
35
|
+
const replay = store.get('replay');
|
|
36
|
+
const cursor = store.get('cursor');
|
|
30
37
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
38
|
+
const workflowTopic = store.get('workflowTopic');
|
|
39
|
+
const namespace = store.get('namespace');
|
|
31
40
|
const workflowTrace = store.get('workflowTrace');
|
|
41
|
+
const canRetry = store.get('canRetry');
|
|
32
42
|
const workflowSpan = store.get('workflowSpan');
|
|
33
43
|
const COUNTER = store.get('counter');
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
handle = await client.workflow.start({
|
|
50
|
-
...options,
|
|
51
|
-
namespace,
|
|
52
|
-
workflowId: childJobId,
|
|
53
|
-
originJobId: originJobId ?? workflowId,
|
|
54
|
-
parentWorkflowId,
|
|
55
|
-
workflowTrace,
|
|
56
|
-
workflowSpan,
|
|
57
|
-
});
|
|
58
|
-
//todo: options.startToCloseTimeout
|
|
59
|
-
const result = await handle.result();
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
44
|
+
const raw = store.get('raw');
|
|
45
|
+
return {
|
|
46
|
+
canRetry,
|
|
47
|
+
COUNTER,
|
|
48
|
+
counter: COUNTER.counter,
|
|
49
|
+
cursor,
|
|
50
|
+
namespace,
|
|
51
|
+
raw,
|
|
52
|
+
replay,
|
|
53
|
+
workflowId,
|
|
54
|
+
workflowDimension,
|
|
55
|
+
workflowTopic,
|
|
56
|
+
workflowTrace,
|
|
57
|
+
workflowSpan,
|
|
58
|
+
};
|
|
62
59
|
}
|
|
63
60
|
/**
|
|
64
|
-
* Spawns a child workflow
|
|
65
|
-
*
|
|
61
|
+
* Spawns a child workflow and awaits the return.
|
|
62
|
+
* @template T - the result type
|
|
66
63
|
* @param {WorkflowOptions} options - the workflow options
|
|
67
|
-
* @returns {Promise<
|
|
64
|
+
* @returns {Promise<T>} - the result of the child workflow
|
|
65
|
+
* @example
|
|
66
|
+
* const result = await Durable.workflow.execChild<typeof resultType>({ ...options });
|
|
68
67
|
*/
|
|
69
|
-
static async
|
|
68
|
+
static async execChild(options) {
|
|
69
|
+
//SYNC
|
|
70
|
+
//check if the activity already ran (check $error/done)
|
|
71
|
+
const isStartChild = options.await === false;
|
|
72
|
+
const [didRun, execIndex, result] = await WorkflowService.didRun(isStartChild ? 'start' : 'child');
|
|
70
73
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
71
|
-
const
|
|
74
|
+
const canRetry = store.get('canRetry');
|
|
75
|
+
if (didRun) {
|
|
76
|
+
if (result?.$error && (!result.$error.is_stream_error || (result.$error.is_stream_error && !canRetry))) {
|
|
77
|
+
if (options?.config?.throwOnError !== false) {
|
|
78
|
+
//rethrow remote execution error (simulates throw)
|
|
79
|
+
const code = result.$error.code;
|
|
80
|
+
const message = result.$error.message;
|
|
81
|
+
const stack = result.$error.stack;
|
|
82
|
+
if (code === enums_1.HMSH_CODE_DURABLE_FATAL) {
|
|
83
|
+
throw new errors_1.DurableFatalError(message, stack);
|
|
84
|
+
}
|
|
85
|
+
else if (code == enums_1.HMSH_CODE_DURABLE_MAXED) {
|
|
86
|
+
throw new errors_1.DurableMaxedError(message, stack);
|
|
87
|
+
}
|
|
88
|
+
else if (code == enums_1.HMSH_CODE_DURABLE_TIMEOUT) {
|
|
89
|
+
throw new errors_1.DurableTimeoutError(message, stack);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
throw new errors_1.DurableRetryError(message, stack);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result.$error;
|
|
96
|
+
}
|
|
97
|
+
else if (result.data) {
|
|
98
|
+
return result.data;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//package the interruption inputs
|
|
102
|
+
const interruptionRegistry = store.get('interruptionRegistry');
|
|
72
103
|
const workflowId = store.get('workflowId');
|
|
104
|
+
const originJobId = store.get('originJobId');
|
|
73
105
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
74
|
-
const workflowTrace = store.get('workflowTrace');
|
|
75
|
-
const workflowSpan = store.get('workflowSpan');
|
|
76
|
-
const COUNTER = store.get('counter');
|
|
77
|
-
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
78
|
-
const sessionId = `-start${workflowDimension}-${execIndex}-`;
|
|
79
|
-
const replay = store.get('replay');
|
|
80
|
-
if (sessionId in replay) {
|
|
81
|
-
return replay[sessionId];
|
|
82
|
-
}
|
|
83
|
-
//NOTE: this is the hash prefix; necessary for the search index to locate the entity
|
|
84
106
|
const entityOrEmptyString = options.entity ?? '';
|
|
85
|
-
|
|
107
|
+
const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
|
|
86
108
|
const parentWorkflowId = workflowId;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const client = new client_1.ClientService({
|
|
99
|
-
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
100
|
-
});
|
|
101
|
-
await client.workflow.start({
|
|
102
|
-
...options,
|
|
103
|
-
namespace,
|
|
104
|
-
workflowId: childJobId,
|
|
109
|
+
const taskQueueName = options.entity ?? options.taskQueue;
|
|
110
|
+
const workflowName = options.entity ?? options.workflowName;
|
|
111
|
+
const workflowTopic = `${taskQueueName}-${workflowName}`;
|
|
112
|
+
const interruptionMessage = {
|
|
113
|
+
arguments: [...(options.args || [])],
|
|
114
|
+
await: options?.await ?? true,
|
|
115
|
+
backoffCoefficient: options?.config?.backoffCoefficient ?? enums_1.HMSH_DURABLE_EXP_BACKOFF,
|
|
116
|
+
index: execIndex,
|
|
117
|
+
maximumAttempts: options?.config?.maximumAttempts ?? enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
|
|
118
|
+
maximumInterval: (0, ms_1.default)(options?.config?.maximumInterval ?? enums_1.HMSH_DURABLE_MAX_INTERVAL) / 1000,
|
|
119
|
+
originJobId: originJobId ?? workflowId,
|
|
105
120
|
parentWorkflowId,
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
workflowDimension: workflowDimension,
|
|
122
|
+
workflowId: childJobId,
|
|
123
|
+
workflowTopic,
|
|
124
|
+
};
|
|
125
|
+
//push the packaged inputs to the registry
|
|
126
|
+
interruptionRegistry.push({
|
|
127
|
+
code: enums_1.HMSH_CODE_DURABLE_CHILD,
|
|
128
|
+
...interruptionMessage,
|
|
108
129
|
});
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
//ASYNC
|
|
131
|
+
//sleep (allow others to be packaged / registered) and throw the error
|
|
132
|
+
await (0, utils_1.sleepFor)(0);
|
|
133
|
+
throw new errors_1.DurableChildError(interruptionMessage);
|
|
111
134
|
}
|
|
112
135
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* should be a key-value pair of activity names and their respective
|
|
118
|
-
* functions. This is typically done by importing the activities.
|
|
136
|
+
* Spawns a child workflow and returns the child Job ID.
|
|
137
|
+
* This method guarantees the spawned child has reserved the Job ID,
|
|
138
|
+
* returning a 'DuplicateJobError' error if not. Otherwise,
|
|
139
|
+
* this is a fire-and-forget method.
|
|
119
140
|
*
|
|
141
|
+
* @param {WorkflowOptions} options - the workflow options
|
|
142
|
+
* @returns {Promise<string>} - the childJobId
|
|
143
|
+
* @example
|
|
144
|
+
* const childJobId = await Durable.workflow.startChild({ ...options });
|
|
145
|
+
*/
|
|
146
|
+
static async startChild(options) {
|
|
147
|
+
return this.execChild({ ...options, await: false });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Wraps activities in a proxy that durably runs/re-runs them to completion.
|
|
151
|
+
* TODO: verify that activities do not collide if named same on same server but bound to different workflows
|
|
152
|
+
*
|
|
153
|
+
* @param {ActivityConfig} options - the activity configuration
|
|
154
|
+
* that will be used to wrap the activities.
|
|
120
155
|
* @returns {ProxyType<ACT>} - a proxy object with the same keys as the
|
|
121
156
|
* activities object, but with the values replaced by a wrapped function
|
|
157
|
+
*
|
|
122
158
|
* @example
|
|
123
159
|
* // import the activities
|
|
124
160
|
* import * as activities from './activities';
|
|
@@ -141,6 +177,69 @@ class WorkflowService {
|
|
|
141
177
|
}
|
|
142
178
|
return proxy;
|
|
143
179
|
}
|
|
180
|
+
static wrapActivity(activityName, options) {
|
|
181
|
+
return async function () {
|
|
182
|
+
//SYNC
|
|
183
|
+
//check if the activity already ran
|
|
184
|
+
const [didRun, execIndex, result] = await WorkflowService.didRun('proxy');
|
|
185
|
+
if (didRun) {
|
|
186
|
+
if (result?.$error) {
|
|
187
|
+
if (options?.retryPolicy?.throwOnError !== false) {
|
|
188
|
+
//rethrow remote execution error (simulates throw)
|
|
189
|
+
const code = result.$error.code;
|
|
190
|
+
const message = result.$error.message;
|
|
191
|
+
const stack = result.$error.stack;
|
|
192
|
+
if (code === enums_1.HMSH_CODE_DURABLE_FATAL) {
|
|
193
|
+
throw new errors_1.DurableFatalError(message, stack);
|
|
194
|
+
}
|
|
195
|
+
else if (code == enums_1.HMSH_CODE_DURABLE_MAXED) {
|
|
196
|
+
throw new errors_1.DurableMaxedError(message, stack);
|
|
197
|
+
}
|
|
198
|
+
else if (code == enums_1.HMSH_CODE_DURABLE_TIMEOUT) {
|
|
199
|
+
throw new errors_1.DurableTimeoutError(message, stack);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return result.$error;
|
|
203
|
+
}
|
|
204
|
+
return result.data;
|
|
205
|
+
}
|
|
206
|
+
//package the interruption inputs
|
|
207
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
208
|
+
const interruptionRegistry = store.get('interruptionRegistry');
|
|
209
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
210
|
+
const workflowId = store.get('workflowId');
|
|
211
|
+
const originJobId = store.get('originJobId');
|
|
212
|
+
const workflowTopic = store.get('workflowTopic');
|
|
213
|
+
const activityTopic = `${workflowTopic}-activity`;
|
|
214
|
+
const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
|
|
215
|
+
let maximumInterval;
|
|
216
|
+
if (options.retryPolicy?.maximumInterval) {
|
|
217
|
+
maximumInterval = (0, ms_1.default)(options.retryPolicy.maximumInterval) / 1000;
|
|
218
|
+
}
|
|
219
|
+
const interruptionMessage = {
|
|
220
|
+
arguments: Array.from(arguments),
|
|
221
|
+
workflowDimension: workflowDimension,
|
|
222
|
+
index: execIndex,
|
|
223
|
+
originJobId: originJobId || workflowId,
|
|
224
|
+
parentWorkflowId: workflowId,
|
|
225
|
+
workflowId: activityJobId,
|
|
226
|
+
workflowTopic: activityTopic,
|
|
227
|
+
activityName,
|
|
228
|
+
backoffCoefficient: options?.retryPolicy?.backoffCoefficient ?? undefined,
|
|
229
|
+
maximumAttempts: options?.retryPolicy?.maximumAttempts ?? undefined,
|
|
230
|
+
maximumInterval: maximumInterval ?? undefined,
|
|
231
|
+
};
|
|
232
|
+
//push the packaged inputs to the registry
|
|
233
|
+
interruptionRegistry.push({
|
|
234
|
+
code: enums_1.HMSH_CODE_DURABLE_PROXY,
|
|
235
|
+
...interruptionMessage,
|
|
236
|
+
});
|
|
237
|
+
//ASYNC
|
|
238
|
+
//sleep (allow others to be packaged / registered) and throw the error
|
|
239
|
+
await (0, utils_1.sleepFor)(0);
|
|
240
|
+
throw new errors_1.DurableProxyError(interruptionMessage);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
144
243
|
/**
|
|
145
244
|
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
146
245
|
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
@@ -160,41 +259,19 @@ class WorkflowService {
|
|
|
160
259
|
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
161
260
|
}
|
|
162
261
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
262
|
+
* Returns the synchronous output from the activity (replay)
|
|
263
|
+
* if available locally
|
|
264
|
+
* @param {string} prefix - one of: proxy, child, start, wait etc
|
|
265
|
+
* @returns
|
|
165
266
|
*/
|
|
166
|
-
static async
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* @returns {WorkflowContext} - the current workflow context
|
|
175
|
-
*/
|
|
176
|
-
static getContext() {
|
|
177
|
-
const store = storage_1.asyncLocalStorage.getStore();
|
|
178
|
-
const workflowId = store.get('workflowId');
|
|
179
|
-
const replay = store.get('replay');
|
|
180
|
-
const cursor = store.get('cursor');
|
|
181
|
-
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
182
|
-
const workflowTopic = store.get('workflowTopic');
|
|
183
|
-
const namespace = store.get('namespace');
|
|
184
|
-
const workflowTrace = store.get('workflowTrace');
|
|
185
|
-
const workflowSpan = store.get('workflowSpan');
|
|
186
|
-
const COUNTER = store.get('counter');
|
|
187
|
-
return {
|
|
188
|
-
counter: COUNTER.counter,
|
|
189
|
-
cursor,
|
|
190
|
-
namespace,
|
|
191
|
-
replay,
|
|
192
|
-
workflowId,
|
|
193
|
-
workflowDimension,
|
|
194
|
-
workflowTopic,
|
|
195
|
-
workflowTrace,
|
|
196
|
-
workflowSpan,
|
|
197
|
-
};
|
|
267
|
+
static async didRun(prefix) {
|
|
268
|
+
const { COUNTER, replay, workflowDimension, } = WorkflowService.getContext();
|
|
269
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
270
|
+
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
271
|
+
if (sessionId in replay) {
|
|
272
|
+
return [true, execIndex, serializer_1.SerializerService.fromString(replay[sessionId])];
|
|
273
|
+
}
|
|
274
|
+
return [false, execIndex, null];
|
|
198
275
|
}
|
|
199
276
|
/**
|
|
200
277
|
* Those methods that may only be called once must be protected by flagging
|
|
@@ -234,8 +311,8 @@ class WorkflowService {
|
|
|
234
311
|
return (0, utils_1.deterministicRandom)(seed);
|
|
235
312
|
}
|
|
236
313
|
/**
|
|
237
|
-
* Sends signal data into any other paused thread (which is
|
|
238
|
-
* awaiting the signal)
|
|
314
|
+
* Sends signal data into any other paused thread (which is currently
|
|
315
|
+
* awaiting the signal)
|
|
239
316
|
* @param {string} signalId - the signal id
|
|
240
317
|
* @param {Record<any, any>} data - the signal data
|
|
241
318
|
* @returns {Promise<string>} - the stream id
|
|
@@ -245,8 +322,6 @@ class WorkflowService {
|
|
|
245
322
|
const workflowTopic = store.get('workflowTopic');
|
|
246
323
|
const namespace = store.get('namespace');
|
|
247
324
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
248
|
-
//todo: this particular one is better patterned as a get/set,
|
|
249
|
-
//since the receipt is a meaningful string (the stream id)
|
|
250
325
|
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
|
|
251
326
|
return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
|
|
252
327
|
}
|
|
@@ -258,50 +333,39 @@ class WorkflowService {
|
|
|
258
333
|
* @param {HookOptions} options - the hook options
|
|
259
334
|
*/
|
|
260
335
|
static async hook(options) {
|
|
261
|
-
const
|
|
262
|
-
const workflowTopic = store.get('workflowTopic');
|
|
263
|
-
const namespace = store.get('namespace');
|
|
336
|
+
const { workflowId, namespace, workflowTopic, } = WorkflowService.getContext();
|
|
264
337
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
265
338
|
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
let workflowTopic = store.get('workflowTopic');
|
|
339
|
+
const targetWorkflowId = options.workflowId ?? workflowId;
|
|
340
|
+
let targetTopic;
|
|
269
341
|
if (options.entity || (options.taskQueue && options.workflowName)) {
|
|
270
|
-
|
|
271
|
-
}
|
|
342
|
+
targetTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
targetTopic = workflowTopic;
|
|
346
|
+
}
|
|
272
347
|
const payload = {
|
|
273
348
|
arguments: [...options.args],
|
|
274
|
-
id:
|
|
349
|
+
id: targetWorkflowId,
|
|
275
350
|
workflowTopic,
|
|
276
|
-
backoffCoefficient: options.config?.backoffCoefficient ||
|
|
351
|
+
backoffCoefficient: options.config?.backoffCoefficient || enums_1.HMSH_DURABLE_EXP_BACKOFF,
|
|
277
352
|
};
|
|
278
353
|
return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
|
|
279
354
|
}
|
|
280
355
|
}
|
|
281
|
-
static getLocalState() {
|
|
282
|
-
const store = storage_1.asyncLocalStorage.getStore();
|
|
283
|
-
return {
|
|
284
|
-
workflowId: store.get('workflowId'),
|
|
285
|
-
namespace: store.get('namespace'),
|
|
286
|
-
workflowTopic: store.get('workflowTopic'),
|
|
287
|
-
workflowDimension: store.get('workflowDimension') ?? '',
|
|
288
|
-
counter: store.get('counter'),
|
|
289
|
-
replay: store.get('replay'),
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
356
|
/**
|
|
293
357
|
* Executes a function once and caches the result. If the function is called
|
|
294
358
|
* again, the cached result is returned. This is useful for wrapping
|
|
295
359
|
* expensive activity calls that should only be run once, but which might
|
|
296
|
-
* not require the
|
|
360
|
+
* not require the cost and safety provided by proxyActivities.
|
|
297
361
|
* @template T - the result type
|
|
298
362
|
*/
|
|
299
363
|
static async once(fn, ...args) {
|
|
300
|
-
const {
|
|
364
|
+
const { COUNTER, namespace, workflowId, workflowTopic, workflowDimension, replay, } = WorkflowService.getContext();
|
|
301
365
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
302
366
|
const sessionId = `-once${workflowDimension}-${execIndex}-`;
|
|
303
367
|
if (sessionId in replay) {
|
|
304
|
-
return
|
|
368
|
+
return serializer_1.SerializerService.fromString(replay[sessionId]).data;
|
|
305
369
|
}
|
|
306
370
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
307
371
|
const keyParams = {
|
|
@@ -309,20 +373,19 @@ class WorkflowService {
|
|
|
309
373
|
jobId: workflowId
|
|
310
374
|
};
|
|
311
375
|
const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
312
|
-
const
|
|
313
|
-
if (value) {
|
|
314
|
-
return JSON.parse(value);
|
|
315
|
-
}
|
|
376
|
+
const t1 = new Date();
|
|
316
377
|
const response = await fn(...args);
|
|
317
|
-
|
|
378
|
+
const t2 = new Date();
|
|
379
|
+
const payload = {
|
|
380
|
+
data: response,
|
|
381
|
+
ac: (0, utils_1.formatISODate)(t1),
|
|
382
|
+
au: (0, utils_1.formatISODate)(t2),
|
|
383
|
+
};
|
|
384
|
+
await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, serializer_1.SerializerService.toString(payload));
|
|
318
385
|
return response;
|
|
319
386
|
}
|
|
320
387
|
/**
|
|
321
388
|
* Interrupts a running job
|
|
322
|
-
*
|
|
323
|
-
* @param {string} jobId - the target job id
|
|
324
|
-
* @param {JobInterruptOptions} options - the interrupt options
|
|
325
|
-
* @returns {Promise<string>} - the stream id
|
|
326
389
|
*/
|
|
327
390
|
static async interrupt(jobId, options = {}) {
|
|
328
391
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
@@ -336,140 +399,73 @@ class WorkflowService {
|
|
|
336
399
|
/**
|
|
337
400
|
* Sleeps the workflow for a duration. As the function is reentrant,
|
|
338
401
|
* upon reentry, the function will traverse prior execution paths up
|
|
339
|
-
* until the sleep command and then resume execution
|
|
340
|
-
* @param {string} duration - for
|
|
341
|
-
* @returns {Promise<number>}
|
|
402
|
+
* until the sleep command and then resume execution thereafter.
|
|
403
|
+
* @param {string} duration - See the `ms` package for syntax examples: '1 minute', '2 hours', '3 days'
|
|
404
|
+
* @returns {Promise<number>} - resolved duration in seconds
|
|
342
405
|
*/
|
|
343
406
|
static async sleepFor(duration) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'sleep')) {
|
|
350
|
-
const workflowId = store.get('workflowId');
|
|
351
|
-
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
352
|
-
const COUNTER = store.get('counter');
|
|
353
|
-
const execIndex = COUNTER.counter;
|
|
354
|
-
// spawn a new sleep job if error code 592 is thrown by the worker
|
|
355
|
-
// NOTE: If this message appears in the stack trace, the `.sleepFor()` method in your workflow code was NOT awaited.
|
|
356
|
-
throw new errors_1.DurableSleepForError(workflowId, seconds, execIndex, workflowDimension);
|
|
407
|
+
//SYNC
|
|
408
|
+
//return early if this sleep command has already run
|
|
409
|
+
const [didRun, execIndex, result] = await WorkflowService.didRun('sleep');
|
|
410
|
+
if (didRun) {
|
|
411
|
+
return result.duration; //in seconds
|
|
357
412
|
}
|
|
358
|
-
|
|
413
|
+
//package the interruption inputs
|
|
414
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
415
|
+
const interruptionRegistry = store.get('interruptionRegistry');
|
|
416
|
+
const workflowId = store.get('workflowId');
|
|
417
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
418
|
+
const interruptionMessage = {
|
|
419
|
+
workflowId,
|
|
420
|
+
duration: (0, ms_1.default)(duration) / 1000,
|
|
421
|
+
index: execIndex,
|
|
422
|
+
workflowDimension,
|
|
423
|
+
};
|
|
424
|
+
interruptionRegistry.push({
|
|
425
|
+
code: enums_1.HMSH_CODE_DURABLE_SLEEP,
|
|
426
|
+
...interruptionMessage,
|
|
427
|
+
});
|
|
428
|
+
//ASYNC
|
|
429
|
+
//sleep to allow other interruptions to be packaged and registered
|
|
430
|
+
await (0, utils_1.sleepFor)(0);
|
|
431
|
+
// NOTE: If you are reading this in the stack trace, await `sleepFor`
|
|
432
|
+
throw new errors_1.DurableSleepError(interruptionMessage);
|
|
359
433
|
}
|
|
360
434
|
/**
|
|
361
|
-
*
|
|
362
|
-
* @
|
|
363
|
-
* @param {
|
|
364
|
-
* @returns {Promise<
|
|
435
|
+
* Pauses the workflow until `signalId` is received.
|
|
436
|
+
* @template T - the result type
|
|
437
|
+
* @param {string} signalId - a unique, shareable guid (e.g, 'abc123')
|
|
438
|
+
* @returns {Promise<T>}
|
|
439
|
+
* @example
|
|
440
|
+
* const result = await Durable.workflow.waitFor<typeof resultType>('abc123');
|
|
365
441
|
*/
|
|
366
|
-
static async
|
|
442
|
+
static async waitFor(signalId) {
|
|
443
|
+
//SYNC
|
|
444
|
+
//return early if this waitFor command has already run
|
|
445
|
+
const [didRun, execIndex, result] = await WorkflowService.didRun('wait');
|
|
446
|
+
if (didRun) {
|
|
447
|
+
return result.data.data;
|
|
448
|
+
}
|
|
449
|
+
//package the interruption inputs
|
|
367
450
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
368
|
-
const
|
|
451
|
+
const interruptionRegistry = store.get('interruptionRegistry');
|
|
369
452
|
const workflowId = store.get('workflowId');
|
|
370
|
-
const workflowTopic = store.get('workflowTopic');
|
|
371
453
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const signalResults = [];
|
|
378
|
-
for (const signal of signals) {
|
|
379
|
-
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
380
|
-
const wfsJobId = `-${workflowId}-$wfs${workflowDimension}-${execIndex}`;
|
|
381
|
-
try {
|
|
382
|
-
if (allAreComplete) {
|
|
383
|
-
const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
|
|
384
|
-
if (state.data?.signalData) {
|
|
385
|
-
//user data is nested to isolate from the signal id; unpackage it
|
|
386
|
-
const signalData = state.data.signalData;
|
|
387
|
-
signalResults.push(signalData.data);
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
allAreComplete = false;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
signalResults.push({ signal, index: execIndex });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
//todo: options.startToCloseTimeout
|
|
399
|
-
allAreComplete = false;
|
|
400
|
-
noneAreComplete = true;
|
|
401
|
-
signalResults.push({ signal, index: execIndex });
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
;
|
|
405
|
-
if (allAreComplete) {
|
|
406
|
-
return signalResults;
|
|
407
|
-
}
|
|
408
|
-
else if (noneAreComplete) {
|
|
409
|
-
//this error is caught by the workflow runner
|
|
410
|
-
//it is then returned as the workflow result (594)
|
|
411
|
-
throw new errors_1.DurableWaitForSignalError(workflowId, signalResults);
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
//this error happens when a signal is received but others are still open
|
|
415
|
-
throw new errors_1.DurableIncompleteSignalError(workflowId);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
static wrapActivity(activityName, options) {
|
|
419
|
-
return async function () {
|
|
420
|
-
const store = storage_1.asyncLocalStorage.getStore();
|
|
421
|
-
const COUNTER = store.get('counter');
|
|
422
|
-
//increment by state (not value) to avoid race conditions
|
|
423
|
-
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
424
|
-
const workflowId = store.get('workflowId');
|
|
425
|
-
const originJobId = store.get('originJobId');
|
|
426
|
-
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
427
|
-
const workflowTopic = store.get('workflowTopic');
|
|
428
|
-
const trc = store.get('workflowTrace');
|
|
429
|
-
const spn = store.get('workflowSpan');
|
|
430
|
-
const namespace = store.get('namespace');
|
|
431
|
-
const activityTopic = `${workflowTopic}-activity`;
|
|
432
|
-
const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
|
|
433
|
-
let activityState;
|
|
434
|
-
try {
|
|
435
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
|
|
436
|
-
activityState = await hotMeshClient.getState(`${hotMeshClient.appId}.activity.execute`, activityJobId);
|
|
437
|
-
if (activityState.metadata.err) {
|
|
438
|
-
await hotMeshClient.scrub(activityJobId);
|
|
439
|
-
throw new Error(activityState.metadata.err);
|
|
440
|
-
}
|
|
441
|
-
else if (activityState.metadata.js === 0 || activityState.data?.done) {
|
|
442
|
-
return activityState.data?.response;
|
|
443
|
-
}
|
|
444
|
-
//one time subscription
|
|
445
|
-
return await new Promise((resolve, reject) => {
|
|
446
|
-
hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
|
|
447
|
-
const response = message.data?.response;
|
|
448
|
-
hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
|
|
449
|
-
// Resolve the Promise when the callback is triggered with a message
|
|
450
|
-
resolve(response);
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
catch (e) {
|
|
455
|
-
//expected; thrown by `getState` when the job cannot be found
|
|
456
|
-
const duration = (0, ms_1.default)(options?.startToCloseTimeout || '1 minute');
|
|
457
|
-
const payload = {
|
|
458
|
-
arguments: Array.from(arguments),
|
|
459
|
-
//when the origin job is removed
|
|
460
|
-
originJobId: originJobId ?? workflowId,
|
|
461
|
-
parentWorkflowId: workflowId,
|
|
462
|
-
workflowId: activityJobId,
|
|
463
|
-
workflowTopic: activityTopic,
|
|
464
|
-
activityName,
|
|
465
|
-
};
|
|
466
|
-
//start the job
|
|
467
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
|
|
468
|
-
const context = { metadata: { trc, spn }, data: {} };
|
|
469
|
-
const jobOutput = await hotMeshClient.pubsub(`${hotMeshClient.appId}.activity.execute`, payload, context, duration);
|
|
470
|
-
return jobOutput.data.response;
|
|
471
|
-
}
|
|
454
|
+
const interruptionMessage = {
|
|
455
|
+
workflowId,
|
|
456
|
+
signalId,
|
|
457
|
+
index: execIndex,
|
|
458
|
+
workflowDimension,
|
|
472
459
|
};
|
|
460
|
+
interruptionRegistry.push({
|
|
461
|
+
code: enums_1.HMSH_CODE_DURABLE_WAIT,
|
|
462
|
+
...interruptionMessage,
|
|
463
|
+
});
|
|
464
|
+
//ASYNC
|
|
465
|
+
//sleep to allow other interruptions to be packaged and registered
|
|
466
|
+
await (0, utils_1.sleepFor)(0);
|
|
467
|
+
// NOTE: If you are reading this in the stack trace, await `waitFor`
|
|
468
|
+
throw new errors_1.DurableWaitForError(interruptionMessage);
|
|
473
469
|
}
|
|
474
470
|
}
|
|
475
471
|
exports.WorkflowService = WorkflowService;
|