@falcondev-oss/workflow 0.5.0 → 0.6.0
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 +7 -4
- package/dist/index.mjs +49 -18
- package/package.json +2 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
import IORedis from "ioredis";
|
|
4
4
|
import { Span } from "@opentelemetry/api";
|
|
@@ -20,7 +20,7 @@ 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
|
};
|
|
@@ -66,7 +66,7 @@ interface WorkflowStepOptions {
|
|
|
66
66
|
//#endregion
|
|
67
67
|
//#region src/types.d.ts
|
|
68
68
|
type WorkflowJobInternal<Input, Output> = Job<Serialized<{
|
|
69
|
-
input: Input;
|
|
69
|
+
input: Input | undefined;
|
|
70
70
|
stepData: Record<string, WorkflowStepData>;
|
|
71
71
|
tracingHeaders: unknown;
|
|
72
72
|
}>, Serialized<Output>, string>;
|
|
@@ -102,7 +102,10 @@ declare class Workflow<RunInput, Input, Output> {
|
|
|
102
102
|
run(input: RunInput, opts?: JobsOptions): Promise<WorkflowJob<Output>>;
|
|
103
103
|
runIn(input: RunInput, delayMs: number, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
|
|
104
104
|
runAt(input: RunInput, date: Date, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
|
|
105
|
-
|
|
105
|
+
private runSchedule;
|
|
106
|
+
runCron(schedulerId: string, cron: string, input: RunInput, opts?: JobSchedulerTemplateOptions): Promise<void>;
|
|
107
|
+
runEvery(schedulerId: string, everyMs: number, input: RunInput, opts?: JobSchedulerTemplateOptions): Promise<void>;
|
|
108
|
+
exportPrometheusMetrics(globalVariables?: Record<string, string>): Promise<string>;
|
|
106
109
|
private getOrCreateQueue;
|
|
107
110
|
private getOrCreateQueueEvents;
|
|
108
111
|
}
|
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,12 +24,15 @@ 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;
|
|
28
|
-
const redis = new IORedis(
|
|
29
|
-
lazyConnect: true,
|
|
30
|
-
maxRetriesPerRequest: null
|
|
31
|
-
});
|
|
34
|
+
if (Settings.defaultConnection) return Settings.defaultConnection();
|
|
35
|
+
const redis = new IORedis(defaultRedisOptions);
|
|
32
36
|
await redis.connect();
|
|
33
37
|
return redis;
|
|
34
38
|
});
|
|
@@ -110,7 +114,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
110
114
|
attempt: initialAttempt
|
|
111
115
|
});
|
|
112
116
|
return pRetry(async (attempt) => {
|
|
113
|
-
const result = await runWithTracing(`step
|
|
117
|
+
const result = await runWithTracing(`workflow-worker/${this.workflowId}/step/${name}`, { attributes: {
|
|
114
118
|
"workflow.id": this.workflowId,
|
|
115
119
|
"workflow.job_id": this.workflowJobId,
|
|
116
120
|
"workflow.step_name": name,
|
|
@@ -153,7 +157,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
153
157
|
startedAt: now
|
|
154
158
|
};
|
|
155
159
|
await this.updateStepData(name, stepData);
|
|
156
|
-
await runWithTracing(`step
|
|
160
|
+
await runWithTracing(`workflow-worker/${this.workflowId}/step/${name}`, { attributes: {
|
|
157
161
|
"workflow.id": this.workflowId,
|
|
158
162
|
"workflow.job_id": this.workflowJobId,
|
|
159
163
|
"workflow.step_name": name
|
|
@@ -203,14 +207,14 @@ var Workflow = class {
|
|
|
203
207
|
}
|
|
204
208
|
async work(opts) {
|
|
205
209
|
const queue = await this.getOrCreateQueue();
|
|
206
|
-
|
|
210
|
+
const worker = new Worker(this.opts.id, async (job) => {
|
|
207
211
|
Settings.logger?.info?.(`Processing workflow job ${job.id} of workflow ${this.opts.id}`);
|
|
208
212
|
const jobId = job.id;
|
|
209
213
|
if (!jobId) throw new Error("Job ID is missing");
|
|
210
214
|
const deserializedData = deserialize$1(job.data);
|
|
211
215
|
const parsedData = this.opts.schema && await this.opts.schema["~standard"].validate(deserializedData.input);
|
|
212
216
|
if (parsedData?.issues) throw new WorkflowInputError("Invalid workflow input", parsedData.issues);
|
|
213
|
-
return runWithTracing(`workflow
|
|
217
|
+
return runWithTracing(`workflow-worker/${this.opts.id}`, {
|
|
214
218
|
attributes: {
|
|
215
219
|
"workflow.id": this.opts.id,
|
|
216
220
|
"workflow.job_id": jobId
|
|
@@ -235,15 +239,20 @@ var Workflow = class {
|
|
|
235
239
|
connection: this.opts.connection ?? await defaultRedisConnection(),
|
|
236
240
|
prefix: Settings.defaultPrefix,
|
|
237
241
|
...opts
|
|
238
|
-
})
|
|
242
|
+
});
|
|
243
|
+
await worker.waitUntilReady();
|
|
239
244
|
Settings.logger?.info?.(`Worker started for workflow ${this.opts.id}`);
|
|
245
|
+
asyncExitHook(async (signal) => {
|
|
246
|
+
Settings.logger?.info?.(`Received ${signal}, shutting down worker for workflow ${this.opts.id}...`);
|
|
247
|
+
await worker.close();
|
|
248
|
+
}, { wait: 1e4 });
|
|
240
249
|
return this;
|
|
241
250
|
}
|
|
242
251
|
async run(input, opts) {
|
|
243
252
|
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
244
253
|
if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
|
|
245
254
|
const queue = await this.getOrCreateQueue();
|
|
246
|
-
return runWithTracing(`workflow
|
|
255
|
+
return runWithTracing(`workflow-producer/${this.opts.id}`, {
|
|
247
256
|
attributes: { "workflow.id": this.opts.id },
|
|
248
257
|
kind: SpanKind.PRODUCER
|
|
249
258
|
}, async () => {
|
|
@@ -269,19 +278,41 @@ var Workflow = class {
|
|
|
269
278
|
const now = Date.now();
|
|
270
279
|
return date.getTime() < now ? this.run(input, opts) : this.runIn(input, date.getTime() - Date.now(), opts);
|
|
271
280
|
}
|
|
272
|
-
async
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
async runSchedule(schedulerId, repeatOpts, input, opts) {
|
|
282
|
+
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
283
|
+
if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
|
|
284
|
+
await (await this.getOrCreateQueue()).upsertJobScheduler(schedulerId, repeatOpts, {
|
|
285
|
+
name: "workflow-job",
|
|
286
|
+
data: serialize$1({
|
|
287
|
+
input: parsedInput?.value,
|
|
288
|
+
stepData: {},
|
|
289
|
+
tracingHeaders: {}
|
|
290
|
+
}),
|
|
291
|
+
opts
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async runCron(schedulerId, cron, input, opts) {
|
|
295
|
+
return this.runSchedule(schedulerId, { pattern: cron }, input, opts);
|
|
296
|
+
}
|
|
297
|
+
async runEvery(schedulerId, everyMs, input, opts) {
|
|
298
|
+
return this.runSchedule(schedulerId, { every: everyMs }, input, opts);
|
|
299
|
+
}
|
|
300
|
+
async exportPrometheusMetrics(globalVariables) {
|
|
301
|
+
return (await this.getOrCreateQueue()).exportPrometheusMetrics({
|
|
302
|
+
workflowId: this.id,
|
|
303
|
+
workflowPrefix: Settings.defaultPrefix,
|
|
304
|
+
...globalVariables
|
|
279
305
|
});
|
|
280
306
|
}
|
|
281
307
|
async getOrCreateQueue() {
|
|
282
308
|
if (!this.queue) this.queue = new Queue(this.opts.id, {
|
|
283
309
|
prefix: Settings.defaultPrefix,
|
|
284
310
|
connection: this.opts.connection ?? await defaultRedisConnection(),
|
|
311
|
+
defaultJobOptions: {
|
|
312
|
+
removeOnComplete: true,
|
|
313
|
+
removeOnFail: { age: 1440 * 60 },
|
|
314
|
+
...this.opts.queueOptions?.defaultJobOptions
|
|
315
|
+
},
|
|
285
316
|
...this.opts.queueOptions
|
|
286
317
|
});
|
|
287
318
|
await this.queue.waitUntilReady();
|
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.0",
|
|
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",
|