@falcondev-oss/workflow 0.2.0 → 0.4.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,10 +1,11 @@
1
1
  import { ConnectionOptions, Job, JobsOptions, Queue, QueueEvents, QueueEventsOptions, QueueOptions, UnrecoverableError, WorkerOptions } from "bullmq";
2
2
  import "@antfu/utils";
3
3
  import IORedis from "ioredis";
4
+ import { Span } from "@opentelemetry/api";
4
5
  import { SuperJSONResult } from "superjson";
5
6
  import { Options } from "p-retry";
6
7
  import { StandardSchemaV1 } from "@standard-schema/spec";
7
- import { Except, SetOptional, Tagged } from "type-fest";
8
+ import { Except, IsUnknown, SetOptional, Tagged } from "type-fest";
8
9
 
9
10
  //#region src/errors.d.ts
10
11
  declare class WorkflowInputError extends UnrecoverableError {
@@ -36,14 +37,20 @@ declare class WorkflowStep {
36
37
  private workflowId;
37
38
  private queue;
38
39
  private workflowJobId;
40
+ private stepNamePrefix;
39
41
  constructor(opts: {
40
42
  queue: WorkflowQueueInternal<any, any>;
41
43
  workflowJobId: string;
42
44
  workflowId: string;
45
+ stepNamePrefix?: string;
43
46
  });
44
- do<R>(name: string, run: () => R, options?: WorkflowStepOptions): Promise<R>;
45
- wait(name: string, durationMs: number): Promise<void>;
46
- waitUntil(name: string, date: Date): Promise<void>;
47
+ private addNamePrefix;
48
+ do<R>(stepName: string, run: (ctx: {
49
+ step: WorkflowStep;
50
+ span: Span;
51
+ }) => R, options?: WorkflowStepOptions): Promise<R>;
52
+ wait(stepName: string, durationMs: number): Promise<void>;
53
+ waitUntil(stepName: string, date: Date): Promise<void>;
47
54
  private getStepData;
48
55
  private updateStepData;
49
56
  private getWorkflowJob;
@@ -56,6 +63,7 @@ interface WorkflowStepOptions {
56
63
  type WorkflowJobInternal<Input, Output> = Job<Serialized<{
57
64
  input: Input;
58
65
  stepData: Record<string, WorkflowStepData>;
66
+ tracingHeaders: unknown;
59
67
  }>, Serialized<Output>, string>;
60
68
  type WorkflowQueueInternal<Input, Output> = Queue<WorkflowJobInternal<Input, Output>>;
61
69
  //#endregion
@@ -73,13 +81,14 @@ declare class WorkflowJob<Output> {
73
81
  //#region src/workflow.d.ts
74
82
  interface WorkflowOptions<RunInput, Input, Output> {
75
83
  id: string;
76
- input: StandardSchemaV1<RunInput, Input>;
77
- run: (context: WorkflowRunContext<Input>) => Promise<Output>;
84
+ schema?: StandardSchemaV1<RunInput, Input>;
85
+ run: (ctx: WorkflowRunContext<Input>) => Promise<Output>;
78
86
  queueOptions?: SetOptional<QueueOptions, 'connection'>;
79
87
  queueEventsOptions?: SetOptional<QueueEventsOptions, 'connection'>;
80
88
  connection?: ConnectionOptions;
81
89
  }
82
90
  declare class Workflow<RunInput, Input, Output> {
91
+ id: string;
83
92
  private opts;
84
93
  private queue?;
85
94
  private queueEvents?;
@@ -93,8 +102,9 @@ declare class Workflow<RunInput, Input, Output> {
93
102
  private getOrCreateQueueEvents;
94
103
  }
95
104
  interface WorkflowRunContext<Input> {
96
- input: Input;
105
+ input: IsUnknown<Input> extends true ? undefined : Input;
97
106
  step: WorkflowStep;
107
+ span: Span;
98
108
  }
99
109
  //#endregion
100
110
  export { Settings, Workflow, WorkflowInputError, WorkflowOptions, WorkflowRunContext };
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Queue, QueueEvents, UnrecoverableError, Worker } from "bullmq";
2
2
  import { createSingletonPromise } from "@antfu/utils";
3
3
  import IORedis from "ioredis";
4
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
4
5
  import { deserialize, serialize } from "superjson";
5
6
  import { setTimeout } from "node:timers/promises";
6
7
  import pRetry from "p-retry";
7
- import { SpanStatusCode, trace } from "@opentelemetry/api";
8
8
 
9
9
  //#region src/errors.ts
10
10
  var WorkflowInputError = class extends UnrecoverableError {
@@ -60,53 +60,65 @@ var WorkflowJob = class {
60
60
  function getTracer() {
61
61
  return trace.getTracer("falcondev-oss-workflow");
62
62
  }
63
- async function runWithTracing(spanName, attributes, fn) {
64
- return getTracer().startActiveSpan(spanName, async (span) => {
65
- try {
66
- span.setAttributes(attributes);
67
- const result = await fn();
68
- span.setStatus({ code: SpanStatusCode.OK });
69
- return result;
70
- } catch (err_) {
71
- const err = err_;
72
- span.recordException(err);
73
- span.setStatus({
74
- code: SpanStatusCode.ERROR,
75
- message: err.message
76
- });
77
- throw err_;
78
- } finally {
79
- span.end();
80
- }
81
- });
63
+ async function runWithTracing(spanName, options, fn, context$1) {
64
+ const span = getTracer().startSpan(spanName, options, context$1);
65
+ try {
66
+ const result = await fn(span);
67
+ span.setStatus({ code: SpanStatusCode.OK });
68
+ return result;
69
+ } catch (err_) {
70
+ const err = err_;
71
+ span.recordException(err);
72
+ span.setStatus({
73
+ code: SpanStatusCode.ERROR,
74
+ message: err.message
75
+ });
76
+ throw err_;
77
+ } finally {
78
+ span.end();
79
+ }
82
80
  }
83
81
 
84
82
  //#endregion
85
83
  //#region src/step.ts
86
- var WorkflowStep = class {
84
+ var WorkflowStep = class WorkflowStep {
87
85
  workflowId;
88
86
  queue;
89
87
  workflowJobId;
88
+ stepNamePrefix;
90
89
  constructor(opts) {
91
90
  this.queue = opts.queue;
92
91
  this.workflowJobId = opts.workflowJobId;
93
92
  this.workflowId = opts.workflowId;
93
+ this.stepNamePrefix = opts.stepNamePrefix ? `${opts.stepNamePrefix}|` : "";
94
+ }
95
+ addNamePrefix(name) {
96
+ return `${this.stepNamePrefix}${name}`;
94
97
  }
95
- async do(name, run, options) {
96
- const stepData = await this.getStepData("do", "name");
97
- if (stepData && "result" in stepData) return stepData.result;
98
+ async do(stepName, run, options) {
99
+ const name = this.addNamePrefix(stepName);
100
+ const stepData = await this.getStepData("do", name);
101
+ if (stepData?.result) return stepData.result;
98
102
  const initialAttempt = stepData?.attempt ?? 0;
99
103
  await this.updateStepData(name, {
100
104
  type: "do",
101
105
  attempt: initialAttempt
102
106
  });
103
107
  return pRetry(async (attempt) => {
104
- const result = await runWithTracing(`step:${name}`, {
108
+ const result = await runWithTracing(`step:${name}`, { attributes: {
105
109
  "workflow.id": this.workflowId,
106
110
  "workflow.job_id": this.workflowJobId,
107
111
  "workflow.step_name": name,
108
112
  "workflow.step.attempt": attempt
109
- }, run);
113
+ } }, async (span) => run({
114
+ step: new WorkflowStep({
115
+ queue: this.queue,
116
+ workflowId: this.workflowId,
117
+ workflowJobId: this.workflowJobId,
118
+ stepNamePrefix: name
119
+ }),
120
+ span
121
+ }));
110
122
  await this.updateStepData(name, {
111
123
  type: "do",
112
124
  result,
@@ -116,16 +128,17 @@ var WorkflowStep = class {
116
128
  }, {
117
129
  ...options?.retry,
118
130
  retries: (options?.retry?.retries ?? 0) - initialAttempt,
119
- onFailedAttempt: async (context) => {
131
+ onFailedAttempt: async (ctx) => {
120
132
  await this.updateStepData(name, {
121
133
  type: "do",
122
- attempt: initialAttempt + context.attemptNumber
134
+ attempt: initialAttempt + ctx.attemptNumber
123
135
  });
124
- return options?.retry?.onFailedAttempt?.(context);
136
+ return options?.retry?.onFailedAttempt?.(ctx);
125
137
  }
126
138
  });
127
139
  }
128
- async wait(name, durationMs) {
140
+ async wait(stepName, durationMs) {
141
+ const name = this.addNamePrefix(stepName);
129
142
  const job = await this.getWorkflowJob();
130
143
  const existingStepData = await this.getStepData("wait", name);
131
144
  const now = Date.now();
@@ -135,11 +148,11 @@ var WorkflowStep = class {
135
148
  startedAt: now
136
149
  };
137
150
  await this.updateStepData(name, stepData);
138
- await runWithTracing(`step:${name}`, {
151
+ await runWithTracing(`step:${name}`, { attributes: {
139
152
  "workflow.id": this.workflowId,
140
153
  "workflow.job_id": this.workflowJobId,
141
154
  "workflow.step_name": name
142
- }, async () => {
155
+ } }, async () => {
143
156
  const remainingMs = Math.max(0, stepData.startedAt + stepData.durationMs - now);
144
157
  const interval = setInterval(() => {
145
158
  job.updateProgress(name);
@@ -148,11 +161,11 @@ var WorkflowStep = class {
148
161
  clearInterval(interval);
149
162
  });
150
163
  }
151
- async waitUntil(name, date) {
164
+ async waitUntil(stepName, date) {
152
165
  const now = Date.now();
153
166
  const targetTime = date.getTime();
154
167
  const durationMs = Math.max(0, targetTime - now);
155
- return this.wait(name, durationMs);
168
+ return this.wait(stepName, durationMs);
156
169
  }
157
170
  async getStepData(type, stepName) {
158
171
  const stepData = deserialize$1((await this.getWorkflowJob()).data).stepData[stepName];
@@ -175,10 +188,12 @@ var WorkflowStep = class {
175
188
  //#endregion
176
189
  //#region src/workflow.ts
177
190
  var Workflow = class {
191
+ id;
178
192
  opts;
179
193
  queue;
180
194
  queueEvents;
181
195
  constructor(opts) {
196
+ this.id = opts.id;
182
197
  this.opts = opts;
183
198
  }
184
199
  async work(opts) {
@@ -187,21 +202,25 @@ var Workflow = class {
187
202
  const jobId = job.id;
188
203
  if (!jobId) throw new Error("Job ID is missing");
189
204
  const deserializedData = deserialize$1(job.data);
190
- const parsedData = await this.opts.input["~standard"].validate(deserializedData.input);
191
- if (parsedData.issues) throw new WorkflowInputError("Invalid workflow input", parsedData.issues);
192
- return runWithTracing(`workflow:${this.opts.id}`, {
193
- "workflow.id": this.opts.id,
194
- "workflow.job_id": jobId
195
- }, async () => {
205
+ const parsedData = this.opts.schema && await this.opts.schema["~standard"].validate(deserializedData.input);
206
+ if (parsedData?.issues) throw new WorkflowInputError("Invalid workflow input", parsedData.issues);
207
+ return runWithTracing(`workflow:work:${this.opts.id}`, {
208
+ attributes: {
209
+ "workflow.id": this.opts.id,
210
+ "workflow.job_id": jobId
211
+ },
212
+ kind: SpanKind.CONSUMER
213
+ }, async (span) => {
196
214
  return serialize$1(await this.opts.run({
197
- input: parsedData.value,
215
+ input: parsedData?.value,
198
216
  step: new WorkflowStep({
199
217
  queue,
200
218
  workflowJobId: jobId,
201
219
  workflowId: this.opts.id
202
- })
220
+ }),
221
+ span
203
222
  }));
204
- });
223
+ }, propagation.extract(ROOT_CONTEXT, deserializedData.tracingHeaders));
205
224
  }, {
206
225
  connection: this.opts.connection ?? await defaultRedisConnection(),
207
226
  prefix: Settings.defaultPrefix,
@@ -210,14 +229,23 @@ var Workflow = class {
210
229
  return this;
211
230
  }
212
231
  async run(input, opts) {
213
- const parsedInput = await this.opts.input["~standard"].validate(input);
214
- if (parsedInput.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
215
- return new WorkflowJob({
216
- job: await (await this.getOrCreateQueue()).add("workflow-job", serialize$1({
217
- input: parsedInput.value,
218
- stepData: {}
219
- }), opts),
220
- queueEvents: await this.getOrCreateQueueEvents()
232
+ const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
233
+ if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
234
+ const queue = await this.getOrCreateQueue();
235
+ return runWithTracing(`workflow:run:${this.opts.id}`, {
236
+ attributes: { "workflow.id": this.opts.id },
237
+ kind: SpanKind.PRODUCER
238
+ }, async () => {
239
+ const tracingHeaders = {};
240
+ propagation.inject(context.active(), tracingHeaders);
241
+ return new WorkflowJob({
242
+ job: await queue.add("workflow-job", serialize$1({
243
+ input: parsedInput?.value,
244
+ stepData: {},
245
+ tracingHeaders
246
+ }), opts),
247
+ queueEvents: await this.getOrCreateQueueEvents()
248
+ });
221
249
  });
222
250
  }
223
251
  async runIn(input, delayMs, opts) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@falcondev-oss/workflow",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.4.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",