@12-apps/jobs 2.0.0 → 4.0.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.
@@ -7,10 +7,10 @@
7
7
  * never the ledger. Payloads carry ids (see `JobDefinition`'s payload rule),
8
8
  * every handler re-reads its row, and every job is paired with a durable table
9
9
  * a sweep can re-derive the work from. Losing Redis therefore costs a delayed
10
- * run — not a lost charge, and not a double one.
10
+ * run — not a lost write, and not a double one.
11
11
  *
12
12
  * That split is what makes the enqueue-outside-the-transaction problem
13
- * survivable. A Prisma commit followed by an enqueue is not atomic: crash in
13
+ * survivable. A database commit followed by an enqueue is not atomic: crash in
14
14
  * between and the job never lands. The paired sweep is the answer — the row is
15
15
  * committed, so the next tick finds it.
16
16
  *
@@ -25,32 +25,29 @@
25
25
 
26
26
  import { Queue, UnrecoverableError, Worker, type JobsOptions } from "bullmq";
27
27
 
28
+ import { createEventEmitter, type EmitJobEvent } from "../core/events";
29
+ import { DEFAULT_QUEUE } from "../core/queues";
30
+ import { resolveRegisteredJob } from "../core/registry";
28
31
  import type {
29
32
  AnyJobDefinition,
30
33
  EnqueueOptions,
31
34
  EnqueueResult,
32
35
  JobContext,
33
36
  JobDriver,
37
+ JobEvents,
34
38
  JobLogger,
39
+ JobRetention,
35
40
  } from "../core/types";
36
41
 
42
+ import {
43
+ DEFAULT_CONCURRENCY,
44
+ DEFAULT_JOB_RETENTION,
45
+ isTerminalFailure,
46
+ resolveConcurrency,
47
+ retentionOptions,
48
+ } from "./bullmq-policy";
37
49
  import { parseRedisUrl, type RedisConnectionOptions } from "./redis-url";
38
50
 
39
- /** The queue a definition lands on when it does not name one. */
40
- const DEFAULT_QUEUE = "default";
41
- /** Worker concurrency when no definition in the queue asks for more. */
42
- const DEFAULT_CONCURRENCY = 5;
43
-
44
- /**
45
- * Retention. Bounded on purpose: an unbounded completed-set is the classic way
46
- * a small Redis fills up and starts refusing writes. A day of successes is
47
- * enough to answer "did it run?"; a week of failures is enough to debug one.
48
- */
49
- const RETENTION: Pick<JobsOptions, "removeOnComplete" | "removeOnFail"> = {
50
- removeOnComplete: { age: 24 * 3600, count: 1_000 },
51
- removeOnFail: { age: 7 * 24 * 3600, count: 5_000 },
52
- };
53
-
54
51
  /** The one ioredis command this driver reads outside BullMQ's own surface. */
55
52
  interface RedisConfigReader {
56
53
  config(command: "GET", parameter: string): Promise<unknown>;
@@ -65,6 +62,12 @@ export interface BullMqJobDriverOptions {
65
62
  * worker consuming production's jobs.
66
63
  */
67
64
  prefix?: string;
65
+ /** Defaults to {@link DEFAULT_JOB_RETENTION}. */
66
+ retention?: JobRetention;
67
+ /** Per-queue concurrency when no definition on the queue states one. */
68
+ defaultConcurrency?: number;
69
+ /** Where completions, dead-letters and removed schedules are reported. */
70
+ events?: JobEvents;
68
71
  }
69
72
 
70
73
  /** Everything the driver's helpers need, threaded instead of closed over. */
@@ -72,18 +75,23 @@ interface DriverState {
72
75
  connection: RedisConnectionOptions;
73
76
  logger: JobLogger;
74
77
  prefix?: string;
78
+ retention: Pick<JobsOptions, "removeOnComplete" | "removeOnFail">;
79
+ defaultConcurrency: number;
80
+ /** Reports to the host's observer; see `core/events`. Never throws. */
81
+ emit: EmitJobEvent;
75
82
  queues: Map<string, Queue>;
76
83
  workers: Worker[];
77
84
  }
78
85
 
79
86
  /** Turn a definition's retry policy into BullMQ's per-job options. */
80
87
  function jobOptionsFor(
88
+ state: DriverState,
81
89
  definition: AnyJobDefinition,
82
90
  enqueueOptions: EnqueueOptions,
83
91
  ): JobsOptions {
84
92
  const options: JobsOptions = {
85
93
  attempts: Math.max(1, definition.attempts ?? 1),
86
- ...RETENTION,
94
+ ...state.retention,
87
95
  };
88
96
  if (definition.backoff) {
89
97
  options.backoff = {
@@ -108,7 +116,7 @@ function queueFor(state: DriverState, name: string): Queue {
108
116
  const queue = new Queue(name, {
109
117
  connection: state.connection,
110
118
  ...(state.prefix ? { prefix: state.prefix } : {}),
111
- defaultJobOptions: RETENTION,
119
+ defaultJobOptions: state.retention,
112
120
  });
113
121
  // Without a listener an emitted 'error' is an unhandled exception that takes
114
122
  // the process down — a Redis blip must not kill the web server.
@@ -165,7 +173,7 @@ async function reconcileSchedules(
165
173
  await queue.upsertJobScheduler(
166
174
  name,
167
175
  { pattern: schedule.pattern, tz: schedule.timezone ?? "UTC" },
168
- { name, data: {}, opts: jobOptionsFor(job, {}) },
176
+ { name, data: {}, opts: jobOptionsFor(state, job, {}) },
169
177
  );
170
178
  state.logger.info(
171
179
  `schedule installed: ${name} (${schedule.pattern} ${schedule.timezone ?? "UTC"})`,
@@ -178,41 +186,37 @@ async function reconcileSchedules(
178
186
  state.logger.warn(
179
187
  `removed stale schedule "${scheduler.key}" (no longer defined in code).`,
180
188
  );
189
+ // The destructive half of the reconcile, and the one worth auditing: a
190
+ // deploy just cancelled a recurring job, permanently, from the queue's
191
+ // point of view.
192
+ state.emit((events) =>
193
+ events.onScheduleRemoved?.({ name: scheduler.key, queue: queue.name }),
194
+ );
181
195
  }
182
196
  }
183
197
 
184
- /**
185
- * A queue's worker concurrency.
186
- *
187
- * The default applies only when NO definition on the queue asked for a
188
- * specific number. A stated `concurrency: 1` means single-flight and must be
189
- * honoured — taking `max(default, …)` would quietly raise it back to the
190
- * default and undo the very property the caller asked for.
191
- */
192
- function resolveConcurrency(group: readonly AnyJobDefinition[]): number {
193
- const stated = group
194
- .map((definition) => definition.concurrency)
195
- .filter((value): value is number => typeof value === "number" && value > 0);
196
- return stated.length > 0 ? Math.max(...stated) : DEFAULT_CONCURRENCY;
197
- }
198
-
199
198
  /** Consume one queue, dispatching by job name to the definitions it carries. */
200
199
  function startWorker(
201
200
  state: DriverState,
202
201
  queueName: string,
203
202
  group: readonly AnyJobDefinition[],
204
203
  ): Worker {
205
- const byName = new Map(group.map((definition) => [definition.name, definition]));
206
- const concurrency = resolveConcurrency(group);
204
+ const onThisQueue = new Set(group.map((definition) => definition.name));
205
+ const concurrency = resolveConcurrency(group, state.defaultConcurrency);
207
206
 
208
207
  const worker = new Worker(
209
208
  queueName,
210
209
  async (job) => {
211
- const definition = byName.get(job.name);
210
+ // THE GATE, execution side — `resolveRegisteredJob` is the same function
211
+ // the enqueue path calls, so the two cannot come to disagree about what
212
+ // a runnable job is. The queue membership check on top of it keeps a job
213
+ // from being run by another queue's worker.
214
+ const registered = resolveRegisteredJob(job.name);
215
+ const definition = registered && onThisQueue.has(job.name) ? registered : undefined;
212
216
  if (!definition) {
213
- // A job left in Redis by a previous deploy whose handler is gone.
214
- // Retrying cannot help, so fail it terminally instead of burning every
215
- // attempt on it.
217
+ // A job left in the backend by a previous deploy whose handler is
218
+ // gone. Retrying cannot help, so fail it terminally instead of burning
219
+ // every attempt on it.
216
220
  throw new UnrecoverableError(`No handler registered for job "${job.name}".`);
217
221
  }
218
222
  const context: JobContext = {
@@ -222,6 +226,15 @@ function startWorker(
222
226
  logger: state.logger,
223
227
  };
224
228
  await definition.handle(job.data as never, context);
229
+ state.emit((events) =>
230
+ events.onJobCompleted?.({
231
+ name: job.name,
232
+ queue: queueName,
233
+ runId: context.runId,
234
+ attempt: context.attempt,
235
+ maxAttempts: context.maxAttempts,
236
+ }),
237
+ );
225
238
  },
226
239
  {
227
240
  connection: state.connection,
@@ -231,11 +244,25 @@ function startWorker(
231
244
  );
232
245
 
233
246
  worker.on("failed", (job, error) => {
234
- const attempts = job ? `${job.attemptsMade}/${job.opts.attempts ?? 1}` : "?";
247
+ const maxAttempts = job?.opts.attempts ?? 1;
248
+ const attempts = job ? `${job.attemptsMade}/${maxAttempts}` : "?";
235
249
  state.logger.error(
236
250
  `job "${job?.name ?? queueName}" failed (attempt ${attempts}):`,
237
251
  error,
238
252
  );
253
+ if (!job) return;
254
+ const terminal = isTerminalFailure(job.attemptsMade, maxAttempts, error);
255
+ state.emit((events) =>
256
+ events.onJobFailed?.({
257
+ name: job.name,
258
+ queue: queueName,
259
+ runId: job.id ?? `${job.name}:unknown`,
260
+ attempt: job.attemptsMade,
261
+ maxAttempts,
262
+ error,
263
+ terminal,
264
+ }),
265
+ );
239
266
  });
240
267
  worker.on("error", (error) =>
241
268
  state.logger.error(`worker "${queueName}" error:`, error),
@@ -262,6 +289,12 @@ export function createBullMqJobDriver(options: BullMqJobDriverOptions): JobDrive
262
289
  connection: parseRedisUrl(options.redisUrl),
263
290
  logger: options.logger,
264
291
  prefix: options.prefix,
292
+ retention: retentionOptions(options.retention ?? DEFAULT_JOB_RETENTION),
293
+ defaultConcurrency:
294
+ typeof options.defaultConcurrency === "number" && options.defaultConcurrency > 0
295
+ ? options.defaultConcurrency
296
+ : DEFAULT_CONCURRENCY,
297
+ emit: createEventEmitter(options.events, options.logger),
265
298
  queues: new Map(),
266
299
  workers: [],
267
300
  };
@@ -270,8 +303,22 @@ export function createBullMqJobDriver(options: BullMqJobDriverOptions): JobDrive
270
303
  kind: "bullmq",
271
304
 
272
305
  async enqueue(definition, payload, enqueueOptions): Promise<EnqueueResult> {
306
+ // The same gate as the worker's dispatch, at the other end of the wire:
307
+ // a name this deployment cannot run must not be written to Redis, where
308
+ // it would sit until a consumer dead-lettered it. Reached directly only
309
+ // by a host that built the driver itself — `enqueueJob` refuses first.
310
+ if (!resolveRegisteredJob(definition)) {
311
+ state.logger.error(
312
+ `refused to enqueue "${definition.name}": it is not the job registered under that name.`,
313
+ );
314
+ return { enqueued: false, reason: "unregistered" };
315
+ }
273
316
  const queue = queueFor(state, definition.queue ?? DEFAULT_QUEUE);
274
- await queue.add(definition.name, payload, jobOptionsFor(definition, enqueueOptions));
317
+ await queue.add(
318
+ definition.name,
319
+ payload,
320
+ jobOptionsFor(state, definition, enqueueOptions),
321
+ );
275
322
  return { enqueued: true };
276
323
  },
277
324
 
@@ -296,7 +343,15 @@ export function createBullMqJobDriver(options: BullMqJobDriverOptions): JobDrive
296
343
  }
297
344
 
298
345
  /**
299
- * Internals exposed for tests only — the concurrency rule is silent when
300
- * wrong, so it is pinned directly rather than inferred from a live Worker.
346
+ * The policy rules, re-exposed for tests through the driver's own subpath.
347
+ *
348
+ * Both are SILENT when wrong — a queue that quietly runs at the default
349
+ * instead of single-flight, and a dead-letter that quietly reports itself as
350
+ * one more retry — so they are pinned directly rather than inferred from a
351
+ * live Worker, which would need a Redis to exist.
301
352
  */
302
- export const __testables = { resolveConcurrency, DEFAULT_CONCURRENCY };
353
+ export const __testables = {
354
+ resolveConcurrency,
355
+ isTerminalFailure,
356
+ DEFAULT_CONCURRENCY,
357
+ };
@@ -1,20 +1,26 @@
1
1
  /**
2
2
  * The INLINE driver: run the handler in the calling process, immediately.
3
3
  *
4
- * For unit and integration tests (which boot PGlite and must not need a Redis
5
- * container) and for local development without `docker compose up`. It is a
6
- * real driver, not a stub — `attempts` are honoured so retry behaviour stays
7
- * testable — but three things it deliberately does NOT do:
4
+ * For unit and integration tests (which must not need a queue backend running)
5
+ * and for local development with no container. It is a real driver, not a stub
6
+ * — `attempts` are honoured so retry behaviour stays testable — but three
7
+ * things it deliberately does NOT do:
8
8
  *
9
9
  * - **No delay.** `delayMs` is ignored and logged. A test asserting a
10
- * three-day dunning gap should assert on the enqueue, not the clock.
10
+ * three-day gap should assert on the enqueue, not on the clock.
11
11
  * - **No schedules.** A cron job is registered but never fires; drive it
12
12
  * directly in a test by calling the handler.
13
13
  * - **No backoff waits.** Retries happen back-to-back so suites stay fast.
14
14
  *
15
15
  * Which is why it must never reach production: work would run inside the
16
- * request that scheduled it, and a crash between the two would lose it. The
17
- * host's driver factory is responsible for refusing it there.
16
+ * request that scheduled it, and a crash between the two would lose it.
17
+ * `createApiJobs` refuses it there, by name and by instance.
18
+ *
19
+ * It also does no registry lookup, so it has no write/run gate of its own to
20
+ * keep in step: it runs the definition OBJECT it was handed, not a name it
21
+ * resolves. `enqueueJob` applies the registry gate before the driver is
22
+ * reached; a test calling `driver.enqueue(definition, …)` directly is
23
+ * deliberately below that line.
18
24
  */
19
25
 
20
26
  import type {
@@ -23,9 +29,13 @@ import type {
23
29
  EnqueueResult,
24
30
  JobContext,
25
31
  JobDriver,
32
+ JobEvents,
26
33
  JobLogger,
27
34
  } from "../core/types";
28
35
 
36
+ import { createEventEmitter, type EmitJobEvent } from "../core/events";
37
+ import { DEFAULT_QUEUE } from "../core/queues";
38
+
29
39
  export interface InlineJobDriverOptions {
30
40
  logger?: JobLogger;
31
41
  /**
@@ -34,6 +44,12 @@ export interface InlineJobDriverOptions {
34
44
  * sees the side effect.
35
45
  */
36
46
  await?: boolean;
47
+ /**
48
+ * Where completions and dead-letters are reported. The same port the BullMQ
49
+ * driver takes, so a host's observer is exercised by its test suite instead
50
+ * of only in production.
51
+ */
52
+ events?: JobEvents;
37
53
  }
38
54
 
39
55
  /** A record of what ran, for assertions. */
@@ -51,45 +67,64 @@ export interface InlineJobDriver extends JobDriver {
51
67
  clearRuns(): void;
52
68
  }
53
69
 
54
- export function createInlineJobDriver(
55
- options: InlineJobDriverOptions = {},
56
- ): InlineJobDriver {
57
- const logger = options.logger ?? console;
58
- const awaitHandlers = options.await ?? true;
59
- const runs: InlineJobRun[] = [];
70
+ /** What one inline execution needs from the driver that owns it. */
71
+ interface InlineRunner {
72
+ logger: JobLogger;
73
+ runs: InlineJobRun[];
74
+ emit: EmitJobEvent;
75
+ }
60
76
 
61
- async function run(
62
- definition: AnyJobDefinition,
63
- payload: unknown,
64
- ): Promise<void> {
65
- const maxAttempts = Math.max(1, definition.attempts ?? 1);
66
- const record: InlineJobRun = { name: definition.name, payload, attempts: 0 };
67
- runs.push(record);
77
+ /**
78
+ * Run one job to its conclusion, retrying up to `attempts` with no waits.
79
+ *
80
+ * Hoisted out of the factory so the factory stays a wiring function: the
81
+ * attempt loop is the part with the rules in it, and it reads better with its
82
+ * inputs named than closed over.
83
+ */
84
+ async function runInline(
85
+ runner: InlineRunner,
86
+ definition: AnyJobDefinition,
87
+ payload: unknown,
88
+ ): Promise<void> {
89
+ const { logger, runs, emit } = runner;
90
+ const maxAttempts = Math.max(1, definition.attempts ?? 1);
91
+ const queue = definition.queue ?? DEFAULT_QUEUE;
92
+ const record: InlineJobRun = { name: definition.name, payload, attempts: 0 };
93
+ runs.push(record);
68
94
 
69
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
70
- record.attempts = attempt;
71
- const context: JobContext = {
72
- runId: `inline:${definition.name}:${runs.length}:${attempt}`,
73
- attempt,
74
- maxAttempts,
75
- logger,
76
- };
77
- try {
78
- await definition.handle(payload as never, context);
79
- record.error = undefined;
95
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
96
+ record.attempts = attempt;
97
+ const runId = `inline:${definition.name}:${runs.length}:${attempt}`;
98
+ const context: JobContext = { runId, attempt, maxAttempts, logger };
99
+ const event = { name: definition.name, queue, runId, attempt, maxAttempts };
100
+ try {
101
+ await definition.handle(payload as never, context);
102
+ record.error = undefined;
103
+ emit((events) => events.onJobCompleted?.(event));
104
+ return;
105
+ } catch (error) {
106
+ record.error = error;
107
+ const terminal = attempt === maxAttempts;
108
+ emit((events) => events.onJobFailed?.({ ...event, error, terminal }));
109
+ if (terminal) {
110
+ logger.error(`"${definition.name}" failed after ${attempt} attempt(s):`, error);
80
111
  return;
81
- } catch (error) {
82
- record.error = error;
83
- if (attempt === maxAttempts) {
84
- logger.error(
85
- `"${definition.name}" failed after ${attempt} attempt(s):`,
86
- error,
87
- );
88
- return;
89
- }
90
112
  }
91
113
  }
92
114
  }
115
+ }
116
+
117
+ export function createInlineJobDriver(
118
+ options: InlineJobDriverOptions = {},
119
+ ): InlineJobDriver {
120
+ const logger = options.logger ?? console;
121
+ const awaitHandlers = options.await ?? true;
122
+ const runs: InlineJobRun[] = [];
123
+ const runner: InlineRunner = {
124
+ logger,
125
+ runs,
126
+ emit: createEventEmitter(options.events, logger),
127
+ };
93
128
 
94
129
  return {
95
130
  kind: "inline",
@@ -107,7 +142,7 @@ export function createInlineJobDriver(
107
142
  `inline driver ignored a ${enqueueOptions.delayMs}ms delay on "${definition.name}".`,
108
143
  );
109
144
  }
110
- const execution = run(definition, payload);
145
+ const execution = runInline(runner, definition, payload);
111
146
  if (awaitHandlers) await execution;
112
147
  else void execution;
113
148
  return { enqueued: true };
package/src/hono/index.ts CHANGED
@@ -21,9 +21,10 @@ import type { JobsRoute } from "../server/create-api-jobs";
21
21
  * must call `start()` on the same instance whose routes report health, and a
22
22
  * router that built its own would answer for a runtime nobody started.
23
23
  *
24
- * Auth stays the host's: mount this under whatever guard the deployment's
25
- * internal probes live behind (future-pay's `/api/internal/*` is answered
26
- * only machine-to-machine; the reverse proxy 404s it from the internet).
24
+ * Auth stays the host's: mount it under whatever guard the deployment's
25
+ * internal probes live behind — a prefix answered only machine-to-machine,
26
+ * with the reverse proxy refusing it from the internet, is the usual shape.
27
+ * This package holds zero authorization logic.
27
28
  */
28
29
  export function jobsRouter(api: { routes: JobsRoute[] }): Hono {
29
30
  const app = new Hono();
package/src/index.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * `@12-apps/jobs` — typed background jobs with retries, backoff and cron, behind
3
- * a swappable driver.
2
+ * `@12-apps/jobs` — typed background jobs with retries, backoff and cron,
3
+ * behind a swappable driver.
4
4
  *
5
5
  * // where the domain lives
6
- * export const dispatchNotification = defineJob<{ notificationId: string }>({
7
- * name: "notifications.dispatch",
6
+ * export const renderReport = defineJob<{ reportId: string }>({
7
+ * name: "reports.render",
8
8
  * attempts: 5,
9
9
  * backoff: { type: "exponential", delayMs: 5_000 },
10
- * handle: async ({ notificationId }) => dispatchDeliveries(notificationId),
10
+ * handle: async ({ reportId }) => renderAndStore(reportId),
11
11
  * });
12
12
  *
13
13
  * // at an emit site
14
- * await dispatchNotification.enqueue({ notificationId }, { dedupeKey: notificationId });
14
+ * await renderReport.enqueue({ reportId }, { dedupeKey: reportId });
15
15
  *
16
16
  * // at process start
17
17
  * configureJobs({ driver: createBullMqJobDriver({ redisUrl, logger }), logger });
@@ -30,14 +30,29 @@
30
30
  * `createApiJobs` in `@12-apps/jobs/server` (mount it with
31
31
  * `@12-apps/jobs/hono` or your own adapter). What this root adds to it:
32
32
  *
33
- * - `SWEEP_QUEUE` — the single-flight queue the scheduled sweeps share.
33
+ * - `DEFAULT_QUEUE` / `SWEEP_QUEUE` — this package's own queue vocabulary,
34
+ * `SWEEP_QUEUE` being the single-flight queue the scheduled sweeps share.
34
35
  * - `createSweepLease` — the named, time-bounded claim that keeps a sweep
35
36
  * to ONE pass per tick across a multi-worker deployment. Its `SweepLease`
36
37
  * table ships in `prisma/jobs.prisma` with its migration; the host syncs
37
38
  * both (see ADOPTING.md).
39
+ *
40
+ * Every guard this package has lives on THIS path as well as on the factory:
41
+ * `defineJob` refuses a definition that cannot run, `startJobWorkers` refuses
42
+ * an empty registry, and `enqueueJob` refuses a job the registry does not
43
+ * hold. A host that wires the runtime by hand is not a host with fewer checks.
38
44
  */
39
45
 
40
- export { defineJob, findJob, listJobs, clearJobs, DuplicateJobError } from "./core/registry";
46
+ export {
47
+ defineJob,
48
+ findJob,
49
+ listJobs,
50
+ clearJobs,
51
+ resolveRegisteredJob,
52
+ DuplicateJobError,
53
+ InvalidJobDefinitionError,
54
+ NoJobsRegisteredError,
55
+ } from "./core/registry";
41
56
  export type { RegisteredJob } from "./core/registry";
42
57
 
43
58
  export {
@@ -56,21 +71,32 @@ export type {
56
71
  EnqueueResult,
57
72
  EnqueueSkipReason,
58
73
  JobBackoff,
74
+ JobCompletedEvent,
59
75
  JobContext,
60
76
  JobDefinition,
61
77
  JobDriver,
78
+ JobEvents,
79
+ JobFailedEvent,
62
80
  JobHandler,
63
81
  JobLogger,
82
+ JobRetention,
83
+ JobRetentionWindow,
84
+ JobRunEvent,
64
85
  JobSchedule,
86
+ ScheduleRemovedEvent,
65
87
  } from "./core/types";
66
88
 
89
+ // Retention validation lives in `core` so this barrel can carry it without
90
+ // pulling `bullmq` (and ioredis) into a bundle that only ever enqueues.
91
+ export { assertValidRetention, InvalidJobRetentionError } from "./core/retention";
92
+
67
93
  export { createInlineJobDriver } from "./drivers/inline";
68
94
  export type { InlineJobDriver, InlineJobRun } from "./drivers/inline";
69
95
 
70
96
  export { parseRedisUrl, InvalidRedisUrlError } from "./drivers/redis-url";
71
97
  export type { RedisConnectionOptions } from "./drivers/redis-url";
72
98
 
73
- export { SWEEP_QUEUE } from "./core/queues";
99
+ export { DEFAULT_QUEUE, SWEEP_QUEUE } from "./core/queues";
74
100
 
75
101
  export { createSweepLease } from "./lease/sweep-lease";
76
102
  export type {