@hotmeshio/hotmesh 0.0.12 → 0.0.14
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 +2 -2
- package/build/modules/errors.d.ts +22 -1
- package/build/modules/errors.js +28 -1
- package/build/modules/utils.d.ts +2 -1
- package/build/modules/utils.js +5 -1
- package/build/package.json +7 -2
- package/build/services/activities/activity.d.ts +2 -0
- package/build/services/activities/activity.js +16 -10
- package/build/services/activities/await.d.ts +2 -6
- package/build/services/activities/await.js +12 -75
- package/build/services/activities/cycle.js +2 -2
- package/build/services/activities/index.d.ts +2 -2
- package/build/services/activities/index.js +2 -2
- package/build/services/activities/signal.d.ts +16 -0
- package/build/services/activities/signal.js +94 -0
- package/build/services/activities/trigger.js +4 -3
- package/build/services/activities/worker.d.ts +2 -1
- package/build/services/activities/worker.js +11 -6
- package/build/services/compiler/deployer.js +3 -1
- package/build/services/durable/client.d.ts +3 -2
- package/build/services/durable/client.js +39 -21
- package/build/services/durable/factory.d.ts +22 -18
- package/build/services/durable/factory.js +722 -50
- package/build/services/durable/handle.d.ts +1 -0
- package/build/services/durable/handle.js +5 -1
- package/build/services/durable/worker.d.ts +3 -8
- package/build/services/durable/worker.js +75 -73
- package/build/services/durable/workflow.d.ts +5 -0
- package/build/services/durable/workflow.js +93 -24
- package/build/services/engine/index.d.ts +6 -6
- package/build/services/engine/index.js +25 -15
- package/build/services/hotmesh/index.d.ts +2 -1
- package/build/services/hotmesh/index.js +3 -1
- package/build/services/mapper/index.js +1 -1
- package/build/services/pipe/functions/array.d.ts +1 -0
- package/build/services/pipe/functions/array.js +3 -0
- package/build/services/reporter/index.js +9 -2
- package/build/services/signaler/store.js +8 -3
- package/build/services/signaler/stream.js +3 -3
- package/build/services/store/clients/ioredis.js +15 -15
- package/build/services/store/clients/redis.js +18 -18
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/index.js +11 -3
- package/build/services/task/index.js +3 -3
- package/build/types/activity.d.ts +15 -6
- package/build/types/durable.d.ts +15 -2
- package/build/types/index.d.ts +2 -2
- package/build/types/stats.d.ts +1 -0
- package/modules/errors.ts +35 -0
- package/modules/utils.ts +5 -1
- package/package.json +7 -2
- package/services/activities/activity.ts +19 -9
- package/services/activities/await.ts +14 -90
- package/services/activities/cycle.ts +2 -2
- package/services/activities/index.ts +2 -2
- package/services/activities/signal.ts +124 -0
- package/services/activities/trigger.ts +4 -3
- package/services/activities/worker.ts +13 -13
- package/services/compiler/deployer.ts +3 -1
- package/services/durable/client.ts +48 -23
- package/services/durable/factory.ts +723 -49
- package/services/durable/handle.ts +6 -1
- package/services/durable/worker.ts +92 -79
- package/services/durable/workflow.ts +95 -25
- package/services/engine/index.ts +33 -24
- package/services/hotmesh/index.ts +7 -4
- package/services/mapper/index.ts +1 -1
- package/services/pipe/functions/array.ts +4 -0
- package/services/reporter/index.ts +10 -2
- package/services/signaler/store.ts +8 -3
- package/services/signaler/stream.ts +3 -3
- package/services/store/clients/ioredis.ts +15 -15
- package/services/store/clients/redis.ts +18 -18
- package/services/store/index.ts +12 -3
- package/services/task/index.ts +3 -3
- package/types/activity.ts +16 -7
- package/types/durable.ts +18 -1
- package/types/index.ts +2 -1
- package/types/stats.ts +1 -0
- package/build/services/activities/emit.d.ts +0 -9
- package/build/services/activities/emit.js +0 -13
- package/services/activities/emit.ts +0 -25
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JobOutput } from '../../types/job';
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
+
import { PUBLISHES_TOPIC } from './factory';
|
|
3
4
|
|
|
4
5
|
export class WorkflowHandleService {
|
|
5
6
|
hotMesh: HotMesh;
|
|
@@ -12,9 +13,13 @@ export class WorkflowHandleService {
|
|
|
12
13
|
this.hotMesh = hotMesh;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
async signal(signalId: string, data: Record<any, any>): Promise<void> {
|
|
17
|
+
await this.hotMesh.hook('durable.wfs.signal', { id: signalId, data });
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
async result(): Promise<any> {
|
|
16
21
|
let status = await this.hotMesh.getStatus(this.workflowId);
|
|
17
|
-
const topic = `${
|
|
22
|
+
const topic = `${PUBLISHES_TOPIC}.${this.workflowId}`;
|
|
18
23
|
|
|
19
24
|
return new Promise((resolve, reject) => {
|
|
20
25
|
let isResolved = false;
|
|
@@ -1,43 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DurableFatalError,
|
|
3
|
+
DurableIncompleteSignalError,
|
|
4
|
+
DurableMaxedError,
|
|
5
|
+
DurableRetryError,
|
|
6
|
+
DurableSleepError,
|
|
7
|
+
DurableTimeoutError,
|
|
8
|
+
DurableWaitForSignalError} from '../../modules/errors';
|
|
1
9
|
import { asyncLocalStorage } from './asyncLocalStorage';
|
|
10
|
+
import { APP_ID, APP_VERSION, getWorkflowYAML } from './factory';
|
|
2
11
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { ActivityWorkflowDataType,
|
|
12
|
+
import {
|
|
13
|
+
ActivityWorkflowDataType,
|
|
6
14
|
Connection,
|
|
7
15
|
Registry,
|
|
8
16
|
WorkerConfig,
|
|
9
17
|
WorkerOptions,
|
|
10
18
|
WorkflowDataType } from "../../types/durable";
|
|
11
|
-
import {
|
|
19
|
+
import { RedisClass, RedisOptions } from '../../types/redis';
|
|
12
20
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
DurableTimeoutError } from '../../modules/errors';
|
|
21
|
+
StreamData,
|
|
22
|
+
StreamDataResponse,
|
|
23
|
+
StreamStatus } from '../../types/stream';
|
|
17
24
|
|
|
18
25
|
/*
|
|
19
26
|
Here is an example of how the methods in this file are used:
|
|
20
27
|
|
|
21
28
|
./worker.ts
|
|
22
29
|
|
|
23
|
-
import { Durable
|
|
30
|
+
import { Durable } from '@hotmeshio/hotmesh';
|
|
24
31
|
import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
|
|
25
32
|
|
|
26
33
|
import * as workflows from './workflows';
|
|
27
34
|
|
|
28
35
|
async function run() {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const worker = await Durable.Worker.create({
|
|
37
|
+
connection: {
|
|
38
|
+
class: Redis,
|
|
39
|
+
options: {
|
|
40
|
+
host: 'localhost',
|
|
41
|
+
port: 6379,
|
|
42
|
+
},
|
|
34
43
|
},
|
|
35
|
-
});
|
|
36
|
-
const worker = await Worker.create({
|
|
37
|
-
connection,
|
|
38
44
|
taskQueue: 'hello-world',
|
|
39
45
|
workflow: workflows.example,
|
|
40
|
-
activities,
|
|
41
46
|
});
|
|
42
47
|
await worker.run();
|
|
43
48
|
}
|
|
@@ -59,30 +64,29 @@ export class WorkerService {
|
|
|
59
64
|
if (WorkerService.instances.has(worflowTopic)) {
|
|
60
65
|
return await WorkerService.instances.get(worflowTopic);
|
|
61
66
|
}
|
|
62
|
-
const
|
|
63
|
-
appId:
|
|
67
|
+
const hotMeshClient = HotMesh.init({
|
|
68
|
+
appId: APP_ID,
|
|
64
69
|
engine: { redis: { ...WorkerService.connection } }
|
|
65
70
|
});
|
|
66
|
-
WorkerService.instances.set(worflowTopic,
|
|
67
|
-
await WorkerService.activateWorkflow(await
|
|
68
|
-
return
|
|
71
|
+
WorkerService.instances.set(worflowTopic, hotMeshClient);
|
|
72
|
+
await WorkerService.activateWorkflow(await hotMeshClient);
|
|
73
|
+
return hotMeshClient;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
static async activateWorkflow(hotMesh: HotMesh
|
|
72
|
-
const
|
|
73
|
-
const app = await hotMesh.engine.store.getApp(topic);
|
|
76
|
+
static async activateWorkflow(hotMesh: HotMesh) {
|
|
77
|
+
const app = await hotMesh.engine.store.getApp(APP_ID);
|
|
74
78
|
const appVersion = app?.version;
|
|
75
79
|
if(!appVersion) {
|
|
76
80
|
try {
|
|
77
|
-
await hotMesh.deploy(
|
|
78
|
-
await hotMesh.activate(
|
|
81
|
+
await hotMesh.deploy(getWorkflowYAML(APP_ID, APP_VERSION));
|
|
82
|
+
await hotMesh.activate(APP_VERSION);
|
|
79
83
|
} catch (err) {
|
|
80
84
|
hotMesh.engine.logger.error('durable-worker-deploy-activate-err', err);
|
|
81
85
|
throw err;
|
|
82
86
|
}
|
|
83
87
|
} else if(app && !app.active) {
|
|
84
88
|
try {
|
|
85
|
-
await hotMesh.activate(
|
|
89
|
+
await hotMesh.activate(APP_VERSION);
|
|
86
90
|
} catch (err) {
|
|
87
91
|
hotMesh.engine.logger.error('durable-worker-activate-err', err);
|
|
88
92
|
throw err;
|
|
@@ -91,10 +95,6 @@ export class WorkerService {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/**
|
|
94
|
-
* The `worker` calls `registerActivities` immediately BEFORE
|
|
95
|
-
* dynamically importing the user's workflow module. That file
|
|
96
|
-
* contains a call, `proxyActivities`, which needs this info.
|
|
97
|
-
*
|
|
98
98
|
* NOTE: Because the worker imports the workflows dynamically AFTER
|
|
99
99
|
* the activities are loaded, there will be items in the registry,
|
|
100
100
|
* allowing proxyActivities to succeed.
|
|
@@ -115,7 +115,6 @@ export class WorkerService {
|
|
|
115
115
|
static async create(config: WorkerConfig) {
|
|
116
116
|
//always call `registerActivities` before `import`
|
|
117
117
|
WorkerService.connection = config.connection;
|
|
118
|
-
//user can provide the workflow file directly
|
|
119
118
|
const workflow = config.workflow;
|
|
120
119
|
const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
|
|
121
120
|
const baseTopic = `${config.taskQueue}-${workflowFunctionName}`;
|
|
@@ -124,10 +123,9 @@ export class WorkerService {
|
|
|
124
123
|
|
|
125
124
|
//initialize supporting workflows
|
|
126
125
|
const worker = new WorkerService();
|
|
127
|
-
worker.activityRunner = await worker.
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
-
await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML, config.options);
|
|
126
|
+
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
127
|
+
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
128
|
+
await WorkerService.activateWorkflow(worker.workflowRunner);
|
|
131
129
|
return worker;
|
|
132
130
|
}
|
|
133
131
|
|
|
@@ -147,13 +145,13 @@ export class WorkerService {
|
|
|
147
145
|
this.workflowRunner.engine.logger.info('WorkerService is running');
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
async
|
|
148
|
+
async initActivityWorker(config: WorkerConfig, activityTopic: string): Promise<HotMesh> {
|
|
151
149
|
const redisConfig = {
|
|
152
150
|
class: config.connection.class as RedisClass,
|
|
153
151
|
options: config.connection.options as RedisOptions
|
|
154
152
|
};
|
|
155
|
-
const
|
|
156
|
-
appId:
|
|
153
|
+
const hotMeshWorker = await HotMesh.init({
|
|
154
|
+
appId: APP_ID,
|
|
157
155
|
engine: { redis: redisConfig },
|
|
158
156
|
workers: [
|
|
159
157
|
{ topic: activityTopic,
|
|
@@ -162,8 +160,8 @@ export class WorkerService {
|
|
|
162
160
|
}
|
|
163
161
|
]
|
|
164
162
|
});
|
|
165
|
-
WorkerService.instances.set(activityTopic,
|
|
166
|
-
return
|
|
163
|
+
WorkerService.instances.set(activityTopic, hotMeshWorker);
|
|
164
|
+
return hotMeshWorker;
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
wrapActivityFunctions(): Function {
|
|
@@ -197,45 +195,22 @@ export class WorkerService {
|
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
197
|
|
|
200
|
-
async
|
|
201
|
-
const version = '1';
|
|
202
|
-
const app = await hotMesh.engine.store.getApp(activityTopic);
|
|
203
|
-
const appVersion = app?.version as unknown as number;
|
|
204
|
-
if(isNaN(appVersion)) {
|
|
205
|
-
try {
|
|
206
|
-
await hotMesh.deploy(getActivityYAML(activityTopic, version));
|
|
207
|
-
await hotMesh.activate(version);
|
|
208
|
-
} catch (err) {
|
|
209
|
-
hotMesh.engine.logger.error('durable-worker-activity-deploy-activate-error', err);
|
|
210
|
-
throw err;
|
|
211
|
-
}
|
|
212
|
-
} else if(app && !app.active) {
|
|
213
|
-
try {
|
|
214
|
-
await hotMesh.activate(version);
|
|
215
|
-
} catch (err) {
|
|
216
|
-
hotMesh.engine.logger.error('durable-worker-activity-activate-err', err);
|
|
217
|
-
throw err;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async initWorkerWorkflow(config: WorkerConfig, workflowTopic: string, workflowFunction: Function): Promise<HotMesh> {
|
|
198
|
+
async initWorkflowWorker(config: WorkerConfig, workflowTopic: string, workflowFunction: Function): Promise<HotMesh> {
|
|
223
199
|
const redisConfig = {
|
|
224
200
|
class: config.connection.class as RedisClass,
|
|
225
201
|
options: config.connection.options as RedisOptions
|
|
226
202
|
};
|
|
227
|
-
const
|
|
228
|
-
appId:
|
|
203
|
+
const hotMeshWorker = await HotMesh.init({
|
|
204
|
+
appId: APP_ID,
|
|
229
205
|
engine: { redis: redisConfig },
|
|
230
|
-
workers: [
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
]
|
|
206
|
+
workers: [{
|
|
207
|
+
topic: workflowTopic,
|
|
208
|
+
redis: redisConfig,
|
|
209
|
+
callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
|
|
210
|
+
}]
|
|
236
211
|
});
|
|
237
|
-
WorkerService.instances.set(workflowTopic,
|
|
238
|
-
return
|
|
212
|
+
WorkerService.instances.set(workflowTopic, hotMeshWorker);
|
|
213
|
+
return hotMeshWorker;
|
|
239
214
|
}
|
|
240
215
|
|
|
241
216
|
static Context = {
|
|
@@ -249,11 +224,11 @@ export class WorkerService {
|
|
|
249
224
|
|
|
250
225
|
wrapWorkflowFunction(workflowFunction: Function, workflowTopic: string): Function {
|
|
251
226
|
return async (data: StreamData): Promise<StreamDataResponse> => {
|
|
227
|
+
const counter = { counter: 0 };
|
|
252
228
|
try {
|
|
253
229
|
//incoming data payload has arguments and workflowId
|
|
254
230
|
const workflowInput = data.data as unknown as WorkflowDataType;
|
|
255
231
|
const context = new Map();
|
|
256
|
-
const counter = { counter: 0 };
|
|
257
232
|
context.set('counter', counter);
|
|
258
233
|
context.set('workflowId', workflowInput.workflowId);
|
|
259
234
|
context.set('workflowTopic', workflowTopic);
|
|
@@ -268,10 +243,48 @@ export class WorkerService {
|
|
|
268
243
|
code: 200,
|
|
269
244
|
status: StreamStatus.SUCCESS,
|
|
270
245
|
metadata: { ...data.metadata },
|
|
271
|
-
data: { response: workflowResponse }
|
|
246
|
+
data: { response: workflowResponse, done: true }
|
|
272
247
|
};
|
|
273
248
|
} catch (err) {
|
|
274
|
-
|
|
249
|
+
|
|
250
|
+
//not an error...just a trigger to sleep
|
|
251
|
+
if (err instanceof DurableSleepError) {
|
|
252
|
+
return {
|
|
253
|
+
status: StreamStatus.SUCCESS,
|
|
254
|
+
code: err.code,
|
|
255
|
+
metadata: { ...data.metadata },
|
|
256
|
+
data: {
|
|
257
|
+
code: err.code,
|
|
258
|
+
message: JSON.stringify({ duration: err.duration, index: err.index }),
|
|
259
|
+
duration: err.duration,
|
|
260
|
+
index: err.index
|
|
261
|
+
}
|
|
262
|
+
} as StreamDataResponse;
|
|
263
|
+
|
|
264
|
+
//not an error...just a trigger to wait for a signal
|
|
265
|
+
} else if (err instanceof DurableWaitForSignalError) {
|
|
266
|
+
return {
|
|
267
|
+
status: StreamStatus.SUCCESS,
|
|
268
|
+
code: err.code,
|
|
269
|
+
metadata: { ...data.metadata },
|
|
270
|
+
data: {
|
|
271
|
+
code: err.code,
|
|
272
|
+
signals: err.signals,
|
|
273
|
+
index: err.signals[0].index
|
|
274
|
+
}
|
|
275
|
+
} as StreamDataResponse;
|
|
276
|
+
|
|
277
|
+
//not an error...still waiting for all the signals to arrive
|
|
278
|
+
} else if (err instanceof DurableIncompleteSignalError) {
|
|
279
|
+
return {
|
|
280
|
+
status: StreamStatus.SUCCESS,
|
|
281
|
+
code: err.code,
|
|
282
|
+
metadata: { ...data.metadata },
|
|
283
|
+
data: { code: err.code }
|
|
284
|
+
} as StreamDataResponse;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// all other errors are fatal (598, 597, 596) or will be retried (599)
|
|
275
288
|
return {
|
|
276
289
|
status: StreamStatus.ERROR,
|
|
277
290
|
code: err.code || new DurableRetryError(err.message).code,
|
|
@@ -6,6 +6,9 @@ import { ClientService as Client } from './client';
|
|
|
6
6
|
import { ConnectionService as Connection } from './connection';
|
|
7
7
|
import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
|
|
8
8
|
import { JobOutput, JobState } from '../../types';
|
|
9
|
+
import { ACTIVITY_PUBLISHES_TOPIC, ACTIVITY_SUBSCRIBES_TOPIC, SLEEP_SUBSCRIBES_TOPIC, WFS_SUBSCRIBES_TOPIC } from './factory';
|
|
10
|
+
import { DurableIncompleteSignalError, DurableSleepError, DurableWaitForSignalError } from '../../modules/errors';
|
|
11
|
+
import { stringify } from 'querystring';
|
|
9
12
|
|
|
10
13
|
/*
|
|
11
14
|
`proxyActivities` returns a wrapped instance of the
|
|
@@ -13,19 +16,6 @@ target activity, so that when the workflow calls a
|
|
|
13
16
|
proxied activity, it is actually calling the proxy
|
|
14
17
|
function, which in turn calls the activity function.
|
|
15
18
|
|
|
16
|
-
`proxyActivities` must be called AFTER the activities
|
|
17
|
-
have been registered in order to work properly.
|
|
18
|
-
If the activities are not already registered,
|
|
19
|
-
`proxyActivities` will throw an error. This is OK.
|
|
20
|
-
|
|
21
|
-
The `client` (client.ts) is equivalent to the
|
|
22
|
-
HotMesh `engine`. The jobs it creates will be
|
|
23
|
-
put in the taskQueue. When the `worker` (worker.ts)
|
|
24
|
-
is eventually initialized (if it happens to be inited later),
|
|
25
|
-
it will see the items in the queue and process them. If it happens
|
|
26
|
-
to already be inited, the jobs will immediately be dequeued and
|
|
27
|
-
processed. In either case, the jobs will be processed.
|
|
28
|
-
|
|
29
19
|
Here is an example of how the methods in this file are used:
|
|
30
20
|
|
|
31
21
|
./workflows.ts
|
|
@@ -50,6 +40,10 @@ export async function example(name: string): Promise<string> {
|
|
|
50
40
|
*/
|
|
51
41
|
|
|
52
42
|
export class WorkflowService {
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Spawn a child workflow. await the result.
|
|
46
|
+
*/
|
|
53
47
|
static async executeChild<T>(options: WorkflowOptions): Promise<T> {
|
|
54
48
|
const store = asyncLocalStorage.getStore();
|
|
55
49
|
if (!store) {
|
|
@@ -62,10 +56,9 @@ export class WorkflowService {
|
|
|
62
56
|
const client = new Client({
|
|
63
57
|
connection: await Connection.connect(WorkerService.connection),
|
|
64
58
|
});
|
|
65
|
-
//todo: allow cross/app callback (pj:'@DURABLE@hello-world@<pjid>'/pa: <paid>/pd: <pdad>)
|
|
66
59
|
const handle = await client.workflow.start({
|
|
67
60
|
...options,
|
|
68
|
-
workflowId: `${workflowId}${options.workflowId}`, //concat
|
|
61
|
+
workflowId: `${workflowId}${options.workflowId}`, //concat (caller MUST PROVIDE)
|
|
69
62
|
workflowTrace,
|
|
70
63
|
workflowSpan,
|
|
71
64
|
});
|
|
@@ -89,6 +82,82 @@ export class WorkflowService {
|
|
|
89
82
|
return proxy;
|
|
90
83
|
}
|
|
91
84
|
|
|
85
|
+
static async sleep(duration: string): Promise<number> {
|
|
86
|
+
const seconds = ms(duration) / 1000;
|
|
87
|
+
|
|
88
|
+
const store = asyncLocalStorage.getStore();
|
|
89
|
+
if (!store) {
|
|
90
|
+
throw new Error('durable-store-not-found');
|
|
91
|
+
}
|
|
92
|
+
const COUNTER = store.get('counter');
|
|
93
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
94
|
+
const workflowId = store.get('workflowId');
|
|
95
|
+
const workflowTopic = store.get('workflowTopic');
|
|
96
|
+
const sleepJobId = `${workflowId}-$sleep-${execIndex}`;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic);
|
|
100
|
+
await hotMeshClient.getState(SLEEP_SUBSCRIBES_TOPIC, sleepJobId);
|
|
101
|
+
//if no error is thrown, we've already slept, return the delay
|
|
102
|
+
return seconds;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
//if an error, the sleep job was not found...rethrow error; sleep job
|
|
105
|
+
// will be automatically created according to the DAG rules (they
|
|
106
|
+
// spawn a new sleep job if error code 595 is thrown by the worker)
|
|
107
|
+
throw new DurableSleepError(workflowId, seconds, execIndex);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static async waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]> {
|
|
112
|
+
const store = asyncLocalStorage.getStore();
|
|
113
|
+
if (!store) {
|
|
114
|
+
throw new Error('durable-store-not-found');
|
|
115
|
+
}
|
|
116
|
+
const COUNTER = store.get('counter');
|
|
117
|
+
const workflowId = store.get('workflowId');
|
|
118
|
+
const workflowTopic = store.get('workflowTopic');
|
|
119
|
+
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic);
|
|
120
|
+
|
|
121
|
+
//iterate the list of signals and check for done
|
|
122
|
+
let allAreComplete = true;
|
|
123
|
+
let noneAreComplete = false;
|
|
124
|
+
const signalResults: any[] = [];
|
|
125
|
+
for (const signal of signals) {
|
|
126
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
127
|
+
const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
|
|
128
|
+
try {
|
|
129
|
+
if (allAreComplete) {
|
|
130
|
+
const state = await hotMeshClient.getState(WFS_SUBSCRIBES_TOPIC, wfsJobId);
|
|
131
|
+
if (state.data?.signalData) {
|
|
132
|
+
//user data is nested to isolate from the signal id; unpackage it
|
|
133
|
+
const signalData = state.data.signalData as { id: string, data: Record<any, any> };
|
|
134
|
+
signalResults.push(signalData.data);
|
|
135
|
+
} else {
|
|
136
|
+
allAreComplete = false;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
signalResults.push({ signal, index: execIndex });
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
//todo: options.startToCloseTimeout
|
|
143
|
+
allAreComplete = false;
|
|
144
|
+
noneAreComplete = true;
|
|
145
|
+
signalResults.push({ signal, index: execIndex });
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if(allAreComplete) {
|
|
150
|
+
return signalResults;
|
|
151
|
+
} else if(noneAreComplete) {
|
|
152
|
+
//this error is caught by the workflow runner
|
|
153
|
+
//it is then returned as the workflow result (594)
|
|
154
|
+
throw new DurableWaitForSignalError(workflowId, signalResults);
|
|
155
|
+
} else {
|
|
156
|
+
//this error happens when a signal is received but others are still open
|
|
157
|
+
throw new DurableIncompleteSignalError(workflowId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
92
161
|
static wrapActivity<T>(activityName: string, options?: ActivityConfig): T {
|
|
93
162
|
return async function() {
|
|
94
163
|
const store = asyncLocalStorage.getStore();
|
|
@@ -107,20 +176,19 @@ export class WorkflowService {
|
|
|
107
176
|
|
|
108
177
|
let activityState: JobOutput
|
|
109
178
|
try {
|
|
110
|
-
const
|
|
111
|
-
activityState = await
|
|
179
|
+
const hotMeshClient = await WorkerService.getHotMesh(activityTopic);
|
|
180
|
+
activityState = await hotMeshClient.getState(ACTIVITY_SUBSCRIBES_TOPIC, activityJobId);
|
|
112
181
|
if (activityState.metadata.err) {
|
|
113
|
-
await
|
|
182
|
+
await hotMeshClient.scrub(activityJobId);
|
|
114
183
|
throw new Error(activityState.metadata.err);
|
|
115
|
-
} else if (activityState.metadata.js === 0) {
|
|
116
|
-
//return immediately
|
|
184
|
+
} else if (activityState.metadata.js === 0 || activityState.data?.done) {
|
|
117
185
|
return activityState.data?.response as T;
|
|
118
186
|
}
|
|
119
187
|
//one time subscription
|
|
120
188
|
return await new Promise((resolve, reject) => {
|
|
121
|
-
|
|
189
|
+
hotMeshClient.sub(`${ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`, async (topic, message) => {
|
|
122
190
|
const response = message.data?.response;
|
|
123
|
-
|
|
191
|
+
hotMeshClient.unsub(`${ACTIVITY_PUBLISHES_TOPIC}.${activityJobId}`);
|
|
124
192
|
// Resolve the Promise when the callback is triggered with a message
|
|
125
193
|
resolve(response);
|
|
126
194
|
});
|
|
@@ -130,14 +198,16 @@ export class WorkflowService {
|
|
|
130
198
|
const duration = ms(options?.startToCloseTimeout || '1 minute');
|
|
131
199
|
const payload = {
|
|
132
200
|
arguments: Array.from(arguments),
|
|
201
|
+
//the parent id is provided to categorize this activity for later cleanup
|
|
202
|
+
parentWorkflowId: `${workflowId}-a`,
|
|
133
203
|
workflowId: activityJobId,
|
|
134
|
-
workflowTopic,
|
|
204
|
+
workflowTopic: activityTopic,
|
|
135
205
|
activityName,
|
|
136
206
|
};
|
|
137
207
|
//start the job
|
|
138
|
-
const
|
|
208
|
+
const hotMeshClient = await WorkerService.getHotMesh(activityTopic);
|
|
139
209
|
const context = { metadata: { trc, spn }, data: {}};
|
|
140
|
-
const jobOutput = await
|
|
210
|
+
const jobOutput = await hotMeshClient.pubsub(ACTIVITY_SUBSCRIBES_TOPIC, payload, context as JobState, duration);
|
|
141
211
|
return jobOutput.data.response as T;
|
|
142
212
|
}
|
|
143
213
|
} as T;
|
package/services/engine/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ import { StringStringType } from '../../types';
|
|
|
68
68
|
//wait time to see if a job is complete
|
|
69
69
|
const OTT_WAIT_TIME = 1000;
|
|
70
70
|
const STATUS_CODE_SUCCESS = 200;
|
|
71
|
+
const STATUS_CODE_PENDING = 202;
|
|
71
72
|
const STATUS_CODE_TIMEOUT = 504;
|
|
72
73
|
|
|
73
74
|
class EngineService {
|
|
@@ -366,7 +367,7 @@ class EngineService {
|
|
|
366
367
|
}
|
|
367
368
|
|
|
368
369
|
// ***************** `AWAIT` ACTIVITY RETURN RESPONSE ****************
|
|
369
|
-
async execAdjacentParent(context: JobState, jobOutput: JobOutput): Promise<string> {
|
|
370
|
+
async execAdjacentParent(context: JobState, jobOutput: JobOutput, emit = false): Promise<string> {
|
|
370
371
|
if (this.hasParentJob(context)) {
|
|
371
372
|
//errors are stringified `StreamError` objects
|
|
372
373
|
const error = this.resolveError(jobOutput.metadata);
|
|
@@ -386,6 +387,9 @@ class EngineService {
|
|
|
386
387
|
streamData.status = StreamStatus.ERROR;
|
|
387
388
|
streamData.data = error;
|
|
388
389
|
streamData.code = error.code;
|
|
390
|
+
} else if (emit) {
|
|
391
|
+
streamData.status = StreamStatus.PENDING;
|
|
392
|
+
streamData.code = STATUS_CODE_PENDING;
|
|
389
393
|
} else {
|
|
390
394
|
streamData.status = StreamStatus.SUCCESS;
|
|
391
395
|
streamData.code = STATUS_CODE_SUCCESS;
|
|
@@ -409,7 +413,7 @@ class EngineService {
|
|
|
409
413
|
}
|
|
410
414
|
|
|
411
415
|
// ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
|
|
412
|
-
async hook(topic: string, data: JobData, dad?: string): Promise<
|
|
416
|
+
async hook(topic: string, data: JobData, dad?: string): Promise<string> {
|
|
413
417
|
const hookRule = await this.storeSignaler.getHookRule(topic);
|
|
414
418
|
const [aid, schema] = await this.getSchema(`.${hookRule.to}`);
|
|
415
419
|
if (!dad) {
|
|
@@ -428,7 +432,7 @@ class EngineService {
|
|
|
428
432
|
},
|
|
429
433
|
data,
|
|
430
434
|
};
|
|
431
|
-
await this.streamSignaler.publishMessage(null, streamData);
|
|
435
|
+
return await this.streamSignaler.publishMessage(null, streamData) as string;
|
|
432
436
|
}
|
|
433
437
|
async hookTime(jobId: string, activityId: string): Promise<JobStatus | void> {
|
|
434
438
|
//the activityid is concatenated with its dimensional address (dad); split to resolve
|
|
@@ -445,24 +449,26 @@ class EngineService {
|
|
|
445
449
|
};
|
|
446
450
|
await this.streamSignaler.publishMessage(null, streamData);
|
|
447
451
|
}
|
|
448
|
-
async hookAll(hookTopic: string, data: JobData,
|
|
452
|
+
async hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
|
|
449
453
|
const config = await this.getVID();
|
|
450
454
|
const hookRule = await this.storeSignaler.getHookRule(hookTopic);
|
|
451
455
|
if (hookRule) {
|
|
452
456
|
const subscriptionTopic = await getSubscriptionTopic(hookRule.to, this.store, config)
|
|
453
|
-
const resolvedQuery = await this.resolveQuery(subscriptionTopic,
|
|
457
|
+
const resolvedQuery = await this.resolveQuery(subscriptionTopic, keyResolver);
|
|
454
458
|
const reporter = new ReporterService(config, this.store, this.logger);
|
|
455
459
|
const workItems = await reporter.getWorkItems(resolvedQuery, queryFacets);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
460
|
+
if (workItems.length) {
|
|
461
|
+
const taskService = new TaskService(this.store, this.logger);
|
|
462
|
+
await taskService.enqueueWorkItems(
|
|
463
|
+
workItems.map(
|
|
464
|
+
workItem => `${hookTopic}::${workItem}::${keyResolver.scrub || false}::${JSON.stringify(data)}`
|
|
465
|
+
));
|
|
466
|
+
this.store.publish(
|
|
467
|
+
KeyType.QUORUM,
|
|
468
|
+
{ type: 'work', originator: this.guid },
|
|
469
|
+
this.appId
|
|
470
|
+
);
|
|
471
|
+
}
|
|
466
472
|
return workItems;
|
|
467
473
|
} else {
|
|
468
474
|
throw new Error(`unable to find hook rule for topic ${hookTopic}`);
|
|
@@ -472,7 +478,7 @@ class EngineService {
|
|
|
472
478
|
|
|
473
479
|
// ********************** PUB/SUB ENTRY POINT **********************
|
|
474
480
|
//publish (returns just the job id)
|
|
475
|
-
async pub(topic: string, data: JobData, context?: JobState) {
|
|
481
|
+
async pub(topic: string, data: JobData, context?: JobState): Promise<string> {
|
|
476
482
|
const activityHandler = await this.initActivity(topic, data, context);
|
|
477
483
|
if (activityHandler) {
|
|
478
484
|
return await activityHandler.process();
|
|
@@ -534,7 +540,7 @@ class EngineService {
|
|
|
534
540
|
}, timeout);
|
|
535
541
|
});
|
|
536
542
|
}
|
|
537
|
-
async resolveOneTimeSubscription(context: JobState, jobOutput: JobOutput) {
|
|
543
|
+
async resolveOneTimeSubscription(context: JobState, jobOutput: JobOutput, emit = false) {
|
|
538
544
|
//todo: subscriber should query for the job...only publish minimum context needed
|
|
539
545
|
if (this.hasOneTimeSubscription(context)) {
|
|
540
546
|
const message: JobMessage = {
|
|
@@ -551,7 +557,7 @@ class EngineService {
|
|
|
551
557
|
const schema = await this.store.getSchema(activityId, config);
|
|
552
558
|
return schema.publishes;
|
|
553
559
|
}
|
|
554
|
-
async resolvePersistentSubscriptions(context: JobState, jobOutput: JobOutput) {
|
|
560
|
+
async resolvePersistentSubscriptions(context: JobState, jobOutput: JobOutput, emit = false) {
|
|
555
561
|
const topic = await this.getPublishesTopic(context);
|
|
556
562
|
if (topic) {
|
|
557
563
|
const message: JobMessage = {
|
|
@@ -577,20 +583,23 @@ class EngineService {
|
|
|
577
583
|
}
|
|
578
584
|
|
|
579
585
|
|
|
580
|
-
//
|
|
581
|
-
async runJobCompletionTasks(context: JobState) {
|
|
586
|
+
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
587
|
+
async runJobCompletionTasks(context: JobState, emit = false) {
|
|
588
|
+
//if 'emit' is true, the job isn't done. it's just emitting
|
|
582
589
|
const isAwait = this.hasParentJob(context);
|
|
583
590
|
const isOneTimeSubscription = this.hasOneTimeSubscription(context);
|
|
584
591
|
const topic = await this.getPublishesTopic(context);
|
|
585
592
|
if (isAwait || isOneTimeSubscription || topic) {
|
|
586
593
|
const jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
|
|
587
594
|
//always wait for stream pub/sub
|
|
588
|
-
await this.execAdjacentParent(context, jobOutput);
|
|
595
|
+
await this.execAdjacentParent(context, jobOutput, emit);
|
|
589
596
|
//no need to wait for standard pub/sub
|
|
590
|
-
this.resolveOneTimeSubscription(context, jobOutput);
|
|
591
|
-
this.resolvePersistentSubscriptions(context, jobOutput);
|
|
597
|
+
this.resolveOneTimeSubscription(context, jobOutput, emit);
|
|
598
|
+
this.resolvePersistentSubscriptions(context, jobOutput, emit);
|
|
599
|
+
}
|
|
600
|
+
if (!emit) {
|
|
601
|
+
this.task.registerJobForCleanup(context.metadata.jid, context.metadata.expire);
|
|
592
602
|
}
|
|
593
|
-
this.task.registerJobForCleanup(context.metadata.jid, context.metadata.expire);
|
|
594
603
|
}
|
|
595
604
|
|
|
596
605
|
|
|
@@ -63,6 +63,10 @@ class HotMeshService {
|
|
|
63
63
|
return instance;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
static guid(): string {
|
|
67
|
+
return nanoid();
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
async initEngine(config: HotMeshConfig, logger: ILogger): Promise<void> {
|
|
67
71
|
if (config.engine) {
|
|
68
72
|
await ConnectorService.initRedisClients(
|
|
@@ -104,7 +108,7 @@ class HotMeshService {
|
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
// ************* PUB/SUB METHODS *************
|
|
107
|
-
async pub(topic: string, data: JobData = {}, context?: JobState) {
|
|
111
|
+
async pub(topic: string, data: JobData = {}, context?: JobState): Promise<string> {
|
|
108
112
|
return await this.engine?.pub(topic, data, context);
|
|
109
113
|
}
|
|
110
114
|
async sub(topic: string, callback: JobMessageCallback): Promise<void> {
|
|
@@ -145,7 +149,7 @@ class HotMeshService {
|
|
|
145
149
|
async getStatus(jobId: string): Promise<JobStatus> {
|
|
146
150
|
return this.engine?.getStatus(jobId);
|
|
147
151
|
}
|
|
148
|
-
async getState(topic: string, jobId: string) {
|
|
152
|
+
async getState(topic: string, jobId: string): Promise<JobOutput> {
|
|
149
153
|
return this.engine?.getState(topic, jobId);
|
|
150
154
|
}
|
|
151
155
|
async getIds(topic: string, query: JobStatsInput, queryFacets = []): Promise<IdsResponse> {
|
|
@@ -161,8 +165,7 @@ class HotMeshService {
|
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
// ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
|
|
164
|
-
async hook(topic: string, data: JobData, dad?: string): Promise<
|
|
165
|
-
//return collation int
|
|
168
|
+
async hook(topic: string, data: JobData, dad?: string): Promise<string> {
|
|
166
169
|
return await this.engine?.hook(topic, data, dad);
|
|
167
170
|
}
|
|
168
171
|
async hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
|
package/services/mapper/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ class MapperService {
|
|
|
59
59
|
if (typeof transitionRule === 'boolean') {
|
|
60
60
|
return transitionRule;
|
|
61
61
|
}
|
|
62
|
-
if (code.toString() === (transitionRule.code || 200).toString()) {
|
|
62
|
+
if ((Array.isArray(transitionRule.code) && transitionRule.code.includes(code || 200)) || code.toString() === (transitionRule.code || 200).toString()) {
|
|
63
63
|
if (!transitionRule.match) {
|
|
64
64
|
return true;
|
|
65
65
|
}
|