@libeilong/mq 0.1.7 → 0.2.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,13 +1,20 @@
1
- import { AddOptions, AddOptions as AddOptions$1, FlowJob, FlowOptions, Job, Job as Job$1, Queue, Queue as Queue$1, ReservedJob, UnrecoverableError, Worker, Worker as Worker$1, WorkerOptions } from "groupmq-plus";
1
+ import { AddOptions, AddOptions as AddOptions$1, FlowOptions, FlowOptions as FlowOptions$1, Job, Job as Job$1, PriorityStrategy, Queue, Queue as Queue$1, QueueOptions, ReservedJob, UnrecoverableError, Worker, Worker as Worker$1, WorkerOptions } from "groupmq-plus";
2
2
  import IORedis from "ioredis";
3
3
 
4
4
  //#region src/BaseTask.d.ts
5
5
  interface BackoffOptions {
6
- /** Name of the backoff strategy. */
6
+ /** 延迟策略:固定间隔、指数递增 */
7
7
  type: 'fixed' | 'exponential';
8
- /** Delay in milliseconds. */
8
+ /** 延迟时间为毫秒 */
9
9
  delay?: number;
10
- /** Percentage of jitter usage. @defaultValue 0 */
10
+ /**
11
+ * 添加到延迟中的随机抖动比例,取值范围为 0 到 1 之间的小数。
12
+ * 抖动用于增加随机性,以防止因多个任务同时重试而导致的惊群效应。
13
+ * 最终的延迟时间将在 `delay ± (delay * jitter)` 的范围内随机波动。
14
+ * @defaultValue 0
15
+ * @example 0.1 // 为延迟添加 ±10% 的随机抖动
16
+ * @example 0.25 // 为延迟添加 ±25% 的随机抖动
17
+ */
11
18
  jitter?: number;
12
19
  }
13
20
  interface RetryOptions {
@@ -24,19 +31,12 @@ interface HandleOptions {
24
31
  }
25
32
  interface BaseTaskOptions {
26
33
  name?: string;
27
- concurrency?: number;
28
- strategyPollInterval?: number;
29
34
  autoStart?: boolean;
30
- queueOptions?: {
31
- keepCompleted?: number;
32
- keepFailed?: number;
33
- maxAttempts?: number;
34
- jobTimeoutMs?: number;
35
- orderingDelayMs?: number;
35
+ queueOptions?: Partial<QueueOptions>;
36
+ workerOptions?: Omit<Partial<WorkerOptions<any>>, 'backoff'> & {
36
37
  backoff?: BackoffOptions | ((attempt: number, error: unknown) => number);
37
38
  };
38
39
  debug?: boolean;
39
- strategy?: WorkerOptions<any>['strategy'];
40
40
  }
41
41
  declare class TaskRetryLaterError extends Error {
42
42
  customDelay?: number;
@@ -59,25 +59,11 @@ declare abstract class BaseTask<D = any> {
59
59
  priority?: number;
60
60
  concurrency?: number;
61
61
  }): Promise<void>;
62
- start(opts: ({
63
- groupId: string;
64
- data: D;
65
- jobId?: string;
66
- delay?: number;
67
- } & Omit<AddOptions$1<D>, 'groupId' | 'data'>) | ({
68
- groupId: string;
69
- data: D;
70
- jobId?: string;
71
- children: ({
72
- groupId: string;
73
- data: D;
74
- jobId?: string;
75
- } & Omit<FlowJob<D>, 'groupId' | 'data'>)[];
76
- } & Omit<FlowJob<D>, 'groupId' | 'data'>)): Promise<Job$1<D> & {
62
+ start(opts: AddOptions$1<D> | FlowOptions$1<D, D>): Promise<Job$1<D> & {
77
63
  finished: () => Promise<any>;
78
64
  }>;
79
65
  close(): Promise<void>;
80
66
  }
81
67
  //#endregion
82
- export { type AddOptions, BaseTask, type BaseTaskOptions, type FlowOptions, type HandleOptions, Job, Queue, type ReservedJob, type RetryOptions, TaskRetryLaterError, UnrecoverableError, Worker };
68
+ export { type AddOptions, BaseTask, type BaseTaskOptions, type FlowOptions, type HandleOptions, Job, PriorityStrategy, Queue, type ReservedJob, type RetryOptions, TaskRetryLaterError, UnrecoverableError, Worker };
83
69
  //# sourceMappingURL=index.d.mts.map
package/dist/index.d.ts CHANGED
@@ -1,13 +1,20 @@
1
- import { AddOptions, AddOptions as AddOptions$1, FlowJob, FlowOptions, Job, Job as Job$1, Queue, Queue as Queue$1, ReservedJob, UnrecoverableError, Worker, Worker as Worker$1, WorkerOptions } from "groupmq-plus";
1
+ import { AddOptions, AddOptions as AddOptions$1, FlowOptions, FlowOptions as FlowOptions$1, Job, Job as Job$1, PriorityStrategy, Queue, Queue as Queue$1, QueueOptions, ReservedJob, UnrecoverableError, Worker, Worker as Worker$1, WorkerOptions } from "groupmq-plus";
2
2
  import IORedis from "ioredis";
3
3
 
4
4
  //#region src/BaseTask.d.ts
5
5
  interface BackoffOptions {
6
- /** Name of the backoff strategy. */
6
+ /** 延迟策略:固定间隔、指数递增 */
7
7
  type: 'fixed' | 'exponential';
8
- /** Delay in milliseconds. */
8
+ /** 延迟时间为毫秒 */
9
9
  delay?: number;
10
- /** Percentage of jitter usage. @defaultValue 0 */
10
+ /**
11
+ * 添加到延迟中的随机抖动比例,取值范围为 0 到 1 之间的小数。
12
+ * 抖动用于增加随机性,以防止因多个任务同时重试而导致的惊群效应。
13
+ * 最终的延迟时间将在 `delay ± (delay * jitter)` 的范围内随机波动。
14
+ * @defaultValue 0
15
+ * @example 0.1 // 为延迟添加 ±10% 的随机抖动
16
+ * @example 0.25 // 为延迟添加 ±25% 的随机抖动
17
+ */
11
18
  jitter?: number;
12
19
  }
13
20
  interface RetryOptions {
@@ -24,19 +31,12 @@ interface HandleOptions {
24
31
  }
25
32
  interface BaseTaskOptions {
26
33
  name?: string;
27
- concurrency?: number;
28
- strategyPollInterval?: number;
29
34
  autoStart?: boolean;
30
- queueOptions?: {
31
- keepCompleted?: number;
32
- keepFailed?: number;
33
- maxAttempts?: number;
34
- jobTimeoutMs?: number;
35
- orderingDelayMs?: number;
35
+ queueOptions?: Partial<QueueOptions>;
36
+ workerOptions?: Omit<Partial<WorkerOptions<any>>, 'backoff'> & {
36
37
  backoff?: BackoffOptions | ((attempt: number, error: unknown) => number);
37
38
  };
38
39
  debug?: boolean;
39
- strategy?: WorkerOptions<any>['strategy'];
40
40
  }
41
41
  declare class TaskRetryLaterError extends Error {
42
42
  customDelay?: number;
@@ -59,25 +59,11 @@ declare abstract class BaseTask<D = any> {
59
59
  priority?: number;
60
60
  concurrency?: number;
61
61
  }): Promise<void>;
62
- start(opts: ({
63
- groupId: string;
64
- data: D;
65
- jobId?: string;
66
- delay?: number;
67
- } & Omit<AddOptions$1<D>, 'groupId' | 'data'>) | ({
68
- groupId: string;
69
- data: D;
70
- jobId?: string;
71
- children: ({
72
- groupId: string;
73
- data: D;
74
- jobId?: string;
75
- } & Omit<FlowJob<D>, 'groupId' | 'data'>)[];
76
- } & Omit<FlowJob<D>, 'groupId' | 'data'>)): Promise<Job$1<D> & {
62
+ start(opts: AddOptions$1<D> | FlowOptions$1<D, D>): Promise<Job$1<D> & {
77
63
  finished: () => Promise<any>;
78
64
  }>;
79
65
  close(): Promise<void>;
80
66
  }
81
67
  //#endregion
82
- export { type AddOptions, BaseTask, type BaseTaskOptions, type FlowOptions, type HandleOptions, Job, Queue, type ReservedJob, type RetryOptions, TaskRetryLaterError, UnrecoverableError, Worker };
68
+ export { type AddOptions, BaseTask, type BaseTaskOptions, type FlowOptions, type HandleOptions, Job, PriorityStrategy, Queue, type ReservedJob, type RetryOptions, TaskRetryLaterError, UnrecoverableError, Worker };
83
69
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`groupmq-plus`);c=s(c);var l=class extends Error{constructor(e,t){super(e),this.name=`TaskRetryLaterError`,this.customDelay=t}},u=class{get name(){return this.opts?.name||this.constructor.name}constructor(e,t){this.connection=e;let n=t??{},r={keepCompleted:0,maxAttempts:3,jobTimeoutMs:3e4};this.opts={concurrency:1,debug:!1,autoStart:!0,...n,queueOptions:{...r,...n.queueOptions}},this.queue=new c.Queue({redis:e,namespace:this.name,jobTimeoutMs:this.opts.queueOptions?.jobTimeoutMs,maxAttempts:this.opts.queueOptions?.maxAttempts,keepCompleted:this.opts.queueOptions?.keepCompleted,keepFailed:this.opts.queueOptions?.keepFailed,orderingDelayMs:this.opts.queueOptions?.orderingDelayMs}),this.worker=new c.Worker({queue:this.queue,name:this.name,concurrency:this.opts.concurrency,strategy:this.opts.strategy,strategyPollInterval:this.opts.strategyPollInterval,autoStart:this.opts.autoStart,handler:async e=>this.handle(e,{token:e.id,retryLater:t=>{let n=e.attemptsMade,r=e.opts.attempts,i={};i=t instanceof Error?{error:t,message:t.message}:typeof t==`string`?{message:t}:t||{};let{error:a,delay:o}=i,s=i.message||(a instanceof Error?a.message:void 0)||`retry later`,c=a instanceof Error?a:void 0;if(n+1>=r)throw c||Error(s);let u=new l(s,o);throw c&&(u.stack=c.stack,u.cause=c),u},abort:e=>{throw new c.UnrecoverableError(e||`failed`)}}),backoff:(e,t)=>{if(t instanceof l&&typeof t.customDelay==`number`)return t.customDelay;let n=this.opts.queueOptions?.backoff;if(typeof n==`function`)return n(e,t);if(n&&typeof n==`object`){let{type:t,delay:r=1e3,jitter:i=0}=n,a;if(a=t===`fixed`?r:t===`exponential`?r*2**(e-1):1e3*2**(e-1),i>0){let e=a*i,t=Math.random()*e*2-e;a=Math.max(0,a+t)}return Math.round(a)}return 1e3*2**(e-1)}}),this.setupEventListeners()}setupEventListeners(){this.worker.on(`completed`,e=>{this.opts.debug&&console.log(`[COMPLETED] ${this.name}-${e.id} (Group: ${e.groupId})`),this.onCompleted(e)}),this.worker.on(`failed`,async e=>{let t=e.attemptsMade>=e.opts.attempts-1,n=Error(e.failedReason||`task failed`);e.stacktrace&&(n.stack=e.stacktrace),this.opts.debug&&(t?console.error(`[FINAL FAILED] ${this.name}-${e.id}: ${n.message}`):console.log(`[RETRY] ${this.name}-${e.id}: ${n.message}`)),await this.onFailed(e,n),t&&await this.onFinalFailed(e,n)}),this.worker.on(`error`,e=>{this.opts.debug&&console.error(`[WORKER ERROR] ${this.name}`,e)})}async run(){return this.worker.run()}async onFailed(e,t){}async onFinalFailed(e,t){}async onCompleted(e){}setGroupConfig(e,t){return this.queue.setGroupConfig(e,t)}async start(e){let t;if(`children`in e){if(e.children.length===0)throw Error(`Flow must include at least one child job`);t=await this.queue.addFlow({parent:{...e,groupId:e.groupId,data:e.data},children:e.children.map(e=>({...e,groupId:e.groupId,data:e.data}))})}else t=await this.queue.add({groupId:e.groupId,data:e.data,jobId:e.jobId,delay:e.delay});return t.finished=async()=>t.waitUntilFinished(),t}async close(){await this.worker.close()}};exports.BaseTask=u,Object.defineProperty(exports,`Job`,{enumerable:!0,get:function(){return c.Job}}),Object.defineProperty(exports,`Queue`,{enumerable:!0,get:function(){return c.Queue}}),exports.TaskRetryLaterError=l,Object.defineProperty(exports,`UnrecoverableError`,{enumerable:!0,get:function(){return c.UnrecoverableError}}),Object.defineProperty(exports,`Worker`,{enumerable:!0,get:function(){return c.Worker}});
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`groupmq-plus`);c=s(c);var l=class extends Error{constructor(e,t){super(e),this.name=`TaskRetryLaterError`,this.customDelay=t}},u=class{get name(){return this.opts?.name||this.constructor.name}constructor(e,t){this.connection=e,this.opts=Object.assign({debug:!1,autoStart:!0,queueOptions:{},workerOptions:{}},t),this.opts.queueOptions=Object.assign({redis:e,namespace:this.name,keepCompleted:0,maxAttempts:3,jobTimeoutMs:5e3},this.opts.queueOptions),this.opts.workerOptions=Object.assign({concurrency:1,name:this.name},this.opts.workerOptions),this.opts.debug&&(this.opts.queueOptions.logger=!0,this.opts.workerOptions.logger=!0),this.queue=new c.Queue(this.opts.queueOptions),this.worker=new c.Worker({...this.opts.workerOptions,queue:this.queue,handler:async e=>this.handle(e,{token:e.id,retryLater:t=>{let n=e.attemptsMade,r=e.opts.attempts,i={};i=t instanceof Error?{error:t,message:t.message}:typeof t==`string`?{message:t}:t||{};let{error:a,delay:o}=i,s=i.message||(a instanceof Error?a.message:void 0)||`retry later`,c=a instanceof Error?a:void 0;if(n+1>=r)throw c||Error(s);let u=new l(s,o);throw c&&(u.stack=c.stack,u.cause=c),u},abort:e=>{throw new c.UnrecoverableError(e||`failed`)}}),backoff:(e,t)=>{if(t instanceof l&&typeof t.customDelay==`number`)return t.customDelay;let n=this.opts.workerOptions?.backoff;if(typeof n==`function`)return n(e,t);if(n&&typeof n==`object`){let{type:t,delay:r=1e3,jitter:i=0}=n,a;if(a=t===`fixed`?r:t===`exponential`?r*2**(e-1):1e3*2**(e-1),i>0){let e=a*i,t=Math.random()*e*2-e;a+=t}return Math.max(0,Math.round(a))}return 1e3*2**(e-1)}}),this.setupEventListeners()}setupEventListeners(){this.worker.on(`completed`,e=>{this.opts.debug&&console.log(`[COMPLETED] ${this.name}-${e.id} (Group: ${e.groupId})`),this.onCompleted(e)}),this.worker.on(`failed`,async e=>{let t=e.attemptsMade>=e.opts.attempts-1,n=Error(e.failedReason||`task failed`);e.stacktrace&&(n.stack=e.stacktrace),this.opts.debug&&(t?console.error(`[FINAL FAILED] ${this.name}-${e.id}: ${n.message}`):console.log(`[RETRY] ${this.name}-${e.id}: ${n.message}`)),await this.onFailed(e,n),t&&await this.onFinalFailed(e,n)}),this.worker.on(`error`,e=>{this.opts.debug&&console.error(`[WORKER ERROR] ${this.name}`,e)})}async run(){return this.worker.run()}async onFailed(e,t){}async onFinalFailed(e,t){}async onCompleted(e){}setGroupConfig(e,t){return this.queue.groups.setConfig(e,t)}async start(e){let t;if(`children`in e&&Array.isArray(e.children)){if(e.children.length===0)throw Error(`Flow must include at least one child job`);t=await this.queue.addFlow(e)}else t=await this.queue.add(e);return t.finished=async()=>t.waitUntilFinished(),t}async close(){await this.worker.close()}};exports.BaseTask=u,Object.defineProperty(exports,`Job`,{enumerable:!0,get:function(){return c.Job}}),Object.defineProperty(exports,`PriorityStrategy`,{enumerable:!0,get:function(){return c.PriorityStrategy}}),Object.defineProperty(exports,`Queue`,{enumerable:!0,get:function(){return c.Queue}}),exports.TaskRetryLaterError=l,Object.defineProperty(exports,`UnrecoverableError`,{enumerable:!0,get:function(){return c.UnrecoverableError}}),Object.defineProperty(exports,`Worker`,{enumerable:!0,get:function(){return c.Worker}});
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["connection: IORedis","Queue","Worker","options: RetryOptions","UnrecoverableError","baseDelay: number","job: Job<D>"],"sources":["../src/BaseTask.ts"],"sourcesContent":["import {\n FlowJob,\n Queue,\n Worker,\n Job,\n UnrecoverableError,\n AddOptions,\n WorkerOptions,\n FlowChildResult,\n} from 'groupmq-plus'\nimport IORedis from 'ioredis'\n\nexport type { FlowChildResult } from 'groupmq-plus'\n\nexport interface BackoffOptions {\n /** Name of the backoff strategy. */\n type: 'fixed' | 'exponential'\n /** Delay in milliseconds. */\n delay?: number\n /** Percentage of jitter usage. @defaultValue 0 */\n jitter?: number\n}\n\nexport interface RetryOptions {\n error?: Error | unknown\n message?: string\n delay?: number\n}\n\nexport interface HandleOptions {\n token: string\n retryLater(options?: RetryOptions): void\n retryLater(errorOrMessage?: Error | string): void\n retryLater(arg?: RetryOptions | Error | string): void\n abort(message?: string): void\n}\n\nexport interface BaseTaskOptions {\n name?: string\n concurrency?: number\n strategyPollInterval?: number\n autoStart?: boolean\n queueOptions?: {\n keepCompleted?: number\n keepFailed?: number\n maxAttempts?: number\n jobTimeoutMs?: number\n orderingDelayMs?: number\n backoff?: BackoffOptions | ((attempt: number, error: unknown) => number)\n }\n debug?: boolean\n strategy?: WorkerOptions<any>['strategy']\n}\n\nexport class TaskRetryLaterError extends Error {\n public customDelay?: number\n\n constructor(message?: string, delay?: number) {\n super(message)\n this.name = 'TaskRetryLaterError'\n this.customDelay = delay\n }\n}\n\nexport abstract class BaseTask<D = any> {\n public queue: Queue<D>\n public worker: Worker<D>\n protected opts: BaseTaskOptions\n\n get name() {\n return this.opts?.name || this.constructor.name\n }\n\n constructor(protected connection: IORedis, opts?: Partial<BaseTaskOptions>) {\n const normalizedOpts = opts ?? {}\n const defaultQueueOptions = {\n keepCompleted: 0,\n maxAttempts: 3,\n jobTimeoutMs: 30000,\n }\n this.opts = {\n concurrency: 1,\n debug: false,\n autoStart: true,\n ...normalizedOpts,\n queueOptions: {\n ...defaultQueueOptions,\n ...normalizedOpts.queueOptions,\n },\n }\n\n this.queue = new Queue<D>({\n redis: connection,\n namespace: this.name,\n jobTimeoutMs: this.opts.queueOptions?.jobTimeoutMs,\n maxAttempts: this.opts.queueOptions?.maxAttempts,\n keepCompleted: this.opts.queueOptions?.keepCompleted,\n keepFailed: this.opts.queueOptions?.keepFailed,\n orderingDelayMs: this.opts.queueOptions?.orderingDelayMs,\n })\n\n this.worker = new Worker<D>({\n queue: this.queue,\n name: this.name,\n concurrency: this.opts.concurrency,\n strategy: this.opts.strategy,\n strategyPollInterval: this.opts.strategyPollInterval,\n autoStart: this.opts.autoStart,\n handler: async (job) => {\n return this.handle(job, {\n token: job.id,\n retryLater: (arg: RetryOptions | Error | string) => {\n const currentAttempts = job.attemptsMade\n const maxAttempts = job.opts.attempts\n\n let options: RetryOptions = {}\n if (arg instanceof Error) {\n options = { error: arg, message: arg.message }\n } else if (typeof arg === 'string') {\n options = { message: arg }\n } else {\n options = arg || {}\n }\n\n const { error, delay } = options\n const message = options.message || (error instanceof Error ? error.message : undefined) || 'retry later'\n const errorObj = error instanceof Error ? error : undefined\n\n if (currentAttempts + 1 >= maxAttempts) {\n if (errorObj) throw errorObj\n throw new Error(message)\n }\n\n const retryErr = new TaskRetryLaterError(message, delay)\n if (errorObj) {\n retryErr.stack = errorObj.stack\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore - preserve cause on older runtimes\n retryErr.cause = errorObj\n }\n throw retryErr\n },\n abort: (message?: string) => {\n throw new UnrecoverableError(message || 'failed')\n },\n })\n },\n backoff: (attempt: number, error: unknown) => {\n // 优先使用 TaskRetryLaterError 的 customDelay\n if (error instanceof TaskRetryLaterError && typeof error.customDelay === 'number') {\n return error.customDelay\n }\n\n const backoffConfig = this.opts.queueOptions?.backoff\n\n // 使用自定义 backoff 函数\n if (typeof backoffConfig === 'function') {\n return backoffConfig(attempt, error)\n }\n\n // 使用 BackoffOptions 配置\n if (backoffConfig && typeof backoffConfig === 'object') {\n const { type, delay = 1000, jitter = 0 } = backoffConfig\n let baseDelay: number\n\n if (type === 'fixed') {\n baseDelay = delay\n } else if (type === 'exponential') {\n baseDelay = delay * Math.pow(2, attempt - 1)\n } else {\n // 不支持的 type,使用默认指数退避\n baseDelay = 1000 * Math.pow(2, attempt - 1)\n }\n\n // 应用 jitter(抖动)\n if (jitter > 0) {\n const jitterAmount = baseDelay * jitter\n const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount\n baseDelay = Math.max(0, baseDelay + randomJitter)\n }\n\n return Math.round(baseDelay)\n }\n\n // 默认指数退避策略\n return 1000 * Math.pow(2, attempt - 1)\n },\n })\n\n this.setupEventListeners()\n }\n\n private setupEventListeners() {\n this.worker.on('completed', (job) => {\n if (this.opts.debug) {\n console.log(`[COMPLETED] ${this.name}-${job.id} (Group: ${job.groupId})`)\n }\n this.onCompleted(job)\n })\n\n this.worker.on('failed', async (job) => {\n const isFinalFailure = job.attemptsMade >= job.opts.attempts - 1\n const error = new Error(job.failedReason || 'task failed')\n if (job.stacktrace) {\n error.stack = job.stacktrace\n }\n\n if (this.opts.debug) {\n if (!isFinalFailure) {\n console.log(`[RETRY] ${this.name}-${job.id}: ${error.message}`)\n } else {\n console.error(`[FINAL FAILED] ${this.name}-${job.id}: ${error.message}`)\n }\n }\n\n await this.onFailed(job, error)\n\n if (isFinalFailure) {\n await this.onFinalFailed(job, error)\n }\n })\n\n this.worker.on('error', (err) => {\n if (this.opts.debug) {\n console.error(`[WORKER ERROR] ${this.name}`, err)\n }\n })\n }\n\n async run() {\n return this.worker.run()\n }\n\n abstract handle(job: Job<D>, opts: HandleOptions): Promise<any>\n\n async onFailed(job: Job<D>, error: Error) {}\n async onFinalFailed(job: Job<D>, error: Error) {}\n async onCompleted(job: Job<D>) {}\n\n setGroupConfig(groupId: string, config: { priority?: number; concurrency?: number }) {\n return this.queue.setGroupConfig(groupId, config)\n }\n\n async start(\n opts:\n | ({ groupId: string; data: D; jobId?: string; delay?: number } & Omit<AddOptions<D>, 'groupId' | 'data'>)\n | ({\n groupId: string\n data: D\n jobId?: string\n children: ({\n groupId: string\n data: D\n jobId?: string\n } & Omit<FlowJob<D>, 'groupId' | 'data'>)[]\n } & Omit<FlowJob<D>, 'groupId' | 'data'>),\n ) {\n let job: Job<D>\n\n if ('children' in opts) {\n if (opts.children.length === 0) {\n throw new Error('Flow must include at least one child job')\n }\n\n job = await this.queue.addFlow({\n parent: {\n ...opts,\n groupId: opts.groupId,\n data: opts.data,\n },\n children: opts.children.map((child) => ({\n ...child,\n groupId: child.groupId,\n data: child.data,\n })),\n })\n } else {\n job = await this.queue.add({\n groupId: opts.groupId,\n data: opts.data,\n jobId: opts.jobId,\n delay: opts.delay,\n })\n }\n\n // provide a BullMQ-like finished helper\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n job['finished'] = async () => {\n return job.waitUntilFinished()\n }\n\n return job as Job<D> & { finished: () => Promise<any> }\n }\n\n async close() {\n await this.worker.close()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["connection: IORedis","Queue","Worker","options: RetryOptions","UnrecoverableError","baseDelay: number","job: Job<D>"],"sources":["../src/BaseTask.ts"],"sourcesContent":["import {\n AddOptions,\n FlowJob,\n FlowOptions,\n GroupOptions,\n Job,\n Queue,\n QueueOptions,\n UnrecoverableError,\n Worker,\n WorkerOptions\n} from 'groupmq-plus'\nimport IORedis from 'ioredis'\n\nexport type { FlowChildResult } from 'groupmq-plus'\n\nexport interface BackoffOptions {\n /** 延迟策略:固定间隔、指数递增 */\n type: 'fixed' | 'exponential'\n /** 延迟时间为毫秒 */\n delay?: number\n /**\n * 添加到延迟中的随机抖动比例,取值范围为 0 到 1 之间的小数。\n * 抖动用于增加随机性,以防止因多个任务同时重试而导致的惊群效应。\n * 最终的延迟时间将在 `delay ± (delay * jitter)` 的范围内随机波动。\n * @defaultValue 0\n * @example 0.1 // 为延迟添加 ±10% 的随机抖动\n * @example 0.25 // 为延迟添加 ±25% 的随机抖动\n */\n jitter?: number\n}\n\nexport interface RetryOptions {\n error?: Error | unknown\n message?: string\n delay?: number\n}\n\nexport interface HandleOptions {\n token: string\n retryLater(options?: RetryOptions): void\n retryLater(errorOrMessage?: Error | string): void\n retryLater(arg?: RetryOptions | Error | string): void\n abort(message?: string): void\n}\n\nexport interface BaseTaskOptions {\n name?: string\n autoStart?: boolean\n queueOptions?: Partial<QueueOptions>\n workerOptions?: Omit<Partial<WorkerOptions<any>>, 'backoff'> & {\n backoff?: BackoffOptions | ((attempt: number, error: unknown) => number)\n }\n debug?: boolean\n}\n\n/**\n * 任务启动选项,用于单个任务或 Flow 中的子任务\n */\nexport type TaskStartOptions<D> = {\n groupId: string\n data: D\n jobId?: string\n delay?: number\n groupConfig?: GroupOptions\n} & Omit<AddOptions<D>, 'groupId' | 'data' | 'groupConfig'>\n\n/**\n * Flow 任务选项\n */\nexport type TaskFlowOptions<D> = {\n groupId: string\n data: D\n jobId?: string\n groupConfig?: GroupOptions\n children: Array<\n {\n groupId: string\n data: D\n jobId?: string\n groupConfig?: GroupOptions\n } & Omit<FlowJob<D>, 'groupId' | 'data'>\n >\n} & Omit<FlowJob<D>, 'groupId' | 'data'>\n\nexport class TaskRetryLaterError extends Error {\n public customDelay?: number\n\n constructor(message?: string, delay?: number) {\n super(message)\n this.name = 'TaskRetryLaterError'\n this.customDelay = delay\n }\n}\n\nexport abstract class BaseTask<D = any> {\n public queue: Queue<D>\n public worker: Worker<D>\n protected opts: BaseTaskOptions\n\n get name() {\n return this.opts?.name || this.constructor.name\n }\n\n constructor(protected connection: IORedis, opts?: Partial<BaseTaskOptions>) {\n this.opts = Object.assign(\n {\n debug: false,\n autoStart: true,\n queueOptions: {},\n workerOptions: {},\n },\n opts,\n )\n\n this.opts.queueOptions = Object.assign(\n {\n redis: connection,\n namespace: this.name,\n keepCompleted: 0,\n maxAttempts: 3,\n jobTimeoutMs: 5000,\n },\n this.opts.queueOptions,\n )\n\n this.opts.workerOptions = Object.assign(\n {\n concurrency: 1,\n name: this.name,\n },\n this.opts.workerOptions,\n )\n\n if (this.opts.debug) {\n this.opts.queueOptions.logger = true\n this.opts.workerOptions.logger = true\n }\n\n this.queue = new Queue<D>(this.opts.queueOptions as QueueOptions)\n\n this.worker = new Worker<D>({\n ...(this.opts.workerOptions as WorkerOptions<D>),\n queue: this.queue,\n handler: async (job) => {\n return this.handle(job, {\n token: job.id,\n retryLater: (arg: RetryOptions | Error | string) => {\n const currentAttempts = job.attemptsMade\n const maxAttempts = job.opts.attempts\n\n let options: RetryOptions = {}\n if (arg instanceof Error) {\n options = { error: arg, message: arg.message }\n } else if (typeof arg === 'string') {\n options = { message: arg }\n } else {\n options = arg || {}\n }\n\n const { error, delay } = options\n const message = options.message || (error instanceof Error ? error.message : undefined) || 'retry later'\n const errorObj = error instanceof Error ? error : undefined\n\n if (currentAttempts + 1 >= maxAttempts) {\n if (errorObj) throw errorObj\n throw new Error(message)\n }\n\n const retryErr = new TaskRetryLaterError(message, delay)\n if (errorObj) {\n retryErr.stack = errorObj.stack\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore - preserve cause on older runtimes\n retryErr.cause = errorObj\n }\n throw retryErr\n },\n abort: (message?: string) => {\n throw new UnrecoverableError(message || 'failed')\n },\n })\n },\n backoff: (attempt: number, error: unknown) => {\n // 优先使用 TaskRetryLaterError 的 customDelay\n if (error instanceof TaskRetryLaterError && typeof error.customDelay === 'number') {\n return error.customDelay\n }\n\n const backoffConfig = this.opts.workerOptions?.backoff\n\n // 使用自定义 backoff 函数\n if (typeof backoffConfig === 'function') {\n return backoffConfig(attempt, error)\n }\n\n // 使用 BackoffOptions 配置\n if (backoffConfig && typeof backoffConfig === 'object') {\n const { type, delay = 1000, jitter = 0 } = backoffConfig\n let baseDelay: number\n\n if (type === 'fixed') {\n baseDelay = delay\n } else if (type === 'exponential') {\n baseDelay = delay * Math.pow(2, attempt - 1)\n } else {\n // 不支持的 type,使用默认指数退避\n baseDelay = 1000 * Math.pow(2, attempt - 1)\n }\n\n // 应用 jitter(抖动):确保结果为正整数\n if (jitter > 0) {\n const jitterAmount = baseDelay * jitter\n const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount\n baseDelay = baseDelay + randomJitter\n }\n\n return Math.max(0, Math.round(baseDelay))\n }\n\n // 默认指数退避策略\n return 1000 * Math.pow(2, attempt - 1)\n },\n })\n\n this.setupEventListeners()\n }\n\n private setupEventListeners() {\n this.worker.on('completed', (job) => {\n if (this.opts.debug) {\n console.log(`[COMPLETED] ${this.name}-${job.id} (Group: ${job.groupId})`)\n }\n this.onCompleted(job)\n })\n\n this.worker.on('failed', async (job) => {\n const isFinalFailure = job.attemptsMade >= job.opts.attempts - 1\n const error = new Error(job.failedReason || 'task failed')\n if (job.stacktrace) {\n error.stack = job.stacktrace\n }\n\n if (this.opts.debug) {\n if (!isFinalFailure) {\n console.log(`[RETRY] ${this.name}-${job.id}: ${error.message}`)\n } else {\n console.error(`[FINAL FAILED] ${this.name}-${job.id}: ${error.message}`)\n }\n }\n\n await this.onFailed(job, error)\n\n if (isFinalFailure) {\n await this.onFinalFailed(job, error)\n }\n })\n\n this.worker.on('error', (err) => {\n if (this.opts.debug) {\n console.error(`[WORKER ERROR] ${this.name}`, err)\n }\n })\n }\n\n async run() {\n return this.worker.run()\n }\n\n abstract handle(job: Job<D>, opts: HandleOptions): Promise<any>\n\n async onFailed(job: Job<D>, error: Error) {}\n async onFinalFailed(job: Job<D>, error: Error) {}\n async onCompleted(job: Job<D>) {}\n\n setGroupConfig(groupId: string, config: { priority?: number; concurrency?: number }) {\n return this.queue.groups.setConfig(groupId, config)\n }\n\n async start(opts: AddOptions<D> | FlowOptions<D, D>) {\n let job: Job<D>\n\n // 检查是否为 Flow (包含 children)\n if ('children' in opts && Array.isArray(opts.children)) {\n if (opts.children.length === 0) {\n throw new Error('Flow must include at least one child job')\n }\n\n job = await this.queue.addFlow(opts)\n } else {\n job = await this.queue.add(opts as AddOptions<D>)\n }\n\n // provide a BullMQ-like finished helper\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n job['finished'] = async () => {\n return job.waitUntilFinished()\n }\n\n return job as Job<D> & { finished: () => Promise<any> }\n }\n\n async close() {\n await this.worker.close()\n }\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{Job as e,Queue as t,Queue as n,UnrecoverableError as r,UnrecoverableError as i,Worker as a,Worker as o}from"groupmq-plus";var s=class extends Error{constructor(e,t){super(e),this.name=`TaskRetryLaterError`,this.customDelay=t}},c=class{get name(){return this.opts?.name||this.constructor.name}constructor(e,t){this.connection=e;let r=t??{},a={keepCompleted:0,maxAttempts:3,jobTimeoutMs:3e4};this.opts={concurrency:1,debug:!1,autoStart:!0,...r,queueOptions:{...a,...r.queueOptions}},this.queue=new n({redis:e,namespace:this.name,jobTimeoutMs:this.opts.queueOptions?.jobTimeoutMs,maxAttempts:this.opts.queueOptions?.maxAttempts,keepCompleted:this.opts.queueOptions?.keepCompleted,keepFailed:this.opts.queueOptions?.keepFailed,orderingDelayMs:this.opts.queueOptions?.orderingDelayMs}),this.worker=new o({queue:this.queue,name:this.name,concurrency:this.opts.concurrency,strategy:this.opts.strategy,strategyPollInterval:this.opts.strategyPollInterval,autoStart:this.opts.autoStart,handler:async e=>this.handle(e,{token:e.id,retryLater:t=>{let n=e.attemptsMade,r=e.opts.attempts,i={};i=t instanceof Error?{error:t,message:t.message}:typeof t==`string`?{message:t}:t||{};let{error:a,delay:o}=i,c=i.message||(a instanceof Error?a.message:void 0)||`retry later`,l=a instanceof Error?a:void 0;if(n+1>=r)throw l||Error(c);let u=new s(c,o);throw l&&(u.stack=l.stack,u.cause=l),u},abort:e=>{throw new i(e||`failed`)}}),backoff:(e,t)=>{if(t instanceof s&&typeof t.customDelay==`number`)return t.customDelay;let n=this.opts.queueOptions?.backoff;if(typeof n==`function`)return n(e,t);if(n&&typeof n==`object`){let{type:t,delay:r=1e3,jitter:i=0}=n,a;if(a=t===`fixed`?r:t===`exponential`?r*2**(e-1):1e3*2**(e-1),i>0){let e=a*i,t=Math.random()*e*2-e;a=Math.max(0,a+t)}return Math.round(a)}return 1e3*2**(e-1)}}),this.setupEventListeners()}setupEventListeners(){this.worker.on(`completed`,e=>{this.opts.debug&&console.log(`[COMPLETED] ${this.name}-${e.id} (Group: ${e.groupId})`),this.onCompleted(e)}),this.worker.on(`failed`,async e=>{let t=e.attemptsMade>=e.opts.attempts-1,n=Error(e.failedReason||`task failed`);e.stacktrace&&(n.stack=e.stacktrace),this.opts.debug&&(t?console.error(`[FINAL FAILED] ${this.name}-${e.id}: ${n.message}`):console.log(`[RETRY] ${this.name}-${e.id}: ${n.message}`)),await this.onFailed(e,n),t&&await this.onFinalFailed(e,n)}),this.worker.on(`error`,e=>{this.opts.debug&&console.error(`[WORKER ERROR] ${this.name}`,e)})}async run(){return this.worker.run()}async onFailed(e,t){}async onFinalFailed(e,t){}async onCompleted(e){}setGroupConfig(e,t){return this.queue.setGroupConfig(e,t)}async start(e){let t;if(`children`in e){if(e.children.length===0)throw Error(`Flow must include at least one child job`);t=await this.queue.addFlow({parent:{...e,groupId:e.groupId,data:e.data},children:e.children.map(e=>({...e,groupId:e.groupId,data:e.data}))})}else t=await this.queue.add({groupId:e.groupId,data:e.data,jobId:e.jobId,delay:e.delay});return t.finished=async()=>t.waitUntilFinished(),t}async close(){await this.worker.close()}};export{c as BaseTask,e as Job,t as Queue,s as TaskRetryLaterError,r as UnrecoverableError,a as Worker};
1
+ import{Job as e,PriorityStrategy as t,Queue as n,Queue as r,UnrecoverableError as i,UnrecoverableError as a,Worker as o,Worker as s}from"groupmq-plus";var c=class extends Error{constructor(e,t){super(e),this.name=`TaskRetryLaterError`,this.customDelay=t}},l=class{get name(){return this.opts?.name||this.constructor.name}constructor(e,t){this.connection=e,this.opts=Object.assign({debug:!1,autoStart:!0,queueOptions:{},workerOptions:{}},t),this.opts.queueOptions=Object.assign({redis:e,namespace:this.name,keepCompleted:0,maxAttempts:3,jobTimeoutMs:5e3},this.opts.queueOptions),this.opts.workerOptions=Object.assign({concurrency:1,name:this.name},this.opts.workerOptions),this.opts.debug&&(this.opts.queueOptions.logger=!0,this.opts.workerOptions.logger=!0),this.queue=new r(this.opts.queueOptions),this.worker=new s({...this.opts.workerOptions,queue:this.queue,handler:async e=>this.handle(e,{token:e.id,retryLater:t=>{let n=e.attemptsMade,r=e.opts.attempts,i={};i=t instanceof Error?{error:t,message:t.message}:typeof t==`string`?{message:t}:t||{};let{error:a,delay:o}=i,s=i.message||(a instanceof Error?a.message:void 0)||`retry later`,l=a instanceof Error?a:void 0;if(n+1>=r)throw l||Error(s);let u=new c(s,o);throw l&&(u.stack=l.stack,u.cause=l),u},abort:e=>{throw new a(e||`failed`)}}),backoff:(e,t)=>{if(t instanceof c&&typeof t.customDelay==`number`)return t.customDelay;let n=this.opts.workerOptions?.backoff;if(typeof n==`function`)return n(e,t);if(n&&typeof n==`object`){let{type:t,delay:r=1e3,jitter:i=0}=n,a;if(a=t===`fixed`?r:t===`exponential`?r*2**(e-1):1e3*2**(e-1),i>0){let e=a*i,t=Math.random()*e*2-e;a+=t}return Math.max(0,Math.round(a))}return 1e3*2**(e-1)}}),this.setupEventListeners()}setupEventListeners(){this.worker.on(`completed`,e=>{this.opts.debug&&console.log(`[COMPLETED] ${this.name}-${e.id} (Group: ${e.groupId})`),this.onCompleted(e)}),this.worker.on(`failed`,async e=>{let t=e.attemptsMade>=e.opts.attempts-1,n=Error(e.failedReason||`task failed`);e.stacktrace&&(n.stack=e.stacktrace),this.opts.debug&&(t?console.error(`[FINAL FAILED] ${this.name}-${e.id}: ${n.message}`):console.log(`[RETRY] ${this.name}-${e.id}: ${n.message}`)),await this.onFailed(e,n),t&&await this.onFinalFailed(e,n)}),this.worker.on(`error`,e=>{this.opts.debug&&console.error(`[WORKER ERROR] ${this.name}`,e)})}async run(){return this.worker.run()}async onFailed(e,t){}async onFinalFailed(e,t){}async onCompleted(e){}setGroupConfig(e,t){return this.queue.groups.setConfig(e,t)}async start(e){let t;if(`children`in e&&Array.isArray(e.children)){if(e.children.length===0)throw Error(`Flow must include at least one child job`);t=await this.queue.addFlow(e)}else t=await this.queue.add(e);return t.finished=async()=>t.waitUntilFinished(),t}async close(){await this.worker.close()}};export{l as BaseTask,e as Job,t as PriorityStrategy,n as Queue,c as TaskRetryLaterError,i as UnrecoverableError,o as Worker};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["connection: IORedis","Queue","Worker","options: RetryOptions","UnrecoverableError","baseDelay: number","job: Job<D>"],"sources":["../src/BaseTask.ts"],"sourcesContent":["import {\n FlowJob,\n Queue,\n Worker,\n Job,\n UnrecoverableError,\n AddOptions,\n WorkerOptions,\n FlowChildResult,\n} from 'groupmq-plus'\nimport IORedis from 'ioredis'\n\nexport type { FlowChildResult } from 'groupmq-plus'\n\nexport interface BackoffOptions {\n /** Name of the backoff strategy. */\n type: 'fixed' | 'exponential'\n /** Delay in milliseconds. */\n delay?: number\n /** Percentage of jitter usage. @defaultValue 0 */\n jitter?: number\n}\n\nexport interface RetryOptions {\n error?: Error | unknown\n message?: string\n delay?: number\n}\n\nexport interface HandleOptions {\n token: string\n retryLater(options?: RetryOptions): void\n retryLater(errorOrMessage?: Error | string): void\n retryLater(arg?: RetryOptions | Error | string): void\n abort(message?: string): void\n}\n\nexport interface BaseTaskOptions {\n name?: string\n concurrency?: number\n strategyPollInterval?: number\n autoStart?: boolean\n queueOptions?: {\n keepCompleted?: number\n keepFailed?: number\n maxAttempts?: number\n jobTimeoutMs?: number\n orderingDelayMs?: number\n backoff?: BackoffOptions | ((attempt: number, error: unknown) => number)\n }\n debug?: boolean\n strategy?: WorkerOptions<any>['strategy']\n}\n\nexport class TaskRetryLaterError extends Error {\n public customDelay?: number\n\n constructor(message?: string, delay?: number) {\n super(message)\n this.name = 'TaskRetryLaterError'\n this.customDelay = delay\n }\n}\n\nexport abstract class BaseTask<D = any> {\n public queue: Queue<D>\n public worker: Worker<D>\n protected opts: BaseTaskOptions\n\n get name() {\n return this.opts?.name || this.constructor.name\n }\n\n constructor(protected connection: IORedis, opts?: Partial<BaseTaskOptions>) {\n const normalizedOpts = opts ?? {}\n const defaultQueueOptions = {\n keepCompleted: 0,\n maxAttempts: 3,\n jobTimeoutMs: 30000,\n }\n this.opts = {\n concurrency: 1,\n debug: false,\n autoStart: true,\n ...normalizedOpts,\n queueOptions: {\n ...defaultQueueOptions,\n ...normalizedOpts.queueOptions,\n },\n }\n\n this.queue = new Queue<D>({\n redis: connection,\n namespace: this.name,\n jobTimeoutMs: this.opts.queueOptions?.jobTimeoutMs,\n maxAttempts: this.opts.queueOptions?.maxAttempts,\n keepCompleted: this.opts.queueOptions?.keepCompleted,\n keepFailed: this.opts.queueOptions?.keepFailed,\n orderingDelayMs: this.opts.queueOptions?.orderingDelayMs,\n })\n\n this.worker = new Worker<D>({\n queue: this.queue,\n name: this.name,\n concurrency: this.opts.concurrency,\n strategy: this.opts.strategy,\n strategyPollInterval: this.opts.strategyPollInterval,\n autoStart: this.opts.autoStart,\n handler: async (job) => {\n return this.handle(job, {\n token: job.id,\n retryLater: (arg: RetryOptions | Error | string) => {\n const currentAttempts = job.attemptsMade\n const maxAttempts = job.opts.attempts\n\n let options: RetryOptions = {}\n if (arg instanceof Error) {\n options = { error: arg, message: arg.message }\n } else if (typeof arg === 'string') {\n options = { message: arg }\n } else {\n options = arg || {}\n }\n\n const { error, delay } = options\n const message = options.message || (error instanceof Error ? error.message : undefined) || 'retry later'\n const errorObj = error instanceof Error ? error : undefined\n\n if (currentAttempts + 1 >= maxAttempts) {\n if (errorObj) throw errorObj\n throw new Error(message)\n }\n\n const retryErr = new TaskRetryLaterError(message, delay)\n if (errorObj) {\n retryErr.stack = errorObj.stack\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore - preserve cause on older runtimes\n retryErr.cause = errorObj\n }\n throw retryErr\n },\n abort: (message?: string) => {\n throw new UnrecoverableError(message || 'failed')\n },\n })\n },\n backoff: (attempt: number, error: unknown) => {\n // 优先使用 TaskRetryLaterError 的 customDelay\n if (error instanceof TaskRetryLaterError && typeof error.customDelay === 'number') {\n return error.customDelay\n }\n\n const backoffConfig = this.opts.queueOptions?.backoff\n\n // 使用自定义 backoff 函数\n if (typeof backoffConfig === 'function') {\n return backoffConfig(attempt, error)\n }\n\n // 使用 BackoffOptions 配置\n if (backoffConfig && typeof backoffConfig === 'object') {\n const { type, delay = 1000, jitter = 0 } = backoffConfig\n let baseDelay: number\n\n if (type === 'fixed') {\n baseDelay = delay\n } else if (type === 'exponential') {\n baseDelay = delay * Math.pow(2, attempt - 1)\n } else {\n // 不支持的 type,使用默认指数退避\n baseDelay = 1000 * Math.pow(2, attempt - 1)\n }\n\n // 应用 jitter(抖动)\n if (jitter > 0) {\n const jitterAmount = baseDelay * jitter\n const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount\n baseDelay = Math.max(0, baseDelay + randomJitter)\n }\n\n return Math.round(baseDelay)\n }\n\n // 默认指数退避策略\n return 1000 * Math.pow(2, attempt - 1)\n },\n })\n\n this.setupEventListeners()\n }\n\n private setupEventListeners() {\n this.worker.on('completed', (job) => {\n if (this.opts.debug) {\n console.log(`[COMPLETED] ${this.name}-${job.id} (Group: ${job.groupId})`)\n }\n this.onCompleted(job)\n })\n\n this.worker.on('failed', async (job) => {\n const isFinalFailure = job.attemptsMade >= job.opts.attempts - 1\n const error = new Error(job.failedReason || 'task failed')\n if (job.stacktrace) {\n error.stack = job.stacktrace\n }\n\n if (this.opts.debug) {\n if (!isFinalFailure) {\n console.log(`[RETRY] ${this.name}-${job.id}: ${error.message}`)\n } else {\n console.error(`[FINAL FAILED] ${this.name}-${job.id}: ${error.message}`)\n }\n }\n\n await this.onFailed(job, error)\n\n if (isFinalFailure) {\n await this.onFinalFailed(job, error)\n }\n })\n\n this.worker.on('error', (err) => {\n if (this.opts.debug) {\n console.error(`[WORKER ERROR] ${this.name}`, err)\n }\n })\n }\n\n async run() {\n return this.worker.run()\n }\n\n abstract handle(job: Job<D>, opts: HandleOptions): Promise<any>\n\n async onFailed(job: Job<D>, error: Error) {}\n async onFinalFailed(job: Job<D>, error: Error) {}\n async onCompleted(job: Job<D>) {}\n\n setGroupConfig(groupId: string, config: { priority?: number; concurrency?: number }) {\n return this.queue.setGroupConfig(groupId, config)\n }\n\n async start(\n opts:\n | ({ groupId: string; data: D; jobId?: string; delay?: number } & Omit<AddOptions<D>, 'groupId' | 'data'>)\n | ({\n groupId: string\n data: D\n jobId?: string\n children: ({\n groupId: string\n data: D\n jobId?: string\n } & Omit<FlowJob<D>, 'groupId' | 'data'>)[]\n } & Omit<FlowJob<D>, 'groupId' | 'data'>),\n ) {\n let job: Job<D>\n\n if ('children' in opts) {\n if (opts.children.length === 0) {\n throw new Error('Flow must include at least one child job')\n }\n\n job = await this.queue.addFlow({\n parent: {\n ...opts,\n groupId: opts.groupId,\n data: opts.data,\n },\n children: opts.children.map((child) => ({\n ...child,\n groupId: child.groupId,\n data: child.data,\n })),\n })\n } else {\n job = await this.queue.add({\n groupId: opts.groupId,\n data: opts.data,\n jobId: opts.jobId,\n delay: opts.delay,\n })\n }\n\n // provide a BullMQ-like finished helper\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n job['finished'] = async () => {\n return job.waitUntilFinished()\n }\n\n return job as Job<D> & { finished: () => Promise<any> }\n }\n\n async close() {\n await this.worker.close()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["connection: IORedis","Queue","Worker","options: RetryOptions","UnrecoverableError","baseDelay: number","job: Job<D>"],"sources":["../src/BaseTask.ts"],"sourcesContent":["import {\n AddOptions,\n FlowJob,\n FlowOptions,\n GroupOptions,\n Job,\n Queue,\n QueueOptions,\n UnrecoverableError,\n Worker,\n WorkerOptions\n} from 'groupmq-plus'\nimport IORedis from 'ioredis'\n\nexport type { FlowChildResult } from 'groupmq-plus'\n\nexport interface BackoffOptions {\n /** 延迟策略:固定间隔、指数递增 */\n type: 'fixed' | 'exponential'\n /** 延迟时间为毫秒 */\n delay?: number\n /**\n * 添加到延迟中的随机抖动比例,取值范围为 0 到 1 之间的小数。\n * 抖动用于增加随机性,以防止因多个任务同时重试而导致的惊群效应。\n * 最终的延迟时间将在 `delay ± (delay * jitter)` 的范围内随机波动。\n * @defaultValue 0\n * @example 0.1 // 为延迟添加 ±10% 的随机抖动\n * @example 0.25 // 为延迟添加 ±25% 的随机抖动\n */\n jitter?: number\n}\n\nexport interface RetryOptions {\n error?: Error | unknown\n message?: string\n delay?: number\n}\n\nexport interface HandleOptions {\n token: string\n retryLater(options?: RetryOptions): void\n retryLater(errorOrMessage?: Error | string): void\n retryLater(arg?: RetryOptions | Error | string): void\n abort(message?: string): void\n}\n\nexport interface BaseTaskOptions {\n name?: string\n autoStart?: boolean\n queueOptions?: Partial<QueueOptions>\n workerOptions?: Omit<Partial<WorkerOptions<any>>, 'backoff'> & {\n backoff?: BackoffOptions | ((attempt: number, error: unknown) => number)\n }\n debug?: boolean\n}\n\n/**\n * 任务启动选项,用于单个任务或 Flow 中的子任务\n */\nexport type TaskStartOptions<D> = {\n groupId: string\n data: D\n jobId?: string\n delay?: number\n groupConfig?: GroupOptions\n} & Omit<AddOptions<D>, 'groupId' | 'data' | 'groupConfig'>\n\n/**\n * Flow 任务选项\n */\nexport type TaskFlowOptions<D> = {\n groupId: string\n data: D\n jobId?: string\n groupConfig?: GroupOptions\n children: Array<\n {\n groupId: string\n data: D\n jobId?: string\n groupConfig?: GroupOptions\n } & Omit<FlowJob<D>, 'groupId' | 'data'>\n >\n} & Omit<FlowJob<D>, 'groupId' | 'data'>\n\nexport class TaskRetryLaterError extends Error {\n public customDelay?: number\n\n constructor(message?: string, delay?: number) {\n super(message)\n this.name = 'TaskRetryLaterError'\n this.customDelay = delay\n }\n}\n\nexport abstract class BaseTask<D = any> {\n public queue: Queue<D>\n public worker: Worker<D>\n protected opts: BaseTaskOptions\n\n get name() {\n return this.opts?.name || this.constructor.name\n }\n\n constructor(protected connection: IORedis, opts?: Partial<BaseTaskOptions>) {\n this.opts = Object.assign(\n {\n debug: false,\n autoStart: true,\n queueOptions: {},\n workerOptions: {},\n },\n opts,\n )\n\n this.opts.queueOptions = Object.assign(\n {\n redis: connection,\n namespace: this.name,\n keepCompleted: 0,\n maxAttempts: 3,\n jobTimeoutMs: 5000,\n },\n this.opts.queueOptions,\n )\n\n this.opts.workerOptions = Object.assign(\n {\n concurrency: 1,\n name: this.name,\n },\n this.opts.workerOptions,\n )\n\n if (this.opts.debug) {\n this.opts.queueOptions.logger = true\n this.opts.workerOptions.logger = true\n }\n\n this.queue = new Queue<D>(this.opts.queueOptions as QueueOptions)\n\n this.worker = new Worker<D>({\n ...(this.opts.workerOptions as WorkerOptions<D>),\n queue: this.queue,\n handler: async (job) => {\n return this.handle(job, {\n token: job.id,\n retryLater: (arg: RetryOptions | Error | string) => {\n const currentAttempts = job.attemptsMade\n const maxAttempts = job.opts.attempts\n\n let options: RetryOptions = {}\n if (arg instanceof Error) {\n options = { error: arg, message: arg.message }\n } else if (typeof arg === 'string') {\n options = { message: arg }\n } else {\n options = arg || {}\n }\n\n const { error, delay } = options\n const message = options.message || (error instanceof Error ? error.message : undefined) || 'retry later'\n const errorObj = error instanceof Error ? error : undefined\n\n if (currentAttempts + 1 >= maxAttempts) {\n if (errorObj) throw errorObj\n throw new Error(message)\n }\n\n const retryErr = new TaskRetryLaterError(message, delay)\n if (errorObj) {\n retryErr.stack = errorObj.stack\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore - preserve cause on older runtimes\n retryErr.cause = errorObj\n }\n throw retryErr\n },\n abort: (message?: string) => {\n throw new UnrecoverableError(message || 'failed')\n },\n })\n },\n backoff: (attempt: number, error: unknown) => {\n // 优先使用 TaskRetryLaterError 的 customDelay\n if (error instanceof TaskRetryLaterError && typeof error.customDelay === 'number') {\n return error.customDelay\n }\n\n const backoffConfig = this.opts.workerOptions?.backoff\n\n // 使用自定义 backoff 函数\n if (typeof backoffConfig === 'function') {\n return backoffConfig(attempt, error)\n }\n\n // 使用 BackoffOptions 配置\n if (backoffConfig && typeof backoffConfig === 'object') {\n const { type, delay = 1000, jitter = 0 } = backoffConfig\n let baseDelay: number\n\n if (type === 'fixed') {\n baseDelay = delay\n } else if (type === 'exponential') {\n baseDelay = delay * Math.pow(2, attempt - 1)\n } else {\n // 不支持的 type,使用默认指数退避\n baseDelay = 1000 * Math.pow(2, attempt - 1)\n }\n\n // 应用 jitter(抖动):确保结果为正整数\n if (jitter > 0) {\n const jitterAmount = baseDelay * jitter\n const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount\n baseDelay = baseDelay + randomJitter\n }\n\n return Math.max(0, Math.round(baseDelay))\n }\n\n // 默认指数退避策略\n return 1000 * Math.pow(2, attempt - 1)\n },\n })\n\n this.setupEventListeners()\n }\n\n private setupEventListeners() {\n this.worker.on('completed', (job) => {\n if (this.opts.debug) {\n console.log(`[COMPLETED] ${this.name}-${job.id} (Group: ${job.groupId})`)\n }\n this.onCompleted(job)\n })\n\n this.worker.on('failed', async (job) => {\n const isFinalFailure = job.attemptsMade >= job.opts.attempts - 1\n const error = new Error(job.failedReason || 'task failed')\n if (job.stacktrace) {\n error.stack = job.stacktrace\n }\n\n if (this.opts.debug) {\n if (!isFinalFailure) {\n console.log(`[RETRY] ${this.name}-${job.id}: ${error.message}`)\n } else {\n console.error(`[FINAL FAILED] ${this.name}-${job.id}: ${error.message}`)\n }\n }\n\n await this.onFailed(job, error)\n\n if (isFinalFailure) {\n await this.onFinalFailed(job, error)\n }\n })\n\n this.worker.on('error', (err) => {\n if (this.opts.debug) {\n console.error(`[WORKER ERROR] ${this.name}`, err)\n }\n })\n }\n\n async run() {\n return this.worker.run()\n }\n\n abstract handle(job: Job<D>, opts: HandleOptions): Promise<any>\n\n async onFailed(job: Job<D>, error: Error) {}\n async onFinalFailed(job: Job<D>, error: Error) {}\n async onCompleted(job: Job<D>) {}\n\n setGroupConfig(groupId: string, config: { priority?: number; concurrency?: number }) {\n return this.queue.groups.setConfig(groupId, config)\n }\n\n async start(opts: AddOptions<D> | FlowOptions<D, D>) {\n let job: Job<D>\n\n // 检查是否为 Flow (包含 children)\n if ('children' in opts && Array.isArray(opts.children)) {\n if (opts.children.length === 0) {\n throw new Error('Flow must include at least one child job')\n }\n\n job = await this.queue.addFlow(opts)\n } else {\n job = await this.queue.add(opts as AddOptions<D>)\n }\n\n // provide a BullMQ-like finished helper\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n job['finished'] = async () => {\n return job.waitUntilFinished()\n }\n\n return job as Job<D> & { finished: () => Promise<any> }\n }\n\n async close() {\n await this.worker.close()\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@libeilong/mq",
3
3
  "author": "lblblong",
4
- "version": "0.1.7",
4
+ "version": "0.2.0",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -20,10 +20,10 @@
20
20
  "node": ">=18"
21
21
  },
22
22
  "dependencies": {
23
- "groupmq-plus": "^1.3.0"
23
+ "groupmq-plus": "^1.4.0"
24
24
  },
25
25
  "peerDependencies": {
26
- "ioredis": "^5.7.0"
26
+ "ioredis": "^5.8.2"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^16.11.12"