@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 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 createRedisConnection(opts: RedisOptions): Promise<IORedis>;
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<any, any>;
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 WorkflowJobInternal<Input, Output> = Job<Serialized<{
65
+ type WorkflowJobPayloadInternal<Input> = Serialized<{
70
66
  input: Input | undefined;
71
67
  stepData: Record<string, WorkflowStepData>;
72
68
  tracingHeaders: unknown;
73
- }>, Serialized<Output>, string>;
74
- type WorkflowQueueInternal<Input, Output> = Queue<WorkflowJobInternal<Input, Output>>;
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
- private queueEvents;
75
+ groupId: string;
76
+ id: string;
80
77
  constructor(opts: {
81
- job: WorkflowJobInternal<unknown, Output>;
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
- queueOptions?: SetOptional<QueueOptions, 'connection'>;
93
- workerOptions?: SetOptional<WorkerOptions, 'connection'>;
94
- queueEventsOptions?: SetOptional<QueueEventsOptions, 'connection'>;
95
- connection?: ConnectionOptions;
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?: Omit<SetOptional<WorkerOptions, 'connection'>, 'autorun'>): Promise<this>;
104
- run(input: RunInput, opts?: JobsOptions): Promise<WorkflowJob<Output>>;
105
- runIn(input: RunInput, delayMs: number, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
106
- runAt(input: RunInput, date: Date, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
107
- private runSchedule;
108
- runCron(schedulerId: string, cron: string, input: RunInput, opts?: JobSchedulerTemplateOptions): Promise<void>;
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 getOrCreateQueueEvents;
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, WorkflowInputError, WorkflowOptions, WorkflowRunContext, createRedisConnection };
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
- defaultCronTimezone: void 0,
25
- logger: void 0
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 createRedisConnection(opts) {
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
- queueEvents;
51
+ groupId;
52
+ id;
62
53
  constructor(opts) {
63
54
  this.job = opts.job;
64
- this.queueEvents = opts.queueEvents;
55
+ this.groupId = opts.job.groupId;
56
+ this.id = opts.job.id;
65
57
  }
66
58
  async wait(timeoutMs) {
67
- return deserialize$1(await this.job.waitUntilFinished(this.queueEvents, timeoutMs));
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?.result) return stepData.result;
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
- const remainingMs = Math.max(0, stepData.startedAt + stepData.durationMs - now);
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 Promise.all([job.updateData(serialize$1(jobData)), job.updateProgress(stepName)]);
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 UnrecoverableError(`Could not find workflow job with ID ${this.workflowJobId}`);
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(this.opts.id, async (job) => {
219
- Settings.logger?.info?.(`Processing workflow job ${job.id} of workflow ${this.opts.id}`);
220
- const jobId = job.id;
221
- if (!jobId) throw new Error("Job ID is missing");
222
- const deserializedData = deserialize$1(job.data);
223
- const parsedData = this.opts.schema && await this.opts.schema["~standard"].validate(deserializedData.input);
224
- if (parsedData?.issues) throw new WorkflowInputError("Invalid workflow input", parsedData.issues);
225
- return runWithTracing(`workflow-worker/${this.opts.id}`, {
226
- attributes: {
227
- "workflow.id": this.opts.id,
228
- "workflow.job_id": jobId
229
- },
230
- kind: SpanKind.CONSUMER
231
- }, async (span) => {
232
- const start = performance.now();
233
- const result = await this.opts.run({
234
- input: parsedData?.value,
235
- step: new WorkflowStep({
236
- queue,
237
- workflowJobId: jobId,
238
- workflowId: this.opts.id
239
- }),
240
- span
241
- });
242
- const end = performance.now();
243
- Settings.logger?.success?.(`Completed workflow job ${job.id} of workflow ${this.opts.id} in ${(end - start).toFixed(2)} ms`);
244
- return serialize$1(result);
245
- }, propagation.extract(ROOT_CONTEXT, deserializedData.tracingHeaders));
246
- }, {
247
- connection: this.opts.connection ?? await defaultRedisConnection(),
248
- prefix: Settings.defaultPrefix,
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
- await worker.waitUntilReady();
253
- Settings.logger?.info?.(`Worker started for workflow ${this.opts.id}`);
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 WorkflowInputError("Invalid workflow input", parsedInput.issues);
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
- job: await queue.add("workflow-job", serialize$1({
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
- }), opts),
276
- queueEvents: await this.getOrCreateQueueEvents()
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
- const now = Date.now();
288
- return date.getTime() < now ? this.run(input, opts) : this.runIn(input, date.getTime() - Date.now(), opts);
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(schedulerId, cron, input, opts) {
304
- return this.runSchedule(schedulerId, { pattern: cron }, input, opts);
305
- }
306
- async runEvery(schedulerId, everyMs, input, opts) {
307
- return this.runSchedule(schedulerId, { every: everyMs }, input, opts);
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 exportPrometheusMetrics(globalVariables) {
310
- return (await this.getOrCreateQueue()).exportPrometheusMetrics({
311
- workflowId: this.id,
312
- workflowPrefix: Settings.defaultPrefix,
313
- ...globalVariables
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(this.opts.id, {
318
- prefix: Settings.defaultPrefix,
319
- connection: this.opts.connection ?? await defaultRedisConnection(),
320
- defaultJobOptions: {
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 getOrCreateQueueEvents() {
331
- if (!this.queueEvents) this.queueEvents = new QueueEvents(this.opts.id, {
332
- prefix: Settings.defaultPrefix,
333
- connection: this.opts.connection ?? await defaultRedisConnection(),
334
- ...this.opts.queueEventsOptions
335
- });
336
- await this.queueEvents.waitUntilReady();
337
- return this.queueEvents;
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, WorkflowInputError, createRedisConnection };
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.6.2",
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
- "tsx": "^4.21.0",
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
- "typescript": "^5.9.3"
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",