@holo-js/queue 0.1.1 → 0.1.3

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 CHANGED
@@ -1,5 +1,3 @@
1
- import { Job, ConnectionOptions } from 'bullmq';
2
-
3
1
  type QueueJsonPrimitive = string | number | boolean | null;
4
2
  type QueueJsonValue = QueueJsonPrimitive | readonly QueueJsonValue[] | {
5
3
  readonly [key: string]: QueueJsonValue;
@@ -102,7 +100,7 @@ interface QueuePendingDispatch<TPayload extends QueueJsonValue = QueueJsonValue>
102
100
  interface QueueConnectionFacade {
103
101
  readonly name: string;
104
102
  dispatch<TJobName extends KnownQueueJobName>(jobName: TJobName, payload: QueuePayloadFor<TJobName>, options?: QueueDispatchOptions): QueuePendingDispatch<QueuePayloadFor<TJobName>>;
105
- dispatch<TPayload extends QueueJsonValue = QueueJsonValue>(jobName: string, payload: TPayload, options?: QueueDispatchOptions): QueuePendingDispatch<TPayload>;
103
+ dispatch<TJobName extends Exclude<string, KnownQueueJobName>, TPayload extends QueueJsonValue = QueueJsonValue>(jobName: TJobName, payload: TPayload, options?: QueueDispatchOptions): QueuePendingDispatch<TPayload>;
106
104
  dispatchSync<TJobName extends KnownQueueJobName>(jobName: TJobName, payload: QueuePayloadFor<TJobName>): Promise<QueueResultFor<TJobName>>;
107
105
  dispatchSync<TPayload extends QueueJsonValue = QueueJsonValue, TResult = unknown>(jobName: string, payload: TPayload): Promise<TResult>;
108
106
  }
@@ -225,10 +223,17 @@ interface QueueFailedStoreConfig {
225
223
  }
226
224
  interface QueueRedisConnectionConfig {
227
225
  readonly driver: 'redis';
226
+ readonly connection?: string;
228
227
  readonly queue?: string;
229
228
  readonly retryAfter?: number | string;
230
229
  readonly blockFor?: number | string;
231
230
  readonly redis?: {
231
+ readonly url?: string;
232
+ readonly clusters?: readonly {
233
+ readonly url?: string;
234
+ readonly host?: string;
235
+ readonly port?: number | string;
236
+ }[];
232
237
  readonly host?: string;
233
238
  readonly port?: number | string;
234
239
  readonly password?: string;
@@ -267,10 +272,17 @@ interface NormalizedQueueSyncConnectionConfig {
267
272
  interface NormalizedQueueRedisConnectionConfig {
268
273
  readonly name: string;
269
274
  readonly driver: 'redis';
275
+ readonly connection: string;
270
276
  readonly queue: string;
271
277
  readonly retryAfter: number;
272
278
  readonly blockFor: number;
273
279
  readonly redis: {
280
+ readonly url?: string;
281
+ readonly clusters?: readonly {
282
+ readonly url?: string;
283
+ readonly host: string;
284
+ readonly port: number;
285
+ }[];
274
286
  readonly host: string;
275
287
  readonly port: number;
276
288
  readonly password?: string;
@@ -278,6 +290,24 @@ interface NormalizedQueueRedisConnectionConfig {
278
290
  readonly db: number;
279
291
  };
280
292
  }
293
+ interface QueueSharedRedisConnectionConfig {
294
+ readonly name: string;
295
+ readonly url?: string;
296
+ readonly clusters?: readonly {
297
+ readonly url?: string;
298
+ readonly host: string;
299
+ readonly port: number;
300
+ }[];
301
+ readonly host: string;
302
+ readonly port: number;
303
+ readonly password?: string;
304
+ readonly username?: string;
305
+ readonly db: number;
306
+ }
307
+ interface QueueSharedRedisConfig {
308
+ readonly default: string;
309
+ readonly connections: Readonly<Record<string, QueueSharedRedisConnectionConfig>>;
310
+ }
281
311
  interface NormalizedQueueDatabaseConnectionConfig {
282
312
  readonly name: string;
283
313
  readonly driver: 'database';
@@ -312,7 +342,7 @@ declare const DEFAULT_DATABASE_QUEUE_TABLE = "jobs";
312
342
  declare function parseInteger(value: number | string | undefined, fallback: number, label: string, options?: {
313
343
  minimum?: number;
314
344
  }): number;
315
- declare function normalizeQueueConfig(config?: HoloQueueConfig): NormalizedHoloQueueConfig;
345
+ declare function normalizeQueueConfig(config?: HoloQueueConfig, redisConfig?: QueueSharedRedisConfig): NormalizedHoloQueueConfig;
316
346
  declare const holoQueueDefaults: Readonly<NormalizedHoloQueueConfig>;
317
347
  declare const queueInternals: {
318
348
  parseInteger: typeof parseInteger;
@@ -332,54 +362,64 @@ declare const queueRegistryInternals: {
332
362
  deriveJobNameFromSourcePath: typeof deriveJobNameFromSourcePath;
333
363
  };
334
364
 
365
+ type RedisDriverModule = {
366
+ redisQueueDriverFactory: QueueDriverFactory<NormalizedQueueRedisConnectionConfig>;
367
+ };
335
368
  type RedisQueuedEnvelope = QueueJobEnvelope<QueueJsonValue>;
336
- type BullJobInstance = Job<RedisQueuedEnvelope, unknown, string>;
369
+ declare function loadRedisDriverModule(): Promise<RedisDriverModule>;
337
370
  declare function normalizeRedisErrorMessage(error: unknown): string;
338
- declare function isQueueEnvelope(value: unknown): value is QueueJobEnvelope<QueueJsonValue>;
339
- declare function resolveBullConnectionOptions(connection: NormalizedQueueRedisConnectionConfig): ConnectionOptions;
371
+ declare function isQueueEnvelope(value: unknown): value is RedisQueuedEnvelope;
372
+ type RedisConnectionOptions = {
373
+ url?: string;
374
+ clusters?: readonly {
375
+ readonly url?: string;
376
+ readonly host: string;
377
+ readonly port: number;
378
+ }[];
379
+ host?: string;
380
+ port?: number;
381
+ path?: string;
382
+ username?: string;
383
+ password?: string;
384
+ db: number;
385
+ maxRetriesPerRequest: null;
386
+ };
387
+ declare function toRedisSocketPath(value: string): string;
388
+ declare function resolveBullConnectionOptions(connection: NormalizedQueueRedisConnectionConfig): RedisConnectionOptions;
340
389
  declare class RedisQueueDriverError extends Error {
341
390
  constructor(connectionName: string, action: string, cause: unknown);
342
391
  }
343
392
  declare function wrapRedisError(connectionName: string, action: string, error: unknown): RedisQueueDriverError;
344
- declare function resolveAttempts(job: BullJobInstance): number;
393
+ declare function resolveAttempts(job: {
394
+ attemptsStarted?: number;
395
+ attemptsMade?: number;
396
+ }): number;
345
397
  declare class RedisQueueDriver implements QueueAsyncDriver {
398
+ private readonly connection;
346
399
  private readonly context;
347
400
  readonly name: string;
348
401
  readonly driver: "redis";
349
402
  readonly mode: "async";
350
- private readonly connection;
351
- private readonly bullConnection;
352
- private readonly queues;
353
- private readonly workers;
354
- private readonly reservations;
355
- private queueCursor;
403
+ private driverInstance?;
404
+ private pending?;
356
405
  constructor(connection: NormalizedQueueRedisConnectionConfig, context: QueueDriverFactoryContext);
357
- private getQueue;
358
- private getWorker;
359
- private normalizeQueueNames;
360
- private rotateQueueNames;
361
- private createReservedJob;
362
- private getReservation;
363
- private settleReservation;
406
+ private resolveDriver;
364
407
  dispatch<TPayload extends QueueJsonValue = QueueJsonValue, TResult = unknown>(job: QueueJobEnvelope<TPayload>): Promise<QueueDriverDispatchResult<TResult>>;
365
- reserve<TPayload extends QueueJsonValue = QueueJsonValue>(input: {
366
- readonly queueNames: readonly string[];
367
- readonly workerId: string;
368
- }): Promise<QueueReservedJob<TPayload> | null>;
408
+ reserve<TPayload extends QueueJsonValue = QueueJsonValue>(input: Parameters<QueueAsyncDriver['reserve']>[0]): Promise<QueueReservedJob<TPayload> | null>;
369
409
  acknowledge(job: QueueReservedJob): Promise<void>;
370
410
  release(job: QueueReservedJob, options?: QueueReleaseOptions): Promise<void>;
371
411
  delete(job: QueueReservedJob): Promise<void>;
372
- clear(input?: {
373
- readonly queueNames?: readonly string[];
374
- }): Promise<number>;
412
+ clear(input?: Parameters<QueueAsyncDriver['clear']>[0]): Promise<number>;
375
413
  close(): Promise<void>;
376
414
  }
377
415
  declare const redisQueueDriverFactory: QueueDriverFactory<NormalizedQueueRedisConnectionConfig>;
378
416
  declare const redisQueueDriverInternals: {
379
417
  isQueueEnvelope: typeof isQueueEnvelope;
418
+ loadRedisDriverModule: typeof loadRedisDriverModule;
380
419
  normalizeRedisErrorMessage: typeof normalizeRedisErrorMessage;
381
420
  resolveAttempts: typeof resolveAttempts;
382
421
  resolveBullConnectionOptions: typeof resolveBullConnectionOptions;
422
+ toRedisSocketPath: typeof toRedisSocketPath;
383
423
  wrapRedisError: typeof wrapRedisError;
384
424
  };
385
425
 
@@ -409,6 +449,7 @@ type QueuePayloadValidationState = {
409
449
  };
410
450
  type ConfigureQueueRuntimeOptions = {
411
451
  readonly config?: HoloQueueConfig | NormalizedHoloQueueConfig;
452
+ readonly redisConfig?: QueueSharedRedisConfig;
412
453
  readonly driverFactories?: ReadonlyArray<QueueDriverFactory> | ReadonlyMap<string, QueueDriverFactory>;
413
454
  readonly failedJobStore?: QueueFailedJobStore;
414
455
  };
@@ -446,7 +487,9 @@ declare function resetQueueRuntime(): void;
446
487
  declare function getQueueRuntime(): QueueRuntimeBinding;
447
488
  declare function useQueueConnection(name?: string): QueueConnectionFacade;
448
489
  declare function dispatchSync<TJobName extends Extract<keyof HoloQueueJobRegistry, string>>(jobName: TJobName, payload: QueuePayloadFor<TJobName>, options?: QueueDispatchOptions): Promise<QueueResultFor<TJobName>>;
490
+ declare function dispatchSync<TPayload extends QueueJsonValue = QueueJsonValue, TResult = unknown>(jobName: string, payload: TPayload, options?: QueueDispatchOptions): Promise<TResult>;
449
491
  declare function dispatch<TJobName extends Extract<keyof HoloQueueJobRegistry, string>>(jobName: TJobName, payload: QueuePayloadFor<TJobName>, options?: QueueDispatchOptions): QueuePendingDispatch<QueuePayloadFor<TJobName>>;
492
+ declare function dispatch<TJobName extends Exclude<string, Extract<keyof HoloQueueJobRegistry, string>>, TPayload extends QueueJsonValue = QueueJsonValue>(jobName: TJobName, payload: TPayload, options?: QueueDispatchOptions): QueuePendingDispatch<TPayload>;
450
493
  declare const Queue: {
451
494
  connection(name?: string): QueueConnectionFacade;
452
495
  dispatch: typeof dispatch;
@@ -557,4 +600,6 @@ declare const queueWorkerInternals: {
557
600
  sleep: typeof sleep;
558
601
  };
559
602
 
560
- export { DEFAULT_DATABASE_QUEUE_TABLE, DEFAULT_FAILED_JOBS_CONNECTION, DEFAULT_FAILED_JOBS_TABLE, DEFAULT_QUEUE_BLOCK_FOR, DEFAULT_QUEUE_CONNECTION, DEFAULT_QUEUE_NAME, DEFAULT_QUEUE_RETRY_AFTER, DEFAULT_QUEUE_SLEEP, type ExportedQueueJobDefinition, type HoloQueueConfig, type HoloQueueJobRegistry, type NormalizedHoloQueueConfig, type NormalizedQueueConnectionConfig, type NormalizedQueueDatabaseConnectionConfig, type NormalizedQueueFailedStoreConfig, type NormalizedQueueRedisConnectionConfig, type NormalizedQueueSyncConnectionConfig, Queue, type QueueAsyncDriver, type QueueClearInput, type QueueConnectionConfig, type QueueConnectionFacade, type QueueDatabaseConnectionConfig, type QueueDelayValue, type QueueDispatchCompletedHook, type QueueDispatchFailedHook, type QueueDispatchOptions, type QueueDispatchResult, type QueueDriver, type QueueDriverDispatchResult, type QueueDriverFactory, type QueueDriverFactoryContext, type QueueEnqueueResult, type QueueFailedJobRecord, type QueueFailedJobStore, type QueueFailedStoreConfig, QueueFailedStoreError, type QueueJobContext, type QueueJobContextOverrides, type QueueJobDefinition, type QueueJobEnvelope, type QueueJsonValue, type QueuePayloadFor, type QueuePendingDispatch, type QueueRedisConnectionConfig, type QueueRegisteredJob, type QueueReleaseOptions, QueueReleaseUnsupportedError, type QueueReserveInput, type QueueReservedJob, type QueueResultFor, type QueueRuntimeBinding, type QueueSyncConnectionConfig, type QueueSyncDriver, type QueueWorkerHooks, type QueueWorkerJobEvent, type QueueWorkerOptions, type QueueWorkerResult, type QueueWorkerRunOptions, QueueWorkerTimeoutError, QueueWorkerUnsupportedDriverError, RedisQueueDriver, RedisQueueDriverError, type RegisterQueueJobOptions, type RegisterableQueueJobDefinition, clearQueueConnection, configureQueueRuntime, defineJob, dispatch, dispatchSync, flushFailedQueueJobs, forgetFailedQueueJob, getQueueRuntime, getRegisteredQueueJob, holoQueueDefaults, isQueueJobDefinition, listFailedQueueJobs, listRegisteredQueueJobs, normalizeQueueConfig, normalizeQueueJobDefinition, persistFailedQueueJob, queueFailedInternals, queueInternals, queueJobInternals, queueRegistryInternals, queueRuntimeInternals, queueWorkerInternals, redisQueueDriverFactory, redisQueueDriverInternals, registerQueueJob, registerQueueJobs, resetQueueRegistry, resetQueueRuntime, retryFailedQueueJobs, runQueueWorker, shutdownQueueRuntime, syncQueueDriverFactory, syncQueueDriverInternals, unregisterQueueJob, useQueueConnection };
603
+ declare function defineQueueConfig<TConfig extends HoloQueueConfig>(config: TConfig): Readonly<TConfig>;
604
+
605
+ export { DEFAULT_DATABASE_QUEUE_TABLE, DEFAULT_FAILED_JOBS_CONNECTION, DEFAULT_FAILED_JOBS_TABLE, DEFAULT_QUEUE_BLOCK_FOR, DEFAULT_QUEUE_CONNECTION, DEFAULT_QUEUE_NAME, DEFAULT_QUEUE_RETRY_AFTER, DEFAULT_QUEUE_SLEEP, type ExportedQueueJobDefinition, type HoloQueueConfig, type HoloQueueJobRegistry, type NormalizedHoloQueueConfig, type NormalizedQueueConnectionConfig, type NormalizedQueueDatabaseConnectionConfig, type NormalizedQueueFailedStoreConfig, type NormalizedQueueRedisConnectionConfig, type NormalizedQueueSyncConnectionConfig, Queue, type QueueAsyncDriver, type QueueClearInput, type QueueConnectionConfig, type QueueConnectionFacade, type QueueDatabaseConnectionConfig, type QueueDelayValue, type QueueDispatchCompletedHook, type QueueDispatchFailedHook, type QueueDispatchOptions, type QueueDispatchResult, type QueueDriver, type QueueDriverDispatchResult, type QueueDriverFactory, type QueueDriverFactoryContext, type QueueEnqueueResult, type QueueFailedJobRecord, type QueueFailedJobStore, type QueueFailedStoreConfig, QueueFailedStoreError, type QueueJobContext, type QueueJobContextOverrides, type QueueJobDefinition, type QueueJobEnvelope, type QueueJsonValue, type QueuePayloadFor, type QueuePendingDispatch, type QueueRedisConnectionConfig, type QueueRegisteredJob, type QueueReleaseOptions, QueueReleaseUnsupportedError, type QueueReserveInput, type QueueReservedJob, type QueueResultFor, type QueueRuntimeBinding, type QueueSharedRedisConfig, type QueueSharedRedisConnectionConfig, type QueueSyncConnectionConfig, type QueueSyncDriver, type QueueWorkerHooks, type QueueWorkerJobEvent, type QueueWorkerOptions, type QueueWorkerResult, type QueueWorkerRunOptions, QueueWorkerTimeoutError, QueueWorkerUnsupportedDriverError, RedisQueueDriver, RedisQueueDriverError, type RegisterQueueJobOptions, type RegisterableQueueJobDefinition, clearQueueConnection, configureQueueRuntime, defineJob, defineQueueConfig, dispatch, dispatchSync, flushFailedQueueJobs, forgetFailedQueueJob, getQueueRuntime, getRegisteredQueueJob, holoQueueDefaults, isQueueJobDefinition, listFailedQueueJobs, listRegisteredQueueJobs, normalizeQueueConfig, normalizeQueueJobDefinition, persistFailedQueueJob, queueFailedInternals, queueInternals, queueJobInternals, queueRegistryInternals, queueRuntimeInternals, queueWorkerInternals, redisQueueDriverFactory, redisQueueDriverInternals, registerQueueJob, registerQueueJobs, resetQueueRegistry, resetQueueRuntime, retryFailedQueueJobs, runQueueWorker, shutdownQueueRuntime, syncQueueDriverFactory, syncQueueDriverInternals, unregisterQueueJob, useQueueConnection };
package/dist/index.mjs CHANGED
@@ -134,11 +134,34 @@ function normalizeSyncConnection(name, config) {
134
134
  queue: normalizeQueueName(config.queue)
135
135
  });
136
136
  }
137
- function normalizeRedisConnection(name, config) {
138
- const redis = config.redis ?? {};
137
+ function resolveSharedRedisConnection(redisConfig, connectionName) {
138
+ const resolvedConnection = redisConfig.connections[connectionName];
139
+ if (!resolvedConnection) {
140
+ const availableConnections = Object.keys(redisConfig.connections);
141
+ throw new Error(
142
+ `[Holo Queue] Queue Redis connection "${connectionName}" was not found in shared Redis config. Available connections: ${availableConnections.join(", ") || "(none)"}.`
143
+ );
144
+ }
145
+ return resolvedConnection;
146
+ }
147
+ function normalizeRedisConnection(name, config, redisConfig) {
148
+ const explicitConnectionName = config.connection?.trim();
149
+ const connectionName = explicitConnectionName || redisConfig?.default;
150
+ if (!connectionName) {
151
+ throw new Error(
152
+ `[Holo Queue] Queue Redis connection "${name}" requires a shared Redis config with a default connection or an explicit connection name.`
153
+ );
154
+ }
155
+ if (!redisConfig) {
156
+ throw new Error(
157
+ `[Holo Queue] Queue Redis connection "${name}" references shared Redis connection "${connectionName}" but no shared Redis config was provided.`
158
+ );
159
+ }
160
+ const resolvedRedisConnection = resolveSharedRedisConnection(redisConfig, connectionName);
139
161
  return Object.freeze({
140
162
  name,
141
163
  driver: "redis",
164
+ connection: resolvedRedisConnection.name,
142
165
  queue: normalizeQueueName(config.queue),
143
166
  retryAfter: parseInteger(config.retryAfter, DEFAULT_QUEUE_RETRY_AFTER, `queue connection "${name}" retryAfter`, {
144
167
  minimum: 0
@@ -147,15 +170,13 @@ function normalizeRedisConnection(name, config) {
147
170
  minimum: 0
148
171
  }),
149
172
  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
- })
173
+ ...typeof resolvedRedisConnection.url === "undefined" ? {} : { url: resolvedRedisConnection.url },
174
+ ...typeof resolvedRedisConnection.clusters === "undefined" ? {} : { clusters: resolvedRedisConnection.clusters },
175
+ host: resolvedRedisConnection.host,
176
+ port: resolvedRedisConnection.port,
177
+ password: resolvedRedisConnection.password,
178
+ username: resolvedRedisConnection.username,
179
+ db: resolvedRedisConnection.db
159
180
  })
160
181
  });
161
182
  }
@@ -174,25 +195,25 @@ function normalizeDatabaseConnection(name, config) {
174
195
  table: config.table?.trim() || DEFAULT_DATABASE_QUEUE_TABLE
175
196
  });
176
197
  }
177
- function normalizeConnectionConfig(name, config) {
198
+ function normalizeConnectionConfig(name, config, redisConfig) {
178
199
  switch (config.driver) {
179
200
  case "sync":
180
201
  return normalizeSyncConnection(name, config);
181
202
  case "redis":
182
- return normalizeRedisConnection(name, config);
203
+ return normalizeRedisConnection(name, config, redisConfig);
183
204
  case "database":
184
205
  return normalizeDatabaseConnection(name, config);
185
206
  default:
186
207
  throw new Error(`[Holo Queue] Unsupported queue driver "${String(config.driver)}" on connection "${name}".`);
187
208
  }
188
209
  }
189
- function normalizeConnections(connections) {
210
+ function normalizeConnections(connections, redisConfig) {
190
211
  if (!connections || Object.keys(connections).length === 0) {
191
212
  return DEFAULT_QUEUE_CONFIG.connections;
192
213
  }
193
214
  const normalizedEntries = Object.entries(connections).map(([name, config]) => {
194
215
  const normalizedName = normalizeConnectionName(name, "Queue connection name");
195
- return [normalizedName, normalizeConnectionConfig(normalizedName, config)];
216
+ return [normalizedName, normalizeConnectionConfig(normalizedName, config, redisConfig)];
196
217
  });
197
218
  return Object.freeze(Object.fromEntries(normalizedEntries));
198
219
  }
@@ -210,8 +231,8 @@ function normalizeFailedStore(config) {
210
231
  table: normalized.table?.trim() || DEFAULT_FAILED_JOBS_TABLE
211
232
  });
212
233
  }
213
- function normalizeQueueConfig(config = {}) {
214
- const connections = normalizeConnections(config.connections);
234
+ function normalizeQueueConfig(config = {}, redisConfig) {
235
+ const connections = normalizeConnections(config.connections, redisConfig);
215
236
  const connectionNames = Object.keys(connections);
216
237
  const defaultConnection = config.default?.trim() || connectionNames[0];
217
238
  if (!connections[defaultConnection]) {
@@ -298,11 +319,22 @@ var queueRegistryInternals = {
298
319
  };
299
320
 
300
321
  // src/drivers/redis.ts
301
- import { randomUUID } from "crypto";
302
- import {
303
- Queue as BullQueue,
304
- Worker as BullWorker
305
- } from "bullmq";
322
+ function isModuleNotFoundError(error) {
323
+ return !!error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND";
324
+ }
325
+ async function loadRedisDriverModule() {
326
+ try {
327
+ const specifier = "@holo-js/queue-redis";
328
+ return await import(specifier);
329
+ } catch (error) {
330
+ if (isModuleNotFoundError(error)) {
331
+ throw new Error("[@holo-js/queue] Redis queue support requires @holo-js/queue-redis to be installed.", {
332
+ cause: error
333
+ });
334
+ }
335
+ throw error;
336
+ }
337
+ }
306
338
  function normalizeRedisErrorMessage(error) {
307
339
  if (error instanceof Error) {
308
340
  return error.message;
@@ -315,10 +347,19 @@ function isRecord(value) {
315
347
  function isQueueEnvelope(value) {
316
348
  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
349
  }
350
+ function isRedisSocketConnectionTarget(value) {
351
+ return value.startsWith("unix://") || value.startsWith("/");
352
+ }
353
+ function toRedisSocketPath(value) {
354
+ return value.startsWith("unix://") ? value.slice("unix://".length) : value;
355
+ }
318
356
  function resolveBullConnectionOptions(connection) {
357
+ const redisHost = connection.redis.host;
319
358
  return {
320
- host: connection.redis.host,
321
- port: connection.redis.port,
359
+ ...typeof connection.redis.url === "string" ? { url: connection.redis.url } : connection.redis.clusters && connection.redis.clusters.length > 0 ? { clusters: connection.redis.clusters } : typeof redisHost === "string" && isRedisSocketConnectionTarget(redisHost) ? { path: toRedisSocketPath(redisHost) } : {
360
+ host: redisHost,
361
+ port: connection.redis.port
362
+ },
322
363
  username: connection.redis.username,
323
364
  password: connection.redis.password,
324
365
  db: connection.redis.db,
@@ -341,230 +382,64 @@ function wrapRedisError(connectionName, action, error) {
341
382
  return new RedisQueueDriverError(connectionName, action, error);
342
383
  }
343
384
  function resolveAttempts(job) {
385
+ const attemptsStarted = typeof job.attemptsStarted === "number" && Number.isInteger(job.attemptsStarted) ? job.attemptsStarted : 0;
386
+ const attemptsMade = typeof job.attemptsMade === "number" && Number.isInteger(job.attemptsMade) ? job.attemptsMade : 0;
344
387
  return Math.max(
345
- Number.isInteger(job.attemptsStarted) ? job.attemptsStarted - 1 : 0,
346
- Number.isInteger(job.attemptsMade) ? job.attemptsMade : 0,
388
+ attemptsStarted > 0 ? attemptsStarted - 1 : 0,
389
+ attemptsMade,
347
390
  0
348
391
  );
349
392
  }
350
393
  var RedisQueueDriver = class {
351
394
  constructor(connection, context) {
395
+ this.connection = connection;
352
396
  this.context = context;
353
397
  this.name = connection.name;
354
- this.connection = connection;
355
- this.bullConnection = resolveBullConnectionOptions(connection);
356
398
  }
357
399
  name;
358
400
  driver = "redis";
359
401
  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;
402
+ driverInstance;
403
+ pending;
404
+ async resolveDriver() {
405
+ if (this.driverInstance) {
406
+ return this.driverInstance;
385
407
  }
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 }
408
+ this.pending ??= loadRedisDriverModule().then((module) => {
409
+ const driver = module.redisQueueDriverFactory.create(this.connection, this.context);
410
+ if (driver.mode !== "async") {
411
+ throw new Error("[Holo Queue] Redis queue driver must be async.");
397
412
  }
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
413
+ this.driverInstance = driver;
414
+ return driver;
415
+ }).finally(() => {
416
+ this.pending = void 0;
446
417
  });
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
- }
418
+ return this.pending;
469
419
  }
470
420
  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
- }
421
+ return (await this.resolveDriver()).dispatch(job);
488
422
  }
489
423
  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
- }
424
+ return (await this.resolveDriver()).reserve(input);
514
425
  }
515
426
  async acknowledge(job) {
516
- await this.settleReservation(job, "acknowledge job", async (reservation) => {
517
- await reservation.job.moveToCompleted(null, reservation.token, false);
518
- });
427
+ await (await this.resolveDriver()).acknowledge(job);
519
428
  }
520
429
  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
- });
430
+ await (await this.resolveDriver()).release(job, options);
528
431
  }
529
432
  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
- });
433
+ await (await this.resolveDriver()).delete(job);
534
434
  }
535
435
  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
- }
436
+ return (await this.resolveDriver()).clear(input);
548
437
  }
549
438
  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);
439
+ if (!this.driverInstance && !this.pending) {
440
+ return;
567
441
  }
442
+ await (await this.resolveDriver()).close();
568
443
  }
569
444
  };
570
445
  var redisQueueDriverFactory = {
@@ -575,9 +450,11 @@ var redisQueueDriverFactory = {
575
450
  };
576
451
  var redisQueueDriverInternals = {
577
452
  isQueueEnvelope,
453
+ loadRedisDriverModule,
578
454
  normalizeRedisErrorMessage,
579
455
  resolveAttempts,
580
456
  resolveBullConnectionOptions,
457
+ toRedisSocketPath,
581
458
  wrapRedisError
582
459
  };
583
460
 
@@ -616,7 +493,7 @@ var syncQueueDriverInternals = {
616
493
  };
617
494
 
618
495
  // src/runtime.ts
619
- import { randomUUID as randomUUID2 } from "crypto";
496
+ import { randomUUID } from "crypto";
620
497
  var INLINE_SYNC_DRIVER_KEY = "__inline_sync__";
621
498
  var QueueReleaseUnsupportedError = class extends Error {
622
499
  constructor() {
@@ -683,6 +560,11 @@ function normalizeQueueName2(name) {
683
560
  }
684
561
  return normalized;
685
562
  }
563
+ function isNormalizedQueueConfig(config) {
564
+ return typeof config.default === "string" && typeof config.connections === "object" && config.connections !== null && Object.values(config.connections).every((connection) => {
565
+ return typeof connection === "object" && connection !== null && "name" in connection && "driver" in connection && "queue" in connection && typeof connection.name === "string" && typeof connection.driver === "string" && typeof connection.queue === "string";
566
+ });
567
+ }
686
568
  function normalizeDispatchCompletedHook(callback) {
687
569
  if (typeof callback !== "function") {
688
570
  throw new Error("[Holo Queue] Queue dispatch onComplete hook must be a function.");
@@ -901,7 +783,7 @@ function createJobEnvelope(jobName, payload, options) {
901
783
  );
902
784
  const queue = normalizeQueueName2(options.queue ?? registered.definition.queue ?? connection.queue ?? DEFAULT_QUEUE_NAME);
903
785
  return Object.freeze({
904
- id: randomUUID2(),
786
+ id: randomUUID(),
905
787
  name: registered.name,
906
788
  connection: connection.name,
907
789
  queue,
@@ -1041,7 +923,7 @@ function configureQueueRuntime(options = {}) {
1041
923
  const state = getQueueRuntimeState();
1042
924
  let shouldResetDrivers = false;
1043
925
  if (options.config) {
1044
- state.config = normalizeQueueConfig(options.config);
926
+ state.config = isNormalizedQueueConfig(options.config) ? options.config : normalizeQueueConfig(options.config, options.redisConfig);
1045
927
  shouldResetDrivers = true;
1046
928
  }
1047
929
  if (options.driverFactories) {
@@ -1241,7 +1123,7 @@ var queueFailedInternals = {
1241
1123
  };
1242
1124
 
1243
1125
  // src/worker.ts
1244
- import { randomUUID as randomUUID3 } from "crypto";
1126
+ import { randomUUID as randomUUID2 } from "crypto";
1245
1127
  var QueueWorkerUnsupportedDriverError = class extends Error {
1246
1128
  constructor(connectionName, driverName) {
1247
1129
  super(
@@ -1484,7 +1366,7 @@ async function runQueueWorker(options = {}) {
1484
1366
  const sleepSeconds = normalizeNonNegativeInteger(options.sleep, "Queue worker sleep") ?? ("sleep" in connection && typeof connection.sleep === "number" ? connection.sleep : 1);
1485
1367
  const maxJobs = normalizePositiveInteger(options.maxJobs, "Queue worker maxJobs");
1486
1368
  const maxTime = normalizePositiveInteger(options.maxTime, "Queue worker maxTime");
1487
- const workerId = options.workerId?.trim() || randomUUID3();
1369
+ const workerId = options.workerId?.trim() || randomUUID2();
1488
1370
  const sleepFn = options.sleepFn ?? sleep;
1489
1371
  const driver = requireAsyncDriver(queueRuntimeInternals.resolveConnectionDriver(connection.name), connection.name);
1490
1372
  const startedAt = Date.now();
@@ -1584,6 +1466,14 @@ var queueWorkerInternals = {
1584
1466
  runWithTimeout,
1585
1467
  sleep
1586
1468
  };
1469
+
1470
+ // src/index.ts
1471
+ function defineConfig(config) {
1472
+ return Object.freeze({ ...config });
1473
+ }
1474
+ function defineQueueConfig(config) {
1475
+ return defineConfig(config);
1476
+ }
1587
1477
  export {
1588
1478
  DEFAULT_DATABASE_QUEUE_TABLE,
1589
1479
  DEFAULT_FAILED_JOBS_CONNECTION,
@@ -1603,6 +1493,7 @@ export {
1603
1493
  clearQueueConnection,
1604
1494
  configureQueueRuntime,
1605
1495
  defineJob,
1496
+ defineQueueConfig,
1606
1497
  dispatch,
1607
1498
  dispatchSync,
1608
1499
  flushFailedQueueJobs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/queue",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Holo-JS Framework - queue contracts and config helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,10 +22,16 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit",
23
23
  "test": "vitest --run"
24
24
  },
25
- "dependencies": {
26
- "bullmq": "^5.71.0"
25
+ "peerDependencies": {
26
+ "@holo-js/queue-redis": "^0.1.3"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "@holo-js/queue-redis": {
30
+ "optional": true
31
+ }
27
32
  },
28
33
  "devDependencies": {
34
+ "@holo-js/queue-redis": "workspace:*",
29
35
  "@types/node": "^22.10.2",
30
36
  "tsup": "^8.3.5",
31
37
  "typescript": "^5.7.2",