@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 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
- repeat(input: RunInput, cronOrInterval: string | number, opts?: Except<JobsOptions, 'repeat'>): Promise<WorkflowJob<Output>>;
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
- lazyConnect: true,
30
- maxRetriesPerRequest: null
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:${name}`, { attributes: {
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:${name}`, { attributes: {
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
- await new Worker(this.opts.id, async (job) => {
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:work:${this.opts.id}`, {
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
- }).waitUntilReady();
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:run:${this.opts.id}`, {
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 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
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.5.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",