@holo-js/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.d.ts +560 -0
- package/dist/index.mjs +1638 -0
- package/package.json +34 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1638 @@
|
|
|
1
|
+
// src/contracts.ts
|
|
2
|
+
function normalizeOptionalString(value, label) {
|
|
3
|
+
if (typeof value === "undefined") {
|
|
4
|
+
return void 0;
|
|
5
|
+
}
|
|
6
|
+
const normalized = value.trim();
|
|
7
|
+
if (!normalized) {
|
|
8
|
+
throw new Error(`[Holo Queue] ${label} must be a non-empty string when provided.`);
|
|
9
|
+
}
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
function normalizeOptionalInteger(value, label, options = {}) {
|
|
13
|
+
if (typeof value === "undefined") {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isInteger(value)) {
|
|
17
|
+
throw new Error(`[Holo Queue] ${label} must be an integer when provided.`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof options.minimum === "number" && value < options.minimum) {
|
|
20
|
+
throw new Error(`[Holo Queue] ${label} must be greater than or equal to ${options.minimum}.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function normalizeBackoff(value) {
|
|
25
|
+
if (typeof value === "undefined") {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "number") {
|
|
29
|
+
return normalizeOptionalInteger(value, "Job backoff", { minimum: 0 });
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
throw new Error("[Holo Queue] Job backoff must be a number or an array of integers.");
|
|
33
|
+
}
|
|
34
|
+
const normalized = value.map((entry, index) => {
|
|
35
|
+
if (!Number.isInteger(entry)) {
|
|
36
|
+
throw new Error(`[Holo Queue] Job backoff entry at index ${index} must be an integer.`);
|
|
37
|
+
}
|
|
38
|
+
if (entry < 0) {
|
|
39
|
+
throw new Error(`[Holo Queue] Job backoff entry at index ${index} must be greater than or equal to 0.`);
|
|
40
|
+
}
|
|
41
|
+
return entry;
|
|
42
|
+
});
|
|
43
|
+
return Object.freeze(normalized);
|
|
44
|
+
}
|
|
45
|
+
function normalizeOptionalHook(value, label) {
|
|
46
|
+
if (typeof value === "undefined") {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== "function") {
|
|
50
|
+
throw new Error(`[Holo Queue] ${label} must be a function when provided.`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function isQueueJobDefinition(value) {
|
|
55
|
+
return value !== null && typeof value === "object" && "handle" in value && typeof value.handle === "function";
|
|
56
|
+
}
|
|
57
|
+
function normalizeQueueJobDefinition(job) {
|
|
58
|
+
if (!isQueueJobDefinition(job)) {
|
|
59
|
+
throw new Error('[Holo Queue] Jobs must define a "handle" function.');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...job,
|
|
63
|
+
...typeof job.connection === "undefined" ? {} : { connection: normalizeOptionalString(job.connection, "Job connection") },
|
|
64
|
+
...typeof job.queue === "undefined" ? {} : { queue: normalizeOptionalString(job.queue, "Job queue") },
|
|
65
|
+
...typeof job.tries === "undefined" ? {} : { tries: normalizeOptionalInteger(job.tries, "Job tries", { minimum: 1 }) },
|
|
66
|
+
...typeof job.timeout === "undefined" ? {} : { timeout: normalizeOptionalInteger(job.timeout, "Job timeout", { minimum: 0 }) },
|
|
67
|
+
...typeof job.backoff === "undefined" ? {} : { backoff: normalizeBackoff(job.backoff) },
|
|
68
|
+
...typeof job.onCompleted === "undefined" ? {} : { onCompleted: normalizeOptionalHook(job.onCompleted, "Job onCompleted hook") },
|
|
69
|
+
...typeof job.onFailed === "undefined" ? {} : { onFailed: normalizeOptionalHook(job.onFailed, "Job onFailed hook") }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function defineJob(job) {
|
|
73
|
+
return Object.freeze(normalizeQueueJobDefinition(job));
|
|
74
|
+
}
|
|
75
|
+
var queueJobInternals = {
|
|
76
|
+
normalizeQueueJobDefinition,
|
|
77
|
+
normalizeBackoff,
|
|
78
|
+
normalizeOptionalHook,
|
|
79
|
+
normalizeOptionalInteger,
|
|
80
|
+
normalizeOptionalString
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/config.ts
|
|
84
|
+
var DEFAULT_QUEUE_CONNECTION = "sync";
|
|
85
|
+
var DEFAULT_QUEUE_NAME = "default";
|
|
86
|
+
var DEFAULT_QUEUE_RETRY_AFTER = 90;
|
|
87
|
+
var DEFAULT_QUEUE_BLOCK_FOR = 5;
|
|
88
|
+
var DEFAULT_QUEUE_SLEEP = 1;
|
|
89
|
+
var DEFAULT_FAILED_JOBS_CONNECTION = "default";
|
|
90
|
+
var DEFAULT_FAILED_JOBS_TABLE = "failed_jobs";
|
|
91
|
+
var DEFAULT_DATABASE_QUEUE_TABLE = "jobs";
|
|
92
|
+
var DEFAULT_QUEUE_CONFIG = Object.freeze({
|
|
93
|
+
default: DEFAULT_QUEUE_CONNECTION,
|
|
94
|
+
failed: Object.freeze({
|
|
95
|
+
driver: "database",
|
|
96
|
+
connection: DEFAULT_FAILED_JOBS_CONNECTION,
|
|
97
|
+
table: DEFAULT_FAILED_JOBS_TABLE
|
|
98
|
+
}),
|
|
99
|
+
connections: Object.freeze({
|
|
100
|
+
[DEFAULT_QUEUE_CONNECTION]: Object.freeze({
|
|
101
|
+
name: DEFAULT_QUEUE_CONNECTION,
|
|
102
|
+
driver: "sync",
|
|
103
|
+
queue: DEFAULT_QUEUE_NAME
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
function parseInteger(value, fallback, label, options = {}) {
|
|
108
|
+
if (typeof value === "undefined") {
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
const normalized = typeof value === "number" ? value : Number.parseInt(value, 10);
|
|
112
|
+
if (!Number.isInteger(normalized)) {
|
|
113
|
+
throw new Error(`[Holo Queue] ${label} must be an integer.`);
|
|
114
|
+
}
|
|
115
|
+
if (typeof options.minimum === "number" && normalized < options.minimum) {
|
|
116
|
+
throw new Error(`[Holo Queue] ${label} must be greater than or equal to ${options.minimum}.`);
|
|
117
|
+
}
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
function normalizeConnectionName(value, label) {
|
|
121
|
+
const normalized = value?.trim();
|
|
122
|
+
if (!normalized) {
|
|
123
|
+
throw new Error(`[Holo Queue] ${label} must be a non-empty string.`);
|
|
124
|
+
}
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
function normalizeQueueName(value) {
|
|
128
|
+
return value?.trim() || DEFAULT_QUEUE_NAME;
|
|
129
|
+
}
|
|
130
|
+
function normalizeSyncConnection(name, config) {
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
name,
|
|
133
|
+
driver: "sync",
|
|
134
|
+
queue: normalizeQueueName(config.queue)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function normalizeRedisConnection(name, config) {
|
|
138
|
+
const redis = config.redis ?? {};
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
name,
|
|
141
|
+
driver: "redis",
|
|
142
|
+
queue: normalizeQueueName(config.queue),
|
|
143
|
+
retryAfter: parseInteger(config.retryAfter, DEFAULT_QUEUE_RETRY_AFTER, `queue connection "${name}" retryAfter`, {
|
|
144
|
+
minimum: 0
|
|
145
|
+
}),
|
|
146
|
+
blockFor: parseInteger(config.blockFor, DEFAULT_QUEUE_BLOCK_FOR, `queue connection "${name}" blockFor`, {
|
|
147
|
+
minimum: 0
|
|
148
|
+
}),
|
|
149
|
+
redis: Object.freeze({
|
|
150
|
+
host: redis.host?.trim() || "127.0.0.1",
|
|
151
|
+
port: parseInteger(redis.port, 6379, `queue connection "${name}" redis.port`, {
|
|
152
|
+
minimum: 1
|
|
153
|
+
}),
|
|
154
|
+
password: redis.password?.trim() || void 0,
|
|
155
|
+
username: redis.username?.trim() || void 0,
|
|
156
|
+
db: parseInteger(redis.db, 0, `queue connection "${name}" redis.db`, {
|
|
157
|
+
minimum: 0
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function normalizeDatabaseConnection(name, config) {
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
name,
|
|
165
|
+
driver: "database",
|
|
166
|
+
queue: normalizeQueueName(config.queue),
|
|
167
|
+
retryAfter: parseInteger(config.retryAfter, DEFAULT_QUEUE_RETRY_AFTER, `queue connection "${name}" retryAfter`, {
|
|
168
|
+
minimum: 0
|
|
169
|
+
}),
|
|
170
|
+
sleep: parseInteger(config.sleep, DEFAULT_QUEUE_SLEEP, `queue connection "${name}" sleep`, {
|
|
171
|
+
minimum: 0
|
|
172
|
+
}),
|
|
173
|
+
connection: config.connection?.trim() || DEFAULT_FAILED_JOBS_CONNECTION,
|
|
174
|
+
table: config.table?.trim() || DEFAULT_DATABASE_QUEUE_TABLE
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function normalizeConnectionConfig(name, config) {
|
|
178
|
+
switch (config.driver) {
|
|
179
|
+
case "sync":
|
|
180
|
+
return normalizeSyncConnection(name, config);
|
|
181
|
+
case "redis":
|
|
182
|
+
return normalizeRedisConnection(name, config);
|
|
183
|
+
case "database":
|
|
184
|
+
return normalizeDatabaseConnection(name, config);
|
|
185
|
+
default:
|
|
186
|
+
throw new Error(`[Holo Queue] Unsupported queue driver "${String(config.driver)}" on connection "${name}".`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function normalizeConnections(connections) {
|
|
190
|
+
if (!connections || Object.keys(connections).length === 0) {
|
|
191
|
+
return DEFAULT_QUEUE_CONFIG.connections;
|
|
192
|
+
}
|
|
193
|
+
const normalizedEntries = Object.entries(connections).map(([name, config]) => {
|
|
194
|
+
const normalizedName = normalizeConnectionName(name, "Queue connection name");
|
|
195
|
+
return [normalizedName, normalizeConnectionConfig(normalizedName, config)];
|
|
196
|
+
});
|
|
197
|
+
return Object.freeze(Object.fromEntries(normalizedEntries));
|
|
198
|
+
}
|
|
199
|
+
function normalizeFailedStore(config) {
|
|
200
|
+
if (config === false) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const normalized = config ?? DEFAULT_QUEUE_CONFIG.failed;
|
|
204
|
+
if (normalized.driver && normalized.driver !== "database") {
|
|
205
|
+
throw new Error(`[Holo Queue] Unsupported failed job store driver "${normalized.driver}".`);
|
|
206
|
+
}
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
driver: "database",
|
|
209
|
+
connection: normalized.connection?.trim() || DEFAULT_FAILED_JOBS_CONNECTION,
|
|
210
|
+
table: normalized.table?.trim() || DEFAULT_FAILED_JOBS_TABLE
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function normalizeQueueConfig(config = {}) {
|
|
214
|
+
const connections = normalizeConnections(config.connections);
|
|
215
|
+
const connectionNames = Object.keys(connections);
|
|
216
|
+
const defaultConnection = config.default?.trim() || connectionNames[0];
|
|
217
|
+
if (!connections[defaultConnection]) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`[Holo Queue] default queue connection "${defaultConnection}" is not configured. Available connections: ${connectionNames.join(", ")}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
default: defaultConnection,
|
|
224
|
+
failed: normalizeFailedStore(config.failed),
|
|
225
|
+
connections
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
var holoQueueDefaults = DEFAULT_QUEUE_CONFIG;
|
|
229
|
+
var queueInternals = {
|
|
230
|
+
parseInteger
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/registry.ts
|
|
234
|
+
function toPosixPath(value) {
|
|
235
|
+
return value.replaceAll("\\", "/");
|
|
236
|
+
}
|
|
237
|
+
function deriveJobNameFromSourcePath(sourcePath) {
|
|
238
|
+
const normalized = toPosixPath(sourcePath).replace(/\.[^.]+$/, "");
|
|
239
|
+
const jobRootIndex = normalized.lastIndexOf("/jobs/");
|
|
240
|
+
const relevant = jobRootIndex >= 0 ? normalized.slice(jobRootIndex + "/jobs/".length) : normalized;
|
|
241
|
+
return relevant.split("/").filter(Boolean).join(".");
|
|
242
|
+
}
|
|
243
|
+
function getQueueRegistryState() {
|
|
244
|
+
const runtime = globalThis;
|
|
245
|
+
runtime.__holoQueueRegistry__ ??= {
|
|
246
|
+
jobs: /* @__PURE__ */ new Map()
|
|
247
|
+
};
|
|
248
|
+
return runtime.__holoQueueRegistry__;
|
|
249
|
+
}
|
|
250
|
+
function resolveRegistrationName(definition, options = {}) {
|
|
251
|
+
const explicit = options.name?.trim();
|
|
252
|
+
if (explicit) {
|
|
253
|
+
return explicit;
|
|
254
|
+
}
|
|
255
|
+
const fromSource = options.sourcePath?.trim();
|
|
256
|
+
if (fromSource) {
|
|
257
|
+
return deriveJobNameFromSourcePath(fromSource);
|
|
258
|
+
}
|
|
259
|
+
throw new Error("[Holo Queue] Registered jobs require an explicit name or a sourcePath-derived name.");
|
|
260
|
+
}
|
|
261
|
+
function registerQueueJob(definition, options = {}) {
|
|
262
|
+
if (!isQueueJobDefinition(definition)) {
|
|
263
|
+
throw new Error('[Holo Queue] Jobs must define a "handle" function.');
|
|
264
|
+
}
|
|
265
|
+
const normalizedDefinition = normalizeQueueJobDefinition(definition);
|
|
266
|
+
const name = resolveRegistrationName(normalizedDefinition, options);
|
|
267
|
+
const registry = getQueueRegistryState().jobs;
|
|
268
|
+
if (registry.has(name) && options.replaceExisting !== true) {
|
|
269
|
+
throw new Error(`[Holo Queue] Queue job "${name}" is already registered.`);
|
|
270
|
+
}
|
|
271
|
+
const entry = Object.freeze({
|
|
272
|
+
name,
|
|
273
|
+
...options.sourcePath ? { sourcePath: options.sourcePath } : {},
|
|
274
|
+
definition: Object.freeze({
|
|
275
|
+
...normalizedDefinition
|
|
276
|
+
})
|
|
277
|
+
});
|
|
278
|
+
registry.set(name, entry);
|
|
279
|
+
return entry;
|
|
280
|
+
}
|
|
281
|
+
function registerQueueJobs(definitions) {
|
|
282
|
+
return Object.freeze(definitions.map((entry) => registerQueueJob(entry.definition, entry.options)));
|
|
283
|
+
}
|
|
284
|
+
function getRegisteredQueueJob(name) {
|
|
285
|
+
return getQueueRegistryState().jobs.get(name);
|
|
286
|
+
}
|
|
287
|
+
function listRegisteredQueueJobs() {
|
|
288
|
+
return Object.freeze([...getQueueRegistryState().jobs.values()].sort((left, right) => left.name.localeCompare(right.name)));
|
|
289
|
+
}
|
|
290
|
+
function unregisterQueueJob(name) {
|
|
291
|
+
return getQueueRegistryState().jobs.delete(name);
|
|
292
|
+
}
|
|
293
|
+
function resetQueueRegistry() {
|
|
294
|
+
getQueueRegistryState().jobs.clear();
|
|
295
|
+
}
|
|
296
|
+
var queueRegistryInternals = {
|
|
297
|
+
deriveJobNameFromSourcePath
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/drivers/redis.ts
|
|
301
|
+
import { randomUUID } from "crypto";
|
|
302
|
+
import {
|
|
303
|
+
Queue as BullQueue,
|
|
304
|
+
Worker as BullWorker
|
|
305
|
+
} from "bullmq";
|
|
306
|
+
function normalizeRedisErrorMessage(error) {
|
|
307
|
+
if (error instanceof Error) {
|
|
308
|
+
return error.message;
|
|
309
|
+
}
|
|
310
|
+
return String(error);
|
|
311
|
+
}
|
|
312
|
+
function isRecord(value) {
|
|
313
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
314
|
+
}
|
|
315
|
+
function isQueueEnvelope(value) {
|
|
316
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.connection === "string" && typeof value.queue === "string" && "payload" in value && typeof value.attempts === "number" && Number.isInteger(value.attempts) && value.attempts >= 0 && typeof value.maxAttempts === "number" && Number.isInteger(value.maxAttempts) && value.maxAttempts >= 1 && typeof value.createdAt === "number" && Number.isFinite(value.createdAt) && (typeof value.availableAt === "undefined" || typeof value.availableAt === "number" && Number.isFinite(value.availableAt));
|
|
317
|
+
}
|
|
318
|
+
function resolveBullConnectionOptions(connection) {
|
|
319
|
+
return {
|
|
320
|
+
host: connection.redis.host,
|
|
321
|
+
port: connection.redis.port,
|
|
322
|
+
username: connection.redis.username,
|
|
323
|
+
password: connection.redis.password,
|
|
324
|
+
db: connection.redis.db,
|
|
325
|
+
maxRetriesPerRequest: null
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
var RedisQueueDriverError = class extends Error {
|
|
329
|
+
constructor(connectionName, action, cause) {
|
|
330
|
+
super(
|
|
331
|
+
`[Holo Queue] Redis queue connection "${connectionName}" failed to ${action}: ${normalizeRedisErrorMessage(cause)}`,
|
|
332
|
+
{ cause }
|
|
333
|
+
);
|
|
334
|
+
this.name = "RedisQueueDriverError";
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function wrapRedisError(connectionName, action, error) {
|
|
338
|
+
if (error instanceof RedisQueueDriverError) {
|
|
339
|
+
return error;
|
|
340
|
+
}
|
|
341
|
+
return new RedisQueueDriverError(connectionName, action, error);
|
|
342
|
+
}
|
|
343
|
+
function resolveAttempts(job) {
|
|
344
|
+
return Math.max(
|
|
345
|
+
Number.isInteger(job.attemptsStarted) ? job.attemptsStarted - 1 : 0,
|
|
346
|
+
Number.isInteger(job.attemptsMade) ? job.attemptsMade : 0,
|
|
347
|
+
0
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
var RedisQueueDriver = class {
|
|
351
|
+
constructor(connection, context) {
|
|
352
|
+
this.context = context;
|
|
353
|
+
this.name = connection.name;
|
|
354
|
+
this.connection = connection;
|
|
355
|
+
this.bullConnection = resolveBullConnectionOptions(connection);
|
|
356
|
+
}
|
|
357
|
+
name;
|
|
358
|
+
driver = "redis";
|
|
359
|
+
mode = "async";
|
|
360
|
+
connection;
|
|
361
|
+
bullConnection;
|
|
362
|
+
queues = /* @__PURE__ */ new Map();
|
|
363
|
+
workers = /* @__PURE__ */ new Map();
|
|
364
|
+
reservations = /* @__PURE__ */ new Map();
|
|
365
|
+
queueCursor = 0;
|
|
366
|
+
getQueue(queueName) {
|
|
367
|
+
const cached = this.queues.get(queueName);
|
|
368
|
+
if (cached) {
|
|
369
|
+
return cached;
|
|
370
|
+
}
|
|
371
|
+
const queue = new BullQueue(queueName, {
|
|
372
|
+
connection: this.bullConnection,
|
|
373
|
+
defaultJobOptions: {
|
|
374
|
+
removeOnComplete: true,
|
|
375
|
+
removeOnFail: true
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
this.queues.set(queueName, queue);
|
|
379
|
+
return queue;
|
|
380
|
+
}
|
|
381
|
+
async getWorker(queueName) {
|
|
382
|
+
const cached = this.workers.get(queueName);
|
|
383
|
+
if (cached) {
|
|
384
|
+
return cached;
|
|
385
|
+
}
|
|
386
|
+
const worker = new BullWorker(
|
|
387
|
+
queueName,
|
|
388
|
+
null,
|
|
389
|
+
{
|
|
390
|
+
autorun: false,
|
|
391
|
+
concurrency: 1,
|
|
392
|
+
connection: this.bullConnection,
|
|
393
|
+
drainDelay: this.connection.blockFor,
|
|
394
|
+
lockDuration: this.connection.retryAfter * 1e3,
|
|
395
|
+
removeOnComplete: { count: 0 },
|
|
396
|
+
removeOnFail: { count: 0 }
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
await worker.waitUntilReady();
|
|
400
|
+
this.workers.set(queueName, worker);
|
|
401
|
+
return worker;
|
|
402
|
+
}
|
|
403
|
+
normalizeQueueNames(queueNames) {
|
|
404
|
+
if (!queueNames || queueNames.length === 0) {
|
|
405
|
+
return [.../* @__PURE__ */ new Set([
|
|
406
|
+
this.connection.queue,
|
|
407
|
+
...this.queues.keys(),
|
|
408
|
+
...this.workers.keys()
|
|
409
|
+
])];
|
|
410
|
+
}
|
|
411
|
+
return [...new Set(queueNames)];
|
|
412
|
+
}
|
|
413
|
+
rotateQueueNames(queueNames) {
|
|
414
|
+
if (queueNames.length <= 1) {
|
|
415
|
+
return queueNames;
|
|
416
|
+
}
|
|
417
|
+
const offset = this.queueCursor % queueNames.length;
|
|
418
|
+
this.queueCursor = (this.queueCursor + 1) % queueNames.length;
|
|
419
|
+
return Object.freeze([
|
|
420
|
+
...queueNames.slice(offset),
|
|
421
|
+
...queueNames.slice(0, offset)
|
|
422
|
+
]);
|
|
423
|
+
}
|
|
424
|
+
createReservedJob(job, token, queueName) {
|
|
425
|
+
if (!job.id) {
|
|
426
|
+
throw new Error("BullMQ returned a reserved job without an id.");
|
|
427
|
+
}
|
|
428
|
+
if (!isQueueEnvelope(job.data)) {
|
|
429
|
+
throw new Error(`BullMQ returned a malformed payload for job "${job.id}".`);
|
|
430
|
+
}
|
|
431
|
+
const attempts = resolveAttempts(job);
|
|
432
|
+
const envelope = Object.freeze({
|
|
433
|
+
id: job.id,
|
|
434
|
+
name: job.data.name,
|
|
435
|
+
connection: job.data.connection,
|
|
436
|
+
queue: job.data.queue || queueName,
|
|
437
|
+
payload: job.data.payload,
|
|
438
|
+
attempts,
|
|
439
|
+
maxAttempts: job.data.maxAttempts,
|
|
440
|
+
...typeof job.data.availableAt === "number" ? { availableAt: job.data.availableAt } : {},
|
|
441
|
+
createdAt: job.data.createdAt
|
|
442
|
+
});
|
|
443
|
+
this.reservations.set(token, {
|
|
444
|
+
job,
|
|
445
|
+
token
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
reservationId: token,
|
|
449
|
+
envelope,
|
|
450
|
+
reservedAt: Date.now()
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
getReservation(reserved) {
|
|
454
|
+
const reservation = this.reservations.get(reserved.reservationId);
|
|
455
|
+
if (!reservation) {
|
|
456
|
+
throw new Error(`Queue reservation "${reserved.reservationId}" is not active.`);
|
|
457
|
+
}
|
|
458
|
+
return reservation;
|
|
459
|
+
}
|
|
460
|
+
async settleReservation(reserved, action, callback) {
|
|
461
|
+
const reservation = this.getReservation(reserved);
|
|
462
|
+
try {
|
|
463
|
+
await callback(reservation);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw wrapRedisError(this.name, action, error);
|
|
466
|
+
} finally {
|
|
467
|
+
this.reservations.delete(reserved.reservationId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async dispatch(job) {
|
|
471
|
+
try {
|
|
472
|
+
const delay = typeof job.availableAt === "number" ? Math.max(job.availableAt - Date.now(), 0) : void 0;
|
|
473
|
+
const queued = await this.getQueue(job.queue).add(job.name, job, {
|
|
474
|
+
attempts: job.maxAttempts,
|
|
475
|
+
...typeof delay === "number" ? { delay } : {},
|
|
476
|
+
jobId: job.id,
|
|
477
|
+
removeOnComplete: true,
|
|
478
|
+
removeOnFail: true,
|
|
479
|
+
timestamp: job.createdAt
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
jobId: queued.id ?? job.id,
|
|
483
|
+
synchronous: false
|
|
484
|
+
};
|
|
485
|
+
} catch (error) {
|
|
486
|
+
throw wrapRedisError(this.name, "enqueue job", error);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async reserve(input) {
|
|
490
|
+
try {
|
|
491
|
+
const queueNames = this.rotateQueueNames(this.normalizeQueueNames(input.queueNames));
|
|
492
|
+
for (const queueName of queueNames) {
|
|
493
|
+
const token2 = `${input.workerId}:${randomUUID()}`;
|
|
494
|
+
const worker2 = await this.getWorker(queueName);
|
|
495
|
+
const job2 = await worker2.getNextJob(token2, { block: false });
|
|
496
|
+
if (job2) {
|
|
497
|
+
return this.createReservedJob(job2, token2, queueName);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const [blockingQueue] = queueNames;
|
|
501
|
+
if (!blockingQueue || this.connection.blockFor <= 0) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const token = `${input.workerId}:${randomUUID()}`;
|
|
505
|
+
const worker = await this.getWorker(blockingQueue);
|
|
506
|
+
const job = await worker.getNextJob(token, { block: true });
|
|
507
|
+
if (!job) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
return this.createReservedJob(job, token, blockingQueue);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
throw wrapRedisError(this.name, "reserve job", error);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async acknowledge(job) {
|
|
516
|
+
await this.settleReservation(job, "acknowledge job", async (reservation) => {
|
|
517
|
+
await reservation.job.moveToCompleted(null, reservation.token, false);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async release(job, options) {
|
|
521
|
+
await this.settleReservation(job, "release job", async (reservation) => {
|
|
522
|
+
if (typeof options?.delaySeconds === "number" && options.delaySeconds > 0) {
|
|
523
|
+
await reservation.job.moveToDelayed(Date.now() + options.delaySeconds * 1e3, reservation.token);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
await reservation.job.moveToWait(reservation.token);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async delete(job) {
|
|
530
|
+
await this.settleReservation(job, "delete job", async (reservation) => {
|
|
531
|
+
reservation.job.discard();
|
|
532
|
+
await reservation.job.moveToFailed(new Error("[Holo Queue] Job deleted."), reservation.token, false);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async clear(input) {
|
|
536
|
+
try {
|
|
537
|
+
const queueNames = this.normalizeQueueNames(input?.queueNames);
|
|
538
|
+
let cleared = 0;
|
|
539
|
+
for (const queueName of queueNames) {
|
|
540
|
+
const queue = this.getQueue(queueName);
|
|
541
|
+
cleared += await queue.getJobCountByTypes("wait", "waiting", "paused", "prioritized", "delayed");
|
|
542
|
+
await queue.drain(true);
|
|
543
|
+
}
|
|
544
|
+
return cleared;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
throw wrapRedisError(this.name, "clear queued jobs", error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async close() {
|
|
550
|
+
const resources = [
|
|
551
|
+
...this.workers.values(),
|
|
552
|
+
...this.queues.values()
|
|
553
|
+
];
|
|
554
|
+
this.reservations.clear();
|
|
555
|
+
this.workers.clear();
|
|
556
|
+
this.queues.clear();
|
|
557
|
+
const results = await Promise.allSettled(resources.map(async (resource) => {
|
|
558
|
+
if (resource instanceof BullWorker) {
|
|
559
|
+
await resource.close(true);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
await resource.close();
|
|
563
|
+
}));
|
|
564
|
+
const rejection = results.find((result) => result.status === "rejected");
|
|
565
|
+
if (rejection) {
|
|
566
|
+
throw wrapRedisError(this.name, "close driver", rejection.reason);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
var redisQueueDriverFactory = {
|
|
571
|
+
driver: "redis",
|
|
572
|
+
create(connection, context) {
|
|
573
|
+
return new RedisQueueDriver(connection, context);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
var redisQueueDriverInternals = {
|
|
577
|
+
isQueueEnvelope,
|
|
578
|
+
normalizeRedisErrorMessage,
|
|
579
|
+
resolveAttempts,
|
|
580
|
+
resolveBullConnectionOptions,
|
|
581
|
+
wrapRedisError
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/drivers/sync.ts
|
|
585
|
+
var SyncQueueDriver = class {
|
|
586
|
+
name;
|
|
587
|
+
driver = "sync";
|
|
588
|
+
mode = "sync";
|
|
589
|
+
context;
|
|
590
|
+
constructor(connection, context) {
|
|
591
|
+
this.name = connection.name;
|
|
592
|
+
this.context = context;
|
|
593
|
+
}
|
|
594
|
+
async dispatch(job) {
|
|
595
|
+
const result = await this.context.execute(job);
|
|
596
|
+
return {
|
|
597
|
+
jobId: job.id,
|
|
598
|
+
synchronous: true,
|
|
599
|
+
result
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async clear() {
|
|
603
|
+
return 0;
|
|
604
|
+
}
|
|
605
|
+
async close() {
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
var syncQueueDriverFactory = {
|
|
609
|
+
driver: "sync",
|
|
610
|
+
create(connection, context) {
|
|
611
|
+
return new SyncQueueDriver(connection, context);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
var syncQueueDriverInternals = {
|
|
615
|
+
SyncQueueDriver
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/runtime.ts
|
|
619
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
620
|
+
var INLINE_SYNC_DRIVER_KEY = "__inline_sync__";
|
|
621
|
+
var QueueReleaseUnsupportedError = class extends Error {
|
|
622
|
+
constructor() {
|
|
623
|
+
super("[Holo Queue] release() is not supported during synchronous queue execution.");
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
function isPlainObject(value) {
|
|
627
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
const prototype = Object.getPrototypeOf(value);
|
|
631
|
+
return prototype === Object.prototype || prototype === null;
|
|
632
|
+
}
|
|
633
|
+
function assertQueueJsonValue(value, path, state) {
|
|
634
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
638
|
+
throw new TypeError(`[Holo Queue] Queue payload at "${path}" must be JSON-serializable.`);
|
|
639
|
+
}
|
|
640
|
+
if (typeof value === "number") {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
644
|
+
throw new TypeError(`[Holo Queue] Queue payload at "${path}" must be JSON-serializable.`);
|
|
645
|
+
}
|
|
646
|
+
if (Array.isArray(value)) {
|
|
647
|
+
if (state.seen.has(value)) {
|
|
648
|
+
throw new TypeError(`[Holo Queue] Queue payload at "${path}" contains a circular reference.`);
|
|
649
|
+
}
|
|
650
|
+
state.seen.add(value);
|
|
651
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
652
|
+
assertQueueJsonValue(value[index], `${path}[${index}]`, state);
|
|
653
|
+
}
|
|
654
|
+
state.seen.delete(value);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (!isPlainObject(value)) {
|
|
658
|
+
throw new TypeError(`[Holo Queue] Queue payload at "${path}" must be a plain JSON object, array, or primitive.`);
|
|
659
|
+
}
|
|
660
|
+
if (state.seen.has(value)) {
|
|
661
|
+
throw new TypeError(`[Holo Queue] Queue payload at "${path}" contains a circular reference.`);
|
|
662
|
+
}
|
|
663
|
+
state.seen.add(value);
|
|
664
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
665
|
+
assertQueueJsonValue(nested, `${path}.${key}`, state);
|
|
666
|
+
}
|
|
667
|
+
state.seen.delete(value);
|
|
668
|
+
}
|
|
669
|
+
function validateQueuePayload(payload) {
|
|
670
|
+
assertQueueJsonValue(payload, "payload", { seen: /* @__PURE__ */ new Set() });
|
|
671
|
+
}
|
|
672
|
+
function normalizeConnectionName2(name) {
|
|
673
|
+
const normalized = name.trim();
|
|
674
|
+
if (!normalized) {
|
|
675
|
+
throw new Error("[Holo Queue] Queue connection names must be non-empty strings.");
|
|
676
|
+
}
|
|
677
|
+
return normalized;
|
|
678
|
+
}
|
|
679
|
+
function normalizeQueueName2(name) {
|
|
680
|
+
const normalized = name.trim();
|
|
681
|
+
if (!normalized) {
|
|
682
|
+
throw new Error("[Holo Queue] Queue names must be non-empty strings.");
|
|
683
|
+
}
|
|
684
|
+
return normalized;
|
|
685
|
+
}
|
|
686
|
+
function normalizeDispatchCompletedHook(callback) {
|
|
687
|
+
if (typeof callback !== "function") {
|
|
688
|
+
throw new Error("[Holo Queue] Queue dispatch onComplete hook must be a function.");
|
|
689
|
+
}
|
|
690
|
+
return callback;
|
|
691
|
+
}
|
|
692
|
+
function normalizeDispatchFailedHook(callback) {
|
|
693
|
+
if (typeof callback !== "function") {
|
|
694
|
+
throw new Error("[Holo Queue] Queue dispatch onFailed hook must be a function.");
|
|
695
|
+
}
|
|
696
|
+
return callback;
|
|
697
|
+
}
|
|
698
|
+
function normalizeDelay(delay) {
|
|
699
|
+
if (typeof delay === "undefined") {
|
|
700
|
+
return void 0;
|
|
701
|
+
}
|
|
702
|
+
if (typeof delay === "number") {
|
|
703
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
704
|
+
throw new TypeError("[Holo Queue] Queue delay must be a finite number greater than or equal to 0.");
|
|
705
|
+
}
|
|
706
|
+
return Date.now() + Math.floor(delay * 1e3);
|
|
707
|
+
}
|
|
708
|
+
const timestamp = delay.getTime();
|
|
709
|
+
if (Number.isNaN(timestamp)) {
|
|
710
|
+
throw new TypeError("[Holo Queue] Queue delay dates must be valid Date instances.");
|
|
711
|
+
}
|
|
712
|
+
return timestamp;
|
|
713
|
+
}
|
|
714
|
+
function createDefaultDriverFactories() {
|
|
715
|
+
return /* @__PURE__ */ new Map([
|
|
716
|
+
[redisQueueDriverFactory.driver, redisQueueDriverFactory],
|
|
717
|
+
[syncQueueDriverFactory.driver, syncQueueDriverFactory]
|
|
718
|
+
]);
|
|
719
|
+
}
|
|
720
|
+
function createQueueDriverFactoryMap(factories) {
|
|
721
|
+
const resolved = createDefaultDriverFactories();
|
|
722
|
+
if (!factories) {
|
|
723
|
+
return resolved;
|
|
724
|
+
}
|
|
725
|
+
if (!Array.isArray(factories)) {
|
|
726
|
+
for (const [name, factory] of factories.entries()) {
|
|
727
|
+
resolved.set(String(name), factory);
|
|
728
|
+
}
|
|
729
|
+
return resolved;
|
|
730
|
+
}
|
|
731
|
+
for (const factory of factories) {
|
|
732
|
+
resolved.set(factory.driver, factory);
|
|
733
|
+
}
|
|
734
|
+
return resolved;
|
|
735
|
+
}
|
|
736
|
+
function getQueueRuntimeState() {
|
|
737
|
+
const runtime = globalThis;
|
|
738
|
+
runtime.__holoQueueRuntime__ ??= {
|
|
739
|
+
config: holoQueueDefaults,
|
|
740
|
+
driverFactories: createDefaultDriverFactories(),
|
|
741
|
+
drivers: /* @__PURE__ */ new Map(),
|
|
742
|
+
failedJobStore: void 0
|
|
743
|
+
};
|
|
744
|
+
return runtime.__holoQueueRuntime__;
|
|
745
|
+
}
|
|
746
|
+
function createQueueRuntimeBinding(state) {
|
|
747
|
+
return Object.freeze({
|
|
748
|
+
config: state.config,
|
|
749
|
+
drivers: state.drivers
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function requireRegisteredQueueJob(jobName) {
|
|
753
|
+
const registered = getRegisteredQueueJob(jobName);
|
|
754
|
+
if (!registered) {
|
|
755
|
+
throw new Error(`[Holo Queue] Queue job "${jobName}" is not registered.`);
|
|
756
|
+
}
|
|
757
|
+
return registered;
|
|
758
|
+
}
|
|
759
|
+
function resolveConnectionConfig(config, requestedConnection) {
|
|
760
|
+
const connectionName = requestedConnection ? normalizeConnectionName2(requestedConnection) : config.default;
|
|
761
|
+
const connection = config.connections[connectionName];
|
|
762
|
+
if (!connection) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`[Holo Queue] Queue connection "${connectionName}" is not configured. Available connections: ${Object.keys(config.connections).join(", ")}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return connection;
|
|
768
|
+
}
|
|
769
|
+
function createJobContext(envelope, overrides = {}) {
|
|
770
|
+
return {
|
|
771
|
+
jobId: envelope.id,
|
|
772
|
+
jobName: envelope.name,
|
|
773
|
+
connection: envelope.connection,
|
|
774
|
+
queue: envelope.queue,
|
|
775
|
+
attempt: envelope.attempts + 1,
|
|
776
|
+
maxAttempts: overrides.maxAttempts ?? envelope.maxAttempts,
|
|
777
|
+
async release(delaySeconds) {
|
|
778
|
+
if (overrides.release) {
|
|
779
|
+
await overrides.release(delaySeconds);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
throw new QueueReleaseUnsupportedError();
|
|
783
|
+
},
|
|
784
|
+
async fail(error) {
|
|
785
|
+
if (overrides.fail) {
|
|
786
|
+
await overrides.fail(error);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
throw error;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
async function executeRegisteredQueueJob(envelope, contextOverrides = {}) {
|
|
794
|
+
const registered = requireRegisteredQueueJob(envelope.name);
|
|
795
|
+
const context = createJobContext(envelope, contextOverrides);
|
|
796
|
+
const result = await registered.definition.handle(
|
|
797
|
+
envelope.payload,
|
|
798
|
+
context
|
|
799
|
+
);
|
|
800
|
+
if (contextOverrides.shouldSkipLifecycleHooks?.()) {
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
await executeRegisteredQueueJobCompletedHook(envelope, result, contextOverrides);
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
async function executeRegisteredQueueJobCompletedHook(envelope, result, contextOverrides = {}) {
|
|
807
|
+
const registered = requireRegisteredQueueJob(envelope.name);
|
|
808
|
+
if (!registered.definition.onCompleted) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
await registered.definition.onCompleted(
|
|
813
|
+
envelope.payload,
|
|
814
|
+
result,
|
|
815
|
+
createJobContext(envelope, contextOverrides)
|
|
816
|
+
);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
819
|
+
console.warn(`[Holo Queue] onCompleted hook failed for job "${envelope.name}": ${resolvedError.message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async function executeRegisteredQueueJobFailedHook(envelope, error, contextOverrides = {}) {
|
|
823
|
+
const registered = requireRegisteredQueueJob(envelope.name);
|
|
824
|
+
if (!registered.definition.onFailed) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
await registered.definition.onFailed(
|
|
829
|
+
envelope.payload,
|
|
830
|
+
error,
|
|
831
|
+
createJobContext(envelope, contextOverrides)
|
|
832
|
+
);
|
|
833
|
+
} catch (hookError) {
|
|
834
|
+
const resolvedError = hookError instanceof Error ? hookError : new Error(String(hookError));
|
|
835
|
+
console.warn(`[Holo Queue] onFailed hook failed for job "${envelope.name}": ${resolvedError.message}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function dispatchThroughDriver(driver, envelope) {
|
|
839
|
+
try {
|
|
840
|
+
return await driver.dispatch(envelope);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (driver.mode === "sync") {
|
|
843
|
+
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
844
|
+
await executeRegisteredQueueJobFailedHook(envelope, resolvedError, {
|
|
845
|
+
maxAttempts: envelope.maxAttempts
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function createQueueDriverFactoryContext() {
|
|
852
|
+
return {
|
|
853
|
+
async execute(job) {
|
|
854
|
+
return await executeRegisteredQueueJob(job);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function resolveDriverFactory(state, connection) {
|
|
859
|
+
const factory = state.driverFactories.get(connection.driver);
|
|
860
|
+
if (!factory) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`[Holo Queue] Queue connection "${connection.name}" uses driver "${connection.driver}" but no queue driver factory is registered.`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return factory;
|
|
866
|
+
}
|
|
867
|
+
function resolveConnectionDriver(connectionName) {
|
|
868
|
+
const state = getQueueRuntimeState();
|
|
869
|
+
const connection = resolveConnectionConfig(state.config, connectionName);
|
|
870
|
+
const cached = state.drivers.get(connection.name);
|
|
871
|
+
if (cached) {
|
|
872
|
+
return cached;
|
|
873
|
+
}
|
|
874
|
+
const driver = resolveDriverFactory(state, connection).create(
|
|
875
|
+
connection,
|
|
876
|
+
createQueueDriverFactoryContext()
|
|
877
|
+
);
|
|
878
|
+
state.drivers.set(connection.name, driver);
|
|
879
|
+
return driver;
|
|
880
|
+
}
|
|
881
|
+
function resolveSyncExecutionDriver() {
|
|
882
|
+
const state = getQueueRuntimeState();
|
|
883
|
+
const cached = state.drivers.get(INLINE_SYNC_DRIVER_KEY);
|
|
884
|
+
if (cached) {
|
|
885
|
+
return cached;
|
|
886
|
+
}
|
|
887
|
+
const syncConnection = normalizeQueueConfig().connections.sync;
|
|
888
|
+
const driver = resolveDriverFactory(state, syncConnection).create(
|
|
889
|
+
syncConnection,
|
|
890
|
+
createQueueDriverFactoryContext()
|
|
891
|
+
);
|
|
892
|
+
state.drivers.set(INLINE_SYNC_DRIVER_KEY, driver);
|
|
893
|
+
return driver;
|
|
894
|
+
}
|
|
895
|
+
function createJobEnvelope(jobName, payload, options) {
|
|
896
|
+
const runtime = getQueueRuntimeState();
|
|
897
|
+
const registered = requireRegisteredQueueJob(jobName);
|
|
898
|
+
const connection = resolveConnectionConfig(
|
|
899
|
+
runtime.config,
|
|
900
|
+
options.connection ?? registered.definition.connection
|
|
901
|
+
);
|
|
902
|
+
const queue = normalizeQueueName2(options.queue ?? registered.definition.queue ?? connection.queue ?? DEFAULT_QUEUE_NAME);
|
|
903
|
+
return Object.freeze({
|
|
904
|
+
id: randomUUID2(),
|
|
905
|
+
name: registered.name,
|
|
906
|
+
connection: connection.name,
|
|
907
|
+
queue,
|
|
908
|
+
payload,
|
|
909
|
+
attempts: 0,
|
|
910
|
+
maxAttempts: registered.definition.tries ?? 1,
|
|
911
|
+
...typeof options.delay === "undefined" ? {} : { availableAt: normalizeDelay(options.delay) },
|
|
912
|
+
createdAt: Date.now()
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function createDispatchEnvelope(jobName, payload, options = {}) {
|
|
916
|
+
validateQueuePayload(payload);
|
|
917
|
+
return createJobEnvelope(jobName, payload, options);
|
|
918
|
+
}
|
|
919
|
+
async function dispatchRecord(envelope) {
|
|
920
|
+
const driver = resolveConnectionDriver(envelope.connection);
|
|
921
|
+
const result = await dispatchThroughDriver(driver, envelope);
|
|
922
|
+
return {
|
|
923
|
+
jobId: result.jobId,
|
|
924
|
+
connection: envelope.connection,
|
|
925
|
+
queue: envelope.queue,
|
|
926
|
+
synchronous: result.synchronous
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
var PendingQueueDispatch = class _PendingQueueDispatch {
|
|
930
|
+
jobName;
|
|
931
|
+
payload;
|
|
932
|
+
options;
|
|
933
|
+
completedHooks;
|
|
934
|
+
failedHooks;
|
|
935
|
+
executionPromise;
|
|
936
|
+
constructor(jobName, payload, options = {}, completedHooks = [], failedHooks = []) {
|
|
937
|
+
this.jobName = jobName;
|
|
938
|
+
this.payload = payload;
|
|
939
|
+
this.options = options;
|
|
940
|
+
this.completedHooks = completedHooks;
|
|
941
|
+
this.failedHooks = failedHooks;
|
|
942
|
+
}
|
|
943
|
+
onConnection(name) {
|
|
944
|
+
return new _PendingQueueDispatch(this.jobName, this.payload, {
|
|
945
|
+
...this.options,
|
|
946
|
+
connection: normalizeConnectionName2(name)
|
|
947
|
+
}, this.completedHooks, this.failedHooks);
|
|
948
|
+
}
|
|
949
|
+
onQueue(name) {
|
|
950
|
+
return new _PendingQueueDispatch(this.jobName, this.payload, {
|
|
951
|
+
...this.options,
|
|
952
|
+
queue: normalizeQueueName2(name)
|
|
953
|
+
}, this.completedHooks, this.failedHooks);
|
|
954
|
+
}
|
|
955
|
+
delay(value) {
|
|
956
|
+
normalizeDelay(value);
|
|
957
|
+
return new _PendingQueueDispatch(this.jobName, this.payload, {
|
|
958
|
+
...this.options,
|
|
959
|
+
delay: value
|
|
960
|
+
}, this.completedHooks, this.failedHooks);
|
|
961
|
+
}
|
|
962
|
+
onComplete(callback) {
|
|
963
|
+
return new _PendingQueueDispatch(
|
|
964
|
+
this.jobName,
|
|
965
|
+
this.payload,
|
|
966
|
+
this.options,
|
|
967
|
+
[...this.completedHooks, normalizeDispatchCompletedHook(callback)],
|
|
968
|
+
this.failedHooks
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
onFailed(callback) {
|
|
972
|
+
return new _PendingQueueDispatch(
|
|
973
|
+
this.jobName,
|
|
974
|
+
this.payload,
|
|
975
|
+
this.options,
|
|
976
|
+
this.completedHooks,
|
|
977
|
+
[...this.failedHooks, normalizeDispatchFailedHook(callback)]
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
then(onfulfilled, onrejected) {
|
|
981
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
982
|
+
}
|
|
983
|
+
catch(onrejected) {
|
|
984
|
+
return this.execute().catch(onrejected);
|
|
985
|
+
}
|
|
986
|
+
finally(onfinally) {
|
|
987
|
+
return this.execute().finally(onfinally ?? void 0);
|
|
988
|
+
}
|
|
989
|
+
async dispatch() {
|
|
990
|
+
return this.execute();
|
|
991
|
+
}
|
|
992
|
+
execute() {
|
|
993
|
+
this.executionPromise ??= (async () => {
|
|
994
|
+
try {
|
|
995
|
+
const envelope = createDispatchEnvelope(this.jobName, this.payload, this.options);
|
|
996
|
+
const result = await dispatchRecord(envelope);
|
|
997
|
+
for (const hook of this.completedHooks) {
|
|
998
|
+
try {
|
|
999
|
+
await hook(result);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
1002
|
+
console.warn(`[Holo Queue] onComplete hook failed during dispatch of "${this.jobName}": ${resolvedError.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return result;
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
for (const hook of this.failedHooks) {
|
|
1008
|
+
try {
|
|
1009
|
+
await hook(error);
|
|
1010
|
+
} catch (hookError) {
|
|
1011
|
+
const resolvedError = hookError instanceof Error ? hookError : new Error(String(hookError));
|
|
1012
|
+
console.warn(`[Holo Queue] onFailed hook failed during dispatch of "${this.jobName}": ${resolvedError.message}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
})();
|
|
1018
|
+
return this.executionPromise;
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
function createQueueConnection(name) {
|
|
1022
|
+
const resolvedName = name ? normalizeConnectionName2(name) : getQueueRuntimeState().config.default;
|
|
1023
|
+
const dispatchViaConnection = ((jobName, payload, options = {}) => {
|
|
1024
|
+
return new PendingQueueDispatch(jobName, payload, {
|
|
1025
|
+
...options,
|
|
1026
|
+
connection: resolvedName
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
const dispatchSyncViaConnection = (async (jobName, payload) => {
|
|
1030
|
+
return dispatchSyncInternal(jobName, payload, {
|
|
1031
|
+
connection: resolvedName
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
return {
|
|
1035
|
+
name: resolvedName,
|
|
1036
|
+
dispatch: dispatchViaConnection,
|
|
1037
|
+
dispatchSync: dispatchSyncViaConnection
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function configureQueueRuntime(options = {}) {
|
|
1041
|
+
const state = getQueueRuntimeState();
|
|
1042
|
+
let shouldResetDrivers = false;
|
|
1043
|
+
if (options.config) {
|
|
1044
|
+
state.config = normalizeQueueConfig(options.config);
|
|
1045
|
+
shouldResetDrivers = true;
|
|
1046
|
+
}
|
|
1047
|
+
if (options.driverFactories) {
|
|
1048
|
+
state.driverFactories = createQueueDriverFactoryMap(options.driverFactories);
|
|
1049
|
+
shouldResetDrivers = true;
|
|
1050
|
+
}
|
|
1051
|
+
if (Object.prototype.hasOwnProperty.call(options, "failedJobStore")) {
|
|
1052
|
+
state.failedJobStore = options.failedJobStore;
|
|
1053
|
+
}
|
|
1054
|
+
if (shouldResetDrivers) {
|
|
1055
|
+
closeQueueDrivers(state.drivers.values());
|
|
1056
|
+
state.drivers.clear();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function resetQueueRuntimeState(state) {
|
|
1060
|
+
state.config = normalizeQueueConfig();
|
|
1061
|
+
state.driverFactories = createDefaultDriverFactories();
|
|
1062
|
+
state.drivers.clear();
|
|
1063
|
+
state.failedJobStore = void 0;
|
|
1064
|
+
}
|
|
1065
|
+
async function shutdownQueueRuntime() {
|
|
1066
|
+
const state = getQueueRuntimeState();
|
|
1067
|
+
await closeQueueDrivers(state.drivers.values());
|
|
1068
|
+
resetQueueRuntimeState(state);
|
|
1069
|
+
}
|
|
1070
|
+
function resetQueueRuntime() {
|
|
1071
|
+
const state = getQueueRuntimeState();
|
|
1072
|
+
void closeQueueDrivers(state.drivers.values());
|
|
1073
|
+
resetQueueRuntimeState(state);
|
|
1074
|
+
}
|
|
1075
|
+
function getQueueRuntime() {
|
|
1076
|
+
return createQueueRuntimeBinding(getQueueRuntimeState());
|
|
1077
|
+
}
|
|
1078
|
+
function useQueueConnection(name) {
|
|
1079
|
+
return createQueueConnection(name);
|
|
1080
|
+
}
|
|
1081
|
+
function dispatchSyncInternal(jobName, payload, options = {}) {
|
|
1082
|
+
return (async () => {
|
|
1083
|
+
const envelope = createDispatchEnvelope(jobName, payload, options);
|
|
1084
|
+
const result = await dispatchThroughDriver(
|
|
1085
|
+
resolveSyncExecutionDriver(),
|
|
1086
|
+
envelope
|
|
1087
|
+
);
|
|
1088
|
+
return result.result;
|
|
1089
|
+
})();
|
|
1090
|
+
}
|
|
1091
|
+
async function dispatchSync(jobName, payload, options = {}) {
|
|
1092
|
+
return await dispatchSyncInternal(jobName, payload, options);
|
|
1093
|
+
}
|
|
1094
|
+
function dispatch(jobName, payload, options = {}) {
|
|
1095
|
+
return new PendingQueueDispatch(jobName, payload, options);
|
|
1096
|
+
}
|
|
1097
|
+
var Queue = {
|
|
1098
|
+
connection(name) {
|
|
1099
|
+
return createQueueConnection(name);
|
|
1100
|
+
},
|
|
1101
|
+
dispatch,
|
|
1102
|
+
dispatchSync
|
|
1103
|
+
};
|
|
1104
|
+
var queueRuntimeInternals = {
|
|
1105
|
+
assertQueueJsonValue,
|
|
1106
|
+
createQueueRuntimeBinding,
|
|
1107
|
+
createDefaultDriverFactories,
|
|
1108
|
+
createDispatchEnvelope,
|
|
1109
|
+
createJobContext,
|
|
1110
|
+
createJobEnvelope,
|
|
1111
|
+
createQueueConnection,
|
|
1112
|
+
createQueueDriverFactoryContext,
|
|
1113
|
+
createQueueDriverFactoryMap,
|
|
1114
|
+
closeQueueDrivers,
|
|
1115
|
+
dispatchRecord,
|
|
1116
|
+
dispatchThroughDriver,
|
|
1117
|
+
executeRegisteredQueueJob,
|
|
1118
|
+
executeRegisteredQueueJobCompletedHook,
|
|
1119
|
+
executeRegisteredQueueJobFailedHook,
|
|
1120
|
+
getQueueRuntimeState,
|
|
1121
|
+
isPlainObject,
|
|
1122
|
+
normalizeConnectionName: normalizeConnectionName2,
|
|
1123
|
+
normalizeDelay,
|
|
1124
|
+
normalizeQueueName: normalizeQueueName2,
|
|
1125
|
+
resolveConnectionConfig,
|
|
1126
|
+
resolveConnectionDriver,
|
|
1127
|
+
resolveDriverFactory,
|
|
1128
|
+
resolveSyncExecutionDriver,
|
|
1129
|
+
resetQueueRuntimeState,
|
|
1130
|
+
validateQueuePayload
|
|
1131
|
+
};
|
|
1132
|
+
async function closeQueueDrivers(drivers) {
|
|
1133
|
+
const pending = [...drivers].map(async (driver) => {
|
|
1134
|
+
try {
|
|
1135
|
+
await driver.close();
|
|
1136
|
+
} catch {
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
if (pending.length === 0) {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
await Promise.allSettled(pending);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/failed.ts
|
|
1146
|
+
function normalizeFailedStoreErrorMessage(error) {
|
|
1147
|
+
if (error instanceof Error) {
|
|
1148
|
+
return error.message;
|
|
1149
|
+
}
|
|
1150
|
+
return String(error);
|
|
1151
|
+
}
|
|
1152
|
+
var QueueFailedStoreError = class extends Error {
|
|
1153
|
+
constructor(action, cause) {
|
|
1154
|
+
super(
|
|
1155
|
+
`[Holo Queue] Failed job store could not ${action}: ${normalizeFailedStoreErrorMessage(cause)}`,
|
|
1156
|
+
{ cause }
|
|
1157
|
+
);
|
|
1158
|
+
this.name = "QueueFailedStoreError";
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function wrapFailedStoreError(action, error) {
|
|
1162
|
+
if (error instanceof QueueFailedStoreError) {
|
|
1163
|
+
return error;
|
|
1164
|
+
}
|
|
1165
|
+
return new QueueFailedStoreError(action, error);
|
|
1166
|
+
}
|
|
1167
|
+
function getFailedJobStore() {
|
|
1168
|
+
return queueRuntimeInternals.getQueueRuntimeState().failedJobStore;
|
|
1169
|
+
}
|
|
1170
|
+
function createRetriedEnvelope(record) {
|
|
1171
|
+
return Object.freeze({
|
|
1172
|
+
...record.job,
|
|
1173
|
+
attempts: 0,
|
|
1174
|
+
createdAt: Date.now(),
|
|
1175
|
+
availableAt: void 0
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
async function persistFailedQueueJob(reserved, error) {
|
|
1179
|
+
const store = getFailedJobStore();
|
|
1180
|
+
if (!store) {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
return await store.persistFailedJob(reserved, error);
|
|
1185
|
+
} catch (cause) {
|
|
1186
|
+
throw wrapFailedStoreError("persist the failed job", cause);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
async function listFailedQueueJobs() {
|
|
1190
|
+
const store = getFailedJobStore();
|
|
1191
|
+
if (!store) {
|
|
1192
|
+
return Object.freeze([]);
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
return await store.listFailedJobs();
|
|
1196
|
+
} catch (cause) {
|
|
1197
|
+
throw wrapFailedStoreError("list failed jobs", cause);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async function retryFailedQueueJobs(identifier) {
|
|
1201
|
+
const store = getFailedJobStore();
|
|
1202
|
+
if (!store) {
|
|
1203
|
+
return 0;
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
return await store.retryFailedJobs(identifier, async (record) => {
|
|
1207
|
+
const driver = queueRuntimeInternals.resolveConnectionDriver(record.job.connection);
|
|
1208
|
+
await driver.dispatch(createRetriedEnvelope(record));
|
|
1209
|
+
});
|
|
1210
|
+
} catch (cause) {
|
|
1211
|
+
throw wrapFailedStoreError("retry failed jobs", cause);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async function forgetFailedQueueJob(id) {
|
|
1215
|
+
const store = getFailedJobStore();
|
|
1216
|
+
if (!store) {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
return await store.forgetFailedJob(id);
|
|
1221
|
+
} catch (cause) {
|
|
1222
|
+
throw wrapFailedStoreError("forget the failed job", cause);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async function flushFailedQueueJobs() {
|
|
1226
|
+
const store = getFailedJobStore();
|
|
1227
|
+
if (!store) {
|
|
1228
|
+
return 0;
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
return await store.flushFailedJobs();
|
|
1232
|
+
} catch (cause) {
|
|
1233
|
+
throw wrapFailedStoreError("flush failed jobs", cause);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
var queueFailedInternals = {
|
|
1237
|
+
createRetriedEnvelope,
|
|
1238
|
+
getFailedJobStore,
|
|
1239
|
+
normalizeFailedStoreErrorMessage,
|
|
1240
|
+
wrapFailedStoreError
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
// src/worker.ts
|
|
1244
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1245
|
+
var QueueWorkerUnsupportedDriverError = class extends Error {
|
|
1246
|
+
constructor(connectionName, driverName) {
|
|
1247
|
+
super(
|
|
1248
|
+
`[Holo Queue] Queue worker requires an async-capable driver, but connection "${connectionName}" uses "${driverName}".`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
var QueueWorkerTimeoutError = class extends Error {
|
|
1253
|
+
constructor(jobName, timeoutSeconds) {
|
|
1254
|
+
super(`[Holo Queue] Queue job "${jobName}" exceeded timeout of ${timeoutSeconds} seconds.`);
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
function sleep(milliseconds) {
|
|
1258
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1259
|
+
}
|
|
1260
|
+
function requireAsyncDriver(driver, connectionName) {
|
|
1261
|
+
if (driver.mode !== "async") {
|
|
1262
|
+
throw new QueueWorkerUnsupportedDriverError(connectionName, driver.driver);
|
|
1263
|
+
}
|
|
1264
|
+
return driver;
|
|
1265
|
+
}
|
|
1266
|
+
function requireRegisteredDefinition(jobName) {
|
|
1267
|
+
const registered = getRegisteredQueueJob(jobName);
|
|
1268
|
+
if (!registered) {
|
|
1269
|
+
throw new Error(`[Holo Queue] Queue job "${jobName}" is not registered.`);
|
|
1270
|
+
}
|
|
1271
|
+
return registered.definition;
|
|
1272
|
+
}
|
|
1273
|
+
function normalizeQueueNames(queueNames, fallbackQueueName) {
|
|
1274
|
+
if (!queueNames || queueNames.length === 0) {
|
|
1275
|
+
return Object.freeze([fallbackQueueName ?? DEFAULT_QUEUE_NAME]);
|
|
1276
|
+
}
|
|
1277
|
+
const normalized = [...new Set(queueNames.map((name) => name.trim()).filter(Boolean))];
|
|
1278
|
+
if (normalized.length === 0) {
|
|
1279
|
+
throw new Error("[Holo Queue] Queue worker queue names must be non-empty strings.");
|
|
1280
|
+
}
|
|
1281
|
+
return Object.freeze(normalized);
|
|
1282
|
+
}
|
|
1283
|
+
function normalizeNonNegativeInteger(value, label) {
|
|
1284
|
+
if (typeof value === "undefined") {
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1288
|
+
throw new Error(`[Holo Queue] ${label} must be a non-negative integer.`);
|
|
1289
|
+
}
|
|
1290
|
+
return value;
|
|
1291
|
+
}
|
|
1292
|
+
function normalizePositiveInteger(value, label) {
|
|
1293
|
+
if (typeof value === "undefined") {
|
|
1294
|
+
return void 0;
|
|
1295
|
+
}
|
|
1296
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1297
|
+
throw new Error(`[Holo Queue] ${label} must be a positive integer.`);
|
|
1298
|
+
}
|
|
1299
|
+
return value;
|
|
1300
|
+
}
|
|
1301
|
+
function createWorkerEvent(envelope, maxAttempts) {
|
|
1302
|
+
return {
|
|
1303
|
+
jobId: envelope.id,
|
|
1304
|
+
jobName: envelope.name,
|
|
1305
|
+
connection: envelope.connection,
|
|
1306
|
+
queue: envelope.queue,
|
|
1307
|
+
attempt: envelope.attempts + 1,
|
|
1308
|
+
maxAttempts
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
function resolveRetryDelaySeconds(definition, attempt) {
|
|
1312
|
+
const { backoff } = definition;
|
|
1313
|
+
if (typeof backoff === "undefined") {
|
|
1314
|
+
return void 0;
|
|
1315
|
+
}
|
|
1316
|
+
if (typeof backoff === "number") {
|
|
1317
|
+
return backoff;
|
|
1318
|
+
}
|
|
1319
|
+
return backoff[Math.min(Math.max(attempt - 1, 0), backoff.length - 1)];
|
|
1320
|
+
}
|
|
1321
|
+
async function runWithTimeout(callback, jobName, timeoutSeconds, onTimeout) {
|
|
1322
|
+
if (typeof timeoutSeconds === "undefined") {
|
|
1323
|
+
return await callback;
|
|
1324
|
+
}
|
|
1325
|
+
return await Promise.race([
|
|
1326
|
+
callback,
|
|
1327
|
+
new Promise((_, reject) => {
|
|
1328
|
+
const timer = setTimeout(() => {
|
|
1329
|
+
onTimeout?.();
|
|
1330
|
+
reject(new QueueWorkerTimeoutError(jobName, timeoutSeconds));
|
|
1331
|
+
}, timeoutSeconds * 1e3);
|
|
1332
|
+
callback.finally(() => {
|
|
1333
|
+
clearTimeout(timer);
|
|
1334
|
+
}).catch(() => {
|
|
1335
|
+
});
|
|
1336
|
+
})
|
|
1337
|
+
]);
|
|
1338
|
+
}
|
|
1339
|
+
async function processReservedJob(driver, reserved, options) {
|
|
1340
|
+
const envelope = reserved.envelope;
|
|
1341
|
+
const definition = requireRegisteredDefinition(envelope.name);
|
|
1342
|
+
const maxAttempts = options.tries ?? envelope.maxAttempts;
|
|
1343
|
+
const event = createWorkerEvent(envelope, maxAttempts);
|
|
1344
|
+
const executionState = {
|
|
1345
|
+
timedOut: false
|
|
1346
|
+
};
|
|
1347
|
+
let requestedReleaseDelay;
|
|
1348
|
+
let requestedFailure;
|
|
1349
|
+
let action;
|
|
1350
|
+
try {
|
|
1351
|
+
await runWithTimeout(
|
|
1352
|
+
queueRuntimeInternals.executeRegisteredQueueJob(envelope, {
|
|
1353
|
+
maxAttempts,
|
|
1354
|
+
shouldSkipLifecycleHooks() {
|
|
1355
|
+
return executionState.timedOut || typeof action !== "undefined";
|
|
1356
|
+
},
|
|
1357
|
+
async release(delaySeconds) {
|
|
1358
|
+
if (!action) {
|
|
1359
|
+
action = "release";
|
|
1360
|
+
requestedReleaseDelay = delaySeconds;
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
async fail(error) {
|
|
1364
|
+
if (!action) {
|
|
1365
|
+
action = "fail";
|
|
1366
|
+
requestedFailure = error;
|
|
1367
|
+
}
|
|
1368
|
+
throw error;
|
|
1369
|
+
}
|
|
1370
|
+
}),
|
|
1371
|
+
envelope.name,
|
|
1372
|
+
options.timeout ?? definition.timeout,
|
|
1373
|
+
() => {
|
|
1374
|
+
executionState.timedOut = true;
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
if (action === "release") {
|
|
1378
|
+
await driver.release(reserved, typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : void 0);
|
|
1379
|
+
await options.onJobReleased?.({
|
|
1380
|
+
...event,
|
|
1381
|
+
...typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : {}
|
|
1382
|
+
});
|
|
1383
|
+
return {
|
|
1384
|
+
kind: "released",
|
|
1385
|
+
...typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : {}
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
if (action === "fail") {
|
|
1389
|
+
const failure = requestedFailure;
|
|
1390
|
+
await persistFailedQueueJob(reserved, failure);
|
|
1391
|
+
await driver.delete(reserved);
|
|
1392
|
+
await queueRuntimeInternals.executeRegisteredQueueJobFailedHook(envelope, failure, {
|
|
1393
|
+
maxAttempts
|
|
1394
|
+
});
|
|
1395
|
+
await options.onJobFailed?.({
|
|
1396
|
+
...event,
|
|
1397
|
+
error: failure
|
|
1398
|
+
});
|
|
1399
|
+
return {
|
|
1400
|
+
kind: "failed",
|
|
1401
|
+
error: failure
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
await driver.acknowledge(reserved);
|
|
1405
|
+
await options.onJobProcessed?.(event);
|
|
1406
|
+
return { kind: "processed" };
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
1409
|
+
const failure = resolvedError instanceof QueueWorkerTimeoutError ? resolvedError : requestedFailure ?? resolvedError;
|
|
1410
|
+
if (resolvedError instanceof QueueWorkerTimeoutError) {
|
|
1411
|
+
await persistFailedQueueJob(reserved, failure);
|
|
1412
|
+
await driver.delete(reserved);
|
|
1413
|
+
await queueRuntimeInternals.executeRegisteredQueueJobFailedHook(envelope, failure, {
|
|
1414
|
+
maxAttempts
|
|
1415
|
+
});
|
|
1416
|
+
await options.onJobFailed?.({
|
|
1417
|
+
...event,
|
|
1418
|
+
error: failure
|
|
1419
|
+
});
|
|
1420
|
+
return {
|
|
1421
|
+
kind: "failed",
|
|
1422
|
+
error: failure
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
if (action === "release") {
|
|
1426
|
+
await driver.release(reserved, typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : void 0);
|
|
1427
|
+
await options.onJobReleased?.({
|
|
1428
|
+
...event,
|
|
1429
|
+
...typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : {},
|
|
1430
|
+
error: failure
|
|
1431
|
+
});
|
|
1432
|
+
return {
|
|
1433
|
+
kind: "released",
|
|
1434
|
+
...typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : {},
|
|
1435
|
+
error: failure
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
if (action === "fail" || event.attempt >= maxAttempts) {
|
|
1439
|
+
await persistFailedQueueJob(reserved, failure);
|
|
1440
|
+
await driver.delete(reserved);
|
|
1441
|
+
await queueRuntimeInternals.executeRegisteredQueueJobFailedHook(envelope, failure, {
|
|
1442
|
+
maxAttempts
|
|
1443
|
+
});
|
|
1444
|
+
await options.onJobFailed?.({
|
|
1445
|
+
...event,
|
|
1446
|
+
error: failure
|
|
1447
|
+
});
|
|
1448
|
+
return {
|
|
1449
|
+
kind: "failed",
|
|
1450
|
+
error: failure
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const delaySeconds = resolveRetryDelaySeconds(definition, event.attempt);
|
|
1454
|
+
await driver.release(reserved, typeof delaySeconds === "number" ? { delaySeconds } : void 0);
|
|
1455
|
+
await options.onJobReleased?.({
|
|
1456
|
+
...event,
|
|
1457
|
+
...typeof delaySeconds === "number" ? { delaySeconds } : {},
|
|
1458
|
+
error: failure
|
|
1459
|
+
});
|
|
1460
|
+
return {
|
|
1461
|
+
kind: "released",
|
|
1462
|
+
...typeof delaySeconds === "number" ? { delaySeconds } : {},
|
|
1463
|
+
error: failure
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function resolveWorkerStopReason(options, state) {
|
|
1468
|
+
const now = Date.now();
|
|
1469
|
+
if (typeof options.maxJobs === "number" && state.processed >= options.maxJobs) {
|
|
1470
|
+
return "max-jobs";
|
|
1471
|
+
}
|
|
1472
|
+
if (typeof options.maxTime === "number" && now - state.startedAt >= options.maxTime * 1e3) {
|
|
1473
|
+
return "max-time";
|
|
1474
|
+
}
|
|
1475
|
+
return void 0;
|
|
1476
|
+
}
|
|
1477
|
+
async function runQueueWorker(options = {}) {
|
|
1478
|
+
const connectionName = options.connection ?? queueRuntimeInternals.getQueueRuntimeState().config.default;
|
|
1479
|
+
const connection = queueRuntimeInternals.resolveConnectionConfig(
|
|
1480
|
+
queueRuntimeInternals.getQueueRuntimeState().config,
|
|
1481
|
+
connectionName
|
|
1482
|
+
);
|
|
1483
|
+
const queueNames = normalizeQueueNames(options.queueNames, connection.queue);
|
|
1484
|
+
const sleepSeconds = normalizeNonNegativeInteger(options.sleep, "Queue worker sleep") ?? ("sleep" in connection && typeof connection.sleep === "number" ? connection.sleep : 1);
|
|
1485
|
+
const maxJobs = normalizePositiveInteger(options.maxJobs, "Queue worker maxJobs");
|
|
1486
|
+
const maxTime = normalizePositiveInteger(options.maxTime, "Queue worker maxTime");
|
|
1487
|
+
const workerId = options.workerId?.trim() || randomUUID3();
|
|
1488
|
+
const sleepFn = options.sleepFn ?? sleep;
|
|
1489
|
+
const driver = requireAsyncDriver(queueRuntimeInternals.resolveConnectionDriver(connection.name), connection.name);
|
|
1490
|
+
const startedAt = Date.now();
|
|
1491
|
+
let processed = 0;
|
|
1492
|
+
let released = 0;
|
|
1493
|
+
let failed = 0;
|
|
1494
|
+
const normalizedOptions = {
|
|
1495
|
+
...options,
|
|
1496
|
+
connection: connection.name,
|
|
1497
|
+
maxJobs,
|
|
1498
|
+
maxTime
|
|
1499
|
+
};
|
|
1500
|
+
while (true) {
|
|
1501
|
+
if (await options.shouldStop?.()) {
|
|
1502
|
+
return {
|
|
1503
|
+
processed,
|
|
1504
|
+
released,
|
|
1505
|
+
failed,
|
|
1506
|
+
stoppedBecause: "signal"
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
const stopReason = resolveWorkerStopReason(normalizedOptions, {
|
|
1510
|
+
processed: processed + released + failed,
|
|
1511
|
+
startedAt
|
|
1512
|
+
});
|
|
1513
|
+
if (stopReason) {
|
|
1514
|
+
return {
|
|
1515
|
+
processed,
|
|
1516
|
+
released,
|
|
1517
|
+
failed,
|
|
1518
|
+
stoppedBecause: stopReason
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const reserved = await driver.reserve({
|
|
1522
|
+
queueNames,
|
|
1523
|
+
workerId
|
|
1524
|
+
});
|
|
1525
|
+
if (!reserved) {
|
|
1526
|
+
await options.onIdle?.();
|
|
1527
|
+
if (options.once === true) {
|
|
1528
|
+
return {
|
|
1529
|
+
processed,
|
|
1530
|
+
released,
|
|
1531
|
+
failed,
|
|
1532
|
+
stoppedBecause: "once"
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
if (options.stopWhenEmpty === true) {
|
|
1536
|
+
return {
|
|
1537
|
+
processed,
|
|
1538
|
+
released,
|
|
1539
|
+
failed,
|
|
1540
|
+
stoppedBecause: "empty"
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
if (sleepSeconds > 0) {
|
|
1544
|
+
await sleepFn(sleepSeconds * 1e3);
|
|
1545
|
+
}
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const outcome = await processReservedJob(driver, reserved, options);
|
|
1549
|
+
if (outcome.kind === "processed") {
|
|
1550
|
+
processed += 1;
|
|
1551
|
+
} else if (outcome.kind === "released") {
|
|
1552
|
+
released += 1;
|
|
1553
|
+
} else {
|
|
1554
|
+
failed += 1;
|
|
1555
|
+
}
|
|
1556
|
+
if (options.once === true) {
|
|
1557
|
+
return {
|
|
1558
|
+
processed,
|
|
1559
|
+
released,
|
|
1560
|
+
failed,
|
|
1561
|
+
stoppedBecause: "once"
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
async function clearQueueConnection(connectionName, input = {}) {
|
|
1567
|
+
const connection = queueRuntimeInternals.resolveConnectionConfig(
|
|
1568
|
+
queueRuntimeInternals.getQueueRuntimeState().config,
|
|
1569
|
+
connectionName
|
|
1570
|
+
);
|
|
1571
|
+
const driver = queueRuntimeInternals.resolveConnectionDriver(connection.name);
|
|
1572
|
+
return await driver.clear(input);
|
|
1573
|
+
}
|
|
1574
|
+
var queueWorkerInternals = {
|
|
1575
|
+
createWorkerEvent,
|
|
1576
|
+
normalizeNonNegativeInteger,
|
|
1577
|
+
normalizePositiveInteger,
|
|
1578
|
+
normalizeQueueNames,
|
|
1579
|
+
processReservedJob,
|
|
1580
|
+
requireAsyncDriver,
|
|
1581
|
+
requireRegisteredDefinition,
|
|
1582
|
+
resolveRetryDelaySeconds,
|
|
1583
|
+
resolveWorkerStopReason,
|
|
1584
|
+
runWithTimeout,
|
|
1585
|
+
sleep
|
|
1586
|
+
};
|
|
1587
|
+
export {
|
|
1588
|
+
DEFAULT_DATABASE_QUEUE_TABLE,
|
|
1589
|
+
DEFAULT_FAILED_JOBS_CONNECTION,
|
|
1590
|
+
DEFAULT_FAILED_JOBS_TABLE,
|
|
1591
|
+
DEFAULT_QUEUE_BLOCK_FOR,
|
|
1592
|
+
DEFAULT_QUEUE_CONNECTION,
|
|
1593
|
+
DEFAULT_QUEUE_NAME,
|
|
1594
|
+
DEFAULT_QUEUE_RETRY_AFTER,
|
|
1595
|
+
DEFAULT_QUEUE_SLEEP,
|
|
1596
|
+
Queue,
|
|
1597
|
+
QueueFailedStoreError,
|
|
1598
|
+
QueueReleaseUnsupportedError,
|
|
1599
|
+
QueueWorkerTimeoutError,
|
|
1600
|
+
QueueWorkerUnsupportedDriverError,
|
|
1601
|
+
RedisQueueDriver,
|
|
1602
|
+
RedisQueueDriverError,
|
|
1603
|
+
clearQueueConnection,
|
|
1604
|
+
configureQueueRuntime,
|
|
1605
|
+
defineJob,
|
|
1606
|
+
dispatch,
|
|
1607
|
+
dispatchSync,
|
|
1608
|
+
flushFailedQueueJobs,
|
|
1609
|
+
forgetFailedQueueJob,
|
|
1610
|
+
getQueueRuntime,
|
|
1611
|
+
getRegisteredQueueJob,
|
|
1612
|
+
holoQueueDefaults,
|
|
1613
|
+
isQueueJobDefinition,
|
|
1614
|
+
listFailedQueueJobs,
|
|
1615
|
+
listRegisteredQueueJobs,
|
|
1616
|
+
normalizeQueueConfig,
|
|
1617
|
+
normalizeQueueJobDefinition,
|
|
1618
|
+
persistFailedQueueJob,
|
|
1619
|
+
queueFailedInternals,
|
|
1620
|
+
queueInternals,
|
|
1621
|
+
queueJobInternals,
|
|
1622
|
+
queueRegistryInternals,
|
|
1623
|
+
queueRuntimeInternals,
|
|
1624
|
+
queueWorkerInternals,
|
|
1625
|
+
redisQueueDriverFactory,
|
|
1626
|
+
redisQueueDriverInternals,
|
|
1627
|
+
registerQueueJob,
|
|
1628
|
+
registerQueueJobs,
|
|
1629
|
+
resetQueueRegistry,
|
|
1630
|
+
resetQueueRuntime,
|
|
1631
|
+
retryFailedQueueJobs,
|
|
1632
|
+
runQueueWorker,
|
|
1633
|
+
shutdownQueueRuntime,
|
|
1634
|
+
syncQueueDriverFactory,
|
|
1635
|
+
syncQueueDriverInternals,
|
|
1636
|
+
unregisterQueueJob,
|
|
1637
|
+
useQueueConnection
|
|
1638
|
+
};
|