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