@zucker-framework/queue 1.0.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.js ADDED
@@ -0,0 +1,911 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ BullMqQueueEvents: () => BullMqQueueEvents,
25
+ BullMqQueueProvider: () => BullMqQueueProvider,
26
+ BullMqWorkerHost: () => BullMqWorkerHost,
27
+ InMemoryQueue: () => InMemoryQueue,
28
+ QUEUE_PROVIDER: () => QUEUE_PROVIDER,
29
+ QUEUE_WORKER_KEY: () => QUEUE_WORKER_KEY,
30
+ QueueService: () => QueueService,
31
+ QueueWorker: () => QueueWorker,
32
+ TaskLifecycleManager: () => TaskLifecycleManager,
33
+ ZuckerQueueModule: () => ZuckerQueueModule,
34
+ bullMqConnectionFromUrl: () => bullMqConnectionFromUrl
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/queue.interface.ts
39
+ var QUEUE_PROVIDER = "QUEUE_PROVIDER";
40
+
41
+ // src/in-memory-queue.ts
42
+ var import_common = require("@nestjs/common");
43
+ var InMemoryQueue = class _InMemoryQueue {
44
+ static {
45
+ __name(this, "InMemoryQueue");
46
+ }
47
+ logger = new import_common.Logger(_InMemoryQueue.name);
48
+ queues = /* @__PURE__ */ new Map();
49
+ paused = /* @__PURE__ */ new Set();
50
+ counter = 0;
51
+ /** timer 引用:queueName:jobId → timer */
52
+ delayTimers = /* @__PURE__ */ new Map();
53
+ getQueue(name) {
54
+ let q = this.queues.get(name);
55
+ if (!q) {
56
+ q = /* @__PURE__ */ new Map();
57
+ this.queues.set(name, q);
58
+ }
59
+ return q;
60
+ }
61
+ nextId() {
62
+ return String(++this.counter);
63
+ }
64
+ async add(queueName, data, options) {
65
+ const q = this.getQueue(queueName);
66
+ const id = options?.jobId ?? this.nextId();
67
+ const job = {
68
+ id,
69
+ data,
70
+ status: "waiting",
71
+ progress: 0,
72
+ attempts: 0,
73
+ createdAt: /* @__PURE__ */ new Date()
74
+ };
75
+ q.set(id, job);
76
+ return job;
77
+ }
78
+ async ensure(queueName, data, options) {
79
+ const existing = await this.getJob(queueName, options.jobId);
80
+ if (existing && existing.status !== "completed" && existing.status !== "failed") {
81
+ return existing;
82
+ }
83
+ if (existing) await this.removeJob(queueName, options.jobId);
84
+ return this.add(queueName, data, options);
85
+ }
86
+ async addDelayed(queueName, data, delayMs, options) {
87
+ const q = this.getQueue(queueName);
88
+ const id = options?.jobId ?? this.nextId();
89
+ const job = {
90
+ id,
91
+ data,
92
+ status: "delayed",
93
+ progress: 0,
94
+ attempts: 0,
95
+ createdAt: /* @__PURE__ */ new Date()
96
+ };
97
+ q.set(id, job);
98
+ const timerKey = `${queueName}:${id}`;
99
+ const timer = setTimeout(() => {
100
+ this.delayTimers.delete(timerKey);
101
+ const existing = q.get(id);
102
+ if (existing && existing.status === "delayed") {
103
+ existing.status = "waiting";
104
+ }
105
+ }, delayMs);
106
+ this.delayTimers.set(timerKey, timer);
107
+ return job;
108
+ }
109
+ async addRepeatable(queueName, data, pattern, options) {
110
+ this.logger.warn(`InMemoryQueue \u4E0D\u652F\u6301\u5B9A\u65F6\u4EFB\u52A1\u3002pattern "${pattern}" \u88AB\u5FFD\u7565\uFF0C\u4EC5\u521B\u5EFA\u4E00\u6B21\u6027\u4EFB\u52A1\u3002\u751F\u4EA7\u73AF\u5883\u8BF7\u4F7F\u7528 BullMQ\u3002`);
111
+ return this.add(queueName, data, options);
112
+ }
113
+ async getJob(queueName, jobId) {
114
+ const q = this.getQueue(queueName);
115
+ return q.get(jobId) ?? null;
116
+ }
117
+ async removeJob(queueName, jobId) {
118
+ const q = this.getQueue(queueName);
119
+ q.delete(jobId);
120
+ const timerKey = `${queueName}:${jobId}`;
121
+ const timer = this.delayTimers.get(timerKey);
122
+ if (timer) {
123
+ clearTimeout(timer);
124
+ this.delayTimers.delete(timerKey);
125
+ }
126
+ }
127
+ async pause(queueName) {
128
+ this.paused.add(queueName);
129
+ }
130
+ async resume(queueName) {
131
+ this.paused.delete(queueName);
132
+ }
133
+ async getStats(queueName) {
134
+ const q = this.getQueue(queueName);
135
+ const stats = {
136
+ waiting: 0,
137
+ active: 0,
138
+ completed: 0,
139
+ failed: 0,
140
+ delayed: 0
141
+ };
142
+ for (const job of q.values()) {
143
+ if (job.status !== "unknown") stats[job.status]++;
144
+ }
145
+ return stats;
146
+ }
147
+ onModuleDestroy() {
148
+ for (const timer of this.delayTimers.values()) {
149
+ clearTimeout(timer);
150
+ }
151
+ this.delayTimers.clear();
152
+ }
153
+ };
154
+
155
+ // src/bullmq-queue.provider.ts
156
+ var import_node_crypto = require("crypto");
157
+ var import_bullmq = require("bullmq");
158
+ var BullMqQueueProvider = class {
159
+ static {
160
+ __name(this, "BullMqQueueProvider");
161
+ }
162
+ options;
163
+ queues = /* @__PURE__ */ new Map();
164
+ defaultJobName;
165
+ queueFactory;
166
+ constructor(options) {
167
+ this.options = options;
168
+ this.defaultJobName = options.defaultJobName ?? "task";
169
+ this.queueFactory = options.queueFactory ?? ((name, queueOptions) => new import_bullmq.Queue(name, queueOptions));
170
+ }
171
+ async add(queueName, data, options) {
172
+ this.assertValidJobId(options?.jobId);
173
+ const job = await this.getQueue(queueName).add(options?.name ?? this.defaultJobName, data, this.toBullMqOptions(options));
174
+ return this.toJob(job);
175
+ }
176
+ async ensure(queueName, data, options) {
177
+ this.assertValidJobId(options.jobId);
178
+ const queue = this.getQueue(queueName);
179
+ const existing = await queue.getJob(options.jobId);
180
+ if (existing) {
181
+ const state = await existing.getState();
182
+ if (state !== "completed" && state !== "failed") {
183
+ return this.toJob(existing);
184
+ }
185
+ await existing.remove();
186
+ }
187
+ return this.add(queueName, data, options);
188
+ }
189
+ async addDelayed(queueName, data, delayMs, options) {
190
+ if (delayMs < 0) throw new RangeError("delayMs cannot be negative");
191
+ this.assertValidJobId(options?.jobId);
192
+ const job = await this.getQueue(queueName).add(options?.name ?? this.defaultJobName, data, {
193
+ ...this.toBullMqOptions(options),
194
+ delay: delayMs
195
+ });
196
+ return this.toJob(job);
197
+ }
198
+ async addRepeatable(queueName, data, pattern, options) {
199
+ this.assertValidJobId(options?.jobId);
200
+ const name = options?.name ?? this.defaultJobName;
201
+ const schedulerId = options?.jobId ?? `scheduler-${(0, import_node_crypto.createHash)("sha256").update(name).update("\0").update(pattern).digest("hex")}`;
202
+ const job = await this.getQueue(queueName).upsertJobScheduler(schedulerId, {
203
+ pattern
204
+ }, {
205
+ name,
206
+ data,
207
+ opts: this.toBullMqSchedulerOptions(options)
208
+ });
209
+ return this.toJob(job);
210
+ }
211
+ async getJob(queueName, jobId) {
212
+ const job = await this.getQueue(queueName).getJob(jobId);
213
+ return job ? this.toJob(job) : null;
214
+ }
215
+ async removeJob(queueName, jobId) {
216
+ const job = await this.getQueue(queueName).getJob(jobId);
217
+ if (job) await job.remove();
218
+ }
219
+ async pause(queueName) {
220
+ await this.getQueue(queueName).pause();
221
+ }
222
+ async resume(queueName) {
223
+ await this.getQueue(queueName).resume();
224
+ }
225
+ async getStats(queueName) {
226
+ const pausedStateForStats = "paused";
227
+ const counts = await this.getQueue(queueName).getJobCounts("wait", "active", "completed", "failed", "delayed", "prioritized", pausedStateForStats);
228
+ return {
229
+ waiting: (counts.wait ?? 0) + (counts.prioritized ?? 0) + (counts.paused ?? 0),
230
+ active: counts.active ?? 0,
231
+ completed: counts.completed ?? 0,
232
+ failed: counts.failed ?? 0,
233
+ delayed: counts.delayed ?? 0
234
+ };
235
+ }
236
+ async waitUntilReady() {
237
+ await Promise.all([
238
+ ...this.queues.values()
239
+ ].map((queue) => queue.waitUntilReady()));
240
+ }
241
+ async close() {
242
+ const queues = [
243
+ ...this.queues.values()
244
+ ];
245
+ this.queues.clear();
246
+ await Promise.all(queues.map((queue) => queue.close()));
247
+ }
248
+ async onModuleDestroy() {
249
+ await this.close();
250
+ }
251
+ getQueue(name) {
252
+ const existing = this.queues.get(name);
253
+ if (existing) return existing;
254
+ const queue = this.queueFactory(name, {
255
+ connection: this.options.connection,
256
+ prefix: this.options.prefix
257
+ });
258
+ this.queues.set(name, queue);
259
+ return queue;
260
+ }
261
+ toBullMqOptions(options) {
262
+ if (!options) return void 0;
263
+ return {
264
+ jobId: options.jobId,
265
+ deduplication: options.deduplicationId ? {
266
+ id: options.deduplicationId
267
+ } : void 0,
268
+ priority: options.priority,
269
+ attempts: options.attempts,
270
+ backoff: options.backoff,
271
+ removeOnComplete: options.removeOnComplete,
272
+ removeOnFail: options.removeOnFail
273
+ };
274
+ }
275
+ toBullMqSchedulerOptions(options) {
276
+ if (!options) return void 0;
277
+ return {
278
+ priority: options.priority,
279
+ attempts: options.attempts,
280
+ backoff: options.backoff,
281
+ removeOnComplete: options.removeOnComplete,
282
+ removeOnFail: options.removeOnFail
283
+ };
284
+ }
285
+ assertValidJobId(jobId) {
286
+ if (jobId?.includes(":")) {
287
+ throw new TypeError("BullMQ jobId cannot contain a colon");
288
+ }
289
+ }
290
+ async toJob(job) {
291
+ const state = await job.getState();
292
+ return {
293
+ id: String(job.id),
294
+ data: job.data,
295
+ status: this.mapState(state),
296
+ progress: job.progress,
297
+ attempts: job.attemptsMade,
298
+ failedReason: job.failedReason || void 0,
299
+ createdAt: new Date(job.timestamp),
300
+ processedAt: job.processedOn ? new Date(job.processedOn) : void 0,
301
+ completedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
302
+ };
303
+ }
304
+ mapState(state) {
305
+ if (state === "active" || state === "completed" || state === "failed" || state === "delayed") {
306
+ return state;
307
+ }
308
+ if (state === "waiting" || state === "waiting-children" || state === "prioritized" || state === "paused") {
309
+ return "waiting";
310
+ }
311
+ return "unknown";
312
+ }
313
+ };
314
+
315
+ // src/bullmq-connection.ts
316
+ function bullMqConnectionFromUrl(redisUrl, options = {}) {
317
+ const parsed = new URL(redisUrl);
318
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
319
+ throw new TypeError("BullMQ connection URL must use redis:// or rediss://");
320
+ }
321
+ const databasePath = parsed.pathname.replace(/^\//, "");
322
+ const database = databasePath === "" ? 0 : Number(databasePath);
323
+ if (!Number.isInteger(database) || database < 0) {
324
+ throw new TypeError("BullMQ Redis database must be a non-negative integer");
325
+ }
326
+ return {
327
+ host: parsed.hostname,
328
+ port: parsed.port ? Number(parsed.port) : 6379,
329
+ username: parsed.username ? decodeURIComponent(parsed.username) : void 0,
330
+ password: parsed.password ? decodeURIComponent(parsed.password) : void 0,
331
+ db: database,
332
+ tls: parsed.protocol === "rediss:" ? {} : void 0,
333
+ maxRetriesPerRequest: options.role === "worker" ? null : 1
334
+ };
335
+ }
336
+ __name(bullMqConnectionFromUrl, "bullMqConnectionFromUrl");
337
+
338
+ // src/bullmq-worker-host.ts
339
+ var import_bullmq2 = require("bullmq");
340
+ var BullMqWorkerHost = class {
341
+ static {
342
+ __name(this, "BullMqWorkerHost");
343
+ }
344
+ options;
345
+ workers = /* @__PURE__ */ new Map();
346
+ readyQueues = /* @__PURE__ */ new Set();
347
+ workerFactory;
348
+ constructor(options) {
349
+ this.options = options;
350
+ this.workerFactory = options.workerFactory ?? ((queueName, processor, workerOptions) => new import_bullmq2.Worker(queueName, processor, workerOptions));
351
+ }
352
+ register(definition) {
353
+ if (this.workers.has(definition.queueName)) {
354
+ throw new Error(`Worker already registered for queue: ${definition.queueName}`);
355
+ }
356
+ if ((definition.concurrency ?? 1) < 1) throw new RangeError("concurrency must be at least 1");
357
+ if ((definition.lockDurationMs ?? 3e4) < 1) {
358
+ throw new RangeError("lockDurationMs must be at least 1");
359
+ }
360
+ const worker = this.workerFactory(definition.queueName, async (job) => definition.handler({
361
+ id: String(job.id),
362
+ name: job.name,
363
+ data: job.data,
364
+ attempt: job.attemptsMade + 1,
365
+ updateProgress: /* @__PURE__ */ __name((progress) => job.updateProgress(progress), "updateProgress"),
366
+ log: /* @__PURE__ */ __name((message) => job.log(message), "log")
367
+ }), {
368
+ connection: this.options.connection,
369
+ prefix: this.options.prefix,
370
+ concurrency: definition.concurrency ?? 1,
371
+ lockDuration: definition.lockDurationMs ?? 3e4,
372
+ autorun: definition.autorun ?? true
373
+ });
374
+ worker.on("ready", () => this.readyQueues.add(definition.queueName));
375
+ worker.on("error", (error) => {
376
+ this.readyQueues.delete(definition.queueName);
377
+ this.options.onError?.(definition.queueName, error);
378
+ });
379
+ this.workers.set(definition.queueName, worker);
380
+ }
381
+ async waitUntilReady() {
382
+ await Promise.all([
383
+ ...this.workers.values()
384
+ ].map((worker) => worker.waitUntilReady()));
385
+ for (const queueName of this.workers.keys()) this.readyQueues.add(queueName);
386
+ }
387
+ health() {
388
+ const registeredQueues = [
389
+ ...this.workers.keys()
390
+ ];
391
+ const runningQueues = registeredQueues.filter((queueName) => this.workers.get(queueName)?.isRunning());
392
+ return {
393
+ ready: registeredQueues.length > 0 && this.readyQueues.size === registeredQueues.length && runningQueues.length === registeredQueues.length,
394
+ registeredQueues,
395
+ runningQueues
396
+ };
397
+ }
398
+ /**
399
+ * Actively verifies the Redis connections behind every worker.
400
+ *
401
+ * BullMQ's ready/error events cannot detect a half-open connection where the
402
+ * TCP socket still exists but Redis no longer answers. Readiness endpoints
403
+ * should use this bounded probe instead of relying on lifecycle events alone.
404
+ */
405
+ async probeHealth(timeoutMs = 1e3) {
406
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
407
+ throw new RangeError("timeoutMs must be at least 1");
408
+ }
409
+ const current = this.health();
410
+ if (!current.ready) return current;
411
+ let timer;
412
+ try {
413
+ await Promise.race([
414
+ Promise.all([
415
+ ...this.workers.values()
416
+ ].map(async (worker) => {
417
+ const client = await worker.getBackend().client;
418
+ await client.get(worker.toKey("health-probe"));
419
+ })),
420
+ new Promise((_, reject) => {
421
+ timer = setTimeout(() => reject(new Error("Redis worker health probe timed out")), timeoutMs);
422
+ timer.unref?.();
423
+ })
424
+ ]);
425
+ return this.health();
426
+ } catch {
427
+ return {
428
+ ...this.health(),
429
+ ready: false
430
+ };
431
+ } finally {
432
+ if (timer) clearTimeout(timer);
433
+ }
434
+ }
435
+ run(queueName) {
436
+ const worker = this.requireWorker(queueName);
437
+ if (worker.isRunning()) return;
438
+ void worker.run().catch((error) => this.options.onError?.(queueName, error));
439
+ }
440
+ async pause() {
441
+ await Promise.all([
442
+ ...this.workers.values()
443
+ ].map((worker) => worker.pause()));
444
+ }
445
+ async resume() {
446
+ await Promise.all([
447
+ ...this.workers.values()
448
+ ].map((worker) => worker.resume()));
449
+ }
450
+ async close(force = false) {
451
+ const workers = [
452
+ ...this.workers.values()
453
+ ];
454
+ this.workers.clear();
455
+ this.readyQueues.clear();
456
+ await Promise.all(workers.map((worker) => worker.close(force)));
457
+ }
458
+ async onModuleDestroy() {
459
+ await this.close();
460
+ }
461
+ requireWorker(queueName) {
462
+ const worker = this.workers.get(queueName);
463
+ if (!worker) throw new Error(`No worker registered for queue: ${queueName}`);
464
+ return worker;
465
+ }
466
+ };
467
+
468
+ // src/bullmq-queue-events.ts
469
+ var import_bullmq3 = require("bullmq");
470
+ var BullMqQueueEvents = class {
471
+ static {
472
+ __name(this, "BullMqQueueEvents");
473
+ }
474
+ options;
475
+ listeners = /* @__PURE__ */ new Map();
476
+ queueEventsFactory;
477
+ constructor(options) {
478
+ this.options = options;
479
+ this.queueEventsFactory = options.queueEventsFactory ?? ((queueName, queueOptions) => new import_bullmq3.QueueEvents(queueName, queueOptions));
480
+ }
481
+ listen(queueName, handlers) {
482
+ const queueEvents = this.getQueueEvents(queueName);
483
+ const subscriptions = [];
484
+ if (handlers.completed) {
485
+ const handler = /* @__PURE__ */ __name(({ jobId, returnvalue }) => handlers.completed?.({
486
+ queueName,
487
+ jobId,
488
+ returnvalue
489
+ }), "handler");
490
+ queueEvents.on("completed", handler);
491
+ subscriptions.push(() => queueEvents.off("completed", handler));
492
+ }
493
+ if (handlers.failed) {
494
+ const handler = /* @__PURE__ */ __name(({ jobId, failedReason }) => handlers.failed?.({
495
+ queueName,
496
+ jobId,
497
+ failedReason
498
+ }), "handler");
499
+ queueEvents.on("failed", handler);
500
+ subscriptions.push(() => queueEvents.off("failed", handler));
501
+ }
502
+ if (handlers.progress) {
503
+ const handler = /* @__PURE__ */ __name(({ jobId, data }) => handlers.progress?.({
504
+ queueName,
505
+ jobId,
506
+ data
507
+ }), "handler");
508
+ queueEvents.on("progress", handler);
509
+ subscriptions.push(() => queueEvents.off("progress", handler));
510
+ }
511
+ if (handlers.waiting) {
512
+ const handler = /* @__PURE__ */ __name(({ jobId }) => handlers.waiting?.({
513
+ queueName,
514
+ jobId
515
+ }), "handler");
516
+ queueEvents.on("waiting", handler);
517
+ subscriptions.push(() => queueEvents.off("waiting", handler));
518
+ }
519
+ if (handlers.active) {
520
+ const handler = /* @__PURE__ */ __name(({ jobId }) => handlers.active?.({
521
+ queueName,
522
+ jobId
523
+ }), "handler");
524
+ queueEvents.on("active", handler);
525
+ subscriptions.push(() => queueEvents.off("active", handler));
526
+ }
527
+ return () => {
528
+ for (const unsubscribe of subscriptions) unsubscribe();
529
+ };
530
+ }
531
+ async waitUntilReady() {
532
+ await Promise.all([
533
+ ...this.listeners.values()
534
+ ].map((listener) => listener.waitUntilReady()));
535
+ }
536
+ async close() {
537
+ const listeners = [
538
+ ...this.listeners.values()
539
+ ];
540
+ this.listeners.clear();
541
+ await Promise.all(listeners.map((listener) => listener.close()));
542
+ }
543
+ async onModuleDestroy() {
544
+ await this.close();
545
+ }
546
+ getQueueEvents(queueName) {
547
+ const existing = this.listeners.get(queueName);
548
+ if (existing) return existing;
549
+ const queueEvents = this.queueEventsFactory(queueName, {
550
+ connection: this.options.connection,
551
+ prefix: this.options.prefix
552
+ });
553
+ queueEvents.on("error", (error) => this.options.onError?.(queueName, error));
554
+ this.listeners.set(queueName, queueEvents);
555
+ return queueEvents;
556
+ }
557
+ };
558
+
559
+ // src/queue.service.ts
560
+ var import_common2 = require("@nestjs/common");
561
+ function _ts_decorate(decorators, target, key, desc) {
562
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
563
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
564
+ r = Reflect.decorate(decorators, target, key, desc);
565
+ } else {
566
+ for (var i = decorators.length - 1; i >= 0; i--) {
567
+ if (d = decorators[i]) {
568
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
569
+ }
570
+ }
571
+ }
572
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
573
+ }
574
+ __name(_ts_decorate, "_ts_decorate");
575
+ function _ts_metadata(metadataKey, metadataValue) {
576
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
577
+ return Reflect.metadata(metadataKey, metadataValue);
578
+ }
579
+ }
580
+ __name(_ts_metadata, "_ts_metadata");
581
+ function _ts_param(paramIndex, decorator) {
582
+ return function(target, key) {
583
+ decorator(target, key, paramIndex);
584
+ };
585
+ }
586
+ __name(_ts_param, "_ts_param");
587
+ var QueueService = class {
588
+ static {
589
+ __name(this, "QueueService");
590
+ }
591
+ provider;
592
+ constructor(provider) {
593
+ this.provider = provider;
594
+ }
595
+ /** 添加任务到队列 */
596
+ add(queueName, data, options) {
597
+ return this.provider.add(queueName, data, options);
598
+ }
599
+ /** Ensure a stable transport job exists for durable queued work. */
600
+ ensure(queueName, data, options) {
601
+ return this.provider.ensure(queueName, data, options);
602
+ }
603
+ /** 添加延迟任务 */
604
+ addDelayed(queueName, data, delayMs, options) {
605
+ return this.provider.addDelayed(queueName, data, delayMs, options);
606
+ }
607
+ /** 添加定时任务 */
608
+ addRepeatable(queueName, data, pattern, options) {
609
+ return this.provider.addRepeatable(queueName, data, pattern, options);
610
+ }
611
+ /** 获取任务状态 */
612
+ getJob(queueName, jobId) {
613
+ return this.provider.getJob(queueName, jobId);
614
+ }
615
+ /** 移除任务 */
616
+ removeJob(queueName, jobId) {
617
+ return this.provider.removeJob(queueName, jobId);
618
+ }
619
+ /** 暂停队列 */
620
+ pause(queueName) {
621
+ return this.provider.pause(queueName);
622
+ }
623
+ /** 恢复队列 */
624
+ resume(queueName) {
625
+ return this.provider.resume(queueName);
626
+ }
627
+ /** 获取队列统计 */
628
+ getStats(queueName) {
629
+ return this.provider.getStats(queueName);
630
+ }
631
+ };
632
+ QueueService = _ts_decorate([
633
+ (0, import_common2.Injectable)(),
634
+ _ts_param(0, (0, import_common2.Inject)(QUEUE_PROVIDER)),
635
+ _ts_metadata("design:type", Function),
636
+ _ts_metadata("design:paramtypes", [
637
+ typeof QueueProvider === "undefined" ? Object : QueueProvider
638
+ ])
639
+ ], QueueService);
640
+
641
+ // src/queue.module.ts
642
+ var import_common3 = require("@nestjs/common");
643
+ function _ts_decorate2(decorators, target, key, desc) {
644
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
645
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
646
+ r = Reflect.decorate(decorators, target, key, desc);
647
+ } else {
648
+ for (var i = decorators.length - 1; i >= 0; i--) {
649
+ if (d = decorators[i]) {
650
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
651
+ }
652
+ }
653
+ }
654
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
655
+ }
656
+ __name(_ts_decorate2, "_ts_decorate");
657
+ var QUEUE_MODULE_OPTIONS = /* @__PURE__ */ Symbol("QUEUE_MODULE_OPTIONS");
658
+ var ZuckerQueueModule = class _ZuckerQueueModule {
659
+ static {
660
+ __name(this, "ZuckerQueueModule");
661
+ }
662
+ static forRoot(options = {}) {
663
+ return {
664
+ module: _ZuckerQueueModule,
665
+ global: true,
666
+ providers: [
667
+ {
668
+ provide: QUEUE_PROVIDER,
669
+ useFactory: /* @__PURE__ */ __name(() => options.provider ?? new InMemoryQueue(), "useFactory")
670
+ },
671
+ QueueService
672
+ ],
673
+ exports: [
674
+ QUEUE_PROVIDER,
675
+ QueueService
676
+ ]
677
+ };
678
+ }
679
+ static forRootAsync(options) {
680
+ const asyncProviders = this.createAsyncProviders(options);
681
+ return {
682
+ module: _ZuckerQueueModule,
683
+ global: true,
684
+ imports: options.imports ?? [],
685
+ providers: [
686
+ ...asyncProviders,
687
+ {
688
+ provide: QUEUE_PROVIDER,
689
+ useFactory: /* @__PURE__ */ __name((opts) => opts.provider ?? new InMemoryQueue(), "useFactory"),
690
+ inject: [
691
+ QUEUE_MODULE_OPTIONS
692
+ ]
693
+ },
694
+ QueueService
695
+ ],
696
+ exports: [
697
+ QUEUE_PROVIDER,
698
+ QueueService
699
+ ]
700
+ };
701
+ }
702
+ static createAsyncProviders(options) {
703
+ if (options.useFactory) {
704
+ return [
705
+ {
706
+ provide: QUEUE_MODULE_OPTIONS,
707
+ useFactory: options.useFactory,
708
+ inject: options.inject ?? []
709
+ }
710
+ ];
711
+ }
712
+ const factoryType = options.useClass ?? options.useExisting;
713
+ if (!factoryType) {
714
+ throw new Error("QueueModuleAsyncOptions requires useFactory, useClass, or useExisting");
715
+ }
716
+ const optionsProvider = {
717
+ provide: QUEUE_MODULE_OPTIONS,
718
+ useFactory: /* @__PURE__ */ __name(async (factory) => factory.createQueueOptions(), "useFactory"),
719
+ inject: [
720
+ factoryType
721
+ ]
722
+ };
723
+ if (options.useClass) {
724
+ return [
725
+ {
726
+ provide: options.useClass,
727
+ useClass: options.useClass
728
+ },
729
+ optionsProvider
730
+ ];
731
+ }
732
+ return [
733
+ optionsProvider
734
+ ];
735
+ }
736
+ };
737
+ ZuckerQueueModule = _ts_decorate2([
738
+ (0, import_common3.Global)(),
739
+ (0, import_common3.Module)({})
740
+ ], ZuckerQueueModule);
741
+
742
+ // src/worker.decorator.ts
743
+ var import_common4 = require("@nestjs/common");
744
+ var QUEUE_WORKER_KEY = "ZUCKER_QUEUE_WORKER";
745
+ function QueueWorker(options) {
746
+ const opts = typeof options === "string" ? {
747
+ queue: options
748
+ } : options;
749
+ return (0, import_common4.SetMetadata)(QUEUE_WORKER_KEY, opts);
750
+ }
751
+ __name(QueueWorker, "QueueWorker");
752
+
753
+ // src/task-lifecycle.ts
754
+ var import_common5 = require("@nestjs/common");
755
+ var taskIdCounter = 0;
756
+ function generateTaskId() {
757
+ return `task_${Date.now()}_${++taskIdCounter}`;
758
+ }
759
+ __name(generateTaskId, "generateTaskId");
760
+ var TaskLifecycleManager = class _TaskLifecycleManager {
761
+ static {
762
+ __name(this, "TaskLifecycleManager");
763
+ }
764
+ logger = new import_common5.Logger(_TaskLifecycleManager.name);
765
+ tasks = /* @__PURE__ */ new Map();
766
+ cancelledTasks = /* @__PURE__ */ new Set();
767
+ deadLetterQueue = [];
768
+ maxDlqSize;
769
+ constructor(options) {
770
+ this.maxDlqSize = options?.maxDlqSize ?? 100;
771
+ }
772
+ /**
773
+ * 提交任务并异步执行
774
+ */
775
+ submit(definition) {
776
+ const id = generateTaskId();
777
+ const info = {
778
+ id,
779
+ name: definition.name,
780
+ status: "pending",
781
+ progress: 0,
782
+ createdAt: Date.now(),
783
+ attempts: 0,
784
+ metadata: definition.metadata
785
+ };
786
+ this.tasks.set(id, info);
787
+ this.executeTask(id, definition);
788
+ return id;
789
+ }
790
+ /**
791
+ * 获取任务状态
792
+ */
793
+ getTask(id) {
794
+ return this.tasks.get(id);
795
+ }
796
+ /**
797
+ * 列出所有任务
798
+ */
799
+ listTasks(filter) {
800
+ const tasks = [
801
+ ...this.tasks.values()
802
+ ];
803
+ if (filter?.status) {
804
+ return tasks.filter((t) => t.status === filter.status);
805
+ }
806
+ return tasks;
807
+ }
808
+ /**
809
+ * 取消任务
810
+ */
811
+ kill(id) {
812
+ const task = this.tasks.get(id);
813
+ if (!task || task.status === "completed" || task.status === "failed" || task.status === "killed") {
814
+ return false;
815
+ }
816
+ this.cancelledTasks.add(id);
817
+ task.status = "killed";
818
+ task.completedAt = Date.now();
819
+ return true;
820
+ }
821
+ /**
822
+ * 获取死信队列中的失败任务
823
+ */
824
+ getDeadLetterQueue() {
825
+ return [
826
+ ...this.deadLetterQueue
827
+ ];
828
+ }
829
+ /**
830
+ * 清理已完成的任务(释放内存)
831
+ */
832
+ cleanup(olderThanMs = 36e5) {
833
+ const cutoff = Date.now() - olderThanMs;
834
+ let cleaned = 0;
835
+ for (const [id, task] of this.tasks) {
836
+ if ((task.status === "completed" || task.status === "failed" || task.status === "killed") && (task.completedAt ?? task.createdAt) < cutoff) {
837
+ this.tasks.delete(id);
838
+ cleaned++;
839
+ }
840
+ }
841
+ return cleaned;
842
+ }
843
+ async executeTask(id, definition) {
844
+ const info = this.tasks.get(id);
845
+ const maxRetries = definition.maxRetries ?? 0;
846
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
847
+ if (this.cancelledTasks.has(id)) return;
848
+ info.status = "running";
849
+ info.startedAt = Date.now();
850
+ info.attempts = attempt + 1;
851
+ const ctx = {
852
+ taskId: id,
853
+ updateProgress: /* @__PURE__ */ __name((progress, message) => {
854
+ info.progress = Math.max(0, Math.min(1, progress));
855
+ info.progressMessage = message;
856
+ }, "updateProgress"),
857
+ isCancelled: /* @__PURE__ */ __name(() => this.cancelledTasks.has(id), "isCancelled")
858
+ };
859
+ try {
860
+ const resultPromise = definition.handler(ctx);
861
+ let result;
862
+ if (definition.timeoutMs) {
863
+ result = await Promise.race([
864
+ resultPromise,
865
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Task timeout after ${definition.timeoutMs}ms`)), definition.timeoutMs))
866
+ ]);
867
+ } else {
868
+ result = await resultPromise;
869
+ }
870
+ if (this.cancelledTasks.has(id)) return;
871
+ info.status = "completed";
872
+ info.progress = 1;
873
+ info.result = result;
874
+ info.completedAt = Date.now();
875
+ return;
876
+ } catch (error) {
877
+ if (this.cancelledTasks.has(id)) return;
878
+ const errorMsg = error instanceof Error ? error.message : String(error);
879
+ if (attempt < maxRetries) {
880
+ this.logger.warn(`Task "${info.name}" attempt ${attempt + 1} failed: ${errorMsg}. Retrying...`);
881
+ await new Promise((r) => setTimeout(r, 1e3 * Math.pow(2, attempt)));
882
+ continue;
883
+ }
884
+ info.status = "failed";
885
+ info.error = errorMsg;
886
+ info.completedAt = Date.now();
887
+ if (this.deadLetterQueue.length >= this.maxDlqSize) {
888
+ this.deadLetterQueue.shift();
889
+ }
890
+ this.deadLetterQueue.push({
891
+ ...info
892
+ });
893
+ this.logger.error(`Task "${info.name}" failed after ${info.attempts} attempts: ${errorMsg}`);
894
+ }
895
+ }
896
+ }
897
+ };
898
+ // Annotate the CommonJS export names for ESM import in node:
899
+ 0 && (module.exports = {
900
+ BullMqQueueEvents,
901
+ BullMqQueueProvider,
902
+ BullMqWorkerHost,
903
+ InMemoryQueue,
904
+ QUEUE_PROVIDER,
905
+ QUEUE_WORKER_KEY,
906
+ QueueService,
907
+ QueueWorker,
908
+ TaskLifecycleManager,
909
+ ZuckerQueueModule,
910
+ bullMqConnectionFromUrl
911
+ });