@falcondev-oss/workflow 0.9.0 → 0.10.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 +6 -0
- package/dist/index.mjs +49 -20
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,7 @@ type WorkflowLogger = {
|
|
|
13
13
|
success?: (...data: any[]) => void;
|
|
14
14
|
error?: (...data: any[]) => void;
|
|
15
15
|
debug?: (...data: any[]) => void;
|
|
16
|
+
warn?: (...data: any[]) => void;
|
|
16
17
|
};
|
|
17
18
|
declare const Settings: {
|
|
18
19
|
defaultConnection: (() => Promise<IORedis> | IORedis) | undefined;
|
|
@@ -45,11 +46,16 @@ declare class WorkflowStep {
|
|
|
45
46
|
private workflowJobId;
|
|
46
47
|
private stepNamePrefix;
|
|
47
48
|
private updateStepDataMutex;
|
|
49
|
+
private meta;
|
|
48
50
|
constructor(opts: {
|
|
49
51
|
queue: WorkflowQueueInternal<unknown>;
|
|
50
52
|
workflowJobId: string;
|
|
51
53
|
workflowId: string;
|
|
52
54
|
stepNamePrefix?: string;
|
|
55
|
+
meta: {
|
|
56
|
+
stepPromises: Set<Promise<any>>;
|
|
57
|
+
isCanceled: boolean;
|
|
58
|
+
};
|
|
53
59
|
});
|
|
54
60
|
private addNamePrefix;
|
|
55
61
|
do<R>(stepName: string, run: (ctx: {
|
package/dist/index.mjs
CHANGED
|
@@ -7296,7 +7296,7 @@ var require_parser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
7296
7296
|
}));
|
|
7297
7297
|
|
|
7298
7298
|
//#endregion
|
|
7299
|
-
//#region node_modules/.pnpm/groupmq@1.1.1-next.2_patch_hash=
|
|
7299
|
+
//#region node_modules/.pnpm/groupmq@1.1.1-next.2_patch_hash=1f311e9b6cc731809ddc67335576540abb3c2e90f98c33be84e8bb3c8113391c_ioredis@5.9.2/node_modules/groupmq/dist/index.js
|
|
7300
7300
|
var import_parser = /* @__PURE__ */ __toESM$1(require_parser(), 1);
|
|
7301
7301
|
const __INLINED_LUA_SCRIPTS__ = {
|
|
7302
7302
|
"change-delay": "-- argv: ns, jobId, newDelayUntil, now\nlocal ns = KEYS[1]\nlocal jobId = ARGV[1]\nlocal newDelayUntil = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\n\nlocal jobKey = ns .. \":job:\" .. jobId\nlocal delayedKey = ns .. \":delayed\"\nlocal readyKey = ns .. \":ready\"\n\n-- Check if job exists\nlocal exists = redis.call(\"EXISTS\", jobKey)\nif exists == 0 then\n return 0\nend\n\nlocal groupId = redis.call(\"HGET\", jobKey, \"groupId\")\nif not groupId then\n return 0\nend\n\nlocal gZ = ns .. \":g:\" .. groupId\n\n-- Update job's delayUntil field\nredis.call(\"HSET\", jobKey, \"delayUntil\", tostring(newDelayUntil))\n\n-- Check if job is currently in delayed set\nlocal inDelayed = redis.call(\"ZSCORE\", delayedKey, jobId)\n\nif newDelayUntil > 0 and newDelayUntil > now then\n -- Job should be delayed\n redis.call(\"HSET\", jobKey, \"status\", \"delayed\")\n if inDelayed then\n -- Update existing delay\n redis.call(\"ZADD\", delayedKey, newDelayUntil, jobId)\n else\n -- Move to delayed\n redis.call(\"ZADD\", delayedKey, newDelayUntil, jobId)\n -- If this is the head job, remove group from ready\n local head = redis.call(\"ZRANGE\", gZ, 0, 0)\n if head and #head > 0 and head[1] == jobId then\n redis.call(\"ZREM\", readyKey, groupId)\n end\n end\nelse\n -- Job should be ready immediately\n redis.call(\"HSET\", jobKey, \"status\", \"waiting\")\n if inDelayed then\n -- Remove from delayed\n redis.call(\"ZREM\", delayedKey, jobId)\n -- If this is the head job, ensure group is in ready\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 and head[1] == jobId then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, groupId)\n end\n end\nend\n\nreturn 1\n\n\n",
|
|
@@ -8782,7 +8782,7 @@ var Queue = class {
|
|
|
8782
8782
|
async addRepeatingJob(opts) {
|
|
8783
8783
|
if (!opts.repeat) throw new Error("Repeat options are required for repeating jobs");
|
|
8784
8784
|
const now = Date.now();
|
|
8785
|
-
const repeatKey = `${opts.groupId}:${
|
|
8785
|
+
const repeatKey = `${opts.groupId}:${now}:${Math.random().toString(36).slice(2)}`;
|
|
8786
8786
|
let nextRunTime;
|
|
8787
8787
|
if ("every" in opts.repeat) nextRunTime = now + opts.repeat.every;
|
|
8788
8788
|
else nextRunTime = this.getNextCronTime(opts.repeat.pattern, now);
|
|
@@ -8799,7 +8799,7 @@ var Queue = class {
|
|
|
8799
8799
|
const repeatJobKey = `${this.ns}:repeat:${repeatKey}`;
|
|
8800
8800
|
await this.r.set(repeatJobKey, JSON.stringify(repeatJobData));
|
|
8801
8801
|
await this.r.zadd(`${this.ns}:repeat:schedule`, nextRunTime, repeatKey);
|
|
8802
|
-
const lookupKey = `${this.ns}:repeat:lookup:${opts.groupId}
|
|
8802
|
+
const lookupKey = `${this.ns}:repeat:lookup:${opts.groupId}`;
|
|
8803
8803
|
await this.r.set(lookupKey, repeatKey);
|
|
8804
8804
|
const repeatId = `repeat:${repeatKey}`;
|
|
8805
8805
|
const jobHashKey = `${this.ns}:job:${repeatId}`;
|
|
@@ -8821,9 +8821,9 @@ var Queue = class {
|
|
|
8821
8821
|
/**
|
|
8822
8822
|
* Remove a repeating job
|
|
8823
8823
|
*/
|
|
8824
|
-
async removeRepeatingJob(groupId
|
|
8824
|
+
async removeRepeatingJob(groupId) {
|
|
8825
8825
|
try {
|
|
8826
|
-
const lookupKey = `${this.ns}:repeat:lookup:${groupId}
|
|
8826
|
+
const lookupKey = `${this.ns}:repeat:lookup:${groupId}`;
|
|
8827
8827
|
const repeatKey = await this.r.get(lookupKey);
|
|
8828
8828
|
if (!repeatKey) return false;
|
|
8829
8829
|
const repeatJobKey = `${this.ns}:repeat:${repeatKey}`;
|
|
@@ -9628,11 +9628,13 @@ var WorkflowStep = class WorkflowStep {
|
|
|
9628
9628
|
workflowJobId;
|
|
9629
9629
|
stepNamePrefix;
|
|
9630
9630
|
updateStepDataMutex = new Mutex();
|
|
9631
|
+
meta;
|
|
9631
9632
|
constructor(opts) {
|
|
9632
9633
|
this.queue = opts.queue;
|
|
9633
9634
|
this.workflowJobId = opts.workflowJobId;
|
|
9634
9635
|
this.workflowId = opts.workflowId;
|
|
9635
9636
|
this.stepNamePrefix = opts.stepNamePrefix ? `${opts.stepNamePrefix}|` : "";
|
|
9637
|
+
this.meta = opts.meta;
|
|
9636
9638
|
}
|
|
9637
9639
|
addNamePrefix(name) {
|
|
9638
9640
|
return `${this.stepNamePrefix}${name}`;
|
|
@@ -9650,7 +9652,7 @@ var WorkflowStep = class WorkflowStep {
|
|
|
9650
9652
|
attempt: initialAttempt
|
|
9651
9653
|
});
|
|
9652
9654
|
Settings.logger?.debug?.(`[${this.workflowId}/${this.workflowJobId}] Running step '${name}' (attempt ${initialAttempt + 1})`);
|
|
9653
|
-
|
|
9655
|
+
const promise = pRetry(async (attempt) => {
|
|
9654
9656
|
const result = await runWithTracing(`workflow-worker/${this.workflowId}/step/${name}`, { attributes: {
|
|
9655
9657
|
"workflow.id": this.workflowId,
|
|
9656
9658
|
"workflow.job_id": this.workflowJobId,
|
|
@@ -9661,7 +9663,8 @@ var WorkflowStep = class WorkflowStep {
|
|
|
9661
9663
|
queue: this.queue,
|
|
9662
9664
|
workflowId: this.workflowId,
|
|
9663
9665
|
workflowJobId: this.workflowJobId,
|
|
9664
|
-
stepNamePrefix: name
|
|
9666
|
+
stepNamePrefix: name,
|
|
9667
|
+
meta: this.meta
|
|
9665
9668
|
}),
|
|
9666
9669
|
span
|
|
9667
9670
|
}));
|
|
@@ -9682,8 +9685,14 @@ var WorkflowStep = class WorkflowStep {
|
|
|
9682
9685
|
attempt: initialAttempt + ctx.attemptNumber
|
|
9683
9686
|
});
|
|
9684
9687
|
return options?.retry?.onFailedAttempt?.(ctx);
|
|
9688
|
+
},
|
|
9689
|
+
shouldRetry: async (context$1) => {
|
|
9690
|
+
if (this.meta.isCanceled) return false;
|
|
9691
|
+
return options?.retry?.shouldRetry?.(context$1) ?? true;
|
|
9685
9692
|
}
|
|
9686
9693
|
});
|
|
9694
|
+
this.meta.stepPromises.add(promise);
|
|
9695
|
+
return promise.finally(() => this.meta.stepPromises.delete(promise));
|
|
9687
9696
|
}
|
|
9688
9697
|
async wait(stepName, durationMs) {
|
|
9689
9698
|
const name = this.addNamePrefix(stepName);
|
|
@@ -9758,19 +9767,33 @@ var Workflow = class {
|
|
|
9758
9767
|
},
|
|
9759
9768
|
kind: SpanKind.CONSUMER
|
|
9760
9769
|
}, async (span) => {
|
|
9770
|
+
const stepMeta = {
|
|
9771
|
+
stepPromises: /* @__PURE__ */ new Set(),
|
|
9772
|
+
isCanceled: false
|
|
9773
|
+
};
|
|
9761
9774
|
const start = performance.now();
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9775
|
+
try {
|
|
9776
|
+
const result = await this.opts.run({
|
|
9777
|
+
input: parsedData?.value,
|
|
9778
|
+
step: new WorkflowStep({
|
|
9779
|
+
queue,
|
|
9780
|
+
workflowJobId: jobId,
|
|
9781
|
+
workflowId: this.opts.id,
|
|
9782
|
+
meta: stepMeta
|
|
9783
|
+
}),
|
|
9784
|
+
span
|
|
9785
|
+
});
|
|
9786
|
+
const end = performance.now();
|
|
9787
|
+
Settings.logger?.success?.(`[${this.opts.id}] Completed job ${job.id} in ${(end - start).toFixed(2)} ms`);
|
|
9788
|
+
return serialize$1(result);
|
|
9789
|
+
} catch (err) {
|
|
9790
|
+
stepMeta.isCanceled = true;
|
|
9791
|
+
if (stepMeta.stepPromises.size > 0) {
|
|
9792
|
+
Settings.logger?.warn?.(`[${this.opts.id}] Job failed but there are still ${stepMeta.stepPromises.size} running step(s), waiting for them to finish. Be careful when using 'Promise.all([step0, step1, ...])', as running steps are not canceled when one of them fails.`);
|
|
9793
|
+
await Promise.allSettled(stepMeta.stepPromises);
|
|
9794
|
+
}
|
|
9795
|
+
throw err;
|
|
9796
|
+
}
|
|
9774
9797
|
}, propagation.extract(ROOT_CONTEXT, deserializedData.tracingHeaders));
|
|
9775
9798
|
},
|
|
9776
9799
|
queue,
|
|
@@ -9796,6 +9819,10 @@ var Workflow = class {
|
|
|
9796
9819
|
const parsedInput = this.opts.schema && await this.opts.schema["~standard"].validate(input);
|
|
9797
9820
|
if (parsedInput?.issues) throw new Error("Invalid workflow input");
|
|
9798
9821
|
const queue = await this.getOrCreateQueue();
|
|
9822
|
+
const groupId = opts?.groupId ?? await this.opts.getGroupId?.(parsedInput?.value) ?? randomUUID();
|
|
9823
|
+
if (opts?.repeat) {
|
|
9824
|
+
if (await queue.removeRepeatingJob(groupId)) Settings.logger?.debug?.(`[${this.opts.id}] Removed existing repeating job with groupId '${groupId}' before adding new one with schedule ${JSON.stringify(opts.repeat)}`);
|
|
9825
|
+
}
|
|
9799
9826
|
return runWithTracing(`workflow-producer/${this.opts.id}`, {
|
|
9800
9827
|
attributes: { "workflow.id": this.opts.id },
|
|
9801
9828
|
kind: SpanKind.PRODUCER
|
|
@@ -9804,7 +9831,6 @@ var Workflow = class {
|
|
|
9804
9831
|
propagation.inject(context.active(), tracingHeaders);
|
|
9805
9832
|
const orderMs = opts?.orderMs ?? (opts?.priority === "high" ? 0 : void 0) ?? this.opts.jobOptions?.orderMs ?? (this.opts.jobOptions?.priority === "high" ? 0 : void 0);
|
|
9806
9833
|
return new WorkflowJob({ job: await queue.add({
|
|
9807
|
-
groupId: await this.opts.getGroupId?.(parsedInput?.value) ?? randomUUID(),
|
|
9808
9834
|
data: serialize$1({
|
|
9809
9835
|
input: parsedInput?.value,
|
|
9810
9836
|
stepData: {},
|
|
@@ -9812,6 +9838,7 @@ var Workflow = class {
|
|
|
9812
9838
|
}),
|
|
9813
9839
|
...this.opts.jobOptions,
|
|
9814
9840
|
...opts,
|
|
9841
|
+
groupId,
|
|
9815
9842
|
orderMs
|
|
9816
9843
|
}) });
|
|
9817
9844
|
});
|
|
@@ -9831,6 +9858,7 @@ var Workflow = class {
|
|
|
9831
9858
|
async "runCron"(scheduleId, cron, input, opts) {
|
|
9832
9859
|
return this.run(input, {
|
|
9833
9860
|
groupId: scheduleId,
|
|
9861
|
+
jobId: scheduleId,
|
|
9834
9862
|
repeat: { pattern: cron },
|
|
9835
9863
|
...opts
|
|
9836
9864
|
});
|
|
@@ -9838,6 +9866,7 @@ var Workflow = class {
|
|
|
9838
9866
|
async "runEvery"(scheduleId, everyMs, input, opts) {
|
|
9839
9867
|
return this.run(input, {
|
|
9840
9868
|
groupId: scheduleId,
|
|
9869
|
+
jobId: scheduleId,
|
|
9841
9870
|
repeat: { every: everyMs },
|
|
9842
9871
|
...opts
|
|
9843
9872
|
});
|
package/package.json
CHANGED