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