create-nexwire 4.1.2 → 4.2.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.
- package/express/gitignore-stub +31 -24
- package/express/package.json +2 -2
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
- package/express/src/database/migrations/sqlite/meta/_journal.json +12 -12
- package/express/src/framework/http/router.ts +16 -2
- package/express/src/framework/maker-cli/db/index.mjs +1 -1
- package/express/src/framework/maker-cli/deploy/core.mjs +0 -1
- package/express/src/framework/maker-cli/index.mjs +13 -3
- package/express/src/framework/maker-cli/module/core.mjs +121 -43
- package/express/src/framework/maker-cli/runtime/core.mjs +26 -7
- package/express/src/framework/maker-cli/runtime/index.mjs +120 -104
- package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
- package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
- package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
- package/express/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
- package/express/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
- package/express/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
- package/express/src/framework/maker-cli/utils/help.mjs +374 -50
- package/express/src/framework/maker-cli/utils/naming.mjs +83 -20
- package/express/src/framework/maker-cli/utils/project-commands.mjs +140 -0
- package/express/src/framework/queue/queue.ts +402 -342
- package/express/src/modules/auth/database/seeders/role.ts +18 -18
- package/express/src/modules/auth/database/seeders/user.ts +35 -35
- package/express/vitest.config.ts +38 -23
- package/hono/gitignore-stub +31 -24
- package/hono/package.json +2 -2
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
- package/hono/src/database/migrations/sqlite/meta/_journal.json +12 -12
- package/hono/src/framework/database/paginate.ts +253 -253
- package/hono/src/framework/http/router.ts +35 -14
- package/hono/src/framework/maker-cli/db/index.mjs +1 -1
- package/hono/src/framework/maker-cli/deploy/core.mjs +0 -1
- package/hono/src/framework/maker-cli/index.mjs +13 -3
- package/hono/src/framework/maker-cli/module/core.mjs +116 -38
- package/hono/src/framework/maker-cli/runtime/core.mjs +24 -6
- package/hono/src/framework/maker-cli/runtime/index.mjs +120 -104
- package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
- package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
- package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
- package/hono/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
- package/hono/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
- package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
- package/hono/src/framework/maker-cli/utils/help.mjs +374 -50
- package/hono/src/framework/maker-cli/utils/naming.mjs +83 -20
- package/hono/src/framework/maker-cli/utils/project-commands.mjs +140 -0
- package/hono/src/framework/queue/queue.ts +402 -342
- package/hono/src/modules/auth/database/seeders/role.ts +18 -18
- package/hono/src/modules/auth/database/seeders/user.ts +35 -35
- package/hono/vitest.config.ts +38 -23
- package/package.json +1 -1
- package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -73
- package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -69
|
@@ -1,342 +1,402 @@
|
|
|
1
|
-
import { type Job, type JobsOptions, Queue, QueueEvents, Worker } from "bullmq";
|
|
2
|
-
import { type DurableContext, type DurableJob, DurableWorker, RedisStateStore } from "bullmq-durable";
|
|
3
|
-
import { queueConfig } from "@/config/index.js";
|
|
4
|
-
import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
|
|
5
|
-
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
6
|
-
|
|
7
|
-
type QueueJob = {
|
|
8
|
-
name: string;
|
|
9
|
-
data: any;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type QueueHandler = (job: Job) => Promise<any>;
|
|
13
|
-
|
|
14
|
-
type DurableQueueHandler = (job: DurableJob, ctx: DurableContext) => Promise<any>;
|
|
15
|
-
|
|
16
|
-
type AnyQueueHandler = QueueHandler | DurableQueueHandler;
|
|
17
|
-
|
|
18
|
-
const queues = new Map<string, Queue>();
|
|
19
|
-
const events = new Map<string, QueueEvents>();
|
|
20
|
-
const handlers = new Map<string, QueueHandler>();
|
|
21
|
-
const durableHandlers = new Map<string, DurableQueueHandler>();
|
|
22
|
-
const workers: Worker[] = [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* When:
|
|
36
|
-
* Where:
|
|
37
|
-
* How:
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Why:
|
|
53
|
-
* When:
|
|
54
|
-
* Where:
|
|
55
|
-
* How:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*/
|
|
236
|
-
export async function
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1
|
+
import { type Job, type JobsOptions, Queue, QueueEvents, Worker } from "bullmq";
|
|
2
|
+
import { type DurableContext, type DurableJob, DurableWorker, RedisStateStore } from "bullmq-durable";
|
|
3
|
+
import { queueConfig } from "@/config/index.js";
|
|
4
|
+
import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
|
|
5
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
6
|
+
|
|
7
|
+
export type QueueJob = {
|
|
8
|
+
name: string;
|
|
9
|
+
data: any;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type QueueHandler = (job: Job) => Promise<any>;
|
|
13
|
+
|
|
14
|
+
export type DurableQueueHandler = (job: DurableJob, ctx: DurableContext) => Promise<any>;
|
|
15
|
+
|
|
16
|
+
export type AnyQueueHandler = QueueHandler | DurableQueueHandler;
|
|
17
|
+
|
|
18
|
+
const queues = new Map<string, Queue>();
|
|
19
|
+
const events = new Map<string, QueueEvents>();
|
|
20
|
+
const handlers = new Map<string, QueueHandler>();
|
|
21
|
+
const durableHandlers = new Map<string, DurableQueueHandler>();
|
|
22
|
+
const workers: Worker[] = [];
|
|
23
|
+
/**
|
|
24
|
+
* Queue names this process already has a worker for. Each BullMQ `Worker` holds
|
|
25
|
+
* its own blocking Redis connection, so starting the runtime twice for one queue
|
|
26
|
+
* would silently double that connection for no benefit. Cleared by
|
|
27
|
+
* `stopQueueRuntime` so a later boot in the same process still works.
|
|
28
|
+
*/
|
|
29
|
+
const workedQueues = new Set<string>();
|
|
30
|
+
let durableStateStore: RedisStateStore | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Why: Jobs already reported as dropped, so that one unreachable queue cannot
|
|
34
|
+
* flood the log from a request hot path.
|
|
35
|
+
* When: A call to queueJob finds no Redis.
|
|
36
|
+
* Where: Queue facade module scope.
|
|
37
|
+
* How: Keyed by queue + job name, because that pair is what identifies a queued
|
|
38
|
+
* job - two queues running the same job name are two separate failures and
|
|
39
|
+
* must each be reported once.
|
|
40
|
+
*/
|
|
41
|
+
const droppedJobs = new Set<string>();
|
|
42
|
+
|
|
43
|
+
function queuePrefix() {
|
|
44
|
+
return queueConfig.prefix;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function key(queue: string, job: string) {
|
|
48
|
+
return `${queue}:${job}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Why: Registers a job handler for a queue + job name.
|
|
53
|
+
* When: Module job files are loaded during boot.
|
|
54
|
+
* Where: Module job definition files under `src/modules/<module>/jobs`.
|
|
55
|
+
* How: Stores handler in an in-memory lookup key. Pass `{ durable: true }` to
|
|
56
|
+
* register a checkpointed handler `(job, ctx)` powered by bullmq-durable;
|
|
57
|
+
* such a handler can split its work into `ctx.step(...)` checkpoints that
|
|
58
|
+
* survive crashes/restarts instead of re-running from scratch.
|
|
59
|
+
*/
|
|
60
|
+
export function shouldQueue(job: string, queue: string, handler: AnyQueueHandler, options: { durable?: boolean } = {}) {
|
|
61
|
+
const target = key(queue || "default", job);
|
|
62
|
+
if (options.durable) {
|
|
63
|
+
durableHandlers.set(target, handler as DurableQueueHandler);
|
|
64
|
+
} else {
|
|
65
|
+
handlers.set(target, handler as QueueHandler);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Why: Gets or lazily creates a BullMQ queue instance.
|
|
71
|
+
* When: Enqueueing jobs or ensuring queue resources.
|
|
72
|
+
* Where: Queue facade and dispatcher.
|
|
73
|
+
* How: Reuses shared Redis connection and memoized queue map.
|
|
74
|
+
*/
|
|
75
|
+
export function getQueue(queue = "default") {
|
|
76
|
+
const client = redisClientIfReady();
|
|
77
|
+
if (!client) return null;
|
|
78
|
+
|
|
79
|
+
if (!queues.has(queue)) {
|
|
80
|
+
queues.set(
|
|
81
|
+
queue,
|
|
82
|
+
new Queue(queue, {
|
|
83
|
+
connection: client as any,
|
|
84
|
+
prefix: queuePrefix(),
|
|
85
|
+
defaultJobOptions: {
|
|
86
|
+
attempts: 3,
|
|
87
|
+
backoff: { type: "exponential", delay: 3000 },
|
|
88
|
+
removeOnComplete: 1000,
|
|
89
|
+
removeOnFail: 5000
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return queues.get(queue)!;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Why: Provides the shared Redis-backed state store for durable queues.
|
|
100
|
+
* When: Durable queue/worker construction.
|
|
101
|
+
* Where: Queue facade internals.
|
|
102
|
+
* How: Creates one `RedisStateStore` reusing the shared Redis client so durable
|
|
103
|
+
* state lives in Redis. Note the store opens its own client on first use -
|
|
104
|
+
* bullmq-durable does this so that merely constructing a store costs no
|
|
105
|
+
* connection - which is why `stopQueueRuntime` must call `close()` on it.
|
|
106
|
+
*/
|
|
107
|
+
function durableStateStoreIfReady() {
|
|
108
|
+
const client = redisClientIfReady();
|
|
109
|
+
if (!client) return null;
|
|
110
|
+
|
|
111
|
+
if (!durableStateStore) {
|
|
112
|
+
durableStateStore = new RedisStateStore({ connection: client as never, prefix: queueConfig.durablePrefix });
|
|
113
|
+
}
|
|
114
|
+
return durableStateStore;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Why: Facade-friendly alias for getQueue.
|
|
119
|
+
* When: Consumers want fluent queue access.
|
|
120
|
+
* Where: Framework facade/API usage.
|
|
121
|
+
* How: Delegates directly to getQueue.
|
|
122
|
+
*/
|
|
123
|
+
export function queue(queueName = "default") {
|
|
124
|
+
return getQueue(queueName);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Why: Ensures configured queues exist before UI/runtime usage.
|
|
129
|
+
* When: Queue dashboard setup or startup warmup.
|
|
130
|
+
* Where: Queue UI/bootstrap code.
|
|
131
|
+
* How: Iterates names and resolves queue instances.
|
|
132
|
+
*/
|
|
133
|
+
export function ensureQueues(queueNames: string[]) {
|
|
134
|
+
for (const queueName of queueNames) {
|
|
135
|
+
getQueue(queueName);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Why: Returns all created queue instances.
|
|
141
|
+
* When: Queue introspection and dashboard binding.
|
|
142
|
+
* Where: Queue dashboard setup code.
|
|
143
|
+
* How: Converts internal queue map to array.
|
|
144
|
+
*/
|
|
145
|
+
export function getAllQueues() {
|
|
146
|
+
return Array.from(queues.values());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Why: Explains that a job was lost instead of letting it vanish.
|
|
151
|
+
* When: queueJob is called and no Redis connection is ready.
|
|
152
|
+
* Where: Called from queueJob.
|
|
153
|
+
* How: Warns once per queue + job. The message names the job and its queue,
|
|
154
|
+
* says plainly that it never ran, and gives both the Redis fix and the
|
|
155
|
+
* in-process alternative, so a caller reading only this line knows what
|
|
156
|
+
* to do without opening framework source.
|
|
157
|
+
*/
|
|
158
|
+
function warnDroppedJob(queueName: string, job: string, delay?: number) {
|
|
159
|
+
const id = key(queueName, job);
|
|
160
|
+
if (droppedJobs.has(id)) return;
|
|
161
|
+
droppedJobs.add(id);
|
|
162
|
+
|
|
163
|
+
const schedule = delay ? ` It was due in ${delay}s, so it was never even scheduled.` : "";
|
|
164
|
+
console.warn(
|
|
165
|
+
`[queue] Dropped job "${job}" on queue "${queueName}". Redis is not available, so this job was never queued and never ran.${schedule} ` +
|
|
166
|
+
`Enable REDIS in .env and run a queue worker, or call dispatchCommand("${job}", payload) without { async: true } to run it in-process now.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Why: Adds a job to BullMQ with project defaults.
|
|
172
|
+
* When: Commands/events need background execution.
|
|
173
|
+
* Where: Dispatcher and feature modules.
|
|
174
|
+
* How: Resolves queue then calls add with retry/backoff options. When Redis is
|
|
175
|
+
* absent there is nowhere to put the job, so it returns null and says so -
|
|
176
|
+
* previously it returned null with no output at all, which meant silently
|
|
177
|
+
* losing the work.
|
|
178
|
+
*/
|
|
179
|
+
export async function queueJob(
|
|
180
|
+
job: string,
|
|
181
|
+
data: any,
|
|
182
|
+
options: {
|
|
183
|
+
queue?: string;
|
|
184
|
+
delay?: number;
|
|
185
|
+
attempts?: number;
|
|
186
|
+
jobId?: string;
|
|
187
|
+
priority?: number;
|
|
188
|
+
removeOnComplete?: JobsOptions["removeOnComplete"];
|
|
189
|
+
removeOnFail?: JobsOptions["removeOnFail"];
|
|
190
|
+
backoff?: JobsOptions["backoff"];
|
|
191
|
+
} = {}
|
|
192
|
+
) {
|
|
193
|
+
const queueName = options.queue || "default";
|
|
194
|
+
const queue = getQueue(queueName);
|
|
195
|
+
if (!queue) {
|
|
196
|
+
warnDroppedJob(queueName, job, options.delay);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return await queue.add(job, data, {
|
|
201
|
+
delay: options.delay ? options.delay * 1000 : 0,
|
|
202
|
+
attempts: options.attempts ?? 3,
|
|
203
|
+
jobId: options.jobId,
|
|
204
|
+
priority: options.priority,
|
|
205
|
+
removeOnComplete: options.removeOnComplete ?? 1000,
|
|
206
|
+
removeOnFail: options.removeOnFail ?? 5000,
|
|
207
|
+
backoff: options.backoff ?? { type: "exponential", delay: 3000 }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Why: Loads module job files so handlers are registered.
|
|
213
|
+
* When: Worker start and explicit queue boot flows.
|
|
214
|
+
* Where: Worker/runtime bootstrap.
|
|
215
|
+
* How: Discovers jobs glob and imports each file.
|
|
216
|
+
*/
|
|
217
|
+
export async function bootQueueJobs() {
|
|
218
|
+
const files = await discoverModuleFiles("**/jobs/*.{ts,js}");
|
|
219
|
+
for (const file of files) await importFile(file);
|
|
220
|
+
return files.length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Why: Deletes phantom queues whose names are neither configured nor worked by
|
|
225
|
+
* this process.
|
|
226
|
+
* When: Queue worker boot, after workers start.
|
|
227
|
+
* Where: Worker runtime bootstrap.
|
|
228
|
+
* How: Scans Redis for `{prefix}*:meta` keys, and for each queue name that is
|
|
229
|
+
* not in queueConfig.queues nor in the queues this worker was launched
|
|
230
|
+
* with (--queue), obliterates it only when it holds no live jobs
|
|
231
|
+
* (waiting/active/delayed/paused/prioritized/waiting-children). Historical
|
|
232
|
+
* completed/failed entries are ignored so leftover history cannot keep a
|
|
233
|
+
* dead queue alive. Recreates nothing permanent: BullMQ re-creates a
|
|
234
|
+
* queue's keys on demand if any code path still dispatches to it.
|
|
235
|
+
*/
|
|
236
|
+
export async function pruneStaleQueues(protectedQueues: string[] = []) {
|
|
237
|
+
if (!queueConfig.autoPruneQueues) return;
|
|
238
|
+
|
|
239
|
+
const client = redisClientIfReady();
|
|
240
|
+
if (!client) return;
|
|
241
|
+
|
|
242
|
+
const configured = new Set([...queueConfig.queues, ...protectedQueues]);
|
|
243
|
+
const prefix = `${queuePrefix()}:`;
|
|
244
|
+
const candidates = new Set<string>();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
let cursor = "0";
|
|
248
|
+
do {
|
|
249
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", `${prefix}*:meta`, "COUNT", 100);
|
|
250
|
+
cursor = nextCursor;
|
|
251
|
+
for (const key of keys) {
|
|
252
|
+
const name = key.slice(prefix.length, -5);
|
|
253
|
+
if (name && !configured.has(name)) candidates.add(name);
|
|
254
|
+
}
|
|
255
|
+
} while (cursor !== "0");
|
|
256
|
+
|
|
257
|
+
for (const name of candidates) {
|
|
258
|
+
try {
|
|
259
|
+
const queue = getQueue(name);
|
|
260
|
+
if (!queue) continue;
|
|
261
|
+
|
|
262
|
+
const counts = await queue.getJobCounts("waiting", "active", "delayed", "paused", "prioritized", "waiting-children");
|
|
263
|
+
const live = Object.values(counts).reduce((sum, n) => sum + (Number(n) || 0), 0);
|
|
264
|
+
if (live !== 0) continue;
|
|
265
|
+
|
|
266
|
+
await queue.obliterate({ force: true });
|
|
267
|
+
console.log(`Pruned stale queue "${name}"`);
|
|
268
|
+
} catch (error: any) {
|
|
269
|
+
console.warn(`Could not prune stale queue "${name}": ${error?.message || error}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (error: any) {
|
|
273
|
+
console.warn(`Could not scan queues for stale cleanup: ${error?.message || error}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Why: Starts BullMQ workers for selected queues.
|
|
279
|
+
* When: Queue worker process boots.
|
|
280
|
+
* Where: Worker runtime entrypoint.
|
|
281
|
+
* How: Boots handlers, creates QueueEvents, and creates workers.
|
|
282
|
+
*/
|
|
283
|
+
export async function startQueueWorker(queueNames = ["default"]) {
|
|
284
|
+
await bootQueueJobs();
|
|
285
|
+
const client = redisClientIfReady();
|
|
286
|
+
if (!client) throw new Error("Redis is required for queue workers");
|
|
287
|
+
|
|
288
|
+
for (const queueName of queueNames) {
|
|
289
|
+
// Already working this queue in this process: a second worker would add a
|
|
290
|
+
// second blocking connection and a second copy of every log line, and would
|
|
291
|
+
// never process a job the first one was not already getting.
|
|
292
|
+
if (workedQueues.has(queueName)) continue;
|
|
293
|
+
workedQueues.add(queueName);
|
|
294
|
+
|
|
295
|
+
if (!events.has(queueName)) {
|
|
296
|
+
events.set(queueName, new QueueEvents(queueName, { connection: client as any, prefix: queuePrefix() }));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const durable = [...durableHandlers.keys()].some((k) => k.startsWith(`${queueName}:`));
|
|
300
|
+
|
|
301
|
+
if (durable) {
|
|
302
|
+
const store = durableStateStoreIfReady();
|
|
303
|
+
if (!store) throw new Error("Redis is required for durable queue workers");
|
|
304
|
+
|
|
305
|
+
const map: Record<string, AnyQueueHandler> = {};
|
|
306
|
+
for (const [k, h] of handlers) {
|
|
307
|
+
if (k.startsWith(`${queueName}:`)) map[k.slice(queueName.length + 1)] = h;
|
|
308
|
+
}
|
|
309
|
+
for (const [k, h] of durableHandlers) {
|
|
310
|
+
if (k.startsWith(`${queueName}:`)) map[k.slice(queueName.length + 1)] = h;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const worker = new DurableWorker(queueName, map as never, {
|
|
314
|
+
connection: client as any,
|
|
315
|
+
prefix: queuePrefix(),
|
|
316
|
+
stateStore: store,
|
|
317
|
+
concurrency: 10
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
worker.on("completed", (job) => {
|
|
321
|
+
console.log(`[${new Date().toLocaleTimeString()}] Processed: ${job.name} (${queueName})`);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
worker.on("failed", (job, error) => {
|
|
325
|
+
const jobName = job?.name ?? "unknown";
|
|
326
|
+
console.log(`[${new Date().toLocaleTimeString()}] Failed: ${jobName} (${queueName}) - ${error.message}`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
workers.push(worker as unknown as Worker);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const worker = new Worker(
|
|
334
|
+
queueName,
|
|
335
|
+
async (job) => {
|
|
336
|
+
console.log(`[${new Date().toLocaleTimeString()}] Processing: ${job.name} (${queueName})`);
|
|
337
|
+
const handler = handlers.get(key(queueName, job.name));
|
|
338
|
+
if (!handler) throw new Error(`No handler registered for ${queueName}:${job.name}`);
|
|
339
|
+
return await handler(job);
|
|
340
|
+
},
|
|
341
|
+
{ connection: client as any, prefix: queuePrefix(), concurrency: 10 }
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
worker.on("completed", (job) => {
|
|
345
|
+
console.log(`[${new Date().toLocaleTimeString()}] Processed: ${job.name} (${queueName})`);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
worker.on("failed", (job, error) => {
|
|
349
|
+
const jobName = job?.name ?? "unknown";
|
|
350
|
+
console.log(`[${new Date().toLocaleTimeString()}] Failed: ${jobName} (${queueName}) - ${error.message}`);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
workers.push(worker);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Why: Hard-clears ALL queue-system state for reset workflows.
|
|
359
|
+
* When: Local cleanup and maintenance commands.
|
|
360
|
+
* Where: `queue:clear` command.
|
|
361
|
+
* How: Wipes every BullMQ queue key (including jobs, job schedulers, and
|
|
362
|
+
* repeat keys for queues not listed in `queues`, e.g. `demo`), plus the
|
|
363
|
+
* durable key space, by scanning each prefix and deleting in batches.
|
|
364
|
+
* Cache/session/rate-limit keys are intentionally left alone.
|
|
365
|
+
*/
|
|
366
|
+
export async function clearQueue() {
|
|
367
|
+
const client = redisClientIfReady();
|
|
368
|
+
if (!client) return;
|
|
369
|
+
|
|
370
|
+
for (const prefix of [queuePrefix(), queueConfig.durablePrefix]) {
|
|
371
|
+
const stream = client.scanStream({ match: `${prefix}*`, count: 100 });
|
|
372
|
+
for await (const keys of stream) {
|
|
373
|
+
if (keys.length) await client.del(keys);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Why: Gracefully stops all workers/events/queues.
|
|
380
|
+
* When: Process shutdown.
|
|
381
|
+
* Where: Server/worker lifecycle hooks.
|
|
382
|
+
* How: Closes resources with Promise.allSettled and clears registries. The
|
|
383
|
+
* durable state store is closed too: it owns a Redis client of its own, and
|
|
384
|
+
* dropping the reference without closing it would leak that connection on
|
|
385
|
+
* every restart until Redis runs out of clients.
|
|
386
|
+
*/
|
|
387
|
+
export async function stopQueueRuntime() {
|
|
388
|
+
await Promise.allSettled(workers.map((worker) => worker.close()));
|
|
389
|
+
workers.length = 0;
|
|
390
|
+
workedQueues.clear();
|
|
391
|
+
|
|
392
|
+
await Promise.allSettled(Array.from(events.values()).map((queueEvents) => queueEvents.close()));
|
|
393
|
+
events.clear();
|
|
394
|
+
|
|
395
|
+
await Promise.allSettled(Array.from(queues.values()).map((queue) => queue.close()));
|
|
396
|
+
queues.clear();
|
|
397
|
+
|
|
398
|
+
if (durableStateStore) {
|
|
399
|
+
await Promise.allSettled([durableStateStore.close()]);
|
|
400
|
+
durableStateStore = null;
|
|
401
|
+
}
|
|
402
|
+
}
|