@falcondev-oss/workflow 0.6.2 → 0.7.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 +37 -34
- package/dist/index.mjs +122 -112
- package/package.json +10 -9
package/dist/index.d.mts
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
|
-
import { ConnectionOptions, Job, JobSchedulerTemplateOptions, JobsOptions, Queue, QueueEvents, QueueEventsOptions, QueueOptions, UnrecoverableError, WorkerOptions } from "bullmq";
|
|
2
1
|
import "@antfu/utils";
|
|
3
2
|
import IORedis, { RedisOptions } from "ioredis";
|
|
4
|
-
import { Span } from "@opentelemetry/api";
|
|
3
|
+
import { Meter, Span } from "@opentelemetry/api";
|
|
4
|
+
import { AddOptions, Job, Queue, QueueOptions, WorkerOptions } from "groupmq";
|
|
5
5
|
import { SuperJSONResult } from "superjson";
|
|
6
6
|
import { Options } from "p-retry";
|
|
7
7
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
8
8
|
import { Except, IsUnknown, SetOptional, Tagged } from "type-fest";
|
|
9
9
|
|
|
10
|
-
//#region src/errors.d.ts
|
|
11
|
-
declare class WorkflowInputError extends UnrecoverableError {
|
|
12
|
-
issues: readonly StandardSchemaV1.Issue[];
|
|
13
|
-
constructor(message: string, issues: readonly StandardSchemaV1.Issue[]);
|
|
14
|
-
}
|
|
15
|
-
//#endregion
|
|
16
10
|
//#region src/settings.d.ts
|
|
17
11
|
type WorkflowLogger = {
|
|
18
12
|
info?: (...data: any[]) => void;
|
|
19
13
|
success?: (...data: any[]) => void;
|
|
20
14
|
};
|
|
21
15
|
declare const Settings: {
|
|
22
|
-
defaultPrefix: string;
|
|
23
16
|
defaultConnection: (() => Promise<IORedis> | IORedis) | undefined;
|
|
24
|
-
defaultCronTimezone: string | undefined;
|
|
25
17
|
logger: WorkflowLogger | undefined;
|
|
18
|
+
metrics: {
|
|
19
|
+
meter: Meter;
|
|
20
|
+
prefix: string;
|
|
21
|
+
} | undefined;
|
|
26
22
|
};
|
|
27
|
-
declare function
|
|
23
|
+
declare function createRedis(opts: RedisOptions): Promise<IORedis>;
|
|
28
24
|
//#endregion
|
|
29
25
|
//#region src/serializer.d.ts
|
|
30
26
|
type Serialized<T> = Tagged<SuperJSONResult, 'data', T>;
|
|
@@ -45,7 +41,7 @@ declare class WorkflowStep {
|
|
|
45
41
|
private workflowJobId;
|
|
46
42
|
private stepNamePrefix;
|
|
47
43
|
constructor(opts: {
|
|
48
|
-
queue: WorkflowQueueInternal<
|
|
44
|
+
queue: WorkflowQueueInternal<unknown>;
|
|
49
45
|
workflowJobId: string;
|
|
50
46
|
workflowId: string;
|
|
51
47
|
stepNamePrefix?: string;
|
|
@@ -66,22 +62,22 @@ interface WorkflowStepOptions {
|
|
|
66
62
|
}
|
|
67
63
|
//#endregion
|
|
68
64
|
//#region src/types.d.ts
|
|
69
|
-
type
|
|
65
|
+
type WorkflowJobPayloadInternal<Input> = Serialized<{
|
|
70
66
|
input: Input | undefined;
|
|
71
67
|
stepData: Record<string, WorkflowStepData>;
|
|
72
68
|
tracingHeaders: unknown;
|
|
73
|
-
}
|
|
74
|
-
type WorkflowQueueInternal<Input
|
|
69
|
+
}>;
|
|
70
|
+
type WorkflowQueueInternal<Input> = Queue<WorkflowJobPayloadInternal<Input>>;
|
|
75
71
|
//#endregion
|
|
76
72
|
//#region src/job.d.ts
|
|
77
73
|
declare class WorkflowJob<Output> {
|
|
78
74
|
private job;
|
|
79
|
-
|
|
75
|
+
groupId: string;
|
|
76
|
+
id: string;
|
|
80
77
|
constructor(opts: {
|
|
81
|
-
job:
|
|
82
|
-
queueEvents: QueueEvents;
|
|
78
|
+
job: Job<unknown>;
|
|
83
79
|
});
|
|
84
|
-
wait(timeoutMs?: number): Promise<Output>;
|
|
80
|
+
wait(timeoutMs?: number): Promise<Output | undefined>;
|
|
85
81
|
}
|
|
86
82
|
//#endregion
|
|
87
83
|
//#region src/workflow.d.ts
|
|
@@ -89,27 +85,34 @@ interface WorkflowOptions<RunInput, Input, Output> {
|
|
|
89
85
|
id: string;
|
|
90
86
|
schema?: StandardSchemaV1<RunInput, Input>;
|
|
91
87
|
run: (ctx: WorkflowRunContext<Input>) => Promise<Output>;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
getGroupId?: (input: IsUnknown<Input> extends true ? undefined : Input) => string | undefined | Promise<string | undefined>;
|
|
89
|
+
queueOptions?: WorkflowQueueOptions;
|
|
90
|
+
workerOptions?: WorkflowWorkerOptions<Input>;
|
|
91
|
+
redis?: IORedis;
|
|
96
92
|
}
|
|
93
|
+
type WorkflowJobRunOptions<Input> = SetOptional<Except<AddOptions<WorkflowJobPayloadInternal<Input>>, 'data'>, 'groupId'> & {
|
|
94
|
+
priority?: 'high' | 'normal';
|
|
95
|
+
};
|
|
96
|
+
type WorkflowQueueOptions = SetOptional<Except<QueueOptions, 'namespace'>, 'redis'>;
|
|
97
|
+
type WorkflowWorkerOptions<Input> = Except<WorkerOptions<WorkflowJobPayloadInternal<Input>>, 'queue' | 'handler' | 'name'> & {
|
|
98
|
+
metrics?: {
|
|
99
|
+
meter: Meter;
|
|
100
|
+
prefix: string;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
97
103
|
declare class Workflow<RunInput, Input, Output> {
|
|
98
104
|
id: string;
|
|
99
105
|
private opts;
|
|
100
106
|
private queue?;
|
|
101
|
-
private queueEvents?;
|
|
102
107
|
constructor(opts: WorkflowOptions<RunInput, Input, Output>);
|
|
103
|
-
work(opts?:
|
|
104
|
-
run(input: RunInput, opts?:
|
|
105
|
-
runIn(input: RunInput, delayMs: number, opts?:
|
|
106
|
-
runAt(input: RunInput, date: Date, opts?:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
runEvery(schedulerId: string, everyMs: number, input: RunInput, opts?: JobSchedulerTemplateOptions): Promise<void>;
|
|
110
|
-
exportPrometheusMetrics(globalVariables?: Record<string, string>): Promise<string>;
|
|
108
|
+
work(opts?: WorkflowWorkerOptions<Input>): Promise<this>;
|
|
109
|
+
run(input: RunInput, opts?: WorkflowJobRunOptions<Input>): Promise<WorkflowJob<Output>>;
|
|
110
|
+
runIn(input: RunInput, delayMs: number, opts?: WorkflowJobRunOptions<Input>): Promise<WorkflowJob<Output>>;
|
|
111
|
+
runAt(input: RunInput, date: Date, opts?: WorkflowJobRunOptions<Input>): Promise<WorkflowJob<Output>>;
|
|
112
|
+
runCron(scheduleId: string, cron: string, input: RunInput, opts?: WorkflowJobRunOptions<Input>): Promise<WorkflowJob<Output>>;
|
|
113
|
+
runEvery(scheduleId: string, everyMs: number, input: RunInput, opts?: WorkflowJobRunOptions<Input>): Promise<WorkflowJob<Output>>;
|
|
111
114
|
private getOrCreateQueue;
|
|
112
|
-
private
|
|
115
|
+
private setupMetrics;
|
|
113
116
|
}
|
|
114
117
|
interface WorkflowRunContext<Input> {
|
|
115
118
|
input: IsUnknown<Input> extends true ? undefined : Input;
|
|
@@ -117,4 +120,4 @@ interface WorkflowRunContext<Input> {
|
|
|
117
120
|
span: Span;
|
|
118
121
|
}
|
|
119
122
|
//#endregion
|
|
120
|
-
export { Settings, Workflow,
|
|
123
|
+
export { Settings, Workflow, WorkflowJobRunOptions, WorkflowOptions, WorkflowQueueOptions, WorkflowRunContext, WorkflowWorkerOptions, createRedis };
|
package/dist/index.mjs
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import { Queue, QueueEvents, UnrecoverableError, Worker } from "bullmq";
|
|
2
1
|
import { createSingletonPromise } from "@antfu/utils";
|
|
3
2
|
import IORedis from "ioredis";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
5
5
|
import { asyncExitHook } from "exit-hook";
|
|
6
|
+
import { Queue, Worker } from "groupmq";
|
|
7
|
+
import { setInterval, setTimeout } from "node:timers/promises";
|
|
6
8
|
import { deserialize, serialize } from "superjson";
|
|
7
|
-
import { setTimeout } from "node:timers/promises";
|
|
8
9
|
import pRetry from "p-retry";
|
|
9
10
|
|
|
10
|
-
//#region src/errors.ts
|
|
11
|
-
var WorkflowInputError = class extends UnrecoverableError {
|
|
12
|
-
issues;
|
|
13
|
-
constructor(message, issues) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.issues = issues;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
//#endregion
|
|
20
11
|
//#region src/settings.ts
|
|
21
12
|
const Settings = {
|
|
22
|
-
defaultPrefix: "falcondev-oss-workflow",
|
|
23
13
|
defaultConnection: void 0,
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
logger: void 0,
|
|
15
|
+
metrics: void 0
|
|
26
16
|
};
|
|
27
17
|
const defaultRedisOptions = {
|
|
28
18
|
lazyConnect: true,
|
|
@@ -36,7 +26,7 @@ const defaultRedisConnection = createSingletonPromise(async () => {
|
|
|
36
26
|
await redis.connect();
|
|
37
27
|
return redis;
|
|
38
28
|
});
|
|
39
|
-
async function
|
|
29
|
+
async function createRedis(opts) {
|
|
40
30
|
const redis = new IORedis({
|
|
41
31
|
...defaultRedisOptions,
|
|
42
32
|
...opts
|
|
@@ -58,13 +48,26 @@ function deserialize$1(data) {
|
|
|
58
48
|
//#region src/job.ts
|
|
59
49
|
var WorkflowJob = class {
|
|
60
50
|
job;
|
|
61
|
-
|
|
51
|
+
groupId;
|
|
52
|
+
id;
|
|
62
53
|
constructor(opts) {
|
|
63
54
|
this.job = opts.job;
|
|
64
|
-
this.
|
|
55
|
+
this.groupId = opts.job.groupId;
|
|
56
|
+
this.id = opts.job.id;
|
|
65
57
|
}
|
|
66
58
|
async wait(timeoutMs) {
|
|
67
|
-
|
|
59
|
+
if (this.job.finishedOn) {
|
|
60
|
+
const returnValue = this.job.returnvalue;
|
|
61
|
+
return returnValue && deserialize$1(returnValue);
|
|
62
|
+
}
|
|
63
|
+
for await (const _ of setInterval(1e3, void 0, { signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0 })) {
|
|
64
|
+
const updatedJob = await this.job.queue.getJob(this.job.id).catch(() => null);
|
|
65
|
+
if (!updatedJob) return;
|
|
66
|
+
if (updatedJob.finishedOn) {
|
|
67
|
+
const returnValue = updatedJob.returnvalue;
|
|
68
|
+
return returnValue && deserialize$1(returnValue);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
68
71
|
}
|
|
69
72
|
};
|
|
70
73
|
|
|
@@ -115,7 +118,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
115
118
|
async do(stepName, run, options) {
|
|
116
119
|
const name = this.addNamePrefix(stepName);
|
|
117
120
|
const stepData = await this.getStepData("do", name);
|
|
118
|
-
if (stepData
|
|
121
|
+
if (stepData && "result" in stepData) return stepData.result;
|
|
119
122
|
const initialAttempt = stepData?.attempt ?? 0;
|
|
120
123
|
await this.updateStepData(name, {
|
|
121
124
|
type: "do",
|
|
@@ -144,7 +147,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
144
147
|
return result;
|
|
145
148
|
}, {
|
|
146
149
|
...options?.retry,
|
|
147
|
-
retries: (options?.retry?.retries ?? 0) - initialAttempt,
|
|
150
|
+
retries: Math.max((options?.retry?.retries ?? 0) - initialAttempt, 0),
|
|
148
151
|
onFailedAttempt: async (ctx) => {
|
|
149
152
|
await this.updateStepData(name, {
|
|
150
153
|
type: "do",
|
|
@@ -156,7 +159,6 @@ var WorkflowStep = class WorkflowStep {
|
|
|
156
159
|
}
|
|
157
160
|
async wait(stepName, durationMs) {
|
|
158
161
|
const name = this.addNamePrefix(stepName);
|
|
159
|
-
const job = await this.getWorkflowJob();
|
|
160
162
|
const existingStepData = await this.getStepData("wait", name);
|
|
161
163
|
const now = Date.now();
|
|
162
164
|
const stepData = existingStepData ?? {
|
|
@@ -170,12 +172,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
170
172
|
"workflow.job_id": this.workflowJobId,
|
|
171
173
|
"workflow.step_name": name
|
|
172
174
|
} }, async () => {
|
|
173
|
-
|
|
174
|
-
const interval = setInterval(() => {
|
|
175
|
-
job.updateProgress(name);
|
|
176
|
-
}, 15e3);
|
|
177
|
-
await setTimeout(remainingMs);
|
|
178
|
-
clearInterval(interval);
|
|
175
|
+
await setTimeout(Math.max(0, stepData.startedAt + stepData.durationMs - now));
|
|
179
176
|
});
|
|
180
177
|
}
|
|
181
178
|
async waitUntil(stepName, date) {
|
|
@@ -193,11 +190,11 @@ var WorkflowStep = class WorkflowStep {
|
|
|
193
190
|
const job = await this.getWorkflowJob();
|
|
194
191
|
const jobData = deserialize$1(job.data);
|
|
195
192
|
jobData.stepData[stepName] = data;
|
|
196
|
-
await
|
|
193
|
+
await job.updateData(serialize$1(jobData));
|
|
197
194
|
}
|
|
198
195
|
async getWorkflowJob() {
|
|
199
196
|
const job = await this.queue.getJob(this.workflowJobId);
|
|
200
|
-
if (!job) throw new
|
|
197
|
+
if (!job) throw new Error(`Could not find workflow job with ID ${this.workflowJobId}`);
|
|
201
198
|
return job;
|
|
202
199
|
}
|
|
203
200
|
};
|
|
@@ -208,49 +205,54 @@ var Workflow = class {
|
|
|
208
205
|
id;
|
|
209
206
|
opts;
|
|
210
207
|
queue;
|
|
211
|
-
queueEvents;
|
|
212
208
|
constructor(opts) {
|
|
213
209
|
this.id = opts.id;
|
|
214
210
|
this.opts = opts;
|
|
215
211
|
}
|
|
216
212
|
async work(opts) {
|
|
217
213
|
const queue = await this.getOrCreateQueue();
|
|
218
|
-
const worker = new Worker(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
214
|
+
const worker = new Worker({
|
|
215
|
+
handler: async (job) => {
|
|
216
|
+
Settings.logger?.info?.(`Processing workflow job ${job.id} of workflow ${this.opts.id}`);
|
|
217
|
+
const jobId = job.id;
|
|
218
|
+
if (!jobId) throw new Error("Job ID is missing");
|
|
219
|
+
const deserializedData = deserialize$1(job.data);
|
|
220
|
+
const parsedData = this.opts.schema && await this.opts.schema["~standard"].validate(deserializedData.input);
|
|
221
|
+
if (parsedData?.issues) throw new Error(`Invalid workflow input`);
|
|
222
|
+
return runWithTracing(`workflow-worker/${this.opts.id}`, {
|
|
223
|
+
attributes: {
|
|
224
|
+
"workflow.id": this.opts.id,
|
|
225
|
+
"workflow.job_id": jobId
|
|
226
|
+
},
|
|
227
|
+
kind: SpanKind.CONSUMER
|
|
228
|
+
}, async (span) => {
|
|
229
|
+
const start = performance.now();
|
|
230
|
+
const result = await this.opts.run({
|
|
231
|
+
input: parsedData?.value,
|
|
232
|
+
step: new WorkflowStep({
|
|
233
|
+
queue,
|
|
234
|
+
workflowJobId: jobId,
|
|
235
|
+
workflowId: this.opts.id
|
|
236
|
+
}),
|
|
237
|
+
span
|
|
238
|
+
});
|
|
239
|
+
const end = performance.now();
|
|
240
|
+
Settings.logger?.success?.(`Completed workflow job ${job.id} of workflow ${this.opts.id} in ${(end - start).toFixed(2)} ms`);
|
|
241
|
+
return serialize$1(result);
|
|
242
|
+
}, propagation.extract(ROOT_CONTEXT, deserializedData.tracingHeaders));
|
|
243
|
+
},
|
|
244
|
+
queue,
|
|
249
245
|
...this.opts.workerOptions,
|
|
250
246
|
...opts
|
|
251
247
|
});
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
worker.on("ready", () => {
|
|
249
|
+
Settings.logger?.info?.(`Worker started for workflow ${this.opts.id}`);
|
|
250
|
+
});
|
|
251
|
+
worker.on("failed", (job) => {
|
|
252
|
+
Settings.logger?.info?.(`Workflow job ${job.id} of workflow ${this.opts.id} failed`);
|
|
253
|
+
});
|
|
254
|
+
const metricsOpts = opts?.metrics ?? this.opts.workerOptions?.metrics ?? Settings.metrics;
|
|
255
|
+
if (metricsOpts) await this.setupMetrics(metricsOpts);
|
|
254
256
|
asyncExitHook(async (signal) => {
|
|
255
257
|
Settings.logger?.info?.(`Received ${signal}, shutting down worker for workflow ${this.opts.id}...`);
|
|
256
258
|
await worker.close();
|
|
@@ -259,7 +261,7 @@ var Workflow = class {
|
|
|
259
261
|
}
|
|
260
262
|
async run(input, opts) {
|
|
261
263
|
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
262
|
-
if (parsedInput?.issues) throw new
|
|
264
|
+
if (parsedInput?.issues) throw new Error("Invalid workflow input");
|
|
263
265
|
const queue = await this.getOrCreateQueue();
|
|
264
266
|
return runWithTracing(`workflow-producer/${this.opts.id}`, {
|
|
265
267
|
attributes: { "workflow.id": this.opts.id },
|
|
@@ -267,14 +269,16 @@ var Workflow = class {
|
|
|
267
269
|
}, async () => {
|
|
268
270
|
const tracingHeaders = {};
|
|
269
271
|
propagation.inject(context.active(), tracingHeaders);
|
|
270
|
-
return new WorkflowJob({
|
|
271
|
-
|
|
272
|
+
return new WorkflowJob({ job: await queue.add({
|
|
273
|
+
groupId: await this.opts.getGroupId?.(parsedInput?.value) ?? randomUUID(),
|
|
274
|
+
data: serialize$1({
|
|
272
275
|
input: parsedInput?.value,
|
|
273
276
|
stepData: {},
|
|
274
277
|
tracingHeaders
|
|
275
|
-
}),
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
}),
|
|
279
|
+
orderMs: opts?.priority === "high" ? 0 : void 0,
|
|
280
|
+
...opts
|
|
281
|
+
}) });
|
|
278
282
|
});
|
|
279
283
|
}
|
|
280
284
|
async runIn(input, delayMs, opts) {
|
|
@@ -284,59 +288,65 @@ var Workflow = class {
|
|
|
284
288
|
});
|
|
285
289
|
}
|
|
286
290
|
async runAt(input, date, opts) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
async runSchedule(schedulerId, repeatOpts, input, opts) {
|
|
291
|
-
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
292
|
-
if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
|
|
293
|
-
await (await this.getOrCreateQueue()).upsertJobScheduler(schedulerId, repeatOpts, {
|
|
294
|
-
name: "workflow-job",
|
|
295
|
-
data: serialize$1({
|
|
296
|
-
input: parsedInput?.value,
|
|
297
|
-
stepData: {},
|
|
298
|
-
tracingHeaders: {}
|
|
299
|
-
}),
|
|
300
|
-
opts
|
|
291
|
+
return this.run(input, {
|
|
292
|
+
runAt: date,
|
|
293
|
+
...opts
|
|
301
294
|
});
|
|
302
295
|
}
|
|
303
|
-
async runCron(
|
|
304
|
-
return this.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
296
|
+
async runCron(scheduleId, cron, input, opts) {
|
|
297
|
+
return this.run(input, {
|
|
298
|
+
groupId: scheduleId,
|
|
299
|
+
repeat: { pattern: cron },
|
|
300
|
+
...opts
|
|
301
|
+
});
|
|
308
302
|
}
|
|
309
|
-
async
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
...
|
|
303
|
+
async runEvery(scheduleId, everyMs, input, opts) {
|
|
304
|
+
return this.run(input, {
|
|
305
|
+
groupId: scheduleId,
|
|
306
|
+
repeat: { every: everyMs },
|
|
307
|
+
...opts
|
|
314
308
|
});
|
|
315
309
|
}
|
|
316
310
|
async getOrCreateQueue() {
|
|
317
|
-
if (!this.queue) this.queue = new Queue(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
removeOnComplete: true,
|
|
322
|
-
removeOnFail: { age: 1440 * 60 },
|
|
323
|
-
...this.opts.queueOptions?.defaultJobOptions
|
|
324
|
-
},
|
|
311
|
+
if (!this.queue) this.queue = new Queue({
|
|
312
|
+
namespace: this.opts.id,
|
|
313
|
+
redis: this.opts.redis ?? await defaultRedisConnection(),
|
|
314
|
+
keepFailed: 100,
|
|
325
315
|
...this.opts.queueOptions
|
|
326
316
|
});
|
|
327
|
-
await this.queue.waitUntilReady();
|
|
328
317
|
return this.queue;
|
|
329
318
|
}
|
|
330
|
-
async
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
|
|
319
|
+
async setupMetrics({ meter, prefix }) {
|
|
320
|
+
const attributes = { workflow_id: this.opts.id };
|
|
321
|
+
const queue = await this.getOrCreateQueue();
|
|
322
|
+
const completedJobsGauge = meter.createObservableGauge(`${prefix}_workflow_completed_jobs`, { description: "Number of completed workflow jobs" });
|
|
323
|
+
const activeJobsGauge = meter.createObservableGauge(`${prefix}_workflow_active_jobs`, { description: "Number of active workflow jobs" });
|
|
324
|
+
const failedJobsGauge = meter.createObservableGauge(`${prefix}_workflow_failed_jobs`, { description: "Number of failed workflow jobs" });
|
|
325
|
+
const waitingJobsGauge = meter.createObservableGauge(`${prefix}_workflow_waiting_jobs`, { description: "Number of waiting workflow jobs" });
|
|
326
|
+
const delayedJobsGauge = meter.createObservableGauge(`${prefix}_workflow_delayed_jobs`, { description: "Number of delayed workflow jobs" });
|
|
327
|
+
const groupCountGauge = meter.createObservableGauge(`${prefix}_workflow_groups`, { description: "Number of workflow job groups" });
|
|
328
|
+
meter.addBatchObservableCallback(async (observableResult) => {
|
|
329
|
+
try {
|
|
330
|
+
const [counts, groupCount] = await Promise.all([queue.getJobCounts(), queue.getUniqueGroupsCount()]);
|
|
331
|
+
observableResult.observe(completedJobsGauge, counts.completed, attributes);
|
|
332
|
+
observableResult.observe(activeJobsGauge, counts.active, attributes);
|
|
333
|
+
observableResult.observe(failedJobsGauge, counts.failed, attributes);
|
|
334
|
+
observableResult.observe(waitingJobsGauge, counts.waiting, attributes);
|
|
335
|
+
observableResult.observe(delayedJobsGauge, counts.delayed, attributes);
|
|
336
|
+
observableResult.observe(groupCountGauge, groupCount, attributes);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error("Error collecting workflow metrics:", err);
|
|
339
|
+
}
|
|
340
|
+
}, [
|
|
341
|
+
completedJobsGauge,
|
|
342
|
+
activeJobsGauge,
|
|
343
|
+
failedJobsGauge,
|
|
344
|
+
waitingJobsGauge,
|
|
345
|
+
delayedJobsGauge,
|
|
346
|
+
groupCountGauge
|
|
347
|
+
]);
|
|
338
348
|
}
|
|
339
349
|
};
|
|
340
350
|
|
|
341
351
|
//#endregion
|
|
342
|
-
export { Settings, Workflow,
|
|
352
|
+
export { Settings, Workflow, createRedis };
|
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.7.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",
|
|
@@ -31,28 +31,29 @@
|
|
|
31
31
|
"node": "24",
|
|
32
32
|
"pnpm": "10"
|
|
33
33
|
},
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"@opentelemetry/api": "^1.9.0"
|
|
36
|
-
},
|
|
37
34
|
"dependencies": {
|
|
38
35
|
"@antfu/utils": "^9.3.0",
|
|
36
|
+
"@opentelemetry/api": "^1.9.0",
|
|
39
37
|
"@standard-schema/spec": "^1.1.0",
|
|
40
38
|
"@types/node": "^25.0.3",
|
|
41
|
-
"bullmq": "^5.66.4",
|
|
42
39
|
"exit-hook": "^5.0.1",
|
|
40
|
+
"groupmq": "^1.1.0",
|
|
43
41
|
"ioredis": "^5.8.2",
|
|
44
42
|
"p-retry": "^7.1.1",
|
|
45
43
|
"superjson": "^2.2.6",
|
|
46
|
-
"
|
|
47
|
-
"type-fest": "^5.3.1",
|
|
48
|
-
"zod": "^4.3.4"
|
|
44
|
+
"type-fest": "^5.3.1"
|
|
49
45
|
},
|
|
50
46
|
"devDependencies": {
|
|
51
47
|
"@falcondev-oss/configs": "^5.0.2",
|
|
48
|
+
"@testcontainers/redis": "^11.11.0",
|
|
49
|
+
"arktype": "^2.1.29",
|
|
52
50
|
"eslint": "^9.39.2",
|
|
53
51
|
"prettier": "^3.7.4",
|
|
54
52
|
"tsdown": "0.19.0-beta.5",
|
|
55
|
-
"
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.0.18",
|
|
56
|
+
"zod": "^4.3.4"
|
|
56
57
|
},
|
|
57
58
|
"scripts": {
|
|
58
59
|
"build": "tsdown",
|