tina4-nodejs 3.10.75 → 3.10.83
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/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/packages/core/src/auth.ts +0 -11
- package/packages/core/src/index.ts +4 -1
- package/packages/core/src/job.ts +86 -0
- package/packages/core/src/queue.ts +60 -463
- package/packages/core/src/queueBackends/liteBackend.ts +369 -0
- package/packages/core/src/websocket.ts +81 -0
- package/packages/orm/src/baseModel.ts +84 -6
|
@@ -28,12 +28,15 @@
|
|
|
28
28
|
* const queue = new Queue();
|
|
29
29
|
* queue.push("emails", { to: "alice@test.com" });
|
|
30
30
|
*/
|
|
31
|
-
import { mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
32
|
-
import { join } from "node:path";
|
|
33
|
-
import { randomUUID } from "node:crypto";
|
|
34
31
|
import { RabbitMQBackend } from "./queueBackends/rabbitmqBackend.js";
|
|
35
32
|
import { KafkaBackend } from "./queueBackends/kafkaBackend.js";
|
|
36
33
|
import { MongoBackend } from "./queueBackends/mongoBackend.js";
|
|
34
|
+
import { LiteBackend } from "./queueBackends/liteBackend.js";
|
|
35
|
+
import { type QueueJob, type JobData, createJob } from "./job.js";
|
|
36
|
+
|
|
37
|
+
export { LiteBackend } from "./queueBackends/liteBackend.js";
|
|
38
|
+
|
|
39
|
+
export { type QueueJob } from "./job.js";
|
|
37
40
|
|
|
38
41
|
// ── Types ────────────────────────────────────────────────────
|
|
39
42
|
|
|
@@ -44,50 +47,6 @@ export interface QueueConfig {
|
|
|
44
47
|
maxRetries?: number;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export interface QueueJob {
|
|
48
|
-
id: string;
|
|
49
|
-
payload: unknown;
|
|
50
|
-
status: "pending" | "reserved" | "failed" | "dead" | "completed";
|
|
51
|
-
createdAt: string;
|
|
52
|
-
attempts: number;
|
|
53
|
-
delayUntil: string | null;
|
|
54
|
-
priority: number;
|
|
55
|
-
topic: string;
|
|
56
|
-
error?: string;
|
|
57
|
-
/** Mark this job as completed. */
|
|
58
|
-
complete(): void;
|
|
59
|
-
/** Mark this job as failed with a reason. */
|
|
60
|
-
fail(reason?: string): void;
|
|
61
|
-
/** Reject this job with a reason. Alias for fail(). */
|
|
62
|
-
reject(reason?: string): void;
|
|
63
|
-
/** Re-queue this job with incremented attempts and optional delay. */
|
|
64
|
-
retry(delaySeconds?: number): void;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Create a QueueJob with lifecycle methods bound to a Queue instance. */
|
|
68
|
-
function createJob(data: Omit<QueueJob, "complete" | "fail" | "reject" | "retry">, queue: Queue, topic: string): QueueJob {
|
|
69
|
-
const job: QueueJob = {
|
|
70
|
-
...data,
|
|
71
|
-
topic,
|
|
72
|
-
complete() {
|
|
73
|
-
job.status = "completed";
|
|
74
|
-
},
|
|
75
|
-
fail(reason = "") {
|
|
76
|
-
job.status = "failed";
|
|
77
|
-
job.error = reason;
|
|
78
|
-
job.attempts = (job.attempts || 0) + 1;
|
|
79
|
-
queue._failJob(topic, job, reason, queue.getMaxRetries());
|
|
80
|
-
},
|
|
81
|
-
reject(reason = "") {
|
|
82
|
-
job.fail(reason);
|
|
83
|
-
},
|
|
84
|
-
retry(delaySeconds?: number) {
|
|
85
|
-
queue._retryJob(topic, job, delaySeconds);
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
return job;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
50
|
export interface ProcessOptions {
|
|
92
51
|
pollInterval?: number;
|
|
93
52
|
maxJobs?: number;
|
|
@@ -108,8 +67,8 @@ export class Queue {
|
|
|
108
67
|
private basePath: string;
|
|
109
68
|
private topic: string;
|
|
110
69
|
private _maxRetries: number;
|
|
111
|
-
private seq: number = 0;
|
|
112
70
|
private externalBackend: QueueBackendInterface | null = null;
|
|
71
|
+
private liteBackend!: LiteBackend;
|
|
113
72
|
|
|
114
73
|
/**
|
|
115
74
|
* Unified Queue constructor.
|
|
@@ -137,6 +96,7 @@ export class Queue {
|
|
|
137
96
|
?? "data/queue";
|
|
138
97
|
this.topic = resolvedConfig.topic ?? "default";
|
|
139
98
|
this._maxRetries = resolvedConfig.maxRetries ?? 3;
|
|
99
|
+
this.liteBackend = new LiteBackend(this.basePath);
|
|
140
100
|
|
|
141
101
|
// Initialize external backends
|
|
142
102
|
if (this.backendName === "rabbitmq") {
|
|
@@ -148,160 +108,53 @@ export class Queue {
|
|
|
148
108
|
}
|
|
149
109
|
}
|
|
150
110
|
|
|
151
|
-
// ── Directory helpers ────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
private ensureDir(queue: string): string {
|
|
154
|
-
const dir = join(this.basePath, queue);
|
|
155
|
-
mkdirSync(dir, { recursive: true });
|
|
156
|
-
return dir;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private ensureFailedDir(queue: string): string {
|
|
160
|
-
const dir = join(this.basePath, queue, "failed");
|
|
161
|
-
mkdirSync(dir, { recursive: true });
|
|
162
|
-
return dir;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
111
|
// ── Unified API (topic-aware) ────────────────────────────────
|
|
166
112
|
|
|
167
113
|
/**
|
|
168
114
|
* Add a job to the queue. Returns job ID.
|
|
169
115
|
*
|
|
170
116
|
* Can be called as:
|
|
171
|
-
* queue.push(payload)
|
|
172
|
-
* queue.push(payload, delay)
|
|
173
|
-
* queue.push(payload, delay, priority)
|
|
174
|
-
* queue.push("queueName", payload) — legacy: explicit queue name
|
|
175
|
-
* queue.push("queueName", payload, delay) — legacy with delay
|
|
117
|
+
* queue.push(payload) — uses constructor topic
|
|
118
|
+
* queue.push(payload, delay) — uses constructor topic with delay
|
|
119
|
+
* queue.push(payload, delay, priority) — with delay and priority
|
|
176
120
|
*
|
|
177
121
|
* @param priority — Higher value = higher priority. Default 0.
|
|
178
122
|
*/
|
|
179
|
-
push(
|
|
180
|
-
let queue: string;
|
|
181
|
-
let payload: unknown;
|
|
182
|
-
let actualDelay: number | undefined;
|
|
183
|
-
let actualPriority: number;
|
|
184
|
-
|
|
185
|
-
if (typeof queueOrPayload === "string" && payloadOrDelay !== undefined && typeof payloadOrDelay !== "number") {
|
|
186
|
-
// Legacy: push("queueName", payload, delay?, priority?)
|
|
187
|
-
queue = queueOrPayload;
|
|
188
|
-
payload = payloadOrDelay;
|
|
189
|
-
actualDelay = delay;
|
|
190
|
-
actualPriority = priority ?? 0;
|
|
191
|
-
} else {
|
|
192
|
-
// Unified: push(payload) or push(payload, delay) or push(payload, delay, priority)
|
|
193
|
-
queue = this.topic;
|
|
194
|
-
payload = queueOrPayload;
|
|
195
|
-
actualDelay = typeof payloadOrDelay === "number" ? payloadOrDelay : delay;
|
|
196
|
-
actualPriority = typeof payloadOrDelay === "number" ? (delay ?? 0) : (priority ?? 0);
|
|
197
|
-
}
|
|
198
|
-
|
|
123
|
+
push(payload: unknown, delay?: number, priority: number = 0): string {
|
|
199
124
|
if (this.externalBackend) {
|
|
200
|
-
return this.externalBackend.push(
|
|
125
|
+
return this.externalBackend.push(this.topic, payload, delay);
|
|
201
126
|
}
|
|
202
|
-
|
|
203
|
-
const id = randomUUID();
|
|
204
|
-
const now = new Date().toISOString();
|
|
205
|
-
this.seq++;
|
|
206
|
-
|
|
207
|
-
const job = {
|
|
208
|
-
id,
|
|
209
|
-
payload,
|
|
210
|
-
status: "pending" as const,
|
|
211
|
-
createdAt: now,
|
|
212
|
-
attempts: 0,
|
|
213
|
-
delayUntil: actualDelay ? new Date(Date.now() + actualDelay * 1000).toISOString() : null,
|
|
214
|
-
priority: actualPriority,
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const prefix = `${Date.now()}-${String(this.seq).padStart(6, "0")}`;
|
|
218
|
-
writeFileSync(join(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
|
|
219
|
-
return id;
|
|
127
|
+
return this.liteBackend.push(this.topic, payload, delay, priority);
|
|
220
128
|
}
|
|
221
129
|
|
|
222
130
|
/**
|
|
223
|
-
* Atomically claim the next available job. Returns null if empty.
|
|
224
|
-
*
|
|
225
|
-
* Can be called as:
|
|
226
|
-
* queue.pop() — uses constructor topic
|
|
227
|
-
* queue.pop("queueName") — legacy: explicit queue name
|
|
131
|
+
* Atomically claim the next available job from this queue's topic. Returns null if empty.
|
|
228
132
|
*/
|
|
229
|
-
pop(
|
|
230
|
-
const q =
|
|
133
|
+
pop(): QueueJob | null {
|
|
134
|
+
const q = this.topic;
|
|
231
135
|
|
|
232
136
|
if (this.externalBackend) {
|
|
233
137
|
return this.externalBackend.pop(q);
|
|
234
138
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let files: string[];
|
|
238
|
-
try {
|
|
239
|
-
files = readdirSync(dir).filter(f => f.endsWith(".queue-data")).sort();
|
|
240
|
-
} catch {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const now = new Date().toISOString();
|
|
245
|
-
|
|
246
|
-
for (const file of files) {
|
|
247
|
-
const filePath = join(dir, file);
|
|
248
|
-
let job: QueueJob;
|
|
249
|
-
try {
|
|
250
|
-
job = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
251
|
-
} catch {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (job.status !== "pending") continue;
|
|
256
|
-
if (job.delayUntil && job.delayUntil > now) continue;
|
|
257
|
-
|
|
258
|
-
job.status = "reserved";
|
|
259
|
-
job.topic = q;
|
|
260
|
-
job.priority = job.priority ?? 0;
|
|
261
|
-
writeFileSync(filePath, JSON.stringify(job, null, 2));
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
unlinkSync(filePath);
|
|
265
|
-
} catch {
|
|
266
|
-
// Already consumed by another worker
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return createJob(job as any, this, q);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return null;
|
|
139
|
+
return this.liteBackend.pop(q, this);
|
|
273
140
|
}
|
|
274
141
|
|
|
275
142
|
/**
|
|
276
143
|
* Process jobs from a queue with a handler function.
|
|
277
144
|
*/
|
|
278
145
|
process(
|
|
279
|
-
|
|
280
|
-
handlerOrOptions?: ((job: QueueJob) => Promise<void> | void) | ProcessOptions,
|
|
146
|
+
handler: (job: QueueJob) => Promise<void> | void,
|
|
281
147
|
options?: ProcessOptions,
|
|
282
148
|
): void {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
let opts: ProcessOptions | undefined;
|
|
286
|
-
|
|
287
|
-
if (typeof handlerOrQueue === "string") {
|
|
288
|
-
// Legacy: process("queueName", handler, options)
|
|
289
|
-
queue = handlerOrQueue;
|
|
290
|
-
handler = handlerOrOptions as (job: QueueJob) => Promise<void> | void;
|
|
291
|
-
opts = options;
|
|
292
|
-
} else {
|
|
293
|
-
// Unified: process(handler, options?)
|
|
294
|
-
queue = this.topic;
|
|
295
|
-
handler = handlerOrQueue;
|
|
296
|
-
opts = handlerOrOptions as ProcessOptions | undefined;
|
|
297
|
-
}
|
|
149
|
+
const queue = this.topic;
|
|
150
|
+
const opts = options;
|
|
298
151
|
|
|
299
152
|
const maxJobs = opts?.maxJobs ?? Infinity;
|
|
300
153
|
const maxRetries = opts?.maxRetries ?? this._maxRetries;
|
|
301
154
|
let processed = 0;
|
|
302
155
|
|
|
303
156
|
while (processed < maxJobs) {
|
|
304
|
-
const job = this.pop(
|
|
157
|
+
const job = this.pop();
|
|
305
158
|
if (!job) break;
|
|
306
159
|
try {
|
|
307
160
|
const result = handler(job);
|
|
@@ -320,294 +173,73 @@ export class Queue {
|
|
|
320
173
|
}
|
|
321
174
|
|
|
322
175
|
/**
|
|
323
|
-
* Count jobs
|
|
324
|
-
*
|
|
325
|
-
* @param queue — Queue/topic name (defaults to constructor topic)
|
|
326
|
-
* @param status — Job status to count: "pending" (default) or "failed"
|
|
176
|
+
* Count jobs filtered by status. Defaults to "pending".
|
|
327
177
|
*/
|
|
328
|
-
size(
|
|
329
|
-
const q =
|
|
178
|
+
size(status: string = "pending"): number {
|
|
179
|
+
const q = this.topic;
|
|
330
180
|
|
|
331
181
|
if (this.externalBackend) {
|
|
332
182
|
return this.externalBackend.size(q);
|
|
333
183
|
}
|
|
334
|
-
|
|
335
|
-
if (status === "failed") {
|
|
336
|
-
const failedDir = this.ensureFailedDir(q);
|
|
337
|
-
let files: string[];
|
|
338
|
-
try {
|
|
339
|
-
files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data"));
|
|
340
|
-
} catch {
|
|
341
|
-
return 0;
|
|
342
|
-
}
|
|
343
|
-
return files.length;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const dir = this.ensureDir(q);
|
|
347
|
-
let files: string[];
|
|
348
|
-
try {
|
|
349
|
-
files = readdirSync(dir).filter(f => f.endsWith(".queue-data"));
|
|
350
|
-
} catch {
|
|
351
|
-
return 0;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
let count = 0;
|
|
355
|
-
for (const file of files) {
|
|
356
|
-
try {
|
|
357
|
-
const job = JSON.parse(readFileSync(join(dir, file), "utf-8"));
|
|
358
|
-
if (job.status === status) count++;
|
|
359
|
-
} catch {
|
|
360
|
-
// skip corrupt files
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return count;
|
|
184
|
+
return this.liteBackend.size(q, status);
|
|
364
185
|
}
|
|
365
186
|
|
|
366
187
|
/**
|
|
367
|
-
* Remove all jobs from
|
|
188
|
+
* Remove all jobs from this queue's topic.
|
|
368
189
|
*/
|
|
369
|
-
clear(
|
|
370
|
-
const q =
|
|
190
|
+
clear(): void {
|
|
191
|
+
const q = this.topic;
|
|
371
192
|
|
|
372
193
|
if (this.externalBackend) {
|
|
373
194
|
this.externalBackend.clear(q);
|
|
374
195
|
return;
|
|
375
196
|
}
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const files = readdirSync(dir).filter(f => f.endsWith(".queue-data"));
|
|
379
|
-
for (const file of files) {
|
|
380
|
-
unlinkSync(join(dir, file));
|
|
381
|
-
}
|
|
382
|
-
} catch {
|
|
383
|
-
// directory might not exist
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Also clear failed jobs
|
|
387
|
-
const failedDir = join(dir, "failed");
|
|
388
|
-
try {
|
|
389
|
-
if (existsSync(failedDir)) {
|
|
390
|
-
const files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data"));
|
|
391
|
-
for (const file of files) {
|
|
392
|
-
unlinkSync(join(failedDir, file));
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
} catch {
|
|
396
|
-
// ignore
|
|
397
|
-
}
|
|
197
|
+
this.liteBackend.clear(q);
|
|
398
198
|
}
|
|
399
199
|
|
|
400
200
|
/**
|
|
401
|
-
* Get all failed jobs for
|
|
201
|
+
* Get all failed jobs for this queue's topic.
|
|
402
202
|
*/
|
|
403
|
-
failed(
|
|
404
|
-
|
|
405
|
-
const failedDir = this.ensureFailedDir(q);
|
|
406
|
-
const results: QueueJob[] = [];
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
const files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data")).sort();
|
|
410
|
-
for (const file of files) {
|
|
411
|
-
try {
|
|
412
|
-
const job: QueueJob = JSON.parse(readFileSync(join(failedDir, file), "utf-8"));
|
|
413
|
-
results.push(job);
|
|
414
|
-
} catch {
|
|
415
|
-
// skip corrupt files
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
} catch {
|
|
419
|
-
// directory might not exist
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return results;
|
|
203
|
+
failed(): QueueJob[] {
|
|
204
|
+
return this.liteBackend.failed(this.topic);
|
|
423
205
|
}
|
|
424
206
|
|
|
425
207
|
/**
|
|
426
208
|
* Retry a failed job by moving it back to the queue.
|
|
427
209
|
*/
|
|
428
|
-
retry(jobId: string): boolean {
|
|
429
|
-
|
|
430
|
-
const queues = readdirSync(this.basePath);
|
|
431
|
-
for (const queue of queues) {
|
|
432
|
-
const failedDir = join(this.basePath, queue, "failed");
|
|
433
|
-
const filePath = join(failedDir, `${jobId}.queue-data`);
|
|
434
|
-
|
|
435
|
-
if (existsSync(filePath)) {
|
|
436
|
-
const job: QueueJob = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
437
|
-
job.status = "pending";
|
|
438
|
-
job.attempts = (job.attempts || 0) + 1;
|
|
439
|
-
job.error = undefined;
|
|
440
|
-
|
|
441
|
-
this.seq++;
|
|
442
|
-
const prefix = `${Date.now()}-${String(this.seq).padStart(6, "0")}`;
|
|
443
|
-
const queueDir = join(this.basePath, queue);
|
|
444
|
-
writeFileSync(join(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
|
|
445
|
-
unlinkSync(filePath);
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
} catch {
|
|
450
|
-
// ignore
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return false;
|
|
210
|
+
retry(jobId: string, delaySeconds?: number): boolean {
|
|
211
|
+
return this.liteBackend.retry(this.topic, jobId, delaySeconds);
|
|
454
212
|
}
|
|
455
213
|
|
|
456
214
|
/**
|
|
457
215
|
* Get dead letter jobs — failed jobs that exceeded max retries.
|
|
458
216
|
*/
|
|
459
|
-
deadLetters(
|
|
460
|
-
|
|
461
|
-
const mr = maxRetries ?? this._maxRetries;
|
|
462
|
-
const failedDir = this.ensureFailedDir(q);
|
|
463
|
-
const results: QueueJob[] = [];
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
const files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data")).sort();
|
|
467
|
-
for (const file of files) {
|
|
468
|
-
try {
|
|
469
|
-
const job: QueueJob = JSON.parse(readFileSync(join(failedDir, file), "utf-8"));
|
|
470
|
-
if ((job.attempts || 0) >= mr) {
|
|
471
|
-
job.status = "dead";
|
|
472
|
-
results.push(job);
|
|
473
|
-
}
|
|
474
|
-
} catch {
|
|
475
|
-
// skip corrupt files
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
} catch {
|
|
479
|
-
// directory might not exist
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return results;
|
|
217
|
+
deadLetters(maxRetries?: number): QueueJob[] {
|
|
218
|
+
return this.liteBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries);
|
|
483
219
|
}
|
|
484
220
|
|
|
485
221
|
/**
|
|
486
|
-
* Delete messages by status.
|
|
222
|
+
* Delete messages by status (e.g. "completed", "failed", "dead").
|
|
487
223
|
*/
|
|
488
|
-
purge(
|
|
489
|
-
|
|
490
|
-
let status: string;
|
|
491
|
-
let mr: number;
|
|
492
|
-
|
|
493
|
-
if (typeof statusOrMaxRetries === "string") {
|
|
494
|
-
// Legacy: purge("queueName", "status", maxRetries?)
|
|
495
|
-
queue = statusOrQueue;
|
|
496
|
-
status = statusOrMaxRetries;
|
|
497
|
-
mr = maxRetries ?? this._maxRetries;
|
|
498
|
-
} else {
|
|
499
|
-
// Unified: purge("status") or purge("status", maxRetries)
|
|
500
|
-
queue = this.topic;
|
|
501
|
-
status = statusOrQueue;
|
|
502
|
-
mr = typeof statusOrMaxRetries === "number" ? statusOrMaxRetries : (maxRetries ?? this._maxRetries);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
let count = 0;
|
|
506
|
-
|
|
507
|
-
if (status === "dead") {
|
|
508
|
-
const failedDir = this.ensureFailedDir(queue);
|
|
509
|
-
try {
|
|
510
|
-
const files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data"));
|
|
511
|
-
for (const file of files) {
|
|
512
|
-
try {
|
|
513
|
-
const job: QueueJob = JSON.parse(readFileSync(join(failedDir, file), "utf-8"));
|
|
514
|
-
if ((job.attempts || 0) >= mr) {
|
|
515
|
-
unlinkSync(join(failedDir, file));
|
|
516
|
-
count++;
|
|
517
|
-
}
|
|
518
|
-
} catch {
|
|
519
|
-
// skip corrupt files
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} catch {
|
|
523
|
-
// directory might not exist
|
|
524
|
-
}
|
|
525
|
-
} else if (status === "failed") {
|
|
526
|
-
const failedDir = this.ensureFailedDir(queue);
|
|
527
|
-
try {
|
|
528
|
-
const files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data"));
|
|
529
|
-
for (const file of files) {
|
|
530
|
-
try {
|
|
531
|
-
const job: QueueJob = JSON.parse(readFileSync(join(failedDir, file), "utf-8"));
|
|
532
|
-
if ((job.attempts || 0) < mr) {
|
|
533
|
-
unlinkSync(join(failedDir, file));
|
|
534
|
-
count++;
|
|
535
|
-
}
|
|
536
|
-
} catch {
|
|
537
|
-
// skip corrupt files
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
} catch {
|
|
541
|
-
// directory might not exist
|
|
542
|
-
}
|
|
543
|
-
} else {
|
|
544
|
-
const dir = this.ensureDir(queue);
|
|
545
|
-
try {
|
|
546
|
-
const files = readdirSync(dir).filter(f => f.endsWith(".queue-data"));
|
|
547
|
-
for (const file of files) {
|
|
548
|
-
try {
|
|
549
|
-
const job: QueueJob = JSON.parse(readFileSync(join(dir, file), "utf-8"));
|
|
550
|
-
if (job.status === status) {
|
|
551
|
-
unlinkSync(join(dir, file));
|
|
552
|
-
count++;
|
|
553
|
-
}
|
|
554
|
-
} catch {
|
|
555
|
-
// skip corrupt files
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
} catch {
|
|
559
|
-
// directory might not exist
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return count;
|
|
224
|
+
purge(status: string, maxRetries?: number): number {
|
|
225
|
+
return this.liteBackend.purge(this.topic, status, maxRetries ?? this._maxRetries);
|
|
564
226
|
}
|
|
565
227
|
|
|
566
228
|
/**
|
|
567
229
|
* Re-queue failed jobs that haven't exceeded max retries back to pending.
|
|
568
230
|
*/
|
|
569
|
-
retryFailed(
|
|
570
|
-
|
|
571
|
-
const mr = maxRetries ?? this._maxRetries;
|
|
572
|
-
const failedDir = this.ensureFailedDir(q);
|
|
573
|
-
const queueDir = this.ensureDir(q);
|
|
574
|
-
let count = 0;
|
|
575
|
-
|
|
576
|
-
try {
|
|
577
|
-
const files = readdirSync(failedDir).filter(f => f.endsWith(".queue-data"));
|
|
578
|
-
for (const file of files) {
|
|
579
|
-
try {
|
|
580
|
-
const filePath = join(failedDir, file);
|
|
581
|
-
const job: QueueJob = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
582
|
-
|
|
583
|
-
if ((job.attempts || 0) >= mr) {
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
job.status = "pending";
|
|
588
|
-
job.error = undefined;
|
|
589
|
-
|
|
590
|
-
this.seq++;
|
|
591
|
-
const prefix = `${Date.now()}-${String(this.seq).padStart(6, "0")}`;
|
|
592
|
-
writeFileSync(join(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
593
|
-
unlinkSync(filePath);
|
|
594
|
-
count++;
|
|
595
|
-
} catch {
|
|
596
|
-
// skip corrupt files
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
} catch {
|
|
600
|
-
// directory might not exist
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return count;
|
|
231
|
+
retryFailed(maxRetries?: number): number {
|
|
232
|
+
return this.liteBackend.retryFailed(this.topic, maxRetries ?? this._maxRetries);
|
|
604
233
|
}
|
|
605
234
|
|
|
606
235
|
/**
|
|
607
236
|
* Produce a message onto a topic. Convenience wrapper around push().
|
|
608
237
|
*/
|
|
609
|
-
produce(topic: string, payload: unknown, delay?: number): string {
|
|
610
|
-
|
|
238
|
+
produce(topic: string, payload: unknown, delay?: number, priority: number = 0): string {
|
|
239
|
+
if (this.externalBackend) {
|
|
240
|
+
return this.externalBackend.push(topic, payload, delay);
|
|
241
|
+
}
|
|
242
|
+
return this.liteBackend.push(topic, payload, delay, priority);
|
|
611
243
|
}
|
|
612
244
|
|
|
613
245
|
/**
|
|
@@ -636,17 +268,19 @@ export class Queue {
|
|
|
636
268
|
* for await (const job of queue.consume("emails")) { ... }
|
|
637
269
|
* for await (const job of queue.consume("emails", undefined, 5000)) { ... }
|
|
638
270
|
*/
|
|
639
|
-
async *consume(topic?: string, id?: string, pollInterval: number = 1000): AsyncGenerator<QueueJob> {
|
|
271
|
+
async *consume(topic?: string, id?: string, pollInterval: number = 1000, iterations: number = 0): AsyncGenerator<QueueJob> {
|
|
640
272
|
const q = topic ?? this.topic;
|
|
641
273
|
|
|
642
274
|
if (id !== undefined) {
|
|
643
|
-
const raw = this.popById(
|
|
644
|
-
if (raw) yield createJob(raw as any, this
|
|
275
|
+
const raw = this.popById(id);
|
|
276
|
+
if (raw) yield createJob(raw as any, this);
|
|
645
277
|
return;
|
|
646
278
|
}
|
|
647
279
|
|
|
648
280
|
// pollInterval=0 → single-pass drain (returns when empty)
|
|
649
281
|
// pollInterval>0 → long-running poll (sleeps when empty, never returns)
|
|
282
|
+
// iterations>0 → stop after consuming N jobs
|
|
283
|
+
let consumed = 0;
|
|
650
284
|
while (true) {
|
|
651
285
|
const raw = this.pop(q) as any;
|
|
652
286
|
if (raw === null) {
|
|
@@ -654,41 +288,17 @@ export class Queue {
|
|
|
654
288
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
655
289
|
continue;
|
|
656
290
|
}
|
|
657
|
-
yield createJob(raw, this
|
|
291
|
+
yield createJob(raw, this);
|
|
292
|
+
consumed++;
|
|
293
|
+
if (iterations > 0 && consumed >= iterations) break;
|
|
658
294
|
}
|
|
659
295
|
}
|
|
660
296
|
|
|
661
297
|
/**
|
|
662
|
-
* Pop a specific job by ID from
|
|
298
|
+
* Pop a specific job by ID from this queue's topic.
|
|
663
299
|
*/
|
|
664
|
-
popById(
|
|
665
|
-
|
|
666
|
-
const dir = this.ensureDir(q);
|
|
667
|
-
|
|
668
|
-
let files: string[];
|
|
669
|
-
try {
|
|
670
|
-
files = readdirSync(dir).filter(f => f.endsWith(".queue-data"));
|
|
671
|
-
} catch {
|
|
672
|
-
return null;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
for (const file of files) {
|
|
676
|
-
const filePath = join(dir, file);
|
|
677
|
-
let job: QueueJob;
|
|
678
|
-
try {
|
|
679
|
-
job = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
680
|
-
} catch {
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
if (job.status !== "pending") continue;
|
|
685
|
-
if (job.id === id) {
|
|
686
|
-
try { unlinkSync(filePath); } catch { /* already consumed */ }
|
|
687
|
-
return job;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return null;
|
|
300
|
+
popById(id: string): QueueJob | null {
|
|
301
|
+
return this.liteBackend.popById(this.topic, id);
|
|
692
302
|
}
|
|
693
303
|
|
|
694
304
|
/**
|
|
@@ -706,26 +316,13 @@ export class Queue {
|
|
|
706
316
|
* Move a job to the failed directory.
|
|
707
317
|
*/
|
|
708
318
|
_failJob(queue: string, job: QueueJob, error: string, maxRetries: number): void {
|
|
709
|
-
|
|
710
|
-
job.status = "failed";
|
|
711
|
-
job.attempts = (job.attempts || 0) + 1;
|
|
712
|
-
job.error = error;
|
|
713
|
-
|
|
714
|
-
writeFileSync(join(failedDir, `${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
319
|
+
this.liteBackend.failJob(queue, job, error, maxRetries);
|
|
715
320
|
}
|
|
716
321
|
|
|
717
322
|
/**
|
|
718
323
|
* Re-queue a job back to the main queue directory with incremented attempts.
|
|
719
324
|
*/
|
|
720
325
|
_retryJob(queue: string, job: QueueJob, delaySeconds?: number): void {
|
|
721
|
-
|
|
722
|
-
job.status = "pending";
|
|
723
|
-
job.attempts = (job.attempts || 0) + 1;
|
|
724
|
-
job.error = undefined;
|
|
725
|
-
job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1000).toISOString() : null;
|
|
726
|
-
|
|
727
|
-
this.seq++;
|
|
728
|
-
const prefix = `${Date.now()}-${String(this.seq).padStart(6, "0")}`;
|
|
729
|
-
writeFileSync(join(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
326
|
+
this.liteBackend.retryJob(queue, job, delaySeconds);
|
|
730
327
|
}
|
|
731
328
|
}
|