@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 +16 -7
- package/dist/index.mjs +77 -51
- package/package.json +1 -1
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
77
|
-
run: (
|
|
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,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(
|
|
96
|
-
const
|
|
97
|
-
|
|
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 (
|
|
131
|
+
onFailedAttempt: async (ctx) => {
|
|
120
132
|
await this.updateStepData(name, {
|
|
121
133
|
type: "do",
|
|
122
|
-
attempt: initialAttempt +
|
|
134
|
+
attempt: initialAttempt + ctx.attemptNumber
|
|
123
135
|
});
|
|
124
|
-
return options?.retry?.onFailedAttempt?.(
|
|
136
|
+
return options?.retry?.onFailedAttempt?.(ctx);
|
|
125
137
|
}
|
|
126
138
|
});
|
|
127
139
|
}
|
|
128
|
-
async wait(
|
|
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(
|
|
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(
|
|
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.
|
|
191
|
-
if (parsedData
|
|
192
|
-
return runWithTracing(`workflow:${this.opts.id}`, {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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.
|
|
214
|
-
if (parsedInput
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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