@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 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
- repeat(input: RunInput, cronOrInterval: string | number, opts?: Except<JobsOptions, 'repeat'>): Promise<WorkflowJob<Output>>;
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:${name}`, { attributes: {
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:${name}`, { attributes: {
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
- await new Worker(this.opts.id, async (job) => {
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:work:${this.opts.id}`, {
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
- }).waitUntilReady();
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:run:${this.opts.id}`, {
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 repeat(input, cronOrInterval, opts) {
273
- return this.run(input, {
274
- repeat: {
275
- tz: Settings.defaultCronTimezone,
276
- ...typeof cronOrInterval === "string" ? { pattern: cronOrInterval } : { every: cronOrInterval }
277
- },
278
- ...opts
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.5.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",