@falcondev-oss/workflow 0.5.0 → 0.6.1
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/dist/index.d.mts +10 -6
- package/dist/index.mjs +57 -18
- package/package.json +2 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ConnectionOptions, Job, JobsOptions, Queue, QueueEvents, QueueEventsOptions, QueueOptions, UnrecoverableError, WorkerOptions } from "bullmq";
|
|
1
|
+
import { ConnectionOptions, Job, JobSchedulerTemplateOptions, JobsOptions, Queue, QueueEvents, QueueEventsOptions, QueueOptions, UnrecoverableError, WorkerOptions } from "bullmq";
|
|
2
2
|
import "@antfu/utils";
|
|
3
|
-
import IORedis from "ioredis";
|
|
3
|
+
import IORedis, { RedisOptions } from "ioredis";
|
|
4
4
|
import { Span } from "@opentelemetry/api";
|
|
5
5
|
import { SuperJSONResult } from "superjson";
|
|
6
6
|
import { Options } from "p-retry";
|
|
@@ -20,10 +20,11 @@ type WorkflowLogger = {
|
|
|
20
20
|
};
|
|
21
21
|
declare const Settings: {
|
|
22
22
|
defaultPrefix: string;
|
|
23
|
-
defaultConnection: IORedis | undefined;
|
|
23
|
+
defaultConnection: (() => Promise<IORedis> | IORedis) | undefined;
|
|
24
24
|
defaultCronTimezone: string | undefined;
|
|
25
25
|
logger: WorkflowLogger | undefined;
|
|
26
26
|
};
|
|
27
|
+
declare function createRedisConnection(opts: RedisOptions): Promise<IORedis>;
|
|
27
28
|
//#endregion
|
|
28
29
|
//#region src/serializer.d.ts
|
|
29
30
|
type Serialized<T> = Tagged<SuperJSONResult, 'data', T>;
|
|
@@ -66,7 +67,7 @@ interface WorkflowStepOptions {
|
|
|
66
67
|
//#endregion
|
|
67
68
|
//#region src/types.d.ts
|
|
68
69
|
type WorkflowJobInternal<Input, Output> = Job<Serialized<{
|
|
69
|
-
input: Input;
|
|
70
|
+
input: Input | undefined;
|
|
70
71
|
stepData: Record<string, WorkflowStepData>;
|
|
71
72
|
tracingHeaders: unknown;
|
|
72
73
|
}>, Serialized<Output>, string>;
|
|
@@ -102,7 +103,10 @@ declare class Workflow<RunInput, Input, Output> {
|
|
|
102
103
|
run(input: RunInput, opts?: JobsOptions): Promise<WorkflowJob<Output>>;
|
|
103
104
|
runIn(input: RunInput, delayMs: number, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
|
|
104
105
|
runAt(input: RunInput, date: Date, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
|
|
105
|
-
|
|
106
|
+
private runSchedule;
|
|
107
|
+
runCron(schedulerId: string, cron: string, input: RunInput, opts?: JobSchedulerTemplateOptions): Promise<void>;
|
|
108
|
+
runEvery(schedulerId: string, everyMs: number, input: RunInput, opts?: JobSchedulerTemplateOptions): Promise<void>;
|
|
109
|
+
exportPrometheusMetrics(globalVariables?: Record<string, string>): Promise<string>;
|
|
106
110
|
private getOrCreateQueue;
|
|
107
111
|
private getOrCreateQueueEvents;
|
|
108
112
|
}
|
|
@@ -112,4 +116,4 @@ interface WorkflowRunContext<Input> {
|
|
|
112
116
|
span: Span;
|
|
113
117
|
}
|
|
114
118
|
//#endregion
|
|
115
|
-
export { Settings, Workflow, WorkflowInputError, WorkflowOptions, WorkflowRunContext };
|
|
119
|
+
export { Settings, Workflow, WorkflowInputError, WorkflowOptions, WorkflowRunContext, createRedisConnection };
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { Queue, QueueEvents, UnrecoverableError, Worker } from "bullmq";
|
|
|
2
2
|
import { createSingletonPromise } from "@antfu/utils";
|
|
3
3
|
import IORedis from "ioredis";
|
|
4
4
|
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
5
|
+
import { asyncExitHook } from "exit-hook";
|
|
5
6
|
import { deserialize, serialize } from "superjson";
|
|
6
7
|
import { setTimeout } from "node:timers/promises";
|
|
7
8
|
import pRetry from "p-retry";
|
|
@@ -23,15 +24,26 @@ const Settings = {
|
|
|
23
24
|
defaultCronTimezone: void 0,
|
|
24
25
|
logger: void 0
|
|
25
26
|
};
|
|
27
|
+
const defaultRedisOptions = {
|
|
28
|
+
lazyConnect: true,
|
|
29
|
+
maxRetriesPerRequest: null,
|
|
30
|
+
retryStrategy: (times) => Math.max(Math.min(Math.exp(times), 2e4), 1e3),
|
|
31
|
+
enableOfflineQueue: false
|
|
32
|
+
};
|
|
26
33
|
const defaultRedisConnection = createSingletonPromise(async () => {
|
|
27
|
-
if (Settings.defaultConnection) return Settings.defaultConnection;
|
|
34
|
+
if (Settings.defaultConnection) return Settings.defaultConnection();
|
|
35
|
+
const redis = new IORedis(defaultRedisOptions);
|
|
36
|
+
await redis.connect();
|
|
37
|
+
return redis;
|
|
38
|
+
});
|
|
39
|
+
async function createRedisConnection(opts) {
|
|
28
40
|
const redis = new IORedis({
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
...defaultRedisOptions,
|
|
42
|
+
...opts
|
|
31
43
|
});
|
|
32
44
|
await redis.connect();
|
|
33
45
|
return redis;
|
|
34
|
-
}
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
//#endregion
|
|
37
49
|
//#region src/serializer.ts
|
|
@@ -110,7 +122,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
110
122
|
attempt: initialAttempt
|
|
111
123
|
});
|
|
112
124
|
return pRetry(async (attempt) => {
|
|
113
|
-
const result = await runWithTracing(`step
|
|
125
|
+
const result = await runWithTracing(`workflow-worker/${this.workflowId}/step/${name}`, { attributes: {
|
|
114
126
|
"workflow.id": this.workflowId,
|
|
115
127
|
"workflow.job_id": this.workflowJobId,
|
|
116
128
|
"workflow.step_name": name,
|
|
@@ -153,7 +165,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
153
165
|
startedAt: now
|
|
154
166
|
};
|
|
155
167
|
await this.updateStepData(name, stepData);
|
|
156
|
-
await runWithTracing(`step
|
|
168
|
+
await runWithTracing(`workflow-worker/${this.workflowId}/step/${name}`, { attributes: {
|
|
157
169
|
"workflow.id": this.workflowId,
|
|
158
170
|
"workflow.job_id": this.workflowJobId,
|
|
159
171
|
"workflow.step_name": name
|
|
@@ -203,14 +215,14 @@ var Workflow = class {
|
|
|
203
215
|
}
|
|
204
216
|
async work(opts) {
|
|
205
217
|
const queue = await this.getOrCreateQueue();
|
|
206
|
-
|
|
218
|
+
const worker = new Worker(this.opts.id, async (job) => {
|
|
207
219
|
Settings.logger?.info?.(`Processing workflow job ${job.id} of workflow ${this.opts.id}`);
|
|
208
220
|
const jobId = job.id;
|
|
209
221
|
if (!jobId) throw new Error("Job ID is missing");
|
|
210
222
|
const deserializedData = deserialize$1(job.data);
|
|
211
223
|
const parsedData = this.opts.schema && await this.opts.schema["~standard"].validate(deserializedData.input);
|
|
212
224
|
if (parsedData?.issues) throw new WorkflowInputError("Invalid workflow input", parsedData.issues);
|
|
213
|
-
return runWithTracing(`workflow
|
|
225
|
+
return runWithTracing(`workflow-worker/${this.opts.id}`, {
|
|
214
226
|
attributes: {
|
|
215
227
|
"workflow.id": this.opts.id,
|
|
216
228
|
"workflow.job_id": jobId
|
|
@@ -235,15 +247,20 @@ var Workflow = class {
|
|
|
235
247
|
connection: this.opts.connection ?? await defaultRedisConnection(),
|
|
236
248
|
prefix: Settings.defaultPrefix,
|
|
237
249
|
...opts
|
|
238
|
-
})
|
|
250
|
+
});
|
|
251
|
+
await worker.waitUntilReady();
|
|
239
252
|
Settings.logger?.info?.(`Worker started for workflow ${this.opts.id}`);
|
|
253
|
+
asyncExitHook(async (signal) => {
|
|
254
|
+
Settings.logger?.info?.(`Received ${signal}, shutting down worker for workflow ${this.opts.id}...`);
|
|
255
|
+
await worker.close();
|
|
256
|
+
}, { wait: 1e4 });
|
|
240
257
|
return this;
|
|
241
258
|
}
|
|
242
259
|
async run(input, opts) {
|
|
243
260
|
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
244
261
|
if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
|
|
245
262
|
const queue = await this.getOrCreateQueue();
|
|
246
|
-
return runWithTracing(`workflow
|
|
263
|
+
return runWithTracing(`workflow-producer/${this.opts.id}`, {
|
|
247
264
|
attributes: { "workflow.id": this.opts.id },
|
|
248
265
|
kind: SpanKind.PRODUCER
|
|
249
266
|
}, async () => {
|
|
@@ -269,19 +286,41 @@ var Workflow = class {
|
|
|
269
286
|
const now = Date.now();
|
|
270
287
|
return date.getTime() < now ? this.run(input, opts) : this.runIn(input, date.getTime() - Date.now(), opts);
|
|
271
288
|
}
|
|
272
|
-
async
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
289
|
+
async runSchedule(schedulerId, repeatOpts, input, opts) {
|
|
290
|
+
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
291
|
+
if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
|
|
292
|
+
await (await this.getOrCreateQueue()).upsertJobScheduler(schedulerId, repeatOpts, {
|
|
293
|
+
name: "workflow-job",
|
|
294
|
+
data: serialize$1({
|
|
295
|
+
input: parsedInput?.value,
|
|
296
|
+
stepData: {},
|
|
297
|
+
tracingHeaders: {}
|
|
298
|
+
}),
|
|
299
|
+
opts
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async runCron(schedulerId, cron, input, opts) {
|
|
303
|
+
return this.runSchedule(schedulerId, { pattern: cron }, input, opts);
|
|
304
|
+
}
|
|
305
|
+
async runEvery(schedulerId, everyMs, input, opts) {
|
|
306
|
+
return this.runSchedule(schedulerId, { every: everyMs }, input, opts);
|
|
307
|
+
}
|
|
308
|
+
async exportPrometheusMetrics(globalVariables) {
|
|
309
|
+
return (await this.getOrCreateQueue()).exportPrometheusMetrics({
|
|
310
|
+
workflowId: this.id,
|
|
311
|
+
workflowPrefix: Settings.defaultPrefix,
|
|
312
|
+
...globalVariables
|
|
279
313
|
});
|
|
280
314
|
}
|
|
281
315
|
async getOrCreateQueue() {
|
|
282
316
|
if (!this.queue) this.queue = new Queue(this.opts.id, {
|
|
283
317
|
prefix: Settings.defaultPrefix,
|
|
284
318
|
connection: this.opts.connection ?? await defaultRedisConnection(),
|
|
319
|
+
defaultJobOptions: {
|
|
320
|
+
removeOnComplete: true,
|
|
321
|
+
removeOnFail: { age: 1440 * 60 },
|
|
322
|
+
...this.opts.queueOptions?.defaultJobOptions
|
|
323
|
+
},
|
|
285
324
|
...this.opts.queueOptions
|
|
286
325
|
});
|
|
287
326
|
await this.queue.waitUntilReady();
|
|
@@ -299,4 +338,4 @@ var Workflow = class {
|
|
|
299
338
|
};
|
|
300
339
|
|
|
301
340
|
//#endregion
|
|
302
|
-
export { Settings, Workflow, WorkflowInputError };
|
|
341
|
+
export { Settings, Workflow, WorkflowInputError, createRedisConnection };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falcondev-oss/workflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"description": "Simple type-safe queue worker with durable execution based on BullMQ.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:falcondev-oss/workflow",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@standard-schema/spec": "^1.1.0",
|
|
40
40
|
"@types/node": "^25.0.3",
|
|
41
41
|
"bullmq": "^5.66.4",
|
|
42
|
+
"exit-hook": "^5.0.1",
|
|
42
43
|
"ioredis": "^5.8.2",
|
|
43
44
|
"p-retry": "^7.1.1",
|
|
44
45
|
"superjson": "^2.2.6",
|