@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.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
+ };