@falcondev-oss/workflow 0.2.0 → 0.3.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,8 +81,8 @@ 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;
@@ -93,8 +101,9 @@ declare class Workflow<RunInput, Input, Output> {
93
101
  private getOrCreateQueueEvents;
94
102
  }
95
103
  interface WorkflowRunContext<Input> {
96
- input: Input;
104
+ input: IsUnknown<Input> extends true ? undefined : Input;
97
105
  step: WorkflowStep;
106
+ span: Span;
98
107
  }
99
108
  //#endregion
100
109
  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];
@@ -187,21 +200,25 @@ var Workflow = class {
187
200
  const jobId = job.id;
188
201
  if (!jobId) throw new Error("Job ID is missing");
189
202
  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 () => {
203
+ const parsedData = this.opts.schema && await this.opts.schema["~standard"].validate(deserializedData.input);
204
+ if (parsedData?.issues) throw new WorkflowInputError("Invalid workflow input", parsedData.issues);
205
+ return runWithTracing(`workflow:work:${this.opts.id}`, {
206
+ attributes: {
207
+ "workflow.id": this.opts.id,
208
+ "workflow.job_id": jobId
209
+ },
210
+ kind: SpanKind.CONSUMER
211
+ }, async (span) => {
196
212
  return serialize$1(await this.opts.run({
197
- input: parsedData.value,
213
+ input: parsedData?.value,
198
214
  step: new WorkflowStep({
199
215
  queue,
200
216
  workflowJobId: jobId,
201
217
  workflowId: this.opts.id
202
- })
218
+ }),
219
+ span
203
220
  }));
204
- });
221
+ }, propagation.extract(ROOT_CONTEXT, deserializedData.tracingHeaders));
205
222
  }, {
206
223
  connection: this.opts.connection ?? await defaultRedisConnection(),
207
224
  prefix: Settings.defaultPrefix,
@@ -210,14 +227,23 @@ var Workflow = class {
210
227
  return this;
211
228
  }
212
229
  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()
230
+ const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
231
+ if (parsedInput?.issues) throw new WorkflowInputError("Invalid workflow input", parsedInput.issues);
232
+ const queue = await this.getOrCreateQueue();
233
+ return runWithTracing(`workflow:run:${this.opts.id}`, {
234
+ attributes: { "workflow.id": this.opts.id },
235
+ kind: SpanKind.PRODUCER
236
+ }, async () => {
237
+ const tracingHeaders = {};
238
+ propagation.inject(context.active(), tracingHeaders);
239
+ return new WorkflowJob({
240
+ job: await queue.add("workflow-job", serialize$1({
241
+ input: parsedInput?.value,
242
+ stepData: {},
243
+ tracingHeaders
244
+ }), opts),
245
+ queueEvents: await this.getOrCreateQueueEvents()
246
+ });
221
247
  });
222
248
  }
223
249
  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.3.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",