584 lines
19 KiB
TypeScript
584 lines
19 KiB
TypeScript
import type { JobsOptions, TypedQueue } from "./types.js";
|
|
import { queueNames, type QueueName } from "./names.js";
|
|
import type {
|
|
AiJob,
|
|
BrowserAutomationJob,
|
|
CleanupJob,
|
|
ExportJob,
|
|
FilePreviewJob,
|
|
FileScanJob,
|
|
ImportJob,
|
|
NotificationJob,
|
|
PluginInvokeJob,
|
|
QueueJobDescriptor,
|
|
QueueJsonValue,
|
|
TransactionalMailJob,
|
|
WebhookDeliveryJob,
|
|
WorkflowExecutionContinueRequestedOutboxEvent,
|
|
WorkflowExecutionRequestedOutboxEvent,
|
|
WorkflowTriggerOutboxEvent,
|
|
} from "./payloads.js";
|
|
import { JOB_PRIORITY_CRITICAL, priorityFromOverride, priorityLabel, resolveJobPriorityForTenant, type PriorityOverride } from "./priority.js";
|
|
|
|
type AiJobForKind<Kind extends AiJob["kind"]> = AiJob & { kind: Kind };
|
|
type BrowserAutomationJobForKind<Kind extends BrowserAutomationJob["kind"]> = BrowserAutomationJob & { kind: Kind };
|
|
|
|
export type TransactionalMailJobInput = Omit<TransactionalMailJob, "kind">;
|
|
export type WebhookDeliveryJobInput = Omit<WebhookDeliveryJob, "kind">;
|
|
export type NotificationJobInput = Omit<NotificationJob, "kind">;
|
|
export type CleanupJobInput = Omit<CleanupJob, "kind">;
|
|
export type AiChatCompletionJobInput = Omit<AiJobForKind<"ai.chat-completion">, "kind">;
|
|
export type AiEmbeddingJobInput = Omit<AiJobForKind<"ai.embedding">, "kind">;
|
|
export type AiBrowserTaskJobInput = Omit<AiJobForKind<"ai.browser-task">, "kind">;
|
|
export type BrowserSessionRunJobInput = Omit<BrowserAutomationJobForKind<"browser.session-run">, "kind">;
|
|
export type BrowserSessionCleanupJobInput = Omit<BrowserAutomationJobForKind<"browser.session-cleanup">, "kind">;
|
|
export type BrowserUsageFlushJobInput = Omit<BrowserAutomationJobForKind<"usage.flush">, "kind">;
|
|
export type FileScanJobInput = Omit<FileScanJob, "kind">;
|
|
export type FilePreviewJobInput = Omit<FilePreviewJob, "kind">;
|
|
export type ExportJobInput = Omit<ExportJob, "kind">;
|
|
export type ImportJobInput = Omit<ImportJob, "kind">;
|
|
export type PluginInvokeJobInput = Omit<PluginInvokeJob, "kind">;
|
|
export type WorkflowTriggerOutboxEventInput = {
|
|
tenantId: string | null;
|
|
event: string;
|
|
data: QueueJsonValue;
|
|
userId: string | null;
|
|
sourceId: string;
|
|
};
|
|
|
|
export type WorkflowExecutionRequestedOutboxEventInput = {
|
|
tenantId: string | null;
|
|
workflowId: string;
|
|
executionId: string;
|
|
type: "manual" | "api";
|
|
input: QueueJsonValue;
|
|
userId: string | null;
|
|
sourceId: string;
|
|
};
|
|
|
|
export type WorkflowExecutionContinueRequestedOutboxEventInput = WorkflowExecutionRequestedOutboxEventInput;
|
|
|
|
const WORKFLOW_OUTBOX_SOURCE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,234}$/u;
|
|
|
|
function requireWorkflowOutboxText(value: string, fieldName: string, eventType: string): string {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
throw new Error(`${eventType} outbox events require ${fieldName}.`);
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
function requireWorkflowOutboxSourceId(value: string, eventType: string): string {
|
|
const sourceId = requireWorkflowOutboxText(value, "sourceId", eventType);
|
|
if (!WORKFLOW_OUTBOX_SOURCE_ID_PATTERN.test(sourceId)) {
|
|
throw new Error(`${eventType} outbox events require sourceId to match ${WORKFLOW_OUTBOX_SOURCE_ID_PATTERN.source}.`);
|
|
}
|
|
return sourceId;
|
|
}
|
|
|
|
export type TransactionalMailJobDescriptor = QueueJobDescriptor<typeof queueNames.email> & {
|
|
jobName: "send-transactional-email";
|
|
payload: TransactionalMailJob;
|
|
};
|
|
|
|
export type WebhookDeliveryJobDescriptor = QueueJobDescriptor<typeof queueNames.email> & {
|
|
jobName: "deliver-webhook";
|
|
payload: WebhookDeliveryJob;
|
|
};
|
|
|
|
export type NotificationJobDescriptor = QueueJobDescriptor<typeof queueNames.notifications> & {
|
|
jobName: "send-notification";
|
|
payload: NotificationJob;
|
|
};
|
|
|
|
export type CleanupJobDescriptor = QueueJobDescriptor<typeof queueNames.maintenance> & {
|
|
jobName: "run-cleanup";
|
|
payload: CleanupJob;
|
|
};
|
|
|
|
export type AiChatCompletionJobDescriptor = QueueJobDescriptor<typeof queueNames.aiChatbot> & {
|
|
jobName: "chat-completion";
|
|
payload: AiJobForKind<"ai.chat-completion">;
|
|
};
|
|
|
|
export type AiEmbeddingJobDescriptor = QueueJobDescriptor<typeof queueNames.aiChatbot> & {
|
|
jobName: "create-embedding";
|
|
payload: AiJobForKind<"ai.embedding">;
|
|
};
|
|
|
|
export type AiBrowserTaskJobDescriptor = QueueJobDescriptor<typeof queueNames.aiChatbot> & {
|
|
jobName: "browser-task";
|
|
payload: AiJobForKind<"ai.browser-task">;
|
|
};
|
|
|
|
export type BrowserSessionRunJobDescriptor = QueueJobDescriptor<typeof queueNames.browserAutomation> & {
|
|
jobName: "browser-session-run";
|
|
payload: BrowserAutomationJobForKind<"browser.session-run">;
|
|
};
|
|
|
|
export type BrowserSessionCleanupJobDescriptor = QueueJobDescriptor<typeof queueNames.browserAutomation> & {
|
|
jobName: "browser-session-cleanup";
|
|
payload: BrowserAutomationJobForKind<"browser.session-cleanup">;
|
|
};
|
|
|
|
export type BrowserUsageFlushJobDescriptor = QueueJobDescriptor<typeof queueNames.browserAutomation> & {
|
|
jobName: "usage-flush";
|
|
payload: BrowserAutomationJobForKind<"usage.flush">;
|
|
};
|
|
|
|
export type FileScanJobDescriptor = QueueJobDescriptor<typeof queueNames.fileScan> & {
|
|
jobName: "scan-file";
|
|
payload: FileScanJob;
|
|
};
|
|
|
|
export type FilePreviewJobDescriptor = QueueJobDescriptor<typeof queueNames.filePreview> & {
|
|
jobName: "generate-file-preview";
|
|
payload: FilePreviewJob;
|
|
};
|
|
|
|
export type ExportJobDescriptor = QueueJobDescriptor<typeof queueNames.exports> & {
|
|
jobName: "run-export";
|
|
payload: ExportJob;
|
|
};
|
|
|
|
export type ImportJobDescriptor = QueueJobDescriptor<typeof queueNames.imports> & {
|
|
jobName: "run-import";
|
|
payload: ImportJob;
|
|
};
|
|
|
|
export type PluginInvokeJobDescriptor = QueueJobDescriptor<typeof queueNames.pluginInvoke> & {
|
|
jobName: "invoke-plugin";
|
|
payload: PluginInvokeJob;
|
|
};
|
|
|
|
export type EmailProducerJobDescriptor = TransactionalMailJobDescriptor | WebhookDeliveryJobDescriptor;
|
|
export type AiProducerJobDescriptor = AiChatCompletionJobDescriptor | AiEmbeddingJobDescriptor | AiBrowserTaskJobDescriptor;
|
|
export type BrowserAutomationProducerJobDescriptor = BrowserSessionRunJobDescriptor | BrowserSessionCleanupJobDescriptor | BrowserUsageFlushJobDescriptor;
|
|
|
|
export type ProducerJobDescriptor =
|
|
| EmailProducerJobDescriptor
|
|
| NotificationJobDescriptor
|
|
| CleanupJobDescriptor
|
|
| AiProducerJobDescriptor
|
|
| BrowserAutomationProducerJobDescriptor
|
|
| FileScanJobDescriptor
|
|
| FilePreviewJobDescriptor
|
|
| ExportJobDescriptor
|
|
| ImportJobDescriptor
|
|
| PluginInvokeJobDescriptor;
|
|
|
|
const retryJobOptionsByQueueAndJobName: {
|
|
[Name in QueueName]: Record<QueueJobDescriptor<Name>["jobName"], Pick<JobsOptions, "attempts" | "backoff">>;
|
|
} = {
|
|
[queueNames.email]: {
|
|
"send-transactional-email": { attempts: 3, backoff: { type: "exponential", delay: 1_000 } },
|
|
"deliver-webhook": { attempts: 5, backoff: { type: "exponential", delay: 2_000 } },
|
|
},
|
|
[queueNames.notifications]: {
|
|
"send-notification": { attempts: 3, backoff: { type: "exponential", delay: 1_000 } },
|
|
},
|
|
[queueNames.maintenance]: {
|
|
"run-cleanup": { attempts: 2, backoff: { type: "fixed", delay: 5_000 } },
|
|
// Cron-scheduled grace-period scan: each tick is idempotent (the
|
|
// underlying SQL filter only finds overdue rows), but we still allow
|
|
// one retry on transient Prisma/Redis hiccups before letting the
|
|
// next 24h tick recover the run.
|
|
"dsgvo-grace-period-scan": { attempts: 2, backoff: { type: "fixed", delay: 30_000 } },
|
|
},
|
|
[queueNames.aiChatbot]: {
|
|
"chat-completion": { attempts: 3, backoff: { type: "exponential", delay: 2_000 } },
|
|
"create-embedding": { attempts: 3, backoff: { type: "exponential", delay: 2_000 } },
|
|
"browser-task": { attempts: 2, backoff: { type: "exponential", delay: 5_000 } },
|
|
},
|
|
[queueNames.browserAutomation]: {
|
|
"browser-session-run": { attempts: 2, backoff: { type: "exponential", delay: 5_000 } },
|
|
"browser-session-cleanup": { attempts: 3, backoff: { type: "fixed", delay: 10_000 } },
|
|
"usage-flush": { attempts: 3, backoff: { type: "fixed", delay: 10_000 } },
|
|
},
|
|
[queueNames.fileScan]: {
|
|
"scan-file": { attempts: 3, backoff: { type: "exponential", delay: 2_000 } },
|
|
},
|
|
[queueNames.filePreview]: {
|
|
// Preview generation is best-effort: a couple of retries cover transient
|
|
// storage/decoder hiccups, then the handler marks previewStatus=failed and
|
|
// the user can re-trigger from the detail view.
|
|
"generate-file-preview": { attempts: 3, backoff: { type: "exponential", delay: 3_000 } },
|
|
},
|
|
[queueNames.exports]: {
|
|
"run-export": { attempts: 2, backoff: { type: "exponential", delay: 10_000 } },
|
|
},
|
|
[queueNames.imports]: {
|
|
"run-import": { attempts: 2, backoff: { type: "exponential", delay: 10_000 } },
|
|
},
|
|
[queueNames.pluginInvoke]: {
|
|
"invoke-plugin": { attempts: 3, backoff: { type: "exponential", delay: 2_000 } },
|
|
},
|
|
};
|
|
|
|
export function buildRetryJobOptions(descriptor: ProducerJobDescriptor): JobsOptions {
|
|
const queueRetryOptions = retryJobOptionsByQueueAndJobName[descriptor.queueName] as Record<
|
|
ProducerJobDescriptor["jobName"],
|
|
Pick<JobsOptions, "attempts" | "backoff">
|
|
>;
|
|
|
|
return {
|
|
...queueRetryOptions[descriptor.jobName],
|
|
removeOnComplete: true,
|
|
removeOnFail: 1_000,
|
|
};
|
|
}
|
|
|
|
export function buildTransactionalMailJob(input: TransactionalMailJobInput): TransactionalMailJobDescriptor {
|
|
return {
|
|
queueName: queueNames.email,
|
|
jobName: "send-transactional-email",
|
|
payload: {
|
|
kind: "mail.transactional",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildWebhookDeliveryJob(input: WebhookDeliveryJobInput): WebhookDeliveryJobDescriptor {
|
|
return {
|
|
queueName: queueNames.email,
|
|
jobName: "deliver-webhook",
|
|
payload: {
|
|
kind: "webhook.deliver",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildNotificationJob(input: NotificationJobInput): NotificationJobDescriptor {
|
|
if (input.channel !== "email" && input.channel !== "in-app") {
|
|
throw new Error(`Unsupported notification channel: ${input.channel}`);
|
|
}
|
|
if (!input.tenantId?.trim()) {
|
|
throw new Error("Notification jobs require tenantId.");
|
|
}
|
|
|
|
return {
|
|
queueName: queueNames.notifications,
|
|
jobName: "send-notification",
|
|
payload: {
|
|
kind: "notification.send",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildCleanupJob(input: CleanupJobInput): CleanupJobDescriptor {
|
|
return {
|
|
queueName: queueNames.maintenance,
|
|
jobName: "run-cleanup",
|
|
payload: {
|
|
kind: "cleanup.run",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildAiChatCompletionJob(input: AiChatCompletionJobInput): AiChatCompletionJobDescriptor {
|
|
return {
|
|
queueName: queueNames.aiChatbot,
|
|
jobName: "chat-completion",
|
|
payload: {
|
|
kind: "ai.chat-completion",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildAiEmbeddingJob(input: AiEmbeddingJobInput): AiEmbeddingJobDescriptor {
|
|
return {
|
|
queueName: queueNames.aiChatbot,
|
|
jobName: "create-embedding",
|
|
payload: {
|
|
kind: "ai.embedding",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildAiBrowserTaskJob(input: AiBrowserTaskJobInput): AiBrowserTaskJobDescriptor {
|
|
return {
|
|
queueName: queueNames.aiChatbot,
|
|
jobName: "browser-task",
|
|
payload: {
|
|
kind: "ai.browser-task",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildBrowserSessionRunJob(input: BrowserSessionRunJobInput): BrowserSessionRunJobDescriptor {
|
|
return {
|
|
queueName: queueNames.browserAutomation,
|
|
jobName: "browser-session-run",
|
|
payload: {
|
|
kind: "browser.session-run",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildBrowserSessionCleanupJob(input: BrowserSessionCleanupJobInput): BrowserSessionCleanupJobDescriptor {
|
|
return {
|
|
queueName: queueNames.browserAutomation,
|
|
jobName: "browser-session-cleanup",
|
|
payload: {
|
|
kind: "browser.session-cleanup",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildBrowserUsageFlushJob(input: BrowserUsageFlushJobInput): BrowserUsageFlushJobDescriptor {
|
|
return {
|
|
queueName: queueNames.browserAutomation,
|
|
jobName: "usage-flush",
|
|
payload: {
|
|
kind: "usage.flush",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildFileScanJob(input: FileScanJobInput): FileScanJobDescriptor {
|
|
return {
|
|
queueName: queueNames.fileScan,
|
|
jobName: "scan-file",
|
|
payload: {
|
|
kind: "scan.file",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildFilePreviewJob(input: FilePreviewJobInput): FilePreviewJobDescriptor {
|
|
if (!input.fileId?.trim()) {
|
|
throw new Error("File preview jobs require fileId.");
|
|
}
|
|
if (!input.tenantId?.trim()) {
|
|
throw new Error("File preview jobs require tenantId.");
|
|
}
|
|
return {
|
|
queueName: queueNames.filePreview,
|
|
jobName: "generate-file-preview",
|
|
payload: {
|
|
kind: "preview.file",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildExportJob(input: ExportJobInput): ExportJobDescriptor {
|
|
return {
|
|
queueName: queueNames.exports,
|
|
jobName: "run-export",
|
|
payload: {
|
|
kind: "export.run",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildImportJob(input: ImportJobInput): ImportJobDescriptor {
|
|
return {
|
|
queueName: queueNames.imports,
|
|
jobName: "run-import",
|
|
payload: {
|
|
kind: "import.run",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildPluginInvokeJob(input: PluginInvokeJobInput): PluginInvokeJobDescriptor {
|
|
return {
|
|
queueName: queueNames.pluginInvoke,
|
|
jobName: "invoke-plugin",
|
|
payload: {
|
|
kind: "plugin.invoke",
|
|
...input,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildWorkflowTriggerOutboxEvent(input: WorkflowTriggerOutboxEventInput): WorkflowTriggerOutboxEvent {
|
|
const eventType = "workflow.trigger.event";
|
|
const event = requireWorkflowOutboxText(input.event, "event", eventType);
|
|
const sourceId = requireWorkflowOutboxSourceId(input.sourceId, eventType);
|
|
return {
|
|
tenantId: input.tenantId,
|
|
type: eventType,
|
|
payload: {
|
|
event,
|
|
data: input.data,
|
|
userId: input.userId,
|
|
},
|
|
dispatchKey: `workflow-trigger:${sourceId}`,
|
|
};
|
|
}
|
|
|
|
export function buildWorkflowExecutionRequestedOutboxEvent(input: WorkflowExecutionRequestedOutboxEventInput): WorkflowExecutionRequestedOutboxEvent {
|
|
const eventType = "workflow.execution.requested";
|
|
const workflowId = requireWorkflowOutboxText(input.workflowId, "workflowId", eventType);
|
|
const executionId = requireWorkflowOutboxText(input.executionId, "executionId", eventType);
|
|
const sourceId = requireWorkflowOutboxSourceId(input.sourceId, eventType);
|
|
return {
|
|
tenantId: input.tenantId,
|
|
type: eventType,
|
|
payload: {
|
|
workflowId,
|
|
executionId,
|
|
type: input.type,
|
|
input: input.input,
|
|
userId: input.userId,
|
|
},
|
|
dispatchKey: `workflow-execution:${sourceId}`,
|
|
};
|
|
}
|
|
|
|
export function buildWorkflowExecutionContinueRequestedOutboxEvent(
|
|
input: WorkflowExecutionContinueRequestedOutboxEventInput
|
|
): WorkflowExecutionContinueRequestedOutboxEvent {
|
|
const eventType = "workflow.execution.continue.requested";
|
|
const workflowId = requireWorkflowOutboxText(input.workflowId, "workflowId", eventType);
|
|
const executionId = requireWorkflowOutboxText(input.executionId, "executionId", eventType);
|
|
const sourceId = requireWorkflowOutboxSourceId(input.sourceId, eventType);
|
|
return {
|
|
tenantId: input.tenantId,
|
|
type: eventType,
|
|
payload: {
|
|
workflowId,
|
|
executionId,
|
|
type: input.type,
|
|
input: input.input,
|
|
userId: input.userId,
|
|
},
|
|
dispatchKey: `workflow-continuation:${sourceId}`,
|
|
};
|
|
}
|
|
|
|
export type QueueEnqueueOptions = JobsOptions & {
|
|
idempotencyKey?: string;
|
|
/**
|
|
* Tenant scope for plan-based priority resolution. Pass `null` for
|
|
* internal/system jobs (cron, retention, audit) which always run at
|
|
* CRITICAL priority. Omitting `tenantId` leaves the priority untouched
|
|
* unless `priorityOverride` is set.
|
|
*/
|
|
tenantId?: string | null;
|
|
/**
|
|
* Explicit priority hint that bypasses tenant-plan lookup. Use for
|
|
* semantically critical jobs (password-reset mail, verification mail)
|
|
* regardless of plan, and for explicit low-priority batches.
|
|
*/
|
|
priorityOverride?: PriorityOverride;
|
|
};
|
|
|
|
/**
|
|
* Optional enqueue observer. Producer-host apps (api / web / worker) can
|
|
* register one during boot to forward priority/queue/jobName info to their
|
|
* Prometheus registry. Kept as a module-level setter so the queue package
|
|
* does not depend on prom-client or any app metrics bundle.
|
|
*/
|
|
export type EnqueueObserver = (event: { queueName: QueueName; jobName: string; priority: number; priorityLabel: ReturnType<typeof priorityLabel> }) => void;
|
|
|
|
let enqueueObserver: EnqueueObserver | null = null;
|
|
|
|
export function setEnqueueObserver(observer: EnqueueObserver | null): void {
|
|
enqueueObserver = observer;
|
|
}
|
|
|
|
export function getEnqueueObserver(): EnqueueObserver | null {
|
|
return enqueueObserver;
|
|
}
|
|
|
|
export function buildIdempotentJobId(descriptor: ProducerJobDescriptor, options?: QueueEnqueueOptions): string | undefined {
|
|
if (typeof options?.jobId === "string") {
|
|
return options.jobId;
|
|
}
|
|
|
|
if (typeof options?.idempotencyKey === "string") {
|
|
return options.idempotencyKey;
|
|
}
|
|
|
|
if (descriptor.payload.kind === "export.run") {
|
|
return descriptor.payload.exportJobId;
|
|
}
|
|
|
|
if (descriptor.payload.kind === "import.run") {
|
|
return descriptor.payload.importJobId;
|
|
}
|
|
|
|
const metadata = descriptor.payload.metadata;
|
|
if (metadata && typeof metadata.idempotencyKey === "string") {
|
|
return metadata.idempotencyKey;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
async function resolvePriorityForEnqueue(options: QueueEnqueueOptions | undefined): Promise<number | undefined> {
|
|
if (!options) return undefined;
|
|
|
|
if (typeof options.priority === "number") {
|
|
// Explicit numeric priority always wins so power-users / tests can pin
|
|
// a precise value.
|
|
return options.priority;
|
|
}
|
|
|
|
if (options.priorityOverride !== undefined) {
|
|
return priorityFromOverride(options.priorityOverride);
|
|
}
|
|
|
|
if (options.tenantId !== undefined) {
|
|
if (options.tenantId === null) {
|
|
// Explicit `null` tenantId → internal job → CRITICAL. Short-circuit
|
|
// the resolver to avoid a useless feature-flag round-trip.
|
|
return JOB_PRIORITY_CRITICAL;
|
|
}
|
|
return resolveJobPriorityForTenant(options.tenantId);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export async function enqueueJob<Name extends QueueName>(queue: TypedQueue<Name>, descriptor: QueueJobDescriptor<Name>, options?: QueueEnqueueOptions) {
|
|
const producerDescriptor = descriptor as ProducerJobDescriptor;
|
|
const callerOptions: JobsOptions = options ? { ...options } : {};
|
|
delete (callerOptions as QueueEnqueueOptions).idempotencyKey;
|
|
delete (callerOptions as QueueEnqueueOptions).tenantId;
|
|
delete (callerOptions as QueueEnqueueOptions).priorityOverride;
|
|
const jobId = buildIdempotentJobId(producerDescriptor, options);
|
|
const idempotentCompletionRetention: Pick<JobsOptions, "removeOnComplete"> =
|
|
jobId && callerOptions.removeOnComplete === undefined ? { removeOnComplete: false } : {};
|
|
const resolvedPriority = await resolvePriorityForEnqueue(options);
|
|
const priorityOptions: Pick<JobsOptions, "priority"> = resolvedPriority !== undefined ? { priority: resolvedPriority } : {};
|
|
const enqueueOptions: JobsOptions = {
|
|
...buildRetryJobOptions(producerDescriptor),
|
|
...idempotentCompletionRetention,
|
|
...(jobId ? { jobId } : {}),
|
|
...priorityOptions,
|
|
...callerOptions,
|
|
};
|
|
|
|
if (enqueueObserver && typeof enqueueOptions.priority === "number") {
|
|
try {
|
|
enqueueObserver({
|
|
queueName: descriptor.queueName as QueueName,
|
|
jobName: descriptor.jobName,
|
|
priority: enqueueOptions.priority,
|
|
priorityLabel: priorityLabel(enqueueOptions.priority),
|
|
});
|
|
} catch {
|
|
// Observer must never block enqueue. Swallow.
|
|
}
|
|
}
|
|
|
|
return queue.add(descriptor.jobName, descriptor.payload, enqueueOptions);
|
|
}
|