Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,583 @@
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user