@falcondev-oss/workflow 0.1.2 → 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 +20 -7
- package/dist/index.mjs +98 -52
- 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 { 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 {
|
|
@@ -16,6 +17,7 @@ declare class WorkflowInputError extends UnrecoverableError {
|
|
|
16
17
|
declare const Settings: {
|
|
17
18
|
defaultPrefix: string;
|
|
18
19
|
defaultConnection: IORedis | undefined;
|
|
20
|
+
defaultCronTimezone: string | undefined;
|
|
19
21
|
};
|
|
20
22
|
//#endregion
|
|
21
23
|
//#region src/serializer.d.ts
|
|
@@ -35,14 +37,20 @@ declare class WorkflowStep {
|
|
|
35
37
|
private workflowId;
|
|
36
38
|
private queue;
|
|
37
39
|
private workflowJobId;
|
|
40
|
+
private stepNamePrefix;
|
|
38
41
|
constructor(opts: {
|
|
39
42
|
queue: WorkflowQueueInternal<any, any>;
|
|
40
43
|
workflowJobId: string;
|
|
41
44
|
workflowId: string;
|
|
45
|
+
stepNamePrefix?: string;
|
|
42
46
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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>;
|
|
46
54
|
private getStepData;
|
|
47
55
|
private updateStepData;
|
|
48
56
|
private getWorkflowJob;
|
|
@@ -55,6 +63,7 @@ interface WorkflowStepOptions {
|
|
|
55
63
|
type WorkflowJobInternal<Input, Output> = Job<Serialized<{
|
|
56
64
|
input: Input;
|
|
57
65
|
stepData: Record<string, WorkflowStepData>;
|
|
66
|
+
tracingHeaders: unknown;
|
|
58
67
|
}>, Serialized<Output>, string>;
|
|
59
68
|
type WorkflowQueueInternal<Input, Output> = Queue<WorkflowJobInternal<Input, Output>>;
|
|
60
69
|
//#endregion
|
|
@@ -72,8 +81,8 @@ declare class WorkflowJob<Output> {
|
|
|
72
81
|
//#region src/workflow.d.ts
|
|
73
82
|
interface WorkflowOptions<RunInput, Input, Output> {
|
|
74
83
|
id: string;
|
|
75
|
-
|
|
76
|
-
run: (
|
|
84
|
+
schema?: StandardSchemaV1<RunInput, Input>;
|
|
85
|
+
run: (ctx: WorkflowRunContext<Input>) => Promise<Output>;
|
|
77
86
|
queueOptions?: SetOptional<QueueOptions, 'connection'>;
|
|
78
87
|
queueEventsOptions?: SetOptional<QueueEventsOptions, 'connection'>;
|
|
79
88
|
connection?: ConnectionOptions;
|
|
@@ -85,12 +94,16 @@ declare class Workflow<RunInput, Input, Output> {
|
|
|
85
94
|
constructor(opts: WorkflowOptions<RunInput, Input, Output>);
|
|
86
95
|
work(opts?: Omit<SetOptional<WorkerOptions, 'connection'>, 'autorun'>): Promise<this>;
|
|
87
96
|
run(input: RunInput, opts?: JobsOptions): Promise<WorkflowJob<Output>>;
|
|
97
|
+
runIn(input: RunInput, delayMs: number, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
|
|
98
|
+
runAt(input: RunInput, date: Date, opts?: Except<JobsOptions, 'delay'>): Promise<WorkflowJob<Output>>;
|
|
99
|
+
repeat(input: RunInput, cronOrInterval: string | number, opts?: Except<JobsOptions, 'repeat'>): Promise<WorkflowJob<Output>>;
|
|
88
100
|
private getOrCreateQueue;
|
|
89
101
|
private getOrCreateQueueEvents;
|
|
90
102
|
}
|
|
91
103
|
interface WorkflowRunContext<Input> {
|
|
92
|
-
input: Input;
|
|
104
|
+
input: IsUnknown<Input> extends true ? undefined : Input;
|
|
93
105
|
step: WorkflowStep;
|
|
106
|
+
span: Span;
|
|
94
107
|
}
|
|
95
108
|
//#endregion
|
|
96
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 {
|
|
@@ -19,7 +19,8 @@ var WorkflowInputError = class extends UnrecoverableError {
|
|
|
19
19
|
//#region src/settings.ts
|
|
20
20
|
const Settings = {
|
|
21
21
|
defaultPrefix: "falcondev-oss-workflow",
|
|
22
|
-
defaultConnection: void 0
|
|
22
|
+
defaultConnection: void 0,
|
|
23
|
+
defaultCronTimezone: void 0
|
|
23
24
|
};
|
|
24
25
|
const defaultRedisConnection = createSingletonPromise(async () => {
|
|
25
26
|
if (Settings.defaultConnection) return Settings.defaultConnection;
|
|
@@ -59,53 +60,65 @@ var WorkflowJob = class {
|
|
|
59
60
|
function getTracer() {
|
|
60
61
|
return trace.getTracer("falcondev-oss-workflow");
|
|
61
62
|
}
|
|
62
|
-
async function runWithTracing(spanName,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
});
|
|
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
|
+
}
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
//#endregion
|
|
84
83
|
//#region src/step.ts
|
|
85
|
-
var WorkflowStep = class {
|
|
84
|
+
var WorkflowStep = class WorkflowStep {
|
|
86
85
|
workflowId;
|
|
87
86
|
queue;
|
|
88
87
|
workflowJobId;
|
|
88
|
+
stepNamePrefix;
|
|
89
89
|
constructor(opts) {
|
|
90
90
|
this.queue = opts.queue;
|
|
91
91
|
this.workflowJobId = opts.workflowJobId;
|
|
92
92
|
this.workflowId = opts.workflowId;
|
|
93
|
+
this.stepNamePrefix = opts.stepNamePrefix ? `${opts.stepNamePrefix}|` : "";
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
addNamePrefix(name) {
|
|
96
|
+
return `${this.stepNamePrefix}${name}`;
|
|
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;
|
|
97
102
|
const initialAttempt = stepData?.attempt ?? 0;
|
|
98
103
|
await this.updateStepData(name, {
|
|
99
104
|
type: "do",
|
|
100
105
|
attempt: initialAttempt
|
|
101
106
|
});
|
|
102
107
|
return pRetry(async (attempt) => {
|
|
103
|
-
const result = await runWithTracing(`step:${name}`, {
|
|
108
|
+
const result = await runWithTracing(`step:${name}`, { attributes: {
|
|
104
109
|
"workflow.id": this.workflowId,
|
|
105
110
|
"workflow.job_id": this.workflowJobId,
|
|
106
111
|
"workflow.step_name": name,
|
|
107
112
|
"workflow.step.attempt": attempt
|
|
108
|
-
}, 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
|
+
}));
|
|
109
122
|
await this.updateStepData(name, {
|
|
110
123
|
type: "do",
|
|
111
124
|
result,
|
|
@@ -115,16 +128,17 @@ var WorkflowStep = class {
|
|
|
115
128
|
}, {
|
|
116
129
|
...options?.retry,
|
|
117
130
|
retries: (options?.retry?.retries ?? 0) - initialAttempt,
|
|
118
|
-
onFailedAttempt: async (
|
|
131
|
+
onFailedAttempt: async (ctx) => {
|
|
119
132
|
await this.updateStepData(name, {
|
|
120
133
|
type: "do",
|
|
121
|
-
attempt: initialAttempt +
|
|
134
|
+
attempt: initialAttempt + ctx.attemptNumber
|
|
122
135
|
});
|
|
123
|
-
return options?.retry?.onFailedAttempt?.(
|
|
136
|
+
return options?.retry?.onFailedAttempt?.(ctx);
|
|
124
137
|
}
|
|
125
138
|
});
|
|
126
139
|
}
|
|
127
|
-
async wait(
|
|
140
|
+
async wait(stepName, durationMs) {
|
|
141
|
+
const name = this.addNamePrefix(stepName);
|
|
128
142
|
const job = await this.getWorkflowJob();
|
|
129
143
|
const existingStepData = await this.getStepData("wait", name);
|
|
130
144
|
const now = Date.now();
|
|
@@ -134,11 +148,11 @@ var WorkflowStep = class {
|
|
|
134
148
|
startedAt: now
|
|
135
149
|
};
|
|
136
150
|
await this.updateStepData(name, stepData);
|
|
137
|
-
await runWithTracing(`step:${name}`, {
|
|
151
|
+
await runWithTracing(`step:${name}`, { attributes: {
|
|
138
152
|
"workflow.id": this.workflowId,
|
|
139
153
|
"workflow.job_id": this.workflowJobId,
|
|
140
154
|
"workflow.step_name": name
|
|
141
|
-
}, async () => {
|
|
155
|
+
} }, async () => {
|
|
142
156
|
const remainingMs = Math.max(0, stepData.startedAt + stepData.durationMs - now);
|
|
143
157
|
const interval = setInterval(() => {
|
|
144
158
|
job.updateProgress(name);
|
|
@@ -147,11 +161,11 @@ var WorkflowStep = class {
|
|
|
147
161
|
clearInterval(interval);
|
|
148
162
|
});
|
|
149
163
|
}
|
|
150
|
-
async waitUntil(
|
|
164
|
+
async waitUntil(stepName, date) {
|
|
151
165
|
const now = Date.now();
|
|
152
166
|
const targetTime = date.getTime();
|
|
153
167
|
const durationMs = Math.max(0, targetTime - now);
|
|
154
|
-
return this.wait(
|
|
168
|
+
return this.wait(stepName, durationMs);
|
|
155
169
|
}
|
|
156
170
|
async getStepData(type, stepName) {
|
|
157
171
|
const stepData = deserialize$1((await this.getWorkflowJob()).data).stepData[stepName];
|
|
@@ -186,21 +200,25 @@ var Workflow = class {
|
|
|
186
200
|
const jobId = job.id;
|
|
187
201
|
if (!jobId) throw new Error("Job ID is missing");
|
|
188
202
|
const deserializedData = deserialize$1(job.data);
|
|
189
|
-
const parsedData = await this.opts.
|
|
190
|
-
if (parsedData
|
|
191
|
-
return runWithTracing(`workflow:${this.opts.id}`, {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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) => {
|
|
195
212
|
return serialize$1(await this.opts.run({
|
|
196
|
-
input: parsedData
|
|
213
|
+
input: parsedData?.value,
|
|
197
214
|
step: new WorkflowStep({
|
|
198
215
|
queue,
|
|
199
216
|
workflowJobId: jobId,
|
|
200
217
|
workflowId: this.opts.id
|
|
201
|
-
})
|
|
218
|
+
}),
|
|
219
|
+
span
|
|
202
220
|
}));
|
|
203
|
-
});
|
|
221
|
+
}, propagation.extract(ROOT_CONTEXT, deserializedData.tracingHeaders));
|
|
204
222
|
}, {
|
|
205
223
|
connection: this.opts.connection ?? await defaultRedisConnection(),
|
|
206
224
|
prefix: Settings.defaultPrefix,
|
|
@@ -209,14 +227,42 @@ var Workflow = class {
|
|
|
209
227
|
return this;
|
|
210
228
|
}
|
|
211
229
|
async run(input, opts) {
|
|
212
|
-
const parsedInput = await this.opts.
|
|
213
|
-
if (parsedInput
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async runIn(input, delayMs, opts) {
|
|
250
|
+
return this.run(input, {
|
|
251
|
+
delay: delayMs,
|
|
252
|
+
...opts
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async runAt(input, date, opts) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
return date.getTime() < now ? this.run(input, opts) : this.runIn(input, date.getTime() - Date.now(), opts);
|
|
258
|
+
}
|
|
259
|
+
async repeat(input, cronOrInterval, opts) {
|
|
260
|
+
return this.run(input, {
|
|
261
|
+
repeat: {
|
|
262
|
+
tz: Settings.defaultCronTimezone,
|
|
263
|
+
...typeof cronOrInterval === "string" ? { pattern: cronOrInterval } : { every: cronOrInterval }
|
|
264
|
+
},
|
|
265
|
+
...opts
|
|
220
266
|
});
|
|
221
267
|
}
|
|
222
268
|
async getOrCreateQueue() {
|
package/package.json
CHANGED