@holo-js/queue 0.1.2 → 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 +74 -29
- package/dist/index.mjs +114 -223
- package/package.json +9 -3
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:
|
|
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
|
-
|
|
369
|
+
declare function loadRedisDriverModule(): Promise<RedisDriverModule>;
|
|
337
370
|
declare function normalizeRedisErrorMessage(error: unknown): string;
|
|
338
|
-
declare function isQueueEnvelope(value: unknown): value is
|
|
339
|
-
|
|
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:
|
|
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
|
|
351
|
-
private
|
|
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
|
|
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
|
-
|
|
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
|
|
138
|
-
const
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
password:
|
|
155
|
-
username:
|
|
156
|
-
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
|
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:
|
|
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
|
|
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() ||
|
|
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.
|
|
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
|
-
"
|
|
26
|
-
"
|
|
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",
|