@lara-node/queue 0.1.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.cjs ADDED
@@ -0,0 +1,1782 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
20
+ key = keys[i];
21
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
22
+ get: ((k) => from[k]).bind(null, key),
23
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
24
+ });
25
+ }
26
+ return to;
27
+ };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
29
+ value: mod,
30
+ enumerable: true
31
+ }) : target, mod));
32
+ //#endregion
33
+ const require_queue_config = require("./queue.config.cjs");
34
+ let crypto = require("crypto");
35
+ crypto = __toESM(crypto, 1);
36
+ let _lara_node_db = require("@lara-node/db");
37
+ let redis = require("redis");
38
+ let _lara_node_cache = require("@lara-node/cache");
39
+ let events = require("events");
40
+ let cron_parser = require("cron-parser");
41
+ let _lara_node_core = require("@lara-node/core");
42
+ //#region src/Job.ts
43
+ var Job_exports = /* @__PURE__ */ __exportAll({
44
+ Job: () => Job,
45
+ PendingDispatch: () => PendingDispatch,
46
+ Queueable: () => Queueable,
47
+ decryptPayload: () => decryptPayload,
48
+ dispatch: () => dispatch,
49
+ encryptPayload: () => encryptPayload,
50
+ getJobClass: () => getJobClass,
51
+ getRegisteredJobs: () => getRegisteredJobs,
52
+ registerJob: () => registerJob
53
+ });
54
+ let _encryptionKey;
55
+ function getEncryptionKey() {
56
+ if (_encryptionKey) return _encryptionKey;
57
+ const raw = process.env.APP_KEY;
58
+ if (!raw) throw new Error("APP_KEY is not set — cannot encrypt job payload.");
59
+ const stripped = raw.replace(/^base64:/, "");
60
+ const decoded = Buffer.from(stripped, "base64");
61
+ _encryptionKey = decoded.length === 32 ? decoded : crypto.default.createHash("sha256").update(decoded).digest();
62
+ return _encryptionKey;
63
+ }
64
+ function encryptPayload(data) {
65
+ const key = getEncryptionKey();
66
+ const iv = crypto.default.randomBytes(12);
67
+ const cipher = crypto.default.createCipheriv("aes-256-gcm", key, iv);
68
+ let encrypted = cipher.update(data, "utf8", "base64");
69
+ encrypted += cipher.final("base64");
70
+ const tag = cipher.getAuthTag();
71
+ return [
72
+ iv.toString("base64"),
73
+ tag.toString("base64"),
74
+ encrypted
75
+ ].join(":");
76
+ }
77
+ function decryptPayload(data) {
78
+ const key = getEncryptionKey();
79
+ const [ivB64, tagB64, encrypted] = data.split(":");
80
+ const iv = Buffer.from(ivB64, "base64");
81
+ const tag = Buffer.from(tagB64, "base64");
82
+ const decipher = crypto.default.createDecipheriv("aes-256-gcm", key, iv);
83
+ decipher.setAuthTag(tag);
84
+ let decrypted = decipher.update(encrypted, "base64", "utf8");
85
+ decrypted += decipher.final("utf8");
86
+ return decrypted;
87
+ }
88
+ const jobRegistry = /* @__PURE__ */ new Map();
89
+ function registerJob(name, jobClass) {
90
+ jobRegistry.set(name, jobClass);
91
+ }
92
+ function getJobClass(name) {
93
+ return jobRegistry.get(name);
94
+ }
95
+ function getRegisteredJobs() {
96
+ return jobRegistry;
97
+ }
98
+ function Queueable(name) {
99
+ return function(constructor) {
100
+ registerJob(name || constructor.name, constructor);
101
+ return constructor;
102
+ };
103
+ }
104
+ var Job = class {
105
+ constructor() {
106
+ this.queue = "default";
107
+ this.connection = require_queue_config.queueConfig.default;
108
+ this.tries = require_queue_config.queueConfig.defaults.tries;
109
+ this.maxExceptions = require_queue_config.queueConfig.defaults.maxExceptions;
110
+ this.timeout = require_queue_config.queueConfig.defaults.timeout;
111
+ this.backoff = require_queue_config.queueConfig.defaults.backoff;
112
+ this.shouldBeEncrypted = false;
113
+ this.delay = 0;
114
+ this._uuid = "";
115
+ this._attempts = 0;
116
+ }
117
+ /**
118
+ * Handle a job failure.
119
+ */
120
+ failed(exception) {}
121
+ /**
122
+ * Determine the time at which the job should timeout.
123
+ */
124
+ retryUntil() {
125
+ return null;
126
+ }
127
+ /**
128
+ * Get the middleware the job should pass through.
129
+ */
130
+ middleware() {
131
+ return [];
132
+ }
133
+ /**
134
+ * Get the tags that should be assigned to the job.
135
+ */
136
+ tags() {
137
+ return [];
138
+ }
139
+ get uuid() {
140
+ return this._uuid;
141
+ }
142
+ get attempts() {
143
+ return this._attempts;
144
+ }
145
+ /**
146
+ * Get the display name for the queued job.
147
+ */
148
+ displayName() {
149
+ return this.constructor.name;
150
+ }
151
+ /**
152
+ * Prepare the job for serialization.
153
+ */
154
+ getSerializableProperties() {
155
+ const props = {};
156
+ for (const key of Object.keys(this)) if (!key.startsWith("_")) props[key] = this[key];
157
+ return props;
158
+ }
159
+ /**
160
+ * Serialize the job to a format that can be stored.
161
+ */
162
+ serialize() {
163
+ const uuid = this._uuid || crypto.default.randomUUID();
164
+ this._uuid = uuid;
165
+ const retryUntilDate = this.retryUntil();
166
+ const rawData = JSON.stringify(this.getSerializableProperties());
167
+ return {
168
+ id: crypto.default.randomUUID(),
169
+ uuid,
170
+ displayName: this.displayName(),
171
+ job: this.constructor.name,
172
+ data: this.shouldBeEncrypted ? encryptPayload(rawData) : rawData,
173
+ encrypted: this.shouldBeEncrypted,
174
+ queue: this.queue,
175
+ attempts: this._attempts,
176
+ maxTries: this.tries,
177
+ maxExceptions: this.maxExceptions,
178
+ exceptionCount: 0,
179
+ timeout: this.timeout,
180
+ backoff: this.backoff,
181
+ retryUntil: retryUntilDate ? retryUntilDate.getTime() : null,
182
+ createdAt: Date.now(),
183
+ availableAt: Date.now() + this.delay * 1e3,
184
+ reservedAt: null
185
+ };
186
+ }
187
+ /**
188
+ * Restore the job from serialized data.
189
+ */
190
+ static deserialize(serialized) {
191
+ const JobClass = getJobClass(serialized.job);
192
+ if (!JobClass) {
193
+ console.error(`Job class "${serialized.job}" not found in registry. Make sure to use @Queueable decorator.`);
194
+ return null;
195
+ }
196
+ const instance = new JobClass();
197
+ let rawData = serialized.data;
198
+ if (serialized.encrypted) try {
199
+ rawData = decryptPayload(rawData);
200
+ } catch (e) {
201
+ console.error(`[Job] Failed to decrypt payload for ${serialized.job}:`, e);
202
+ return null;
203
+ }
204
+ const data = JSON.parse(rawData);
205
+ for (const [key, value] of Object.entries(data)) instance[key] = value;
206
+ instance._uuid = serialized.uuid;
207
+ instance._attempts = serialized.attempts;
208
+ return instance;
209
+ }
210
+ /**
211
+ * Dispatch the job with the given arguments.
212
+ */
213
+ static dispatch(...args) {
214
+ return new PendingDispatch(new this());
215
+ }
216
+ /**
217
+ * Dispatch the job synchronously.
218
+ */
219
+ static dispatchSync() {
220
+ return new this().handle();
221
+ }
222
+ /**
223
+ * Dispatch the job after the response is sent.
224
+ */
225
+ static dispatchAfterResponse() {
226
+ return new PendingDispatch(new this()).afterResponse();
227
+ }
228
+ onQueue(queue) {
229
+ this.queue = queue;
230
+ return this;
231
+ }
232
+ onConnection(connection) {
233
+ this.connection = connection;
234
+ return this;
235
+ }
236
+ withDelay(seconds) {
237
+ this.delay = seconds;
238
+ return this;
239
+ }
240
+ withTries(tries) {
241
+ this.tries = tries;
242
+ return this;
243
+ }
244
+ withTimeout(seconds) {
245
+ this.timeout = seconds;
246
+ return this;
247
+ }
248
+ withBackoff(backoff) {
249
+ this.backoff = backoff;
250
+ return this;
251
+ }
252
+ };
253
+ var PendingDispatch = class {
254
+ constructor(job) {
255
+ this.job = job;
256
+ this._afterResponse = false;
257
+ }
258
+ onQueue(queue) {
259
+ this.job.onQueue(queue);
260
+ return this;
261
+ }
262
+ onConnection(connection) {
263
+ this.job.onConnection(connection);
264
+ return this;
265
+ }
266
+ delay(seconds) {
267
+ this.job.withDelay(seconds);
268
+ return this;
269
+ }
270
+ tries(n) {
271
+ this.job.withTries(n);
272
+ return this;
273
+ }
274
+ timeout(seconds) {
275
+ this.job.withTimeout(seconds);
276
+ return this;
277
+ }
278
+ backoff(b) {
279
+ this.job.withBackoff(b);
280
+ return this;
281
+ }
282
+ maxExceptions(n) {
283
+ this.job.maxExceptions = n;
284
+ return this;
285
+ }
286
+ afterResponse() {
287
+ this._afterResponse = true;
288
+ return this;
289
+ }
290
+ async dispatch() {
291
+ const { Queue } = await Promise.resolve().then(() => Queue_exports);
292
+ if (this._afterResponse) {
293
+ setImmediate(async () => {
294
+ await Queue.push(this.job);
295
+ });
296
+ return this.job.uuid;
297
+ }
298
+ return Queue.push(this.job);
299
+ }
300
+ };
301
+ function dispatch(job) {
302
+ return new PendingDispatch(job);
303
+ }
304
+ //#endregion
305
+ //#region src/Drivers/SyncDriver.ts
306
+ var SyncDriver = class {
307
+ constructor() {
308
+ this.jobs = /* @__PURE__ */ new Map();
309
+ }
310
+ async size(queue = "default") {
311
+ return this.jobs.get(queue)?.length ?? 0;
312
+ }
313
+ async push(job, queue = "default") {
314
+ if (!this.jobs.has(queue)) this.jobs.set(queue, []);
315
+ const { Job: JobBase } = await Promise.resolve().then(() => Job_exports);
316
+ const maxTries = job.maxTries || require_queue_config.queueConfig.defaults.tries;
317
+ const maxExceptions = job.maxExceptions ?? require_queue_config.queueConfig.defaults.maxExceptions;
318
+ let lastError = null;
319
+ while (job.attempts < maxTries && (job.exceptionCount ?? 0) < maxExceptions) {
320
+ job.attempts += 1;
321
+ job.exceptionCount = job.exceptionCount ?? 0;
322
+ const instance = JobBase.deserialize(job);
323
+ if (!instance) break;
324
+ try {
325
+ await instance.handle();
326
+ return job.id;
327
+ } catch (error) {
328
+ lastError = error;
329
+ job.exceptionCount += 1;
330
+ if (!(job.attempts < maxTries && job.exceptionCount < maxExceptions)) break;
331
+ console.warn(`[Sync] Job ${job.displayName} failed (attempt ${job.attempts}/${maxTries}), retrying...`);
332
+ }
333
+ }
334
+ if (lastError) {
335
+ console.error(`[Sync] Job ${job.displayName} failed permanently after ${job.attempts} attempt(s):`, lastError.message);
336
+ const instance = JobBase.deserialize(job);
337
+ if (instance) try {
338
+ instance.failed(lastError);
339
+ } catch {}
340
+ throw lastError;
341
+ }
342
+ return job.id;
343
+ }
344
+ async later(delay, job, queue = "default") {
345
+ setTimeout(() => {
346
+ this.push(job, queue).catch((err) => {
347
+ console.error(`[Sync] Delayed job ${job.displayName} failed:`, err);
348
+ });
349
+ }, delay * 1e3);
350
+ return job.id;
351
+ }
352
+ async pop(queue = "default") {
353
+ const jobs = this.jobs.get(queue);
354
+ if (!jobs || jobs.length === 0) return null;
355
+ return jobs.shift() ?? null;
356
+ }
357
+ async delete(job, queue = "default") {
358
+ const jobs = this.jobs.get(queue);
359
+ if (jobs) {
360
+ const index = jobs.findIndex((j) => j.id === job.id);
361
+ if (index > -1) jobs.splice(index, 1);
362
+ }
363
+ }
364
+ async release(job, delay, queue = "default") {
365
+ job.availableAt = Date.now() + delay * 1e3;
366
+ job.reservedAt = null;
367
+ if (!this.jobs.has(queue)) this.jobs.set(queue, []);
368
+ this.jobs.get(queue).push(job);
369
+ }
370
+ async clear(queue = "default") {
371
+ const count = this.jobs.get(queue)?.length ?? 0;
372
+ this.jobs.set(queue, []);
373
+ return count;
374
+ }
375
+ async getJobs(queue = "default") {
376
+ return this.jobs.get(queue) ?? [];
377
+ }
378
+ };
379
+ //#endregion
380
+ //#region src/Queue/QueueJob.ts
381
+ var QueueJob = class extends _lara_node_db.Model {
382
+ static {
383
+ this.table = "jobs";
384
+ }
385
+ static {
386
+ this.primaryKey = "id";
387
+ }
388
+ static {
389
+ this.timestamps = false;
390
+ }
391
+ static {
392
+ this.fillable = [
393
+ "uuid",
394
+ "queue",
395
+ "payload",
396
+ "attempts",
397
+ "reserved_at",
398
+ "available_at",
399
+ "created_at"
400
+ ];
401
+ }
402
+ static {
403
+ this.casts = {
404
+ attempts: "int",
405
+ reserved_at: "int",
406
+ available_at: "int",
407
+ created_at: "int"
408
+ };
409
+ }
410
+ /**
411
+ * Scope to get jobs for a specific queue.
412
+ */
413
+ static scopeForQueue(query, queue) {
414
+ return query.where("queue", queue);
415
+ }
416
+ /**
417
+ * Scope to get available jobs (not reserved or reservation expired).
418
+ */
419
+ static scopeAvailable(query, now, retryAfter) {
420
+ const expiredReservation = now - retryAfter;
421
+ return query.where(function(q) {
422
+ q.whereNull("reserved_at").orWhere("reserved_at", "<", expiredReservation);
423
+ }).where("available_at", "<=", now);
424
+ }
425
+ /**
426
+ * Scope to get the next available job.
427
+ */
428
+ static scopeNextAvailable(query, queue, now, retryAfter) {
429
+ return query.forQueue(queue).available(now, retryAfter).orderBy("id", "asc");
430
+ }
431
+ /**
432
+ * Get the parsed payload.
433
+ */
434
+ getParsedPayload() {
435
+ try {
436
+ return JSON.parse(this.payload);
437
+ } catch {
438
+ return null;
439
+ }
440
+ }
441
+ /**
442
+ * Mark the job as reserved.
443
+ */
444
+ async reserve(timestamp) {
445
+ this.reserved_at = timestamp;
446
+ this.attempts = (this.attempts || 0) + 1;
447
+ await this.save();
448
+ }
449
+ /**
450
+ * Release the job back to the queue.
451
+ */
452
+ async release(availableAt, payload) {
453
+ this.reserved_at = null;
454
+ this.available_at = availableAt;
455
+ if (payload) this.payload = payload;
456
+ await this.save();
457
+ }
458
+ };
459
+ //#endregion
460
+ //#region src/Queue/FailedJob.ts
461
+ var FailedJob = class extends _lara_node_db.Model {
462
+ static {
463
+ this.table = "failed_jobs";
464
+ }
465
+ static {
466
+ this.primaryKey = "id";
467
+ }
468
+ static {
469
+ this.timestamps = false;
470
+ }
471
+ static {
472
+ this.fillable = [
473
+ "uuid",
474
+ "connection",
475
+ "queue",
476
+ "payload",
477
+ "exception",
478
+ "failed_at"
479
+ ];
480
+ }
481
+ static {
482
+ this.casts = {
483
+ id: "int",
484
+ failed_at: "datetime"
485
+ };
486
+ }
487
+ /**
488
+ * Scope to find by UUID.
489
+ */
490
+ static scopeByUuid(query, uuid) {
491
+ return query.where("uuid", uuid);
492
+ }
493
+ /**
494
+ * Scope to get jobs for a specific queue.
495
+ */
496
+ static scopeForQueue(query, queue) {
497
+ return query.where("queue", queue);
498
+ }
499
+ /**
500
+ * Scope to get jobs for a specific connection.
501
+ */
502
+ static scopeForConnection(query, connection) {
503
+ return query.where("connection", connection);
504
+ }
505
+ /**
506
+ * Get the parsed payload.
507
+ */
508
+ getParsedPayload() {
509
+ try {
510
+ return JSON.parse(this.payload);
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+ /**
516
+ * Get a summary of the exception.
517
+ */
518
+ getExceptionSummary(maxLength = 100) {
519
+ if (!this.exception) return "";
520
+ const firstLine = this.exception.split("\n")[0];
521
+ return firstLine.length > maxLength ? firstLine.substring(0, maxLength) + "..." : firstLine;
522
+ }
523
+ };
524
+ //#endregion
525
+ //#region src/Drivers/DatabaseDriver.ts
526
+ var DatabaseDriver = class {
527
+ constructor(config) {
528
+ const dbConfig = require_queue_config.queueConfig.connections.database;
529
+ this.defaultQueue = config?.queue || dbConfig.queue || "default";
530
+ this.retryAfter = config?.retry_after || dbConfig.retry_after || 90;
531
+ }
532
+ async size(queue = this.defaultQueue) {
533
+ return await QueueJob.query().where("queue", queue).count();
534
+ }
535
+ async push(job, queue = this.defaultQueue) {
536
+ const now = Math.floor(Date.now() / 1e3);
537
+ const availableAt = Math.floor(job.availableAt / 1e3);
538
+ await QueueJob.create({
539
+ uuid: job.uuid,
540
+ queue,
541
+ payload: JSON.stringify(job),
542
+ attempts: 0,
543
+ available_at: availableAt,
544
+ created_at: now
545
+ });
546
+ return job.id;
547
+ }
548
+ async later(delay, job, queue = this.defaultQueue) {
549
+ job.availableAt = Date.now() + delay * 1e3;
550
+ return this.push(job, queue);
551
+ }
552
+ async pop(queue = this.defaultQueue) {
553
+ const now = Math.floor(Date.now() / 1e3);
554
+ const expiredReservation = now - this.retryAfter;
555
+ const queueJob = await QueueJob.query().where("queue", queue).where(function(q) {
556
+ q.whereNull("reserved_at").orWhere("reserved_at", "<", expiredReservation);
557
+ }).where("available_at", "<=", now).orderBy("id", "asc").first();
558
+ if (!queueJob) return null;
559
+ const newAttempts = (queueJob.attempts || 0) + 1;
560
+ await QueueJob.query().where("id", queueJob.id).update({
561
+ reserved_at: now,
562
+ attempts: newAttempts
563
+ });
564
+ const job = JSON.parse(queueJob.payload);
565
+ job.reservedAt = now * 1e3;
566
+ job.attempts = newAttempts;
567
+ job.__dbRowId = queueJob.id;
568
+ return job;
569
+ }
570
+ async delete(job, _queue = this.defaultQueue) {
571
+ const rowId = job.__dbRowId;
572
+ if (rowId != null) await QueueJob.query().where("id", rowId).delete();
573
+ else await QueueJob.query().where("uuid", job.uuid).delete();
574
+ }
575
+ async release(job, delay, _queue = this.defaultQueue) {
576
+ const availableAt = Math.floor((Date.now() + delay * 1e3) / 1e3);
577
+ const rowId = job.__dbRowId;
578
+ job.availableAt = availableAt * 1e3;
579
+ job.reservedAt = null;
580
+ const { __dbRowId: _strip, ...cleanJob } = job;
581
+ const payload = JSON.stringify(cleanJob);
582
+ if (rowId != null) await QueueJob.query().where("id", rowId).update({
583
+ reserved_at: null,
584
+ available_at: availableAt,
585
+ attempts: job.attempts,
586
+ payload
587
+ });
588
+ else await QueueJob.query().where("uuid", job.uuid).update({
589
+ reserved_at: null,
590
+ available_at: availableAt,
591
+ attempts: job.attempts,
592
+ payload
593
+ });
594
+ }
595
+ async clear(queue = this.defaultQueue) {
596
+ const count = await this.size(queue);
597
+ await QueueJob.query().where("queue", queue).delete();
598
+ return count;
599
+ }
600
+ async getJobs(queue = this.defaultQueue) {
601
+ return (await QueueJob.query().where("queue", queue).orderBy("id", "asc").get()).map((job) => JSON.parse(job.payload));
602
+ }
603
+ async logFailed(connection, queue, job, exception) {
604
+ const { __dbRowId: _strip, ...cleanJob } = job;
605
+ await FailedJob.create({
606
+ uuid: job.uuid,
607
+ connection,
608
+ queue,
609
+ payload: JSON.stringify(cleanJob),
610
+ exception: exception.stack || exception.message
611
+ });
612
+ }
613
+ async getFailedJobs() {
614
+ return (await FailedJob.query().orderBy("failed_at", "desc").get()).map((job) => job.toJSON());
615
+ }
616
+ async retryFailed(uuid) {
617
+ const failedJob = await FailedJob.query().where("uuid", uuid).first();
618
+ if (!failedJob) return false;
619
+ const job = JSON.parse(failedJob.payload);
620
+ job.attempts = 0;
621
+ job.exceptionCount = 0;
622
+ job.reservedAt = null;
623
+ job.availableAt = Date.now();
624
+ await this.push(job, failedJob.queue);
625
+ await this.forgetFailed(uuid);
626
+ return true;
627
+ }
628
+ async forgetFailed(uuid) {
629
+ return await FailedJob.query().where("uuid", uuid).delete() > 0;
630
+ }
631
+ async flushFailed() {
632
+ const count = await FailedJob.query().count();
633
+ await FailedJob.query().delete();
634
+ return count;
635
+ }
636
+ };
637
+ //#endregion
638
+ //#region src/Drivers/RedisDriver.ts
639
+ var RedisDriver = class {
640
+ constructor(config) {
641
+ this.client = null;
642
+ this.initialized = false;
643
+ const redisConfig = require_queue_config.queueConfig.connections.redis;
644
+ this.defaultQueue = config?.queue || redisConfig.queue || "default";
645
+ this.retryAfter = config?.retry_after || redisConfig.retry_after || 90;
646
+ const appName = process.env.APP_NAME || "app";
647
+ this.prefix = config?.prefix || process.env.REDIS_PREFIX || `${appName}_queue`;
648
+ }
649
+ key(queue, suffix = "") {
650
+ const base = `${this.prefix}:${queue}`;
651
+ return suffix ? `${base}:${suffix}` : base;
652
+ }
653
+ async init() {
654
+ if (this.initialized && this.client) return;
655
+ try {
656
+ const redisUrl = process.env.REDIS_URL || `redis://${process.env.REDIS_HOST || "localhost"}:${process.env.REDIS_PORT || 6379}`;
657
+ this.client = (0, redis.createClient)({
658
+ url: redisUrl,
659
+ password: process.env.REDIS_PASSWORD || void 0
660
+ });
661
+ this.client.on("error", (err) => {
662
+ console.error("[RedisDriver] Connection error:", err);
663
+ });
664
+ await this.client.connect();
665
+ this.initialized = true;
666
+ } catch (error) {
667
+ console.error("[RedisDriver] Failed to connect:", error);
668
+ throw error;
669
+ }
670
+ }
671
+ async ensureConnected() {
672
+ if (!this.client || !this.initialized) await this.init();
673
+ return this.client;
674
+ }
675
+ async size(queue = this.defaultQueue) {
676
+ const client = await this.ensureConnected();
677
+ const pending = await client.lLen(this.key(queue));
678
+ const delayed = await client.zCard(this.key(queue, "delayed:score"));
679
+ const reserved = await client.hLen(this.key(queue, "reserved"));
680
+ return pending + delayed + reserved;
681
+ }
682
+ async push(job, queue = this.defaultQueue) {
683
+ const client = await this.ensureConnected();
684
+ if (job.availableAt > Date.now()) {
685
+ await client.hSet(this.key(queue, "delayed:body"), job.uuid, JSON.stringify(job));
686
+ await client.zAdd(this.key(queue, "delayed:score"), {
687
+ score: job.availableAt,
688
+ value: job.uuid
689
+ });
690
+ } else await client.rPush(this.key(queue), JSON.stringify(job));
691
+ return job.id;
692
+ }
693
+ async later(delay, job, queue = this.defaultQueue) {
694
+ job.availableAt = Date.now() + delay * 1e3;
695
+ return this.push(job, queue);
696
+ }
697
+ async pop(queue = this.defaultQueue) {
698
+ const client = await this.ensureConnected();
699
+ await this.migrateDelayedJobs(queue);
700
+ await this.migrateExpiredReserved(queue);
701
+ const payload = await client.lPop(this.key(queue));
702
+ if (!payload) return null;
703
+ const job = JSON.parse(payload);
704
+ job.attempts += 1;
705
+ job.reservedAt = Date.now();
706
+ await client.hSet(this.key(queue, "reserved"), job.uuid, JSON.stringify(job));
707
+ return job;
708
+ }
709
+ async migrateDelayedJobs(queue) {
710
+ const client = await this.ensureConnected();
711
+ const now = Date.now();
712
+ const uuids = await client.zRangeByScore(this.key(queue, "delayed:score"), "-inf", now.toString());
713
+ for (const uuid of uuids) {
714
+ const body = await client.hGet(this.key(queue, "delayed:body"), uuid);
715
+ if (body) {
716
+ await client.rPush(this.key(queue), body);
717
+ await client.hDel(this.key(queue, "delayed:body"), uuid);
718
+ }
719
+ await client.zRem(this.key(queue, "delayed:score"), uuid);
720
+ }
721
+ }
722
+ async migrateExpiredReserved(queue) {
723
+ const client = await this.ensureConnected();
724
+ const cutoff = Date.now() - this.retryAfter * 1e3;
725
+ const entries = await client.hGetAll(this.key(queue, "reserved"));
726
+ for (const [uuid, body] of Object.entries(entries)) {
727
+ const job = JSON.parse(body);
728
+ if (job.reservedAt != null && job.reservedAt < cutoff) {
729
+ await client.rPush(this.key(queue), body);
730
+ await client.hDel(this.key(queue, "reserved"), uuid);
731
+ }
732
+ }
733
+ }
734
+ async delete(job, queue = this.defaultQueue) {
735
+ await (await this.ensureConnected()).hDel(this.key(queue, "reserved"), job.uuid);
736
+ }
737
+ async release(job, delay, queue = this.defaultQueue) {
738
+ const client = await this.ensureConnected();
739
+ await client.hDel(this.key(queue, "reserved"), job.uuid);
740
+ job.reservedAt = null;
741
+ job.availableAt = Date.now() + delay * 1e3;
742
+ const newPayload = JSON.stringify(job);
743
+ if (delay > 0) {
744
+ await client.hSet(this.key(queue, "delayed:body"), job.uuid, newPayload);
745
+ await client.zAdd(this.key(queue, "delayed:score"), {
746
+ score: job.availableAt,
747
+ value: job.uuid
748
+ });
749
+ } else await client.rPush(this.key(queue), newPayload);
750
+ }
751
+ async clear(queue = this.defaultQueue) {
752
+ const client = await this.ensureConnected();
753
+ const size = await this.size(queue);
754
+ await client.del(this.key(queue));
755
+ await client.del(this.key(queue, "delayed:score"));
756
+ await client.del(this.key(queue, "delayed:body"));
757
+ await client.del(this.key(queue, "reserved"));
758
+ return size;
759
+ }
760
+ async getJobs(queue = this.defaultQueue) {
761
+ const client = await this.ensureConnected();
762
+ const pending = await client.lRange(this.key(queue), 0, -1);
763
+ const delayedBodies = Object.values(await client.hGetAll(this.key(queue, "delayed:body")));
764
+ const reservedBodies = Object.values(await client.hGetAll(this.key(queue, "reserved")));
765
+ return [
766
+ ...pending,
767
+ ...delayedBodies,
768
+ ...reservedBodies
769
+ ].map((p) => JSON.parse(p));
770
+ }
771
+ async logFailed(connection, queue, job, exception) {
772
+ const client = await this.ensureConnected();
773
+ const failedJob = {
774
+ uuid: job.uuid,
775
+ connection,
776
+ queue,
777
+ payload: job,
778
+ exception: exception.stack || exception.message,
779
+ failed_at: (/* @__PURE__ */ new Date()).toISOString()
780
+ };
781
+ await client.hSet(`${this.prefix}:failed_jobs`, job.uuid, JSON.stringify(failedJob));
782
+ }
783
+ async getFailedJobs() {
784
+ const jobs = await (await this.ensureConnected()).hGetAll(`${this.prefix}:failed_jobs`);
785
+ return Object.values(jobs).map((j) => JSON.parse(j));
786
+ }
787
+ async retryFailed(uuid) {
788
+ const data = await (await this.ensureConnected()).hGet(`${this.prefix}:failed_jobs`, uuid);
789
+ if (!data) return false;
790
+ const failedJob = JSON.parse(data);
791
+ const job = failedJob.payload;
792
+ job.attempts = 0;
793
+ job.exceptionCount = 0;
794
+ job.reservedAt = null;
795
+ job.availableAt = Date.now();
796
+ await this.push(job, failedJob.queue);
797
+ await this.forgetFailed(uuid);
798
+ return true;
799
+ }
800
+ async forgetFailed(uuid) {
801
+ return await (await this.ensureConnected()).hDel(`${this.prefix}:failed_jobs`, uuid) > 0;
802
+ }
803
+ async flushFailed() {
804
+ const client = await this.ensureConnected();
805
+ const count = await client.hLen(`${this.prefix}:failed_jobs`);
806
+ await client.del(`${this.prefix}:failed_jobs`);
807
+ return count;
808
+ }
809
+ async disconnect() {
810
+ if (this.client) {
811
+ await this.client.quit();
812
+ this.client = null;
813
+ this.initialized = false;
814
+ }
815
+ }
816
+ };
817
+ //#endregion
818
+ //#region src/Queue.ts
819
+ var Queue_exports = /* @__PURE__ */ __exportAll({
820
+ Queue: () => Queue,
821
+ QueueManager: () => QueueManager
822
+ });
823
+ var QueueManager = class {
824
+ constructor() {
825
+ this.connections = /* @__PURE__ */ new Map();
826
+ this.defaultConnection = require_queue_config.queueConfig.default;
827
+ }
828
+ /**
829
+ * Get a queue connection instance.
830
+ * All drivers are cached — database queries are stateless per-call so sharing
831
+ * the driver instance is safe and avoids repeated object allocation on every poll.
832
+ */
833
+ connection(name) {
834
+ const connectionName = name || this.defaultConnection;
835
+ if (!this.connections.has(connectionName)) {
836
+ if (!require_queue_config.queueConfig.connections[connectionName]) throw new Error(`Queue connection [${connectionName}] is not defined.`);
837
+ this.connections.set(connectionName, this.resolve(connectionName));
838
+ }
839
+ return this.connections.get(connectionName);
840
+ }
841
+ /**
842
+ * Resolve a queue connection instance.
843
+ */
844
+ resolve(name) {
845
+ const config = require_queue_config.queueConfig.connections[name];
846
+ if (!config) throw new Error(`Queue connection [${name}] is not defined.`);
847
+ switch (config.driver) {
848
+ case "sync": return new SyncDriver();
849
+ case "database": return new DatabaseDriver({
850
+ table: config.table,
851
+ queue: config.queue,
852
+ retry_after: config.retry_after
853
+ });
854
+ case "redis": return new RedisDriver({
855
+ queue: config.queue,
856
+ retry_after: config.retry_after
857
+ });
858
+ default: throw new Error(`Queue driver [${config.driver}] is not supported.`);
859
+ }
860
+ }
861
+ /**
862
+ * Get the default connection name.
863
+ */
864
+ getDefaultDriver() {
865
+ return this.defaultConnection;
866
+ }
867
+ /**
868
+ * Set the default connection name.
869
+ */
870
+ setDefaultDriver(name) {
871
+ this.defaultConnection = name;
872
+ }
873
+ /**
874
+ * Push a new job onto the queue.
875
+ * Respects uniqueId/uniqueFor — silently deduplicates if a lock already exists.
876
+ */
877
+ async push(job, queue) {
878
+ if (job.uniqueId) {
879
+ const lockKey = `queue:unique:${job.uniqueId}`;
880
+ const existingUuid = await _lara_node_cache.Cache.get(lockKey).catch(() => null);
881
+ if (existingUuid != null) return existingUuid ?? job.uniqueId;
882
+ const ttl = job.uniqueFor && job.uniqueFor > 0 ? job.uniqueFor : null;
883
+ await _lara_node_cache.Cache.set(lockKey, job.uuid || "pending", ttl).catch(() => {});
884
+ }
885
+ const serialized = job.serialize();
886
+ const connection = this.connection(job.connection);
887
+ const queueName = queue || job.queue;
888
+ if (job.delay > 0) return connection.later(job.delay, serialized, queueName);
889
+ return connection.push(serialized, queueName);
890
+ }
891
+ /**
892
+ * Release the unique lock for a job after it completes or permanently fails.
893
+ */
894
+ async releaseUniqueLock(job) {
895
+ const uniqueId = job.uniqueId;
896
+ if (uniqueId) await _lara_node_cache.Cache.del(`queue:unique:${uniqueId}`).catch(() => {});
897
+ }
898
+ /**
899
+ * Push a new job onto the queue after a delay.
900
+ */
901
+ async later(delay, job, queue) {
902
+ job.withDelay(delay);
903
+ return this.push(job, queue);
904
+ }
905
+ /**
906
+ * Push a raw payload onto the queue.
907
+ */
908
+ async pushRaw(payload, queue, connection) {
909
+ return this.connection(connection).push(payload, queue);
910
+ }
911
+ /**
912
+ * Push multiple jobs onto the queue.
913
+ */
914
+ async bulk(jobs, queue) {
915
+ return Promise.all(jobs.map((job) => this.push(job, queue)));
916
+ }
917
+ /**
918
+ * Pop the next job from the queue.
919
+ */
920
+ async pop(queue, connection) {
921
+ return this.connection(connection).pop(queue);
922
+ }
923
+ /**
924
+ * Get the size of the queue.
925
+ */
926
+ async size(queue, connection) {
927
+ return this.connection(connection).size(queue);
928
+ }
929
+ /**
930
+ * Clear all jobs from the queue.
931
+ */
932
+ async clear(queue, connection) {
933
+ return this.connection(connection).clear(queue);
934
+ }
935
+ /**
936
+ * Get all jobs from a queue.
937
+ */
938
+ async getJobs(queue, connection) {
939
+ return this.connection(connection).getJobs(queue);
940
+ }
941
+ asFailedDriver(driver) {
942
+ return "logFailed" in driver ? driver : null;
943
+ }
944
+ async logFailed(connectionName, queue, job, exception) {
945
+ const failed = this.asFailedDriver(this.connection(connectionName));
946
+ if (failed) await failed.logFailed(connectionName, queue, job, exception);
947
+ }
948
+ async getFailedJobs(connection) {
949
+ const failed = this.asFailedDriver(this.connection(connection));
950
+ return failed ? failed.getFailedJobs() : [];
951
+ }
952
+ async retryFailed(uuid, connection) {
953
+ const failed = this.asFailedDriver(this.connection(connection));
954
+ return failed ? failed.retryFailed(uuid) : false;
955
+ }
956
+ async forgetFailed(uuid, connection) {
957
+ const failed = this.asFailedDriver(this.connection(connection));
958
+ return failed ? failed.forgetFailed(uuid) : false;
959
+ }
960
+ async flushFailed(connection) {
961
+ const failed = this.asFailedDriver(this.connection(connection));
962
+ return failed ? failed.flushFailed() : 0;
963
+ }
964
+ };
965
+ const Queue = new QueueManager();
966
+ //#endregion
967
+ //#region src/Worker.ts
968
+ let signalsRegistered = false;
969
+ var Worker = class Worker extends events.EventEmitter {
970
+ static {
971
+ this.IDLE_CHECK_INTERVAL = 5;
972
+ }
973
+ constructor(connectionName, queues = "default", options = {}) {
974
+ super();
975
+ this.running = false;
976
+ this.paused = false;
977
+ this.shouldQuit = false;
978
+ this.jobsProcessed = 0;
979
+ this.startTime = 0;
980
+ this.currentJob = null;
981
+ this.idleTicks = 0;
982
+ this.connectionName = connectionName || require_queue_config.queueConfig.default;
983
+ this.queues = Array.isArray(queues) ? queues : [queues];
984
+ this.restartKey = `${process.env.APP_NAME || "app"}:queue:restart`;
985
+ this.horizonCtrlPrefix = `${process.env.APP_NAME || "app"}:horizon:ctrl`;
986
+ this.workerId = options.workerId;
987
+ this.options = {
988
+ connection: this.connectionName,
989
+ queue: queues,
990
+ delay: options.delay ?? 0,
991
+ memory: options.memory ?? 128,
992
+ timeout: options.timeout ?? require_queue_config.queueConfig.defaults.timeout,
993
+ sleep: options.sleep ?? 3,
994
+ maxTries: options.maxTries ?? require_queue_config.queueConfig.defaults.tries,
995
+ maxJobs: options.maxJobs ?? 0,
996
+ maxTime: options.maxTime ?? 0,
997
+ force: options.force ?? false,
998
+ stopWhenEmpty: options.stopWhenEmpty ?? false,
999
+ rest: options.rest ?? 0,
1000
+ verbose: options.verbose ?? false
1001
+ };
1002
+ }
1003
+ async daemon() {
1004
+ if (this.running) return;
1005
+ this.running = true;
1006
+ this.shouldQuit = false;
1007
+ this.startTime = Date.now();
1008
+ this.jobsProcessed = 0;
1009
+ console.log(`[Worker] Starting on connection [${this.connectionName}] processing queues: ${this.queues.join(", ")}`);
1010
+ this.emit("worker:start", {
1011
+ connection: this.connectionName,
1012
+ queues: this.queues
1013
+ });
1014
+ this.registerSignalHandlers();
1015
+ while (this.running && !this.shouldQuit) {
1016
+ if (this.paused) {
1017
+ await this.sleep(this.options.sleep * 1e3);
1018
+ continue;
1019
+ }
1020
+ if (this.options.maxJobs > 0 && this.jobsProcessed >= this.options.maxJobs) {
1021
+ console.log(`[Worker] Max jobs (${this.options.maxJobs}) reached, stopping...`);
1022
+ this.stop();
1023
+ break;
1024
+ }
1025
+ if (this.options.maxTime > 0) {
1026
+ if ((Date.now() - this.startTime) / 1e3 >= this.options.maxTime) {
1027
+ console.log(`[Worker] Max time (${this.options.maxTime}s) reached, stopping...`);
1028
+ this.stop();
1029
+ break;
1030
+ }
1031
+ }
1032
+ if (this.idleTicks % Worker.IDLE_CHECK_INTERVAL === 0) {
1033
+ if (this.memoryExceeded()) {
1034
+ console.log("[Worker] Memory limit exceeded, stopping...");
1035
+ this.stop();
1036
+ break;
1037
+ }
1038
+ const [inMaintenance, restart, horizonSignal] = await Promise.all([
1039
+ this.options.force ? Promise.resolve(false) : this.isInMaintenanceMode(),
1040
+ this.shouldRestart(),
1041
+ this.checkHorizonSignal()
1042
+ ]);
1043
+ if (horizonSignal === "stop") {
1044
+ console.log("[Worker] Horizon stop signal received, stopping...");
1045
+ this.stop();
1046
+ break;
1047
+ }
1048
+ if (horizonSignal === "pause" && !this.paused) {
1049
+ console.log("[Worker] Horizon pause signal received.");
1050
+ this.pause();
1051
+ }
1052
+ if (horizonSignal === "resume" && this.paused) {
1053
+ console.log("[Worker] Horizon resume signal received.");
1054
+ this.resume();
1055
+ }
1056
+ if (restart) {
1057
+ console.log("[Worker] Restart signal detected, stopping...");
1058
+ this.stop();
1059
+ break;
1060
+ }
1061
+ if (inMaintenance) {
1062
+ if (this.options.verbose) console.log("[Worker] Application in maintenance mode, sleeping...");
1063
+ this.idleTicks++;
1064
+ await this.sleep(this.options.sleep * 1e3);
1065
+ continue;
1066
+ }
1067
+ }
1068
+ const job = await this.getNextJob();
1069
+ if (job) {
1070
+ this.idleTicks = 0;
1071
+ await this.process(job);
1072
+ this.jobsProcessed++;
1073
+ if (this.options.rest > 0) await this.sleep(this.options.rest * 1e3);
1074
+ } else {
1075
+ if (this.options.stopWhenEmpty) {
1076
+ console.log("[Worker] Queue is empty, stopping...");
1077
+ this.stop();
1078
+ break;
1079
+ }
1080
+ this.idleTicks++;
1081
+ await this.sleep(this.options.sleep * 1e3);
1082
+ }
1083
+ }
1084
+ this.emit("worker:stop", {
1085
+ connection: this.connectionName,
1086
+ jobsProcessed: this.jobsProcessed,
1087
+ runtime: (Date.now() - this.startTime) / 1e3
1088
+ });
1089
+ console.log(`[Worker] Stopped. Processed ${this.jobsProcessed} jobs.`);
1090
+ }
1091
+ async runNextJob() {
1092
+ const job = await this.getNextJob();
1093
+ if (!job) return false;
1094
+ await this.process(job);
1095
+ return true;
1096
+ }
1097
+ stop() {
1098
+ this.shouldQuit = true;
1099
+ this.running = false;
1100
+ }
1101
+ pause() {
1102
+ this.paused = true;
1103
+ this.emit("worker:pause");
1104
+ }
1105
+ resume() {
1106
+ this.paused = false;
1107
+ this.emit("worker:resume");
1108
+ }
1109
+ async getNextJob() {
1110
+ for (const queue of this.queues) try {
1111
+ if (this.options.verbose) console.log(`[Worker] Polling queue [${queue}] on connection [${this.connectionName}]...`);
1112
+ const job = await Queue.pop(queue, this.connectionName);
1113
+ if (job) return job;
1114
+ } catch (error) {
1115
+ console.error(`[Worker] Error popping job from queue [${queue}]:`, error);
1116
+ }
1117
+ if (this.options.verbose) console.log(`[Worker] No jobs found, sleeping for ${this.options.sleep}s...`);
1118
+ return null;
1119
+ }
1120
+ async process(serializedJob) {
1121
+ this.currentJob = serializedJob;
1122
+ this.emit("job:processing", {
1123
+ connectionName: this.connectionName,
1124
+ job: serializedJob
1125
+ });
1126
+ console.log(`[Worker] Processing job: ${serializedJob.displayName} (${serializedJob.uuid})`);
1127
+ try {
1128
+ const job = Job.deserialize(serializedJob);
1129
+ if (!job) throw new Error(`Failed to deserialize job: ${serializedJob.job}`);
1130
+ const timeoutMs = (serializedJob.timeout || this.options.timeout) * 1e3;
1131
+ let timeoutHandle;
1132
+ const timeoutPromise = new Promise((_, reject) => {
1133
+ timeoutHandle = setTimeout(() => reject(/* @__PURE__ */ new Error("Job timed out")), timeoutMs);
1134
+ });
1135
+ try {
1136
+ await Promise.race([job.handle(), timeoutPromise]);
1137
+ } finally {
1138
+ clearTimeout(timeoutHandle);
1139
+ }
1140
+ await this.handleSuccess(serializedJob);
1141
+ } catch (error) {
1142
+ await this.handleFailure(serializedJob, error);
1143
+ } finally {
1144
+ this.currentJob = null;
1145
+ }
1146
+ }
1147
+ async handleSuccess(job) {
1148
+ await Queue.connection(this.connectionName).delete(job, job.queue);
1149
+ await Queue.releaseUniqueLock(job);
1150
+ this.emit("job:processed", {
1151
+ connectionName: this.connectionName,
1152
+ job
1153
+ });
1154
+ console.log(`[Worker] Job completed: ${job.displayName} (${job.uuid})`);
1155
+ }
1156
+ async handleFailure(job, error) {
1157
+ console.error(`[Worker] Job failed: ${job.displayName} (${job.uuid}) — ${error.message}`);
1158
+ this.emit("job:exception", {
1159
+ connectionName: this.connectionName,
1160
+ job,
1161
+ exception: error
1162
+ });
1163
+ job.exceptionCount = (job.exceptionCount ?? 0) + 1;
1164
+ const maxTries = job.maxTries || this.options.maxTries;
1165
+ const maxExceptions = job.maxExceptions ?? require_queue_config.queueConfig.defaults.maxExceptions;
1166
+ const retryUntilExpired = job.retryUntil != null && Date.now() > job.retryUntil;
1167
+ const shouldFailPermanently = retryUntilExpired || job.attempts >= maxTries || job.exceptionCount >= maxExceptions;
1168
+ const driver = Queue.connection(this.connectionName);
1169
+ if (!shouldFailPermanently) {
1170
+ const delay = this.calculateBackoff(job);
1171
+ console.log(`[Worker] Releasing job for retry — attempt ${job.attempts}/${maxTries}, exceptions ${job.exceptionCount}/${maxExceptions}, delay ${delay}s`);
1172
+ await driver.release(job, delay, job.queue);
1173
+ } else {
1174
+ const reason = retryUntilExpired ? "retryUntil deadline passed" : job.exceptionCount >= maxExceptions ? `maxExceptions (${maxExceptions}) reached` : `maxTries (${maxTries}) reached`;
1175
+ console.log(`[Worker] Job failed permanently after ${job.attempts} attempt(s): ${reason}`);
1176
+ await driver.delete(job, job.queue);
1177
+ await Queue.logFailed(this.connectionName, job.queue, job, error);
1178
+ await Queue.releaseUniqueLock(job);
1179
+ const jobInstance = Job.deserialize(job);
1180
+ if (jobInstance) try {
1181
+ jobInstance.failed(error);
1182
+ } catch (e) {
1183
+ console.error("[Worker] Error in job.failed():", e);
1184
+ }
1185
+ this.emit("job:failed", {
1186
+ connectionName: this.connectionName,
1187
+ job,
1188
+ exception: error
1189
+ });
1190
+ }
1191
+ }
1192
+ calculateBackoff(job) {
1193
+ const backoff = job.backoff || require_queue_config.queueConfig.defaults.backoff;
1194
+ if (typeof backoff === "number") return backoff;
1195
+ if (Array.isArray(backoff)) {
1196
+ const index = Math.min(job.attempts - 1, backoff.length - 1);
1197
+ return backoff[Math.max(0, index)];
1198
+ }
1199
+ return 0;
1200
+ }
1201
+ memoryExceeded() {
1202
+ return process.memoryUsage().heapUsed / 1024 / 1024 >= this.options.memory;
1203
+ }
1204
+ async isInMaintenanceMode() {
1205
+ try {
1206
+ return await _lara_node_cache.Cache.has(`${process.env.APP_NAME || "app"}:maintenance`);
1207
+ } catch {
1208
+ return false;
1209
+ }
1210
+ }
1211
+ async shouldRestart() {
1212
+ try {
1213
+ const restartTs = await _lara_node_cache.Cache.get(this.restartKey);
1214
+ return restartTs != null && Number(restartTs) > this.startTime;
1215
+ } catch {
1216
+ return false;
1217
+ }
1218
+ }
1219
+ /** Read and immediately clear a Horizon dashboard control signal for this worker. */
1220
+ async checkHorizonSignal() {
1221
+ if (!this.workerId) return null;
1222
+ const key = `${this.horizonCtrlPrefix}:${this.workerId}`;
1223
+ try {
1224
+ const sig = await _lara_node_cache.Cache.get(key);
1225
+ if (sig) await _lara_node_cache.Cache.del(key);
1226
+ return sig ?? null;
1227
+ } catch {
1228
+ return null;
1229
+ }
1230
+ }
1231
+ sleep(ms) {
1232
+ return new Promise((resolve) => setTimeout(resolve, ms));
1233
+ }
1234
+ registerSignalHandlers() {
1235
+ if (signalsRegistered) return;
1236
+ signalsRegistered = true;
1237
+ for (const signal of [
1238
+ "SIGINT",
1239
+ "SIGTERM",
1240
+ "SIGQUIT"
1241
+ ]) process.on(signal, () => {
1242
+ console.log(`\n[Worker] Received ${signal}, stopping gracefully...`);
1243
+ this.stop();
1244
+ });
1245
+ }
1246
+ isRunning() {
1247
+ return this.running;
1248
+ }
1249
+ isPaused() {
1250
+ return this.paused;
1251
+ }
1252
+ getJobsProcessed() {
1253
+ return this.jobsProcessed;
1254
+ }
1255
+ getCurrentJob() {
1256
+ return this.currentJob;
1257
+ }
1258
+ getRuntime() {
1259
+ return this.startTime > 0 ? (Date.now() - this.startTime) / 1e3 : 0;
1260
+ }
1261
+ getStatus() {
1262
+ return {
1263
+ running: this.running,
1264
+ paused: this.paused,
1265
+ jobsProcessed: this.jobsProcessed,
1266
+ runtime: this.getRuntime(),
1267
+ currentJob: this.currentJob,
1268
+ memory: process.memoryUsage().heapUsed / 1024 / 1024
1269
+ };
1270
+ }
1271
+ };
1272
+ //#endregion
1273
+ //#region src/Scheduler.ts
1274
+ const SCHEDULER_LOCK_PREFIX = `${process.env.APP_NAME || "app"}:scheduler:lock`;
1275
+ async function acquireLock(key, ttlSeconds) {
1276
+ const lockKey = `${SCHEDULER_LOCK_PREFIX}:${key}`;
1277
+ if (await _lara_node_cache.Cache.get(lockKey)) return false;
1278
+ await _lara_node_cache.Cache.set(lockKey, Date.now(), ttlSeconds);
1279
+ return true;
1280
+ }
1281
+ async function releaseLock(key) {
1282
+ await _lara_node_cache.Cache.del(`${SCHEDULER_LOCK_PREFIX}:${key}`);
1283
+ }
1284
+ function createObservableTask(task) {
1285
+ try {
1286
+ task.nextRun = cron_parser.CronExpressionParser.parse(task.expression, { tz: task.timezone || "UTC" }).next().toDate();
1287
+ } catch {}
1288
+ return task;
1289
+ }
1290
+ var Schedule = class {
1291
+ constructor() {
1292
+ this.tasks = [];
1293
+ this.running = false;
1294
+ this.events = new events.EventEmitter();
1295
+ this.cronPartsCache = /* @__PURE__ */ new Map();
1296
+ this.options = { verbose: false };
1297
+ }
1298
+ call(callback) {
1299
+ const task = createObservableTask(this.defaultTask(`closure-${Date.now()}`, callback));
1300
+ this.tasks.push(task);
1301
+ return new ScheduledTaskBuilder(task);
1302
+ }
1303
+ command(command, args = []) {
1304
+ const callback = async () => {
1305
+ const { exec } = await import("child_process");
1306
+ const { promisify } = await import("util");
1307
+ const execAsync = promisify(exec);
1308
+ const fullCommand = `npm run artisan -- ${command} ${args.join(" ")}`;
1309
+ console.log(`[Scheduler] Running command: ${fullCommand}`);
1310
+ const { stdout, stderr } = await execAsync(fullCommand);
1311
+ if (stdout) console.log(stdout);
1312
+ if (stderr) console.error(stderr);
1313
+ };
1314
+ const task = createObservableTask(this.defaultTask(`command:${command}`, callback, `Artisan command: ${command}`));
1315
+ this.tasks.push(task);
1316
+ return new ScheduledTaskBuilder(task);
1317
+ }
1318
+ exec(command) {
1319
+ const callback = async () => {
1320
+ const { exec } = await import("child_process");
1321
+ const { promisify } = await import("util");
1322
+ const execAsync = promisify(exec);
1323
+ console.log(`[Scheduler] Executing: ${command}`);
1324
+ const { stdout, stderr } = await execAsync(command);
1325
+ if (stdout) console.log(stdout);
1326
+ if (stderr) console.error(stderr);
1327
+ };
1328
+ const task = createObservableTask(this.defaultTask(`exec:${command.slice(0, 50)}`, callback, `Shell command: ${command}`));
1329
+ this.tasks.push(task);
1330
+ return new ScheduledTaskBuilder(task);
1331
+ }
1332
+ job(JobClass, queue) {
1333
+ const callback = async () => {
1334
+ const pending = JobClass.dispatch();
1335
+ if (queue) pending.onQueue(queue);
1336
+ await pending.dispatch();
1337
+ };
1338
+ const task = createObservableTask(this.defaultTask(`job:${JobClass.name || "anonymous"}`, callback, `Dispatch job: ${JobClass.name}`));
1339
+ this.tasks.push(task);
1340
+ return new ScheduledTaskBuilder(task);
1341
+ }
1342
+ defaultTask(name, callback, description) {
1343
+ return {
1344
+ name,
1345
+ callback,
1346
+ expression: "* * * * *",
1347
+ description,
1348
+ withoutOverlapping: false,
1349
+ onOneServer: false,
1350
+ evenInMaintenanceMode: false,
1351
+ runInBackground: false,
1352
+ conditions: [],
1353
+ skipConditions: [],
1354
+ isRunning: false
1355
+ };
1356
+ }
1357
+ updateNextTaskRun(task) {
1358
+ task.nextRun = cron_parser.CronExpressionParser.parse(task.expression, { tz: task.timezone || "UTC" }).next().toDate();
1359
+ }
1360
+ getTasks() {
1361
+ return this.tasks;
1362
+ }
1363
+ getDueTasks() {
1364
+ const now = /* @__PURE__ */ new Date();
1365
+ return this.tasks.filter((task) => this.isDue(task, now));
1366
+ }
1367
+ isDue(task, now = /* @__PURE__ */ new Date()) {
1368
+ if (!this.matchesCronExpression(task.expression, now)) return false;
1369
+ if (task.betweenStart && task.betweenEnd) {
1370
+ const [sh, sm] = task.betweenStart.split(":").map(Number);
1371
+ const [eh, em] = task.betweenEnd.split(":").map(Number);
1372
+ const current = now.getHours() * 60 + now.getMinutes();
1373
+ const start = sh * 60 + sm;
1374
+ const end = eh * 60 + em;
1375
+ if (current < start || current > end) return false;
1376
+ }
1377
+ return true;
1378
+ }
1379
+ matchesCronExpression(expression, date) {
1380
+ let parts = this.cronPartsCache.get(expression);
1381
+ if (!parts) {
1382
+ parts = expression.split(" ");
1383
+ if (parts.length !== 5) {
1384
+ console.warn(`[Scheduler] Invalid cron expression: ${expression}`);
1385
+ return false;
1386
+ }
1387
+ this.cronPartsCache.set(expression, parts);
1388
+ }
1389
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
1390
+ return this.matchCronPart(minute, date.getMinutes()) && this.matchCronPart(hour, date.getHours()) && this.matchCronPart(dayOfMonth, date.getDate()) && this.matchCronPart(month, date.getMonth() + 1) && this.matchCronPart(dayOfWeek, date.getDay());
1391
+ }
1392
+ matchCronPart(pattern, value) {
1393
+ if (pattern === "*") return true;
1394
+ if (pattern.startsWith("*/")) return value % parseInt(pattern.slice(2), 10) === 0;
1395
+ if (pattern.includes("-")) {
1396
+ const [start, end] = pattern.split("-").map(Number);
1397
+ return value >= start && value <= end;
1398
+ }
1399
+ if (pattern.includes(",")) return pattern.split(",").map(Number).includes(value);
1400
+ return parseInt(pattern, 10) === value;
1401
+ }
1402
+ async runDueTasks() {
1403
+ const maintenanceKey = `${process.env.APP_NAME || "app"}:maintenance`;
1404
+ const inMaintenance = await _lara_node_cache.Cache.has(maintenanceKey).catch(() => false);
1405
+ const dueTasks = this.getDueTasks();
1406
+ console.log(`[Scheduler] Found ${dueTasks.length} due task(s)`);
1407
+ for (const task of dueTasks) {
1408
+ if (inMaintenance && !task.evenInMaintenanceMode) {
1409
+ console.log(`[Scheduler] Skipping task in maintenance mode: ${task.name}`);
1410
+ continue;
1411
+ }
1412
+ if (task.conditions.length > 0) {
1413
+ if (!(await Promise.all(task.conditions.map((fn) => fn()))).every(Boolean)) {
1414
+ if (this.options.verbose) console.log(`[Scheduler] Skipping task (when() failed): ${task.name}`);
1415
+ continue;
1416
+ }
1417
+ }
1418
+ if (task.skipConditions.length > 0) {
1419
+ if ((await Promise.all(task.skipConditions.map((fn) => fn()))).some(Boolean)) {
1420
+ if (this.options.verbose) console.log(`[Scheduler] Skipping task (skip() matched): ${task.name}`);
1421
+ continue;
1422
+ }
1423
+ }
1424
+ if (task.withoutOverlapping && task.isRunning) {
1425
+ console.log(`[Scheduler] Skipping overlapping task (in-memory): ${task.name}`);
1426
+ continue;
1427
+ }
1428
+ if (task.withoutOverlapping) {
1429
+ if (!await acquireLock(`overlap:${task.name}`, 300).catch(() => false)) {
1430
+ console.log(`[Scheduler] Skipping overlapping task (distributed lock): ${task.name}`);
1431
+ continue;
1432
+ }
1433
+ }
1434
+ if (task.onOneServer) {
1435
+ if (!await acquireLock(`once:${task.name}:${Math.floor(Date.now() / 6e4)}`, 65).catch(() => false)) {
1436
+ console.log(`[Scheduler] Skipping task (onOneServer, another server has it): ${task.name}`);
1437
+ continue;
1438
+ }
1439
+ }
1440
+ await this.runTask(task);
1441
+ }
1442
+ }
1443
+ async runTask(task) {
1444
+ task.isRunning = true;
1445
+ task.lastRun = /* @__PURE__ */ new Date();
1446
+ this.updateNextTaskRun(task);
1447
+ console.log(`[Scheduler] Running task: ${task.name}`);
1448
+ this.events.emit("task:start", task);
1449
+ const finalize = async (error) => {
1450
+ task.isRunning = false;
1451
+ if (task.withoutOverlapping) await releaseLock(`overlap:${task.name}`).catch(() => {});
1452
+ if (error) task.onFailureHook?.(task, error);
1453
+ else task.onSuccessHook?.(task);
1454
+ };
1455
+ try {
1456
+ if (task.runInBackground) setImmediate(async () => {
1457
+ try {
1458
+ await task.callback();
1459
+ this.events.emit("task:success", task);
1460
+ await finalize();
1461
+ } catch (error) {
1462
+ console.error(`[Scheduler] Task failed: ${task.name}`, error);
1463
+ this.events.emit("task:failed", task, error);
1464
+ await finalize(error);
1465
+ }
1466
+ });
1467
+ else {
1468
+ await task.callback();
1469
+ this.events.emit("task:success", task);
1470
+ await finalize();
1471
+ }
1472
+ } catch (error) {
1473
+ console.error(`[Scheduler] Task failed: ${task.name}`, error);
1474
+ this.events.emit("task:failed", task, error);
1475
+ await finalize(error);
1476
+ }
1477
+ }
1478
+ async start() {
1479
+ if (this.running) {
1480
+ console.log("[Scheduler] Already running");
1481
+ return;
1482
+ }
1483
+ this.running = true;
1484
+ console.log("[Scheduler] Starting scheduler daemon...");
1485
+ await this.runDueTasks();
1486
+ while (this.running) {
1487
+ const now = /* @__PURE__ */ new Date();
1488
+ const msUntilNextMinute = (60 - now.getSeconds()) * 1e3 - now.getMilliseconds();
1489
+ await this.sleep(msUntilNextMinute);
1490
+ if (this.running) await this.runDueTasks();
1491
+ }
1492
+ }
1493
+ stop() {
1494
+ this.running = false;
1495
+ console.log("[Scheduler] Stopping...");
1496
+ }
1497
+ isRunning() {
1498
+ return this.running;
1499
+ }
1500
+ on(event, listener) {
1501
+ this.events.on(event, listener);
1502
+ }
1503
+ sleep(ms) {
1504
+ return new Promise((resolve) => setTimeout(resolve, ms));
1505
+ }
1506
+ };
1507
+ var ScheduledTaskBuilder = class {
1508
+ constructor(task) {
1509
+ this.task = task;
1510
+ }
1511
+ everyMinute() {
1512
+ return this.cron("* * * * *");
1513
+ }
1514
+ everyTwoMinutes() {
1515
+ return this.cron("*/2 * * * *");
1516
+ }
1517
+ everyThreeMinutes() {
1518
+ return this.cron("*/3 * * * *");
1519
+ }
1520
+ everyFiveMinutes() {
1521
+ return this.cron("*/5 * * * *");
1522
+ }
1523
+ everyTenMinutes() {
1524
+ return this.cron("*/10 * * * *");
1525
+ }
1526
+ everyFifteenMinutes() {
1527
+ return this.cron("*/15 * * * *");
1528
+ }
1529
+ everyTwentyMinutes() {
1530
+ return this.cron("*/20 * * * *");
1531
+ }
1532
+ everyThirtyMinutes() {
1533
+ return this.cron("*/30 * * * *");
1534
+ }
1535
+ everyFortyFiveMinutes() {
1536
+ return this.cron("*/45 * * * *");
1537
+ }
1538
+ /** Run every N minutes (arbitrary interval). */
1539
+ everyNMinutes(n) {
1540
+ return this.cron(`*/${n} * * * *`);
1541
+ }
1542
+ hourly() {
1543
+ return this.cron("0 * * * *");
1544
+ }
1545
+ hourlyAt(minute) {
1546
+ return this.cron(`${minute} * * * *`);
1547
+ }
1548
+ everyTwoHours() {
1549
+ return this.cron("0 */2 * * *");
1550
+ }
1551
+ everyFourHours() {
1552
+ return this.cron("0 */4 * * *");
1553
+ }
1554
+ everySixHours() {
1555
+ return this.cron("0 */6 * * *");
1556
+ }
1557
+ /** Run every N hours (arbitrary interval). */
1558
+ everyNHours(n) {
1559
+ return this.cron(`0 */${n} * * *`);
1560
+ }
1561
+ daily() {
1562
+ return this.cron("0 0 * * *");
1563
+ }
1564
+ dailyAt(time) {
1565
+ const [hour, minute] = time.split(":").map(Number);
1566
+ return this.cron(`${minute ?? 0} ${hour} * * *`);
1567
+ }
1568
+ twiceDaily(firstHour = 1, secondHour = 13) {
1569
+ return this.cron(`0 ${firstHour},${secondHour} * * *`);
1570
+ }
1571
+ twiceDailyAt(firstHour, firstMinute, secondHour, secondMinute) {
1572
+ return this.cron(`${firstMinute} ${firstHour},${secondHour} * * *`);
1573
+ }
1574
+ weekly() {
1575
+ return this.cron("0 0 * * 0");
1576
+ }
1577
+ weeklyOn(dayOfWeek, time = "0:0") {
1578
+ const [hour, minute] = time.split(":").map(Number);
1579
+ return this.cron(`${minute ?? 0} ${hour} * * ${dayOfWeek}`);
1580
+ }
1581
+ monthly() {
1582
+ return this.cron("0 0 1 * *");
1583
+ }
1584
+ monthlyOn(day, time = "0:0") {
1585
+ const [hour, minute] = time.split(":").map(Number);
1586
+ return this.cron(`${minute ?? 0} ${hour} ${day} * *`);
1587
+ }
1588
+ /**
1589
+ * Run on the last day of the month at midnight.
1590
+ * Uses day 28 as a universally-safe proxy (real last-day check runs at runtime).
1591
+ */
1592
+ lastDayOfMonth(time = "0:0") {
1593
+ const [hour, minute] = time.split(":").map(Number);
1594
+ this.cron(`${minute ?? 0} ${hour} 28-31 * *`);
1595
+ this.task.conditions.push(() => {
1596
+ const now = /* @__PURE__ */ new Date();
1597
+ const tomorrow = new Date(now);
1598
+ tomorrow.setDate(now.getDate() + 1);
1599
+ return tomorrow.getDate() === 1;
1600
+ });
1601
+ return this;
1602
+ }
1603
+ quarterly() {
1604
+ return this.cron("0 0 1 1,4,7,10 *");
1605
+ }
1606
+ yearly() {
1607
+ return this.cron("0 0 1 1 *");
1608
+ }
1609
+ yearlyOn(month, day = 1, time = "0:0") {
1610
+ const [hour, minute] = time.split(":").map(Number);
1611
+ return this.cron(`${minute ?? 0} ${hour} ${day} ${month} *`);
1612
+ }
1613
+ weekdays() {
1614
+ const parts = this.task.expression.split(" ");
1615
+ parts[4] = "1-5";
1616
+ return this.cron(parts.join(" "));
1617
+ }
1618
+ weekends() {
1619
+ const parts = this.task.expression.split(" ");
1620
+ parts[4] = "0,6";
1621
+ return this.cron(parts.join(" "));
1622
+ }
1623
+ sundays() {
1624
+ return this._setDow("0");
1625
+ }
1626
+ mondays() {
1627
+ return this._setDow("1");
1628
+ }
1629
+ tuesdays() {
1630
+ return this._setDow("2");
1631
+ }
1632
+ wednesdays() {
1633
+ return this._setDow("3");
1634
+ }
1635
+ thursdays() {
1636
+ return this._setDow("4");
1637
+ }
1638
+ fridays() {
1639
+ return this._setDow("5");
1640
+ }
1641
+ saturdays() {
1642
+ return this._setDow("6");
1643
+ }
1644
+ _setDow(dow) {
1645
+ const parts = this.task.expression.split(" ");
1646
+ parts[4] = dow;
1647
+ return this.cron(parts.join(" "));
1648
+ }
1649
+ /**
1650
+ * Only run the task if current time is between start and end (inclusive).
1651
+ * @param start "HH:MM"
1652
+ * @param end "HH:MM"
1653
+ */
1654
+ between(start, end) {
1655
+ this.task.betweenStart = start;
1656
+ this.task.betweenEnd = end;
1657
+ return this;
1658
+ }
1659
+ /** Only run when all provided callbacks return true. */
1660
+ when(callback) {
1661
+ this.task.conditions.push(callback);
1662
+ return this;
1663
+ }
1664
+ /** Skip the task when callback returns true. */
1665
+ skip(callback) {
1666
+ this.task.skipConditions.push(callback);
1667
+ return this;
1668
+ }
1669
+ cron(expression) {
1670
+ this.task.expression = expression;
1671
+ try {
1672
+ const interval = cron_parser.CronExpressionParser.parse(expression, { tz: this.task.timezone || "UTC" });
1673
+ this.task.nextRun = interval.next().toDate();
1674
+ } catch {}
1675
+ return this;
1676
+ }
1677
+ name(name) {
1678
+ this.task.name = name;
1679
+ return this;
1680
+ }
1681
+ description(description) {
1682
+ this.task.description = description;
1683
+ return this;
1684
+ }
1685
+ timezone(timezone) {
1686
+ this.task.timezone = timezone;
1687
+ try {
1688
+ const interval = cron_parser.CronExpressionParser.parse(this.task.expression, { tz: timezone });
1689
+ this.task.nextRun = interval.next().toDate();
1690
+ } catch {}
1691
+ return this;
1692
+ }
1693
+ withoutOverlapping() {
1694
+ this.task.withoutOverlapping = true;
1695
+ return this;
1696
+ }
1697
+ onOneServer() {
1698
+ this.task.onOneServer = true;
1699
+ return this;
1700
+ }
1701
+ evenInMaintenanceMode() {
1702
+ this.task.evenInMaintenanceMode = true;
1703
+ return this;
1704
+ }
1705
+ runInBackground() {
1706
+ this.task.runInBackground = true;
1707
+ return this;
1708
+ }
1709
+ onSuccess(callback) {
1710
+ this.task.onSuccessHook = callback;
1711
+ return this;
1712
+ }
1713
+ onFailure(callback) {
1714
+ this.task.onFailureHook = callback;
1715
+ return this;
1716
+ }
1717
+ };
1718
+ const scheduler = new Schedule();
1719
+ //#endregion
1720
+ //#region src/QueueServiceProvider.ts
1721
+ /**
1722
+ * Framework QueueServiceProvider.
1723
+ *
1724
+ * Override `schedule()` in your app's Console Kernel (or your own QueueServiceProvider)
1725
+ * to register cron tasks.
1726
+ *
1727
+ * @example
1728
+ * // app/Providers/QueueServiceProvider.ts
1729
+ * import { QueueServiceProvider as BaseProvider } from '@lara-node/queue';
1730
+ *
1731
+ * export class QueueServiceProvider extends BaseProvider {
1732
+ * protected schedule(): void {
1733
+ * this.scheduler.command('invoice:mark-overdue').dailyAt('00:00');
1734
+ * }
1735
+ * }
1736
+ */
1737
+ var QueueServiceProvider = class extends _lara_node_core.ServiceProvider {
1738
+ constructor(..._args) {
1739
+ super(..._args);
1740
+ this.scheduler = scheduler;
1741
+ }
1742
+ register() {
1743
+ this.container.singleton(QueueManager, () => Queue);
1744
+ this.container.alias(QueueManager, "queue");
1745
+ this.container.singleton(Schedule, () => scheduler);
1746
+ this.container.alias(Schedule, "schedule");
1747
+ }
1748
+ boot() {
1749
+ this.schedule();
1750
+ }
1751
+ /** Override to define scheduled tasks. */
1752
+ schedule() {}
1753
+ provides() {
1754
+ return [
1755
+ "queue",
1756
+ "schedule",
1757
+ QueueManager.name,
1758
+ Schedule.name
1759
+ ];
1760
+ }
1761
+ };
1762
+ //#endregion
1763
+ exports.DatabaseDriver = DatabaseDriver;
1764
+ exports.Job = Job;
1765
+ exports.PendingDispatch = PendingDispatch;
1766
+ exports.Queue = Queue;
1767
+ exports.QueueManager = QueueManager;
1768
+ exports.QueueServiceProvider = QueueServiceProvider;
1769
+ exports.Queueable = Queueable;
1770
+ exports.RedisDriver = RedisDriver;
1771
+ exports.Schedule = Schedule;
1772
+ exports.ScheduledTaskBuilder = ScheduledTaskBuilder;
1773
+ exports.SyncDriver = SyncDriver;
1774
+ exports.Worker = Worker;
1775
+ exports.decryptPayload = decryptPayload;
1776
+ exports.dispatch = dispatch;
1777
+ exports.encryptPayload = encryptPayload;
1778
+ exports.getJobClass = getJobClass;
1779
+ exports.getRegisteredJobs = getRegisteredJobs;
1780
+ exports.queueConfig = require_queue_config.queueConfig;
1781
+ exports.registerJob = registerJob;
1782
+ exports.scheduler = scheduler;