@devmoods/express-extras 0.76.1 → 0.77.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/context.js CHANGED
@@ -7,7 +7,7 @@ export const JOB_ID_CONTEXT_KEY = 'jobId';
7
7
  /**
8
8
  * Middleware for setting a `requestId` on the request context.
9
9
  */
10
- export function withRequestContext({ getRequestId = (req) => req.headers['X-Request-ID'], } = {}) {
10
+ export function withRequestContext({ getRequestId = (req) => req.get('X-Request-ID'), } = {}) {
11
11
  return (req, res, next) => {
12
12
  const requestId = getRequestId(req) || randomUUID();
13
13
  requestContext.run(new Map(), () => {
@@ -20,6 +20,9 @@ export function withRequestContext({ getRequestId = (req) => req.headers['X-Requ
20
20
  * Return a value from the request context
21
21
  */
22
22
  export function useRequestContext(key) {
23
+ return getRequestContextValue(key);
24
+ }
25
+ function getRequestContextValue(key) {
23
26
  const store = requestContext.getStore();
24
27
  if (store) {
25
28
  return store.get(key);
@@ -36,8 +39,8 @@ export function setRequestContext(key, value) {
36
39
  }
37
40
  }
38
41
  export function useRequestCounter(key) {
39
- const value = useRequestContext(key) || 0;
40
- const increase = () => setRequestContext(key, value + 1);
42
+ const value = getRequestContextValue(key) || 0;
43
+ const increase = () => setRequestContext(key, (getRequestContextValue(key) || 0) + 1);
41
44
  return {
42
45
  value,
43
46
  increase,
package/dist/email.d.ts CHANGED
@@ -13,5 +13,5 @@ export interface SendEmailOptions extends nodemailer.SendMailOptions {
13
13
  }
14
14
  export declare function createEmail({ host, port, user, pass, senderName, senderAddress, onEmailSent, }?: EmailOptions): {
15
15
  send: ({ to, subject, message, ...rest }: SendEmailOptions) => Promise<void>;
16
- transporter: nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo, import("nodemailer/lib/smtp-transport/index.js").Options>;
16
+ transporter: nodemailer.Mail<nodemailer.SMTPSentMessageInfo, nodemailer.SMTPTransportOptions>;
17
17
  };
package/dist/faktory.d.ts CHANGED
@@ -1,34 +1,21 @@
1
1
  import { create, type WorkerOptions } from 'faktory-worker';
2
- export interface JobsOptions<QueueName extends string> {
3
- queues: QueueName[];
2
+ import { type JobBackend, type JobSubmission, type JobWorkerOptions } from './jobs.js';
3
+ export { createJobs, createJobWorker, type AnyJobContext, type Job, type JobBackend, type JobOptions, type JobSubmission, type JobWorker, type JobWorkerOptions, type JobsOptions, } from './jobs.js';
4
+ export interface FaktoryJobBackendOptions {
4
5
  faktory?: typeof create;
5
6
  }
6
- export type AnyJobContext = Record<string, any>;
7
- export interface JobOptions<QueueName extends string> {
8
- retry?: number;
9
- priority?: number;
10
- at?: Date | string;
11
- queue?: QueueName;
7
+ export declare class FaktoryJobBackend implements JobBackend {
8
+ private createFactory;
9
+ constructor({ faktory }?: FaktoryJobBackendOptions);
10
+ enqueue({ jid, jobName, args, queue, ...options }: JobSubmission): Promise<void>;
11
+ createWorker(options: JobWorkerOptions<any>): Promise<{
12
+ start(workerOptions?: WorkerOptions): Promise<void>;
13
+ stop(): Promise<void>;
14
+ }>;
12
15
  }
13
- type JobFn<Arguments, Context extends AnyJobContext> = (input: Arguments, ctx: Context) => undefined | Promise<any>;
14
- export declare function createJobs<QueueName extends string, GlobalContext extends AnyJobContext>({ queues, faktory: createFactory }: JobsOptions<QueueName>): {
15
- job: <Arguments>(optionsOrFn: JobOptions<QueueName> | JobFn<Arguments, GlobalContext>, fnOrUndefined?: JobFn<Arguments, GlobalContext>) => {
16
- $job: boolean;
17
- jobName: string;
18
- options: JobOptions<QueueName>;
19
- delay(args?: Parameters<JobFn<Arguments, GlobalContext>>[0]): Promise<`${string}-${string}-${string}-${string}-${string}`>;
20
- execute(args: Parameters<JobFn<Arguments, GlobalContext>>[0], context: GlobalContext): Promise<any>;
21
- };
22
- queues: QueueName[];
23
- };
24
- export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
25
- export interface FaktoryWorkerOptions<GlobalContext> {
26
- jobs: unknown[];
27
- queues: string[];
16
+ export interface FaktoryWorkerOptions<GlobalContext> extends JobWorkerOptions<GlobalContext> {
28
17
  faktory?: typeof create;
29
- jobContext?: GlobalContext;
30
18
  }
31
- export { createFaktoryWorker as createJobWorker };
32
19
  export declare function createFaktoryWorker<GlobalContext>({ queues, jobs, faktory: createFactory, jobContext, }: FaktoryWorkerOptions<GlobalContext>): Promise<{
33
20
  start(workerOptions?: WorkerOptions): Promise<void>;
34
21
  stop(): Promise<void>;
package/dist/faktory.js CHANGED
@@ -1,61 +1,56 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { startTimer } from '@devmoods/observability';
3
2
  import { create } from 'faktory-worker';
4
3
  import { JOB_ID_CONTEXT_KEY, requestContext, setRequestContext, } from './context.js';
4
+ import {} from './jobs.js';
5
5
  import { getLogger } from './logger.js';
6
+ export { createJobs, createJobWorker, } from './jobs.js';
6
7
  const logger = getLogger('faktory');
7
- export function createJobs({ queues, faktory: createFactory = create }) {
8
- const faktory = createFactory();
9
- function job(optionsOrFn, fnOrUndefined) {
10
- const fn = typeof optionsOrFn === 'function' ? optionsOrFn : fnOrUndefined;
11
- const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn;
12
- if (!fn) {
13
- throw new Error('job() must get a function as the first or second argument');
8
+ export class FaktoryJobBackend {
9
+ createFactory;
10
+ constructor({ faktory = create } = {}) {
11
+ this.createFactory = faktory;
12
+ }
13
+ async enqueue({ jid, jobName, args, queue, ...options }) {
14
+ const faktory = this.createFactory();
15
+ let client;
16
+ let enqueueError;
17
+ let closeError;
18
+ try {
19
+ client = await faktory.connect();
20
+ const job = client.job(jobName, args);
21
+ job.jid = jid;
22
+ job.queue = queue;
23
+ Object.assign(job, options);
24
+ await job.push();
14
25
  }
15
- const jobName = fn.name;
16
- if (!jobName) {
17
- throw new Error('Can not create job without a name');
26
+ catch (error) {
27
+ enqueueError = error;
18
28
  }
19
- return {
20
- $job: true,
21
- jobName,
22
- options,
23
- async delay(args) {
24
- const jid = randomUUID();
29
+ finally {
30
+ if (client) {
25
31
  try {
26
- const client = await faktory.connect();
27
- const j = client.job(jobName, args);
28
- j.jid = jid;
29
- j.queue = options.queue || queues[0];
30
- Object.assign(j, options);
31
- await j.push();
32
32
  await client.close();
33
33
  }
34
34
  catch (error) {
35
- logger.error(error, 'failed to create job', {
35
+ closeError = error;
36
+ logger.error(error, 'failed to close faktory client', {
36
37
  jobId: jid,
37
38
  jobName,
38
- args: JSON.stringify(args),
39
39
  });
40
- throw error;
41
- }
42
- return jid;
43
- },
44
- async execute(args, context) {
45
- try {
46
- const result = await fn(args, context);
47
- return result;
48
40
  }
49
- catch (error) {
50
- logger.error(error);
51
- throw error;
52
- }
53
- },
54
- };
41
+ }
42
+ }
43
+ if (enqueueError) {
44
+ throw enqueueError;
45
+ }
46
+ if (closeError) {
47
+ throw closeError;
48
+ }
49
+ }
50
+ createWorker(options) {
51
+ return createFaktoryWorker({ ...options, faktory: this.createFactory });
55
52
  }
56
- return { job, queues };
57
53
  }
58
- export { createFaktoryWorker as createJobWorker };
59
54
  export async function createFaktoryWorker({ queues, jobs, faktory: createFactory = create, jobContext, }) {
60
55
  const faktory = createFactory();
61
56
  const filter = (j) => !!Object.prototype.hasOwnProperty.call(j, '$job');
@@ -83,25 +78,18 @@ export async function createFaktoryWorker({ queues, jobs, faktory: createFactory
83
78
  faktory.use(async (ctx, next) => {
84
79
  const jobId = ctx.job.jid;
85
80
  const timer = startTimer();
86
- logger.info('job started', {
87
- jobName: ctx.job.jobtype,
88
- jobId,
89
- });
81
+ logger.info('job started', { jobName: ctx.job.jobtype, jobId });
90
82
  await requestContext.run(new Map(), async () => {
91
83
  setRequestContext(JOB_ID_CONTEXT_KEY, jobId);
92
84
  await next();
93
85
  });
94
- const duration = timer.getDuration();
95
86
  logger.info('job succeeded', {
96
87
  jobName: ctx.job.jobtype,
97
88
  jobId,
98
- duration,
89
+ duration: timer.getDuration(),
99
90
  });
100
91
  });
101
- await faktory.work({
102
- queues,
103
- ...workerOptions,
104
- });
92
+ await faktory.work({ queues, ...workerOptions });
105
93
  }
106
94
  catch (error) {
107
95
  logger.error(error);
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export * from './manage.js';
17
17
  export * from './middleware.js';
18
18
  export * from './rate-limit.js';
19
19
  export * from './redis.js';
20
+ export * from './redis-jobs.js';
20
21
  export * from './router.js';
21
22
  export * from './routes.js';
22
23
  export * from './slack.js';
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export * from './manage.js';
18
18
  export * from './middleware.js';
19
19
  export * from './rate-limit.js';
20
20
  export * from './redis.js';
21
+ export * from './redis-jobs.js';
21
22
  export * from './router.js';
22
23
  export * from './routes.js';
23
24
  export * from './slack.js';
package/dist/jobs.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { type create } from 'faktory-worker';
2
+ export type AnyJobContext = Record<string, any>;
3
+ export interface JobOptions<QueueName extends string> {
4
+ retry?: number;
5
+ priority?: number;
6
+ at?: Date | string;
7
+ queue?: QueueName;
8
+ }
9
+ export interface JobSubmission {
10
+ jid: string;
11
+ jobName: string;
12
+ args: unknown;
13
+ queue: string;
14
+ retry?: number;
15
+ priority?: number;
16
+ at?: Date | string;
17
+ }
18
+ export interface JobWorker {
19
+ start(options?: any): Promise<void>;
20
+ stop(): Promise<void>;
21
+ }
22
+ export interface JobBackend {
23
+ enqueue(job: JobSubmission): Promise<void>;
24
+ createWorker(options: JobWorkerOptions<any>): Promise<JobWorker> | JobWorker;
25
+ }
26
+ export interface JobWorkerOptions<GlobalContext> {
27
+ jobs: unknown[];
28
+ queues: string[];
29
+ jobContext?: GlobalContext;
30
+ backend?: JobBackend;
31
+ }
32
+ export interface JobsOptions<QueueName extends string> {
33
+ queues: QueueName[];
34
+ backend?: JobBackend;
35
+ /** @deprecated Pass `new FaktoryJobBackend({ faktory })` as backend. */
36
+ faktory?: typeof create;
37
+ }
38
+ type JobFn<Arguments, Context extends AnyJobContext> = (input: Arguments, ctx: Context) => undefined | Promise<any>;
39
+ export declare function createJobs<QueueName extends string, GlobalContext extends AnyJobContext>({ queues, backend, faktory }: JobsOptions<QueueName>): {
40
+ job: <Arguments>(optionsOrFn: JobOptions<QueueName> | JobFn<Arguments, GlobalContext>, fnOrUndefined?: JobFn<Arguments, GlobalContext>) => {
41
+ $job: boolean;
42
+ jobName: string;
43
+ options: JobOptions<QueueName>;
44
+ delay(args?: Arguments): Promise<`${string}-${string}-${string}-${string}-${string}`>;
45
+ execute(args: Arguments, context: GlobalContext): Promise<any>;
46
+ };
47
+ queues: QueueName[];
48
+ backend: JobBackend;
49
+ };
50
+ export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
51
+ export declare function createJobWorker<GlobalContext>({ backend, ...options }: JobWorkerOptions<GlobalContext>): JobWorker | Promise<JobWorker>;
52
+ export {};
package/dist/jobs.js ADDED
@@ -0,0 +1,58 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {} from 'faktory-worker';
3
+ import { FaktoryJobBackend } from './faktory.js';
4
+ import { getLogger } from './logger.js';
5
+ const logger = getLogger('jobs');
6
+ export function createJobs({ queues, backend, faktory }) {
7
+ const jobBackend = backend ?? new FaktoryJobBackend({ faktory });
8
+ function job(optionsOrFn, fnOrUndefined) {
9
+ const fn = typeof optionsOrFn === 'function' ? optionsOrFn : fnOrUndefined;
10
+ const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn;
11
+ if (!fn) {
12
+ throw new Error('job() must get a function as the first or second argument');
13
+ }
14
+ const jobName = fn.name;
15
+ if (!jobName) {
16
+ throw new Error('Can not create job without a name');
17
+ }
18
+ return {
19
+ $job: true,
20
+ jobName,
21
+ options,
22
+ async delay(args) {
23
+ const jid = randomUUID();
24
+ try {
25
+ await jobBackend.enqueue({
26
+ jid,
27
+ jobName,
28
+ args,
29
+ queue: options.queue ?? queues[0],
30
+ ...options,
31
+ });
32
+ }
33
+ catch (error) {
34
+ logger.error(error, 'failed to create job', {
35
+ jobId: jid,
36
+ jobName,
37
+ args: JSON.stringify(args),
38
+ });
39
+ throw error;
40
+ }
41
+ return jid;
42
+ },
43
+ async execute(args, context) {
44
+ try {
45
+ return await fn(args, context);
46
+ }
47
+ catch (error) {
48
+ logger.error(error);
49
+ throw error;
50
+ }
51
+ },
52
+ };
53
+ }
54
+ return { job, queues, backend: jobBackend };
55
+ }
56
+ export function createJobWorker({ backend = new FaktoryJobBackend(), ...options }) {
57
+ return backend.createWorker(options);
58
+ }
package/dist/listen.js CHANGED
@@ -77,10 +77,28 @@ export function stoppable(server, gracePeriodMs = Infinity) {
77
77
  return address.port;
78
78
  };
79
79
  const start = (port = 0) => {
80
- return new Promise((resolve) => {
81
- server.listen(port, () => {
80
+ return new Promise((resolve, reject) => {
81
+ const cleanup = () => {
82
+ server.off('listening', onListening);
83
+ server.off('error', onError);
84
+ };
85
+ const onListening = () => {
86
+ cleanup();
82
87
  resolve();
83
- });
88
+ };
89
+ const onError = (error) => {
90
+ cleanup();
91
+ reject(error);
92
+ };
93
+ server.once('listening', onListening);
94
+ server.once('error', onError);
95
+ try {
96
+ server.listen(port);
97
+ }
98
+ catch (error) {
99
+ cleanup();
100
+ reject(error);
101
+ }
84
102
  });
85
103
  };
86
104
  const inspect = () => {
@@ -109,6 +127,12 @@ export function createExpressServer(app, options = {}) {
109
127
  app.set('trust proxy', trustProxy);
110
128
  }
111
129
  let stopping = false;
130
+ const signals = ['SIGTERM', 'SIGINT'];
131
+ function removeSignalHandlers() {
132
+ for (const signal of signals) {
133
+ process.off(signal, onSignal);
134
+ }
135
+ }
112
136
  async function onSignal(signal) {
113
137
  if (stopping) {
114
138
  return;
@@ -126,16 +150,18 @@ export function createExpressServer(app, options = {}) {
126
150
  gracefully,
127
151
  });
128
152
  await options.afterShutdown?.();
153
+ removeSignalHandlers();
129
154
  process.exit(0);
130
155
  }
131
156
  catch (error) {
132
157
  logger.error(error, 'error during shutdown', {
133
158
  duration: timer.getDuration(),
134
159
  });
160
+ removeSignalHandlers();
135
161
  process.exit(1);
136
162
  }
137
163
  }
138
- for (const signal of ['SIGTERM', 'SIGINT']) {
164
+ for (const signal of signals) {
139
165
  process.on(signal, onSignal);
140
166
  }
141
167
  const healthTypes = ['live', 'ready'];
@@ -182,8 +208,13 @@ export function createExpressServer(app, options = {}) {
182
208
  duration: appTimer.getDuration(),
183
209
  });
184
210
  };
185
- enhanced.stop = () => {
186
- return ctl.stop();
211
+ enhanced.stop = async () => {
212
+ try {
213
+ return await ctl.stop();
214
+ }
215
+ finally {
216
+ removeSignalHandlers();
217
+ }
187
218
  };
188
219
  enhanced.terminate = function terminate() {
189
220
  return onSignal('SIGTERM');
@@ -6,11 +6,17 @@ export interface RateLimitMiddleware extends RequestHandler {
6
6
  */
7
7
  clear: (key: string) => Promise<void>;
8
8
  /**
9
- * Clear all rate limits
9
+ * Clear this namespace's rate limits and return the number actually deleted.
10
+ * Not atomic with concurrent requests: quiesce traffic for a complete reset.
10
11
  */
11
12
  clearAll: () => Promise<number>;
12
13
  }
13
14
  export interface RateLimitOptions {
15
+ /**
16
+ * Stable, nonempty policy name. Use the same namespace and options on all
17
+ * workers sharing a quota, and different namespaces for independent policies.
18
+ */
19
+ namespace: string;
14
20
  /**
15
21
  * Maximum number of requests allowed within the window
16
22
  * @default 100
@@ -28,6 +34,8 @@ export interface RateLimitOptions {
28
34
  getKey?: (req: Request) => string;
29
35
  }
30
36
  /**
31
- * Returns a middleware for rate limiting requests.
37
+ * Fixed-window rate limiting shared across workers through Redis.
38
+ * The window starts on the first accepted request. Redis errors go to next(error)
39
+ * without admitting the request. Requires permission to execute Redis scripts.
32
40
  */
33
- export declare function rateLimit(redis: RedisClient, { max, windowSizeInSeconds, getKey, }?: RateLimitOptions): RateLimitMiddleware;
41
+ export declare function rateLimit(redis: RedisClient, { namespace, max, windowSizeInSeconds, getKey, }: RateLimitOptions): RateLimitMiddleware;
@@ -2,33 +2,64 @@ import { config } from '@devmoods/config';
2
2
  import {} from 'express';
3
3
  import { makeError } from './errors.js';
4
4
  import {} from './redis.js';
5
+ // Redis executes this entire decision atomically, including key expiration.
6
+ // Rejected requests neither increment the counter nor extend the window.
7
+ const consumeScript = `
8
+ local count = tonumber(redis.call('GET', KEYS[1]) or '0')
9
+ local ttl = redis.call('PTTL', KEYS[1])
10
+ local window = tonumber(ARGV[2])
11
+
12
+ if ttl == -2 then
13
+ redis.call('SET', KEYS[1], '1', 'PX', ARGV[2])
14
+ return {1, 1, window}
15
+ end
16
+
17
+ -- Repair counters that have lost their expiry without resetting their usage.
18
+ if ttl == -1 then
19
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
20
+ ttl = window
21
+ end
22
+
23
+ if count >= tonumber(ARGV[1]) then
24
+ return {0, count, ttl}
25
+ end
26
+
27
+ count = redis.call('INCR', KEYS[1])
28
+ return {1, count, ttl}
29
+ `;
5
30
  /**
6
- * Returns a middleware for rate limiting requests.
31
+ * Fixed-window rate limiting shared across workers through Redis.
32
+ * The window starts on the first accepted request. Redis errors go to next(error)
33
+ * without admitting the request. Requires permission to execute Redis scripts.
7
34
  */
8
- export function rateLimit(redis, { max = 100, windowSizeInSeconds = 60,
9
- /* v8 ignore start -- @preserve */
10
- getKey = (req) => req.ip || 'unknown',
11
- /* v8 ignore end -- @preserve */
12
- } = {}) {
13
- const keyPrefix = `${config.REDIS_KEY_PREFIX}rate-limit:`;
35
+ export function rateLimit(redis, { namespace, max = 100, windowSizeInSeconds = 60, getKey = (req) => req.ip || 'unknown', }) {
36
+ if (typeof namespace !== 'string' || namespace.trim().length === 0) {
37
+ throw new TypeError('namespace must be a nonempty string');
38
+ }
39
+ if (!Number.isSafeInteger(max) || max <= 0) {
40
+ throw new RangeError('max must be a positive safe integer');
41
+ }
42
+ const windowMs = windowSizeInSeconds * 1000;
43
+ if (!Number.isSafeInteger(windowSizeInSeconds) ||
44
+ windowSizeInSeconds <= 0 ||
45
+ !Number.isSafeInteger(windowMs)) {
46
+ throw new RangeError('windowSizeInSeconds must be a positive integer representable safely in milliseconds');
47
+ }
48
+ const keyPrefix = `${config.REDIS_KEY_PREFIX}rate-limit:v2:${encodeURIComponent(namespace)}:`;
14
49
  const middleware = async (req, res, next) => {
15
50
  try {
16
51
  const key = `${keyPrefix}${getKey(req)}`;
17
- // Use Redis to track requests
18
- const currentCount = await redis.get(key);
19
- const count = currentCount ? parseInt(currentCount, 10) : 0;
52
+ const [allowed, count, ttlMs] = (await redis.eval(consumeScript, {
53
+ keys: [key],
54
+ arguments: [String(max), String(windowMs)],
55
+ }));
20
56
  // Set headers for rate limiting info
21
57
  res.setHeader('X-RateLimit-Limit', max.toString());
22
- res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count - 1).toString());
23
- if (count >= max) {
58
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count).toString());
59
+ if (!allowed) {
60
+ res.setHeader('Retry-After', Math.max(1, Math.ceil(ttlMs / 1000)).toString());
24
61
  throw makeError(429, 'Rate limit exceeded');
25
62
  }
26
- if (count === 0) {
27
- await redis.set(key, '1', { EX: windowSizeInSeconds });
28
- }
29
- else {
30
- await redis.incr(key);
31
- }
32
63
  next();
33
64
  }
34
65
  catch (error) {
@@ -41,7 +72,8 @@ getKey = (req) => req.ip || 'unknown',
41
72
  const clearAll = async () => {
42
73
  let cursor = '0';
43
74
  let deletedCount = 0;
44
- const pattern = `${config.REDIS_KEY_PREFIX}rate-limit:*`;
75
+ // Escape Redis glob metacharacters in both the app prefix and namespace.
76
+ const pattern = `${keyPrefix.replace(/[\\*?[\]]/g, '\\$&')}*`;
45
77
  do {
46
78
  // Use SCAN instead of KEYS - non-blocking iteration
47
79
  const result = await redis.scan(cursor, {
@@ -50,11 +82,9 @@ getKey = (req) => req.ip || 'unknown',
50
82
  });
51
83
  cursor = result.cursor;
52
84
  const keys = result.keys;
53
- /* v8 ignore next -- @preserve */
54
85
  if (keys.length > 0) {
55
86
  // Use UNLINK instead of DEL - asynchronous deletion
56
- await redis.unlink(keys);
57
- deletedCount += keys.length;
87
+ deletedCount += await redis.unlink(keys);
58
88
  }
59
89
  } while (cursor !== '0');
60
90
  return deletedCount;
@@ -0,0 +1,23 @@
1
+ import { type JobBackend, type JobSubmission, type JobWorkerOptions } from './jobs.js';
2
+ import { type RedisClient } from './redis.js';
3
+ export interface RedisJobBackendOptions {
4
+ /** Defaults to the configured Redis prefix followed by `jobs:`. */
5
+ keyPrefix?: string;
6
+ /** Reservation lifetime in milliseconds. Defaults to 30 seconds. */
7
+ leaseMs?: number;
8
+ }
9
+ /** Persistent, at least once job processing using Redis sorted sets. */
10
+ export declare class RedisJobBackend implements JobBackend {
11
+ private redis;
12
+ private options;
13
+ constructor(redis: RedisClient, options?: RedisJobBackendOptions);
14
+ private key;
15
+ private get leaseMs();
16
+ enqueue(job: JobSubmission): Promise<void>;
17
+ private claim;
18
+ private finish;
19
+ createWorker<GlobalContext>({ jobs, queues, jobContext, }: JobWorkerOptions<GlobalContext>): {
20
+ start(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ };
23
+ }