@nebutra/queue 0.1.0 → 0.1.1

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.
Files changed (44) hide show
  1. package/README.md +69 -2
  2. package/dist/factory.d.ts +42 -0
  3. package/dist/factory.d.ts.map +1 -0
  4. package/dist/factory.js +156 -0
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +27 -0
  8. package/dist/middleware/qstash-verify.d.ts +16 -0
  9. package/dist/middleware/qstash-verify.d.ts.map +1 -0
  10. package/dist/middleware/qstash-verify.js +87 -0
  11. package/dist/providers/bullmq.d.ts +32 -0
  12. package/dist/providers/bullmq.d.ts.map +1 -0
  13. package/dist/providers/bullmq.js +258 -0
  14. package/dist/providers/memory.d.ts +23 -0
  15. package/dist/providers/memory.d.ts.map +1 -0
  16. package/dist/providers/memory.js +220 -0
  17. package/dist/providers/qstash.d.ts +26 -0
  18. package/dist/providers/qstash.d.ts.map +1 -0
  19. package/dist/providers/qstash.js +226 -0
  20. package/dist/providers/sqs.d.ts +18 -0
  21. package/dist/providers/sqs.d.ts.map +1 -0
  22. package/dist/providers/sqs.js +197 -0
  23. package/dist/queuebase-webhook.d.ts +45 -0
  24. package/dist/queuebase-webhook.d.ts.map +1 -0
  25. package/dist/queuebase-webhook.js +113 -0
  26. package/dist/queuebase.d.ts +27 -0
  27. package/dist/queuebase.d.ts.map +1 -0
  28. package/dist/queuebase.js +48 -0
  29. package/dist/scheduled/index.d.ts +15 -0
  30. package/dist/scheduled/index.d.ts.map +1 -0
  31. package/dist/scheduled/index.js +25 -0
  32. package/dist/scheduled/jobs/invitation-cleanup.d.ts +40 -0
  33. package/dist/scheduled/jobs/invitation-cleanup.d.ts.map +1 -0
  34. package/dist/scheduled/jobs/invitation-cleanup.js +44 -0
  35. package/dist/scheduled/jobs/session-cleanup.d.ts +24 -0
  36. package/dist/scheduled/jobs/session-cleanup.d.ts.map +1 -0
  37. package/dist/scheduled/jobs/session-cleanup.js +32 -0
  38. package/dist/scheduled/scheduler.d.ts +44 -0
  39. package/dist/scheduled/scheduler.d.ts.map +1 -0
  40. package/dist/scheduled/scheduler.js +52 -0
  41. package/dist/types.d.ts +202 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +32 -0
  44. package/package.json +17 -4
@@ -0,0 +1,258 @@
1
+ import { logger } from "@nebutra/logger";
2
+ import { Queue, Worker } from "bullmq";
3
+ import IORedis from "ioredis";
4
+ export function toBullMQDeadLetterJob(queue, job) {
5
+ const maxRetries = job.opts.attempts ?? job.data.options?.maxRetries ?? 3;
6
+ if (job.attemptsMade < maxRetries)
7
+ return undefined;
8
+ return {
9
+ id: job.id ?? job.data.id,
10
+ queue,
11
+ type: job.data.type,
12
+ originalJob: job.data,
13
+ attempts: job.attemptsMade,
14
+ maxRetries,
15
+ failedReason: job.failedReason ?? "Job failed without a provider reason",
16
+ provider: "bullmq",
17
+ failedAt: new Date(job.finishedOn ?? Date.now()).toISOString(),
18
+ };
19
+ }
20
+ // BullMQ Provider — self-hosted Redis queue backend
21
+ // =============================================================================
22
+ // Uses BullMQ (the successor to Bull) for job queuing over a standard Redis
23
+ // instance. Ideal for customers who self-host Redis or need features like
24
+ // priority queues, rate limiting, and the Bull Board dashboard.
25
+ //
26
+ // Architecture:
27
+ // enqueue() → Redis LPUSH via BullMQ Queue
28
+ // registerHandler → BullMQ Worker (long-polling BRPOPLPUSH)
29
+ // getJobStatus → Redis HGET on the job hash
30
+ // =============================================================================
31
+ export class BullMQProvider {
32
+ name = "bullmq";
33
+ connection;
34
+ queues = new Map();
35
+ workers = new Map();
36
+ handlers = new Map();
37
+ prefix;
38
+ concurrency;
39
+ constructor(config) {
40
+ const redisUrl = config.redisUrl ?? process.env.REDIS_URL;
41
+ if (!redisUrl) {
42
+ throw new Error("Redis URL not configured. Set REDIS_URL env var or pass `redisUrl` in config.");
43
+ }
44
+ this.prefix = config.prefix ?? "nebutra:queue";
45
+ this.concurrency = config.concurrency ?? 5;
46
+ this.connection = new IORedis(redisUrl, {
47
+ maxRetriesPerRequest: null, // Required by BullMQ
48
+ enableReadyCheck: false,
49
+ lazyConnect: true,
50
+ });
51
+ this.connection.on("error", (err) => {
52
+ logger.error("[queue:bullmq] Redis connection error", {
53
+ error: err.message,
54
+ });
55
+ });
56
+ logger.info("[queue:bullmq] Provider initialised", {
57
+ prefix: this.prefix,
58
+ concurrency: this.concurrency,
59
+ });
60
+ }
61
+ // ── Internal helpers ────────────────────────────────────────────────────
62
+ getOrCreateQueue(queueName) {
63
+ let queue = this.queues.get(queueName);
64
+ if (!queue) {
65
+ queue = new Queue(queueName, {
66
+ connection: this.connection.duplicate(),
67
+ prefix: this.prefix,
68
+ defaultJobOptions: {
69
+ attempts: 3,
70
+ backoff: { type: "exponential", delay: 1000 },
71
+ removeOnComplete: { count: 1000 },
72
+ removeOnFail: { count: 5000 },
73
+ },
74
+ });
75
+ this.queues.set(queueName, queue);
76
+ }
77
+ return queue;
78
+ }
79
+ // ── Enqueue ─────────────────────────────────────────────────────────────
80
+ async enqueue(job) {
81
+ const queue = this.getOrCreateQueue(job.queue);
82
+ try {
83
+ const bullJob = await queue.add(job.type, job, {
84
+ jobId: job.id,
85
+ ...(job.options?.delaySec ? { delay: job.options.delaySec * 1000 } : {}),
86
+ attempts: job.options?.maxRetries ?? 3,
87
+ ...(job.options?.priority !== undefined ? { priority: job.options.priority } : {}),
88
+ ...(job.options?.cron ? { repeat: { pattern: job.options.cron } } : {}),
89
+ });
90
+ logger.info("[queue:bullmq] Job enqueued", {
91
+ jobId: bullJob.id,
92
+ queue: job.queue,
93
+ type: job.type,
94
+ });
95
+ return {
96
+ jobId: bullJob.id ?? job.id,
97
+ accepted: true,
98
+ provider: "bullmq",
99
+ };
100
+ }
101
+ catch (error) {
102
+ logger.error("[queue:bullmq] Failed to enqueue job", {
103
+ jobId: job.id,
104
+ error: error instanceof Error ? error.message : String(error),
105
+ });
106
+ throw error;
107
+ }
108
+ }
109
+ async enqueueBatch(jobs) {
110
+ // Group jobs by queue name for efficient bulk add
111
+ const grouped = new Map();
112
+ for (const job of jobs) {
113
+ const list = grouped.get(job.queue) ?? [];
114
+ list.push(job);
115
+ grouped.set(job.queue, list);
116
+ }
117
+ const results = [];
118
+ for (const [queueName, queueJobs] of grouped) {
119
+ const queue = this.getOrCreateQueue(queueName);
120
+ const bulkData = queueJobs.map((job) => ({
121
+ name: job.type,
122
+ data: job,
123
+ opts: {
124
+ jobId: job.id,
125
+ ...(job.options?.delaySec ? { delay: job.options.delaySec * 1000 } : {}),
126
+ attempts: job.options?.maxRetries ?? 3,
127
+ ...(job.options?.priority !== undefined ? { priority: job.options.priority } : {}),
128
+ },
129
+ }));
130
+ const bullJobs = await queue.addBulk(bulkData);
131
+ for (let i = 0; i < bullJobs.length; i++) {
132
+ results.push({
133
+ jobId: bullJobs[i]?.id ?? queueJobs[i]?.id ?? "",
134
+ accepted: true,
135
+ provider: "bullmq",
136
+ });
137
+ }
138
+ }
139
+ logger.info("[queue:bullmq] Batch enqueued", { count: jobs.length });
140
+ return results;
141
+ }
142
+ // ── Handler Registration ────────────────────────────────────────────────
143
+ registerHandler(queue, type, handler) {
144
+ const key = `${queue}:${type}`;
145
+ this.handlers.set(key, handler);
146
+ // Create or update the worker for this queue
147
+ this.ensureWorker(queue);
148
+ logger.info("[queue:bullmq] Handler registered", { key });
149
+ }
150
+ ensureWorker(queueName) {
151
+ if (this.workers.has(queueName))
152
+ return;
153
+ const worker = new Worker(queueName, async (bullJob) => {
154
+ const payload = bullJob.data;
155
+ const key = `${payload.queue}:${payload.type}`;
156
+ const handler = this.handlers.get(key);
157
+ if (!handler) {
158
+ logger.warn("[queue:bullmq] No handler registered for job type", {
159
+ key,
160
+ jobId: payload.id,
161
+ });
162
+ throw new Error(`No handler registered for ${key}`);
163
+ }
164
+ await handler(payload);
165
+ }, {
166
+ connection: this.connection.duplicate(),
167
+ prefix: this.prefix,
168
+ concurrency: this.concurrency,
169
+ removeOnComplete: { count: 1000 },
170
+ removeOnFail: { count: 5000 },
171
+ });
172
+ worker.on("completed", (job) => {
173
+ logger.info("[queue:bullmq] Job completed", {
174
+ jobId: job.id,
175
+ queue: queueName,
176
+ });
177
+ });
178
+ worker.on("failed", (job, err) => {
179
+ logger.error("[queue:bullmq] Job failed", {
180
+ jobId: job?.id,
181
+ queue: queueName,
182
+ error: err.message,
183
+ attempts: job?.attemptsMade,
184
+ });
185
+ });
186
+ worker.on("error", (err) => {
187
+ logger.error("[queue:bullmq] Worker error", {
188
+ queue: queueName,
189
+ error: err.message,
190
+ });
191
+ });
192
+ this.workers.set(queueName, worker);
193
+ }
194
+ // ── Job Status ──────────────────────────────────────────────────────────
195
+ async getJobStatus(jobId, queue) {
196
+ const q = this.queues.get(queue);
197
+ if (!q)
198
+ return undefined;
199
+ const bullJob = await q.getJob(jobId);
200
+ if (!bullJob)
201
+ return undefined;
202
+ const state = await bullJob.getState();
203
+ const statusMap = {
204
+ completed: "completed",
205
+ failed: "failed",
206
+ delayed: "delayed",
207
+ active: "active",
208
+ waiting: "waiting",
209
+ "waiting-children": "waiting",
210
+ prioritized: "pending",
211
+ unknown: "pending",
212
+ };
213
+ return {
214
+ id: bullJob.id ?? jobId,
215
+ queue,
216
+ status: statusMap[state] ?? "pending",
217
+ attempts: bullJob.attemptsMade,
218
+ maxRetries: bullJob.opts.attempts ?? 3,
219
+ ...(bullJob.failedReason ? { failedReason: bullJob.failedReason } : {}),
220
+ createdAt: new Date(bullJob.timestamp).toISOString(),
221
+ ...(bullJob.processedOn ? { processedAt: new Date(bullJob.processedOn).toISOString() } : {}),
222
+ ...(bullJob.finishedOn ? { completedAt: new Date(bullJob.finishedOn).toISOString() } : {}),
223
+ };
224
+ }
225
+ async getDeadLetteredJobs(queue) {
226
+ const entries = queue !== undefined ? [[queue, this.getOrCreateQueue(queue)]] : this.queues;
227
+ const deadLetters = [];
228
+ for (const [queueName, q] of entries) {
229
+ const failedJobs = await q.getFailed(0, -1);
230
+ for (const failedJob of failedJobs) {
231
+ const deadLetter = toBullMQDeadLetterJob(queueName, failedJob);
232
+ if (deadLetter)
233
+ deadLetters.push(deadLetter);
234
+ }
235
+ }
236
+ return deadLetters;
237
+ }
238
+ // ── Lifecycle ───────────────────────────────────────────────────────────
239
+ async close() {
240
+ const closeTasks = [];
241
+ for (const [name, worker] of this.workers) {
242
+ closeTasks.push(worker.close().then(() => {
243
+ logger.info("[queue:bullmq] Worker closed", { queue: name });
244
+ }));
245
+ }
246
+ for (const [name, queue] of this.queues) {
247
+ closeTasks.push(queue.close().then(() => {
248
+ logger.info("[queue:bullmq] Queue closed", { queue: name });
249
+ }));
250
+ }
251
+ await Promise.allSettled(closeTasks);
252
+ await this.connection.quit();
253
+ this.workers.clear();
254
+ this.queues.clear();
255
+ this.handlers.clear();
256
+ logger.info("[queue:bullmq] Provider closed");
257
+ }
258
+ }
@@ -0,0 +1,23 @@
1
+ import type { DeadLetterJob, JobHandler, JobLifecycleActionResult, JobLifecycleEvent, JobPayload, JobResult, JobStatusInfo, QueueProvider } from "../types";
2
+ export declare class MemoryProvider implements QueueProvider {
3
+ readonly name: "memory";
4
+ private handlers;
5
+ private jobs;
6
+ private deadLetters;
7
+ private timers;
8
+ constructor();
9
+ enqueue(job: JobPayload): Promise<JobResult>;
10
+ enqueueBatch(jobs: JobPayload[]): Promise<JobResult[]>;
11
+ registerHandler<T extends Record<string, unknown>>(queue: string, type: string, handler: JobHandler<T>): void;
12
+ getJobStatus(jobId: string, _queue?: string): Promise<JobStatusInfo | undefined>;
13
+ getDeadLetteredJobs(queue?: string): Promise<DeadLetterJob[]>;
14
+ getJobLogs(jobId: string, _queue: string): Promise<JobLifecycleEvent[]>;
15
+ cancelJob(jobId: string, queue: string, reason?: string): Promise<JobLifecycleActionResult>;
16
+ retryJob(jobId: string, queue: string, reason?: string): Promise<JobLifecycleActionResult>;
17
+ private processJob;
18
+ private deadLetter;
19
+ close(): Promise<void>;
20
+ private trackTimer;
21
+ private appendLog;
22
+ }
23
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/providers/memory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EAEV,wBAAwB,EACxB,iBAAiB,EACjB,UAAU,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACd,MAAM,UAAU,CAAC;AA4BlB,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,MAAM,CAAuC;;IAQ/C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IA6B5C,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAU5D,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GACrB,IAAI;IAQD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAoBhF,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAK7D,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAIvE,SAAS,CACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,SAAa,GAClB,OAAO,CAAC,wBAAwB,CAAC;IAyB9B,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,SAAiB,GACtB,OAAO,CAAC,wBAAwB,CAAC;YA6BtB,UAAU;IAyDxB,OAAO,CAAC,UAAU;IA0BZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,SAAS;CAiBlB"}
@@ -0,0 +1,220 @@
1
+ import { logger } from "@nebutra/logger";
2
+ export class MemoryProvider {
3
+ name = "memory";
4
+ handlers = new Map();
5
+ jobs = new Map();
6
+ deadLetters = new Map();
7
+ timers = [];
8
+ constructor() {
9
+ logger.info("[queue:memory] Provider initialised (dev/test only)");
10
+ }
11
+ // ── Enqueue ─────────────────────────────────────────────────────────────
12
+ async enqueue(job) {
13
+ const delaySec = job.options?.delaySec ?? 0;
14
+ this.jobs.set(job.id, {
15
+ payload: job,
16
+ status: delaySec > 0 ? "delayed" : "pending",
17
+ attempts: 0,
18
+ logs: [],
19
+ });
20
+ this.appendLog(job, "enqueued", {
21
+ ...(delaySec > 0 ? { metadata: { delaySec } } : {}),
22
+ });
23
+ if (delaySec > 0) {
24
+ const timer = setTimeout(() => this.processJob(job), delaySec * 1000);
25
+ this.trackTimer(job.id, timer);
26
+ }
27
+ else {
28
+ // Process on next tick to simulate async behaviour
29
+ const timer = setTimeout(() => this.processJob(job), 0);
30
+ this.trackTimer(job.id, timer);
31
+ }
32
+ return {
33
+ jobId: job.id,
34
+ accepted: true,
35
+ provider: "memory",
36
+ };
37
+ }
38
+ async enqueueBatch(jobs) {
39
+ const results = [];
40
+ for (const job of jobs) {
41
+ results.push(await this.enqueue(job));
42
+ }
43
+ return results;
44
+ }
45
+ // ── Handler Registration ────────────────────────────────────────────────
46
+ registerHandler(queue, type, handler) {
47
+ const key = `${queue}:${type}`;
48
+ this.handlers.set(key, handler);
49
+ logger.info("[queue:memory] Handler registered", { key });
50
+ }
51
+ // ── Job Status ──────────────────────────────────────────────────────────
52
+ async getJobStatus(jobId, _queue) {
53
+ const stored = this.jobs.get(jobId);
54
+ if (!stored)
55
+ return undefined;
56
+ return {
57
+ id: jobId,
58
+ queue: stored.payload.queue,
59
+ status: stored.status,
60
+ attempts: stored.attempts,
61
+ maxRetries: stored.payload.options?.maxRetries ?? 3,
62
+ ...(stored.failedReason !== undefined ? { failedReason: stored.failedReason } : {}),
63
+ ...(stored.canceledReason !== undefined ? { canceledReason: stored.canceledReason } : {}),
64
+ createdAt: stored.payload.createdAt,
65
+ ...(stored.processedAt !== undefined ? { processedAt: stored.processedAt } : {}),
66
+ ...(stored.completedAt !== undefined ? { completedAt: stored.completedAt } : {}),
67
+ ...(stored.deadLetteredAt !== undefined ? { deadLetteredAt: stored.deadLetteredAt } : {}),
68
+ ...(stored.canceledAt !== undefined ? { canceledAt: stored.canceledAt } : {}),
69
+ };
70
+ }
71
+ async getDeadLetteredJobs(queue) {
72
+ const jobs = [...this.deadLetters.values()];
73
+ return queue === undefined ? jobs : jobs.filter((job) => job.queue === queue);
74
+ }
75
+ async getJobLogs(jobId, _queue) {
76
+ return [...(this.jobs.get(jobId)?.logs ?? [])];
77
+ }
78
+ async cancelJob(jobId, queue, reason = "canceled") {
79
+ const stored = this.jobs.get(jobId);
80
+ if (!stored || stored.payload.queue !== queue) {
81
+ return { jobId, queue, accepted: false, action: "cancel", reason: "Job not found" };
82
+ }
83
+ if (!["pending", "delayed"].includes(stored.status)) {
84
+ return { jobId, queue, accepted: false, action: "cancel", reason: `Job is ${stored.status}` };
85
+ }
86
+ if (stored.timer) {
87
+ clearTimeout(stored.timer);
88
+ this.timers = this.timers.filter((timer) => timer !== stored.timer);
89
+ stored.timer = undefined;
90
+ }
91
+ const canceledAt = new Date().toISOString();
92
+ stored.status = "canceled";
93
+ stored.canceledReason = reason;
94
+ stored.canceledAt = canceledAt;
95
+ this.appendLog(stored.payload, "canceled", { reason });
96
+ return { jobId, queue, accepted: true, action: "cancel", reason };
97
+ }
98
+ async retryJob(jobId, queue, reason = "manual-retry") {
99
+ const stored = this.jobs.get(jobId);
100
+ if (!stored || stored.payload.queue !== queue) {
101
+ return { jobId, queue, accepted: false, action: "retry", reason: "Job not found" };
102
+ }
103
+ if (!["failed", "dead-lettered", "canceled"].includes(stored.status)) {
104
+ return { jobId, queue, accepted: false, action: "retry", reason: `Job is ${stored.status}` };
105
+ }
106
+ stored.status = "pending";
107
+ stored.attempts = 0;
108
+ stored.failedReason = undefined;
109
+ stored.canceledReason = undefined;
110
+ stored.processedAt = undefined;
111
+ stored.completedAt = undefined;
112
+ stored.deadLetteredAt = undefined;
113
+ stored.canceledAt = undefined;
114
+ this.deadLetters.delete(jobId);
115
+ this.appendLog(stored.payload, "retried", { reason });
116
+ const timer = setTimeout(() => this.processJob(stored.payload), 0);
117
+ this.trackTimer(jobId, timer);
118
+ return { jobId, queue, accepted: true, action: "retry", reason };
119
+ }
120
+ // ── Internal Processing ─────────────────────────────────────────────────
121
+ async processJob(job) {
122
+ const key = `${job.queue}:${job.type}`;
123
+ const handler = this.handlers.get(key);
124
+ const stored = this.jobs.get(job.id);
125
+ if (!stored)
126
+ return;
127
+ stored.timer = undefined;
128
+ if (stored.status === "canceled") {
129
+ logger.info("[queue:memory] Skipping canceled job", { jobId: job.id, key });
130
+ return;
131
+ }
132
+ if (!handler) {
133
+ logger.warn("[queue:memory] No handler for job type", { key, jobId: job.id });
134
+ stored.status = "failed";
135
+ stored.failedReason = `No handler registered for ${key}`;
136
+ this.appendLog(job, "failed", { reason: stored.failedReason });
137
+ return;
138
+ }
139
+ const maxRetries = job.options?.maxRetries ?? 3;
140
+ stored.status = "active";
141
+ stored.processedAt = new Date().toISOString();
142
+ this.appendLog(job, "started");
143
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
144
+ stored.attempts = attempt;
145
+ try {
146
+ await handler(job);
147
+ stored.status = "completed";
148
+ stored.completedAt = new Date().toISOString();
149
+ this.appendLog(job, "completed", { attempt });
150
+ logger.info("[queue:memory] Job completed", { jobId: job.id, key });
151
+ return;
152
+ }
153
+ catch (error) {
154
+ const msg = error instanceof Error ? error.message : String(error);
155
+ this.appendLog(job, "attempt-failed", { attempt, reason: msg });
156
+ logger.warn("[queue:memory] Job attempt failed", {
157
+ jobId: job.id,
158
+ attempt,
159
+ maxRetries,
160
+ error: msg,
161
+ });
162
+ if (attempt === maxRetries) {
163
+ this.deadLetter(job, stored, msg, maxRetries);
164
+ logger.error("[queue:memory] Job exhausted retries", {
165
+ jobId: job.id,
166
+ key,
167
+ });
168
+ }
169
+ }
170
+ }
171
+ }
172
+ deadLetter(job, stored, failedReason, maxRetries) {
173
+ const failedAt = new Date().toISOString();
174
+ stored.status = "dead-lettered";
175
+ stored.failedReason = failedReason;
176
+ stored.deadLetteredAt = failedAt;
177
+ this.appendLog(job, "dead-lettered", { reason: failedReason });
178
+ this.deadLetters.set(job.id, {
179
+ id: job.id,
180
+ queue: job.queue,
181
+ type: job.type,
182
+ originalJob: job,
183
+ attempts: stored.attempts,
184
+ maxRetries,
185
+ failedReason,
186
+ provider: "memory",
187
+ failedAt,
188
+ });
189
+ }
190
+ // ── Lifecycle ───────────────────────────────────────────────────────────
191
+ async close() {
192
+ for (const timer of this.timers) {
193
+ clearTimeout(timer);
194
+ }
195
+ this.timers = [];
196
+ this.handlers.clear();
197
+ this.jobs.clear();
198
+ this.deadLetters.clear();
199
+ logger.info("[queue:memory] Provider closed");
200
+ }
201
+ trackTimer(jobId, timer) {
202
+ this.timers.push(timer);
203
+ const stored = this.jobs.get(jobId);
204
+ if (stored)
205
+ stored.timer = timer;
206
+ }
207
+ appendLog(job, action, details = {}) {
208
+ const stored = this.jobs.get(job.id);
209
+ if (!stored)
210
+ return;
211
+ stored.logs.push({
212
+ id: `log_${crypto.randomUUID()}`,
213
+ jobId: job.id,
214
+ queue: job.queue,
215
+ action,
216
+ at: new Date().toISOString(),
217
+ ...details,
218
+ });
219
+ }
220
+ }
@@ -0,0 +1,26 @@
1
+ import type { DeadLetterJob, JobHandler, JobPayload, JobResult, QStashProviderConfig, QueueDeadLetterRecord, QueueProvider } from "../types";
2
+ /**
3
+ * Get a registered handler by its composite key (`queue:type`).
4
+ * Used by the QStash webhook verification middleware.
5
+ */
6
+ export declare function getQStashHandler(queue: string, type: string): JobHandler | undefined;
7
+ /**
8
+ * Get ALL registered handlers (for introspection / health checks).
9
+ */
10
+ export declare function getQStashHandlerKeys(): string[];
11
+ export declare function toQStashDeadLetterJob(record: QueueDeadLetterRecord): DeadLetterJob | undefined;
12
+ export declare class QStashProvider implements QueueProvider {
13
+ readonly name: "qstash";
14
+ private client;
15
+ private callbackBaseUrl;
16
+ private token;
17
+ private dlqEndpoint;
18
+ private dlqFetcher;
19
+ constructor(config: Omit<QStashProviderConfig, "provider">);
20
+ enqueue(job: JobPayload): Promise<JobResult>;
21
+ enqueueBatch(jobs: JobPayload[]): Promise<JobResult[]>;
22
+ registerHandler<T extends Record<string, unknown>>(queue: string, type: string, handler: JobHandler<T>): void;
23
+ getDeadLetteredJobs(queue?: string): Promise<DeadLetterJob[]>;
24
+ close(): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=qstash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qstash.d.ts","sourceRoot":"","sources":["../../src/providers/qstash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,UAAU,EACV,SAAS,EACT,oBAAoB,EAEpB,qBAAqB,EACrB,aAAa,EACd,MAAM,UAAU,CAAC;AAoBlB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAEpF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAE/C;AAiDD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,aAAa,GAAG,SAAS,CAmB9F;AAED,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,UAAU,CAAqC;gBAE3C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IAqBpD,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IA2C5C,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA4C5D,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GACrB,IAAI;IAMD,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IA2B7D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}