@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 +17 -7
- package/dist/index.mjs +79 -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,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
|
-
|
|
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;
|
|
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,
|
|
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];
|
|
@@ -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.
|
|
191
|
-
if (parsedData
|
|
192
|
-
return runWithTracing(`workflow:${this.opts.id}`, {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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.
|
|
214
|
-
if (parsedInput
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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