@workglow/job-queue 0.2.27 → 0.2.28
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/browser.js +457 -43
- package/dist/browser.js.map +15 -10
- package/dist/bun.js +457 -43
- package/dist/bun.js.map +15 -10
- package/dist/common.d.ts +5 -0
- package/dist/common.d.ts.map +1 -1
- package/dist/job/Job.d.ts +1 -1
- package/dist/job/Job.d.ts.map +1 -1
- package/dist/job/JobQueueClient.d.ts +2 -1
- package/dist/job/JobQueueClient.d.ts.map +1 -1
- package/dist/job/JobQueueServer.d.ts +1 -1
- package/dist/job/JobQueueServer.d.ts.map +1 -1
- package/dist/job/JobQueueWorker.d.ts +1 -1
- package/dist/job/JobQueueWorker.d.ts.map +1 -1
- package/dist/job/JobStorageConverters.d.ts +1 -1
- package/dist/job/JobStorageConverters.d.ts.map +1 -1
- package/dist/limiter/CompositeLimiter.d.ts.map +1 -1
- package/dist/limiter/RateLimiter.d.ts +1 -1
- package/dist/limiter/RateLimiter.d.ts.map +1 -1
- package/dist/node.js +457 -43
- package/dist/node.js.map +15 -10
- package/dist/queue-storage/IQueueStorage.d.ts +229 -0
- package/dist/queue-storage/IQueueStorage.d.ts.map +1 -0
- package/dist/queue-storage/InMemoryQueueStorage.d.ts +149 -0
- package/dist/queue-storage/InMemoryQueueStorage.d.ts.map +1 -0
- package/dist/queue-storage/TelemetryQueueStorage.d.ts +33 -0
- package/dist/queue-storage/TelemetryQueueStorage.d.ts.map +1 -0
- package/dist/rate-limiter-storage/IRateLimiterStorage.d.ts +127 -0
- package/dist/rate-limiter-storage/IRateLimiterStorage.d.ts.map +1 -0
- package/dist/rate-limiter-storage/InMemoryRateLimiterStorage.d.ts +43 -0
- package/dist/rate-limiter-storage/InMemoryRateLimiterStorage.d.ts.map +1 -0
- package/package.json +3 -8
package/dist/node.js.map
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
3
|
+
"sources": ["../src/queue-storage/IQueueStorage.ts", "../src/job/JobError.ts", "../src/job/Job.ts", "../src/job/JobErrorDiagnostics.ts", "../src/job/JobQueueClient.ts", "../src/job/JobStorageConverters.ts", "../src/job/JobQueueServer.ts", "../src/limiter/NullLimiter.ts", "../src/job/JobQueueWorker.ts", "../src/limiter/CompositeLimiter.ts", "../src/limiter/ConcurrencyLimiter.ts", "../src/limiter/DelayLimiter.ts", "../src/limiter/EvenlySpacedRateLimiter.ts", "../src/limiter/ILimiter.ts", "../src/limiter/RateLimiter.ts", "../src/queue-storage/InMemoryQueueStorage.ts", "../src/queue-storage/TelemetryQueueStorage.ts", "../src/rate-limiter-storage/IRateLimiterStorage.ts", "../src/rate-limiter-storage/InMemoryRateLimiterStorage.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {
|
|
5
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken } from \"@workglow/util\";\n\nexport const QUEUE_STORAGE = createServiceToken<IQueueStorage<any, any>>(\"jobqueue.storage\");\n\n/**\n * The type of a prefix column.\n * - \"uuid\" maps to UUID in PostgreSQL/Supabase, TEXT in SQLite/IndexedDB/InMemory\n * - \"number\" maps to INTEGER in PostgreSQL/Supabase/SQLite, number in IndexedDB/InMemory\n */\nexport type PrefixColumnType = \"uuid\" | \"number\";\n\n/**\n * Defines a prefix column for queue storage filtering.\n */\nexport interface PrefixColumn {\n readonly name: string;\n readonly type: PrefixColumnType;\n}\n\n/**\n * Options for configuring queue storage with prefix filters.\n */\nexport interface QueueStorageOptions {\n /** The prefix column definitions for this storage */\n readonly prefixes?: readonly PrefixColumn[];\n /** The values for each prefix column */\n readonly prefixValues?: Readonly<Record<string, string | number>>;\n}\n\nexport type JobStatus = \"PENDING\" | \"PROCESSING\" | \"COMPLETED\" | \"ABORTING\" | \"FAILED\" | \"DISABLED\";\nexport const JobStatus = {\n PENDING: \"PENDING\",\n PROCESSING: \"PROCESSING\",\n COMPLETED: \"COMPLETED\",\n ABORTING: \"ABORTING\",\n FAILED: \"FAILED\",\n DISABLED: \"DISABLED\",\n} as const satisfies Record<JobStatus, JobStatus>;\n\n/**\n * Type of change that occurred in the queue.\n *\n * `RESYNC` is a synthetic event emitted by some backends (e.g. Postgres\n * LISTEN/NOTIFY) after a (re)connect to indicate that arbitrary changes may\n * have happened during a disconnect window. Subscribers should treat it as a\n * \"kick the workers, re-poll state\" signal — `old` and `new` are both\n * undefined.\n */\nexport type QueueChangeType = \"INSERT\" | \"UPDATE\" | \"DELETE\" | \"RESYNC\";\n\n/**\n * Payload describing a change to a job\n */\nexport interface QueueChangePayload<Input, Output> {\n readonly type: QueueChangeType;\n readonly old?: JobStorageFormat<Input, Output>;\n readonly new?: JobStorageFormat<Input, Output>;\n}\n\n/**\n * Options for subscribing to queue changes\n */\nexport interface QueueSubscribeOptions {\n /** Polling interval in milliseconds (used by implementations that rely on polling) */\n readonly pollingIntervalMs?: number;\n /**\n * Custom prefix filter for this subscription.\n *\n * - If not provided (undefined): Uses the storage instance's configured prefixValues\n * - If empty object ({}): Receives ALL changes across all prefix combinations\n * - If partial object: Receives changes matching the specified subset of prefixes\n *\n * @example\n * // Storage configured with prefixes: [{name: \"user_id\"}, {name: \"project_id\"}]\n * // and prefixValues: {user_id: \"abc\", project_id: \"123\"}\n *\n * // Subscribe to only this user+project (default behavior)\n * storage.subscribeToChanges(callback);\n *\n * // Subscribe to all projects for this user\n * storage.subscribeToChanges(callback, { prefixFilter: { user_id: \"abc\" } });\n *\n * // Subscribe to ALL jobs in this queue (admin/supervisor view)\n * storage.subscribeToChanges(callback, { prefixFilter: {} });\n */\n readonly prefixFilter?: Readonly<Record<string, string | number>>;\n}\n\n/**\n * Details about a job that reflect the structure in the database.\n */\nexport type JobStorageFormat<Input, Output> = {\n id?: unknown;\n job_run_id?: string;\n queue?: string;\n input: Input;\n output?: Output | null;\n error?: string | null;\n error_code?: string | null;\n fingerprint?: string;\n max_retries?: number;\n status?: JobStatus;\n created_at?: string;\n deadline_at?: string | null;\n last_ran_at?: string | null;\n run_after: string | null;\n completed_at: string | null;\n run_attempts?: number;\n progress?: number;\n progress_message?: string;\n progress_details?: Record<string, any> | null;\n worker_id?: string | null;\n};\n\n/**\n * Whether a queue storage's state is shared across processes.\n *\n * - `\"process\"` — in-memory / per-process state. Workers in the same process\n * share it, but separate processes do not.\n * - `\"cluster\"` — state lives in shared external storage (Postgres, Supabase,\n * etc.) visible to every process. Pairing a `\"process\"`-scoped limiter with\n * a `\"cluster\"`-scoped queue almost always indicates a misconfiguration.\n */\nexport type QueueStorageScope = \"process\" | \"cluster\";\n\n/**\n * Interface defining the storage operations for a job queue\n */\nexport interface IQueueStorage<Input, Output> {\n /**\n * Whether this storage is shared across processes. In-memory / browser\n * backends MUST report `\"process\"`. Shared databases (Postgres, Supabase)\n * report `\"cluster\"`. Used by JobQueueServer to detect process-scoped\n * limiters paired with cluster-scoped queues.\n */\n readonly scope: QueueStorageScope;\n\n /**\n * Adds a job to the queue storage\n * @param job - The job to add to the queue storage\n * @returns The ID of the job\n */\n add(job: JobStorageFormat<Input, Output>): Promise<unknown>;\n\n /**\n * Gets a job from the queue storage by ID\n * @param id - The ID of the job to get\n * @returns The job with the given ID\n */\n get(id: unknown): Promise<JobStorageFormat<Input, Output> | undefined>;\n\n /**\n * Gets the next job from the queue storage\n * @param workerId - Worker ID to associate with the job (required)\n * @returns The next job from the queue storage\n */\n next(workerId: string): Promise<JobStorageFormat<Input, Output> | undefined>;\n\n /**\n * Releases a job that was just claimed by {@link next} but won't be\n * processed (e.g. the worker was stopped mid-claim). Resets status to\n * PENDING and clears worker_id WITHOUT incrementing run_attempts —\n * the worker never actually attempted execution, so the retry budget\n * must be preserved.\n * @param id - The id of the claimed job to release.\n */\n release(id: unknown): Promise<void>;\n\n /**\n * Peeks at the next job(s) from the queue storage without removing them\n * @param status - The status of the jobs to peek at\n * @param num - The number of jobs to peek at\n * @returns The jobs with the given status\n */\n peek(status?: JobStatus, num?: number): Promise<Array<JobStorageFormat<Input, Output>>>;\n\n /**\n * Gets the size of the queue storage\n * @param status - The status of the jobs to get the size for\n * @returns The size of the queue storage\n */\n size(status?: JobStatus): Promise<number>;\n\n /**\n * Completes a job in the queue storage\n * @param job - The job to complete\n */\n complete(job: JobStorageFormat<Input, Output>): Promise<void>;\n\n /**\n * Deletes all jobs from the queue storage\n */\n deleteAll(): Promise<void>;\n\n /**\n * Gets the output for a given input from the queue storage\n * @param input - The input to get the output for\n * @returns The output of the job\n */\n outputForInput(input: Input): Promise<Output | null>;\n\n /**\n * Aborts a job in the queue storage\n * @param id - The ID of the job to abort\n */\n abort(id: unknown): Promise<void>;\n\n /**\n * Gets the jobs by job run ID from the queue storage\n * @param runId - The job run ID of the jobs to get\n * @returns The jobs with the given job run ID\n */\n getByRunId(runId: string): Promise<Array<JobStorageFormat<Input, Output>>>;\n\n /**\n * Saves progress updates for a job in the queue storage\n * @param id - The ID of the job to save the progress for\n * @param progress - The progress of the job\n * @param message - The message of the job\n * @param details - The details of the job\n */\n saveProgress(\n id: unknown,\n progress: number,\n message: string,\n details: Record<string, any> | null\n ): Promise<void>;\n\n /**\n * Deletes a job by its ID from the queue storage\n * @param id - The ID of the job to delete\n */\n delete(id: unknown): Promise<void>;\n\n /**\n * Deletes jobs from the queue storage that are of a specific status and older than the specified time\n * @param status - The status of the jobs to delete\n * @param olderThanMs - The time in milliseconds that the jobs must be older than to be deleted\n */\n deleteJobsByStatusAndAge(status: JobStatus, olderThanMs: number): Promise<void>;\n\n /**\n * Sets up the database schema and tables.\n * This method should be called before using the storage in tests.\n * For production use, database setup should be done via migrations.\n */\n setupDatabase(): Promise<void>;\n\n /**\n * Subscribes to changes in the queue (including remote changes).\n * @param callback - Function called when a change occurs\n * @param options - Optional subscription options (e.g., polling interval)\n * @returns Unsubscribe function\n */\n subscribeToChanges(\n callback: (change: QueueChangePayload<Input, Output>) => void,\n options?: QueueSubscribeOptions\n ): () => void;\n}\n",
|
|
6
6
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { BaseError } from \"@workglow/util\";\n\nexport class JobError extends BaseError {\n public static override type: string = \"JobError\";\n public retryable = false;\n}\n\n/**\n * A job error that is caused by a job not being found\n *\n * Examples: job.id is undefined, job.id is not found in the storage, etc.\n */\nexport class JobNotFoundError extends JobError {\n public static override type: string = \"JobNotFoundError\";\n constructor(message: string = \"Job not found\") {\n super(message);\n }\n}\n\n/**\n * A job error that is retryable\n *\n * Examples: network timeouts, temporary unavailability of an external service, or rate-limiting\n */\nexport class RetryableJobError extends JobError {\n public static override type: string = \"RetryableJobError\";\n constructor(\n message: string,\n public retryDate?: Date\n ) {\n super(message);\n this.retryable = true;\n }\n}\n\n/**\n * A job error that is not retryable\n *\n * Examples: invalid input, missing required parameters, or a permanent failure of\n * an external service, permission errors, running out of money for an API, etc.\n */\nexport class PermanentJobError extends JobError {\n public static override type: string = \"PermanentJobError\";\n}\n\n/**\n * A job error that is caused by an abort signal,\n * meaning the client aborted the job on purpose,\n * not by the queue going down or similar.\n *\n * Example: job.abort()\n */\nexport class AbortSignalJobError extends PermanentJobError {\n public static override type: string = \"AbortSignalJobError\";\n}\n\n/**\n * A job error that is caused by a job being disabled\n *\n * Examples: job.disable()\n */\nexport class JobDisabledError extends PermanentJobError {\n public static override type: string = \"JobDisabledError\";\n}\n",
|
|
7
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus } from \"../queue-storage/IQueueStorage\";\nimport { JobError } from \"./JobError\";\nimport type { JobProgressListener } from \"./JobQueueEventListeners\";\n\nexport { JobStatus };\n\n/**\n * Context for job execution\n */\nexport interface IJobExecuteContext {\n signal: AbortSignal;\n updateProgress: (\n progress: number,\n message?: string,\n details?: Record<string, any> | null\n ) => Promise<void>;\n}\n\n/**\n * Details about a job that reflect the structure in the database.\n */\nexport type JobConstructorParam<Input, Output> = {\n id?: unknown;\n jobRunId?: string;\n queueName?: string;\n input: Input;\n output?: Output | null;\n error?: string | null;\n errorCode?: string | null;\n fingerprint?: string;\n maxRetries?: number;\n status?: JobStatus;\n createdAt?: Date;\n deadlineAt?: Date | null;\n lastRanAt?: Date | null;\n runAfter?: Date | null;\n completedAt?: Date | null;\n runAttempts?: number;\n progress?: number;\n progressMessage?: string;\n progressDetails?: Record<string, any> | null;\n /** The ID of the worker that claimed this job, null if unclaimed */\n workerId?: string | null;\n};\n\nexport type JobClass<Input, Output> = new (\n param: JobConstructorParam<Input, Output>\n) => Job<Input, Output>;\n\n/**\n * A job that can be executed by a JobQueue.\n *\n * @template Input - The type of the job's input\n * @template Output - The type of the job's output\n */\nexport class Job<Input, Output> {\n public id: unknown;\n public jobRunId: string | undefined;\n public queueName: string | undefined;\n public input: Input;\n public maxRetries: number;\n public createdAt: Date;\n public fingerprint: string | undefined;\n public status: JobStatus = JobStatus.PENDING;\n public runAfter: Date;\n public output: Output | null = null;\n public runAttempts: number = 0;\n public lastRanAt: Date | null = null;\n public completedAt: Date | null = null;\n public deadlineAt: Date | null = null;\n public error: string | null = null;\n public errorCode: string | null = null;\n public progress: number = 0;\n public progressMessage: string = \"\";\n public progressDetails: Record<string, any> | null = null;\n /** The ID of the worker that claimed this job */\n public workerId: string | null = null;\n\n constructor({\n queueName,\n input,\n jobRunId,\n id,\n error = null,\n errorCode = null,\n fingerprint = undefined,\n output = null,\n maxRetries = 10,\n createdAt = new Date(),\n completedAt = null,\n status = JobStatus.PENDING,\n deadlineAt = null,\n runAttempts = 0,\n lastRanAt = null,\n runAfter = new Date(),\n progress = 0,\n progressMessage = \"\",\n progressDetails = null,\n workerId = null,\n }: JobConstructorParam<Input, Output>) {\n this.runAfter = runAfter ?? new Date();\n this.createdAt = createdAt ?? new Date();\n this.lastRanAt = lastRanAt ?? null;\n this.deadlineAt = deadlineAt ?? null;\n this.completedAt = completedAt ?? null;\n\n this.queueName = queueName;\n this.id = id;\n this.jobRunId = jobRunId;\n this.status = status;\n this.fingerprint = fingerprint;\n this.input = input;\n this.maxRetries = maxRetries;\n this.runAttempts = runAttempts;\n this.output = output;\n this.error = error;\n this.errorCode = errorCode;\n this.progress = progress;\n this.progressMessage = progressMessage;\n this.progressDetails = progressDetails;\n this.workerId = workerId ?? null;\n }\n\n async execute(_input: Input, _context: IJobExecuteContext): Promise<Output> {\n throw new JobError(\"Method not implemented.\");\n }\n\n public progressListeners: Set<JobProgressListener> = new Set();\n\n /**\n * Update the job's progress (for direct execution without a worker)\n * @param progress - Progress value between 0 and 100\n * @param message - Optional progress message\n * @param details - Optional progress details\n */\n public async updateProgress(\n progress: number,\n message: string = \"\",\n details: Record<string, any> | null = null\n ): Promise<void> {\n this.progress = progress;\n this.progressMessage = message;\n this.progressDetails = details;\n\n // Notify direct listeners\n for (const listener of this.progressListeners) {\n listener(progress, message, details);\n }\n }\n\n /**\n * Adds a progress listener for this job\n *\n * Only used if run directly (not via a queue)\n *\n * @param listener - The callback function to be called when progress updates occur\n * @returns A cleanup function to remove the listener\n */\n public onJobProgress(listener: JobProgressListener): () => void {\n this.progressListeners.add(listener);\n\n return () => {\n this.progressListeners.delete(listener);\n };\n }\n}\n",
|
|
7
8
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/** Appended to job error messages; {@link JobQueueClient.buildErrorFromCode} maps this back onto `.stack`. */\nexport const JOB_ERROR_DIAGNOSTICS_MARKER = \"\\n\\n--- Error diagnostics ---\\n\";\n\nconst DEFAULT_MAX_DIAGNOSTICS_CHARS = 48_000;\n\n/**\n * Formats an error and its `.cause` chain (name, message, stack) for logs and persisted job errors.\n */\nexport function formatErrorChainForDiagnostics(\n err: unknown,\n maxChars: number = DEFAULT_MAX_DIAGNOSTICS_CHARS\n): string {\n const lines: string[] = [];\n let current: unknown = err;\n for (let depth = 0; depth < 8 && current != null; depth++) {\n if (current instanceof Error) {\n lines.push(`${current.name}: ${current.message}`);\n if (current.stack) {\n lines.push(current.stack);\n }\n const next = current.cause;\n if (next === undefined || next === null) {\n break;\n }\n lines.push(\"\");\n current = next;\n } else {\n lines.push(typeof current === \"string\" ? current : String(current));\n break;\n }\n }\n const text = lines.join(\"\\n\");\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars)}\\n…(truncated)`;\n}\n\n/**\n * Combines a short summary line with a full diagnostic dump (for storage on the failed job).\n */\nexport function withJobErrorDiagnostics(summaryLine: string, err: unknown): string {\n const diag = formatErrorChainForDiagnostics(err);\n if (diag.length === 0) {\n return summaryLine;\n }\n return `${summaryLine}${JOB_ERROR_DIAGNOSTICS_MARKER}${diag}`;\n}\n\n/**\n * When a persisted job error includes {@link JOB_ERROR_DIAGNOSTICS_MARKER}, set `.stack` so runtimes\n * (e.g. Vitest) print the worker-side trace instead of only the queue client frames.\n */\nexport function applyPersistedDiagnosticsToStack(\n jobError: JobErrorLike,\n fullMessage: string\n): void {\n if (!fullMessage.includes(JOB_ERROR_DIAGNOSTICS_MARKER)) {\n return;\n }\n const firstLine = fullMessage.split(\"\\n\")[0] ?? fullMessage;\n jobError.stack = `${jobError.name}: ${firstLine}\\n${fullMessage}`;\n}\n\ninterface JobErrorLike {\n readonly name: string;\n stack?: string;\n}\n",
|
|
8
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { IQueueStorage, JobStatus, JobStorageFormat, QueueChangePayload } from \"@workglow/storage\";\nimport { EventEmitter } from \"@workglow/util\";\nimport { Job } from \"./Job\";\nimport {\n AbortSignalJobError,\n JobDisabledError,\n JobError,\n JobNotFoundError,\n PermanentJobError,\n RetryableJobError,\n} from \"./JobError\";\nimport { applyPersistedDiagnosticsToStack } from \"./JobErrorDiagnostics\";\nimport {\n JobProgressListener,\n JobQueueEventListener,\n JobQueueEventListeners,\n JobQueueEventParameters,\n JobQueueEvents,\n} from \"./JobQueueEventListeners\";\nimport type { JobQueueServer } from \"./JobQueueServer\";\nimport { storageToClass } from \"./JobStorageConverters\";\n\n/**\n * Handle returned when submitting a job, providing methods to interact with the job\n */\nexport interface JobHandle<Output> {\n readonly id: unknown;\n waitFor(): Promise<Output>;\n abort(): Promise<void>;\n onProgress(callback: JobProgressListener): () => void;\n}\n\n/**\n * Options for creating a JobQueueClient\n */\nexport interface JobQueueClientOptions<Input, Output> {\n readonly storage: IQueueStorage<Input, Output>;\n readonly queueName: string;\n}\n\n/**\n * Client for submitting jobs and monitoring their progress.\n * Connect to a JobQueueServer for same-process optimization,\n * or use storage subscriptions for cross-process communication.\n */\nexport class JobQueueClient<Input, Output> {\n public readonly queueName: string;\n protected readonly storage: IQueueStorage<Input, Output>;\n protected readonly events = new EventEmitter<JobQueueEventListeners<Input, Output>>();\n protected server: JobQueueServer<Input, Output> | null = null;\n protected storageUnsubscribe: (() => void) | null = null;\n\n /**\n * Map of job IDs to their pending promise resolvers\n */\n protected readonly activeJobPromises: Map<\n unknown,\n Array<{\n resolve: (value: Output) => void;\n reject: (err: JobError) => void;\n }>\n > = new Map();\n\n /**\n * Map of job IDs to their progress listeners\n */\n protected readonly jobProgressListeners: Map<unknown, Set<JobProgressListener>> = new Map();\n\n /**\n * Last known progress state for each job\n */\n protected readonly lastKnownProgress: Map<\n unknown,\n {\n readonly progress: number;\n readonly message: string;\n readonly details: Record<string, unknown> | null;\n }\n > = new Map();\n\n constructor(options: JobQueueClientOptions<Input, Output>) {\n this.queueName = options.queueName;\n this.storage = options.storage;\n }\n\n /**\n * Attach to a local JobQueueServer for same-process event optimization.\n * When attached, events flow directly from server without storage polling.\n */\n public attach(server: JobQueueServer<Input, Output>): void {\n if (this.server) {\n this.detach();\n }\n this.server = server;\n server.addClient(this);\n\n // Unsubscribe from storage if we were using it\n if (this.storageUnsubscribe) {\n this.storageUnsubscribe();\n this.storageUnsubscribe = null;\n }\n }\n\n /**\n * Detach from the current server\n */\n public detach(): void {\n if (this.server) {\n this.server.removeClient(this);\n this.server = null;\n }\n }\n\n /**\n * Connect to storage for cross-process communication (when no local server).\n * Uses storage subscriptions to receive job updates.\n */\n public connect(): void {\n if (this.server) {\n return; // Already connected via server\n }\n\n if (this.storageUnsubscribe) {\n return; // Already subscribed\n }\n\n this.storageUnsubscribe = this.storage.subscribeToChanges(\n (change: QueueChangePayload<Input, Output>) => {\n this.handleStorageChange(change);\n }\n );\n }\n\n /**\n * Disconnect from storage subscriptions\n */\n public disconnect(): void {\n if (this.storageUnsubscribe) {\n this.storageUnsubscribe();\n this.storageUnsubscribe = null;\n }\n this.detach();\n }\n\n /**\n * Submit a job to the queue\n */\n public async submit(\n input: Input,\n options?: {\n readonly jobRunId?: string;\n readonly fingerprint?: string;\n readonly maxRetries?: number;\n readonly runAfter?: Date;\n readonly deadlineAt?: Date;\n }\n ): Promise<JobHandle<Output>> {\n const job: JobStorageFormat<Input, Output> = {\n queue: this.queueName,\n input,\n job_run_id: options?.jobRunId,\n fingerprint: options?.fingerprint,\n max_retries: options?.maxRetries ?? 10,\n run_after: options?.runAfter?.toISOString() ?? new Date().toISOString(),\n deadline_at: options?.deadlineAt?.toISOString() ?? null,\n completed_at: null,\n status: JobStatus.PENDING,\n };\n\n const id = await this.storage.add(job);\n\n // Same-process fast path: poke the worker directly so it doesn't have to\n // wait for the poll interval (crucial for Sqlite/Postgres, whose\n // subscribeToChanges throws).\n this.server?.handleJobAdded(id);\n\n return this.createJobHandle(id);\n }\n\n /**\n * Submit multiple jobs to the queue\n */\n public async submitBatch(\n inputs: readonly Input[],\n options?: {\n readonly jobRunId?: string;\n readonly maxRetries?: number;\n }\n ): Promise<readonly JobHandle<Output>[]> {\n const handles: JobHandle<Output>[] = [];\n for (const input of inputs) {\n const handle = await this.submit(input, options);\n handles.push(handle);\n }\n return handles;\n }\n\n /**\n * Get a job by ID\n */\n public async getJob(id: unknown): Promise<Job<Input, Output> | undefined> {\n if (!id) throw new JobNotFoundError(\"Cannot get undefined job\");\n const job = await this.storage.get(id);\n if (!job) return undefined;\n return this.storageToClass(job);\n }\n\n /**\n * Get jobs by run ID\n */\n public async getJobsByRunId(runId: string): Promise<readonly Job<Input, Output>[]> {\n if (!runId) throw new JobNotFoundError(\"Cannot get jobs by undefined runId\");\n const jobs = await this.storage.getByRunId(runId);\n return jobs.map((job) => this.storageToClass(job));\n }\n\n /**\n * Peek at jobs in the queue\n */\n public async peek(status?: JobStatus, num?: number): Promise<readonly Job<Input, Output>[]> {\n const jobs = await this.storage.peek(status, num);\n return jobs.map((job) => this.storageToClass(job));\n }\n\n /**\n * Get the size of the queue\n */\n public async size(status?: JobStatus): Promise<number> {\n return this.storage.size(status);\n }\n\n /**\n * Get the output for an input (if job completed)\n */\n public async outputForInput(input: Input): Promise<Output | null> {\n if (!input) throw new JobNotFoundError(\"Cannot get output for undefined input\");\n return this.storage.outputForInput(input);\n }\n\n /**\n * Wait for a job to complete.\n *\n * Registers the resolver BEFORE reading storage so that a `handleJobError`\n * / `handleJobComplete` event fired during the storage read isn't dropped\n * on the floor. The previous order (read first, register after) had a\n * TOCTOU window where a fast same-process abort could complete between the\n * read and the registration, leaving `waitFor` to register against an\n * already-finished job and hang forever.\n */\n public async waitFor(jobId: unknown): Promise<Output> {\n if (!jobId) throw new JobNotFoundError(\"Cannot wait for undefined job\");\n\n const { promise, resolve, reject } = Promise.withResolvers<Output>();\n promise.catch(() => {}); // Prevent unhandled rejection\n\n const promises = this.activeJobPromises.get(jobId) || [];\n promises.push({ resolve, reject });\n this.activeJobPromises.set(jobId, promises);\n\n // Now check storage — if the job is already terminal (raced us to it),\n // settle the promise ourselves and clean up the registration. The\n // handler paths (handleJobComplete/Error/Disabled) are idempotent on\n // already-settled promises.\n const job = await this.getJob(jobId);\n if (!job) {\n this.removePromise(jobId, resolve, reject);\n throw new JobNotFoundError(`Job ${jobId} not found`);\n }\n if (job.status === JobStatus.COMPLETED) {\n this.removePromise(jobId, resolve, reject);\n return job.output as Output;\n }\n if (job.status === JobStatus.DISABLED) {\n this.removePromise(jobId, resolve, reject);\n throw new JobDisabledError(`Job ${jobId} was disabled`);\n }\n if (job.status === JobStatus.FAILED) {\n this.removePromise(jobId, resolve, reject);\n throw this.buildErrorFromJob(job);\n }\n\n return promise;\n }\n\n private removePromise(\n jobId: unknown,\n resolve: (output: Output) => void,\n reject: (err: unknown) => void\n ): void {\n const list = this.activeJobPromises.get(jobId);\n if (!list) return;\n const idx = list.findIndex((p) => p.resolve === resolve && p.reject === reject);\n if (idx !== -1) list.splice(idx, 1);\n if (list.length === 0) this.activeJobPromises.delete(jobId);\n }\n\n /**\n * Abort a job.\n *\n * Same-process path: fires the in-memory abort controller on the attached\n * server — `handleAbort` will write FAILED directly, so we skip the\n * `storage.abort(…)` ABORTING write. Writing both would race (last-writer-\n * wins) and can leave the row stuck at ABORTING on async storages.\n *\n * Cross-process path (or job not currently running on any local worker):\n * write ABORTING to storage so the remote worker's poll picks it up.\n *\n * Crash window: if the process dies after the in-memory abort fires but\n * before `failJob` writes FAILED, the row stays PROCESSING. `fixupJobs()`\n * resets it to PENDING on next start and the job will re-run. Make handlers\n * idempotent (or use `uniquenessKey`) if that's not acceptable.\n */\n public async abort(jobId: unknown): Promise<void> {\n if (!jobId) throw new JobNotFoundError(\"Cannot abort undefined job\");\n const firedLocally = this.server?.abortJob(jobId) ?? false;\n if (!firedLocally) {\n try {\n await this.storage.abort(jobId);\n } finally {\n this.events.emit(\"job_aborting\", this.queueName, jobId);\n }\n return;\n }\n this.events.emit(\"job_aborting\", this.queueName, jobId);\n }\n\n /**\n * Abort all jobs in a job run\n */\n public async abortJobRun(jobRunId: string): Promise<void> {\n if (!jobRunId) throw new JobNotFoundError(\"Cannot abort job run with undefined jobRunId\");\n const jobs = await this.getJobsByRunId(jobRunId);\n await Promise.allSettled(\n jobs.map((job) => {\n if (job.status === JobStatus.PROCESSING || job.status === JobStatus.PENDING) {\n return this.abort(job.id);\n }\n })\n );\n }\n\n /**\n * Subscribe to progress updates for a specific job\n */\n public onJobProgress(jobId: unknown, listener: JobProgressListener): () => void {\n if (!this.jobProgressListeners.has(jobId)) {\n this.jobProgressListeners.set(jobId, new Set());\n }\n const listeners = this.jobProgressListeners.get(jobId)!;\n listeners.add(listener);\n\n return () => {\n const listeners = this.jobProgressListeners.get(jobId);\n if (listeners) {\n listeners.delete(listener);\n if (listeners.size === 0) {\n this.jobProgressListeners.delete(jobId);\n }\n }\n };\n }\n\n // ========================================================================\n // Event handling\n // ========================================================================\n\n public on<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): void {\n this.events.on(event, listener);\n }\n\n public off<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): void {\n this.events.off(event, listener);\n }\n\n public once<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): void {\n this.events.once(event, listener);\n }\n\n public waitOn<Event extends JobQueueEvents>(\n event: Event\n ): Promise<JobQueueEventParameters<Event, Input, Output>> {\n return this.events.waitOn(event) as Promise<JobQueueEventParameters<Event, Input, Output>>;\n }\n\n /**\n * Subscribes to an event and returns a function to unsubscribe\n * @param event - The event name to subscribe to\n * @param listener - The listener function to add\n * @returns a function to unsubscribe from the event\n */\n public subscribe<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): () => void {\n return this.events.subscribe(event, listener);\n }\n\n // ========================================================================\n // Internal methods called by JobQueueServer for same-process optimization\n // ========================================================================\n\n /**\n * Called by server when a job starts processing\n * @internal\n */\n public handleJobStart(jobId: unknown): void {\n this.lastKnownProgress.set(jobId, {\n progress: 0,\n message: \"\",\n details: null,\n });\n this.events.emit(\"job_start\", this.queueName, jobId);\n }\n\n /**\n * Called by server when a job completes\n * @internal\n */\n public handleJobComplete(jobId: unknown, output: Output): void {\n this.events.emit(\"job_complete\", this.queueName, jobId, output);\n\n const promises = this.activeJobPromises.get(jobId);\n if (promises) {\n promises.forEach(({ resolve }) => resolve(output));\n }\n this.cleanupJob(jobId);\n }\n\n /**\n * Called by server when a job fails\n * @internal\n */\n public handleJobError(jobId: unknown, error: string, errorCode?: string): void {\n this.events.emit(\"job_error\", this.queueName, jobId, error);\n\n const promises = this.activeJobPromises.get(jobId);\n if (promises) {\n const jobError = this.buildErrorFromCode(error, errorCode);\n promises.forEach(({ reject }) => reject(jobError));\n }\n this.cleanupJob(jobId);\n }\n\n /**\n * Called by server when a job is disabled\n * @internal\n */\n public handleJobDisabled(jobId: unknown): void {\n this.events.emit(\"job_disabled\", this.queueName, jobId);\n\n const promises = this.activeJobPromises.get(jobId);\n if (promises) {\n promises.forEach(({ reject }) => reject(new JobDisabledError(\"Job was disabled\")));\n }\n this.cleanupJob(jobId);\n }\n\n /**\n * Called by server when a job is retried\n * @internal\n */\n public handleJobRetry(jobId: unknown, runAfter: Date): void {\n this.events.emit(\"job_retry\", this.queueName, jobId, runAfter);\n }\n\n /**\n * Called by server when job progress updates\n * @internal\n */\n public handleJobProgress(\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ): void {\n this.lastKnownProgress.set(jobId, { progress, message, details });\n this.events.emit(\"job_progress\", this.queueName, jobId, progress, message, details);\n\n const listeners = this.jobProgressListeners.get(jobId);\n if (listeners) {\n for (const listener of listeners) {\n listener(progress, message, details);\n }\n }\n }\n\n // ========================================================================\n // Private helpers\n // ========================================================================\n\n private createJobHandle(id: unknown): JobHandle<Output> {\n return {\n id,\n waitFor: () => this.waitFor(id),\n abort: () => this.abort(id),\n onProgress: (callback: JobProgressListener) => this.onJobProgress(id, callback),\n };\n }\n\n private cleanupJob(jobId: unknown): void {\n this.activeJobPromises.delete(jobId);\n this.lastKnownProgress.delete(jobId);\n this.jobProgressListeners.delete(jobId);\n }\n\n private handleStorageChange(change: QueueChangePayload<Input, Output>): void {\n if (!change.new && !change.old) return;\n\n const jobId = change.new?.id ?? change.old?.id;\n if (!jobId) return;\n\n // Only process changes for our queue\n const queueName = change.new?.queue ?? change.old?.queue;\n if (queueName !== this.queueName) return;\n\n if (change.type === \"UPDATE\" && change.new) {\n const newStatus = change.new.status;\n const oldStatus = change.old?.status;\n\n if (newStatus === JobStatus.PROCESSING && oldStatus === JobStatus.PENDING) {\n this.handleJobStart(jobId);\n } else if (newStatus === JobStatus.COMPLETED) {\n this.handleJobComplete(jobId, change.new.output as Output);\n } else if (newStatus === JobStatus.FAILED) {\n this.handleJobError(\n jobId,\n change.new.error ?? \"Job failed\",\n change.new.error_code ?? undefined\n );\n } else if (newStatus === JobStatus.DISABLED) {\n this.handleJobDisabled(jobId);\n } else if (newStatus === JobStatus.PENDING && oldStatus === JobStatus.PROCESSING) {\n // Retry\n const runAfter = change.new.run_after ? new Date(change.new.run_after) : new Date();\n this.handleJobRetry(jobId, runAfter);\n }\n\n // Progress update\n if (\n change.new.progress !== change.old?.progress ||\n change.new.progress_message !== change.old?.progress_message\n ) {\n this.handleJobProgress(\n jobId,\n change.new.progress ?? 0,\n change.new.progress_message ?? \"\",\n change.new.progress_details ?? null\n );\n }\n }\n }\n\n protected storageToClass(details: JobStorageFormat<Input, Output>): Job<Input, Output> {\n return storageToClass(details, Job, { includeWorkerId: true });\n }\n\n protected buildErrorFromJob(job: Job<Input, Output>): JobError {\n return this.buildErrorFromCode(job.error || \"Job failed\", job.errorCode ?? undefined);\n }\n\n protected buildErrorFromCode(message: string, errorCode?: string): JobError {\n if (errorCode === \"PermanentJobError\") {\n const err = new PermanentJobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n if (errorCode === \"RetryableJobError\") {\n const err = new RetryableJobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n if (errorCode === \"AbortSignalJobError\") {\n const err = new AbortSignalJobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n if (errorCode === \"JobDisabledError\") {\n const err = new JobDisabledError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n const err = new JobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n}\n",
|
|
9
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus
|
|
10
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { IQueueStorage, JobStatus, JobStorageFormat, QueueChangePayload } from \"@workglow/storage\";\nimport { EventEmitter, getLogger } from \"@workglow/util\";\nimport { ILimiter } from \"../limiter/ILimiter\";\nimport { NullLimiter } from \"../limiter/NullLimiter\";\nimport { Job, JobClass } from \"./Job\";\nimport { JobQueueClient } from \"./JobQueueClient\";\nimport { JobQueueWorker } from \"./JobQueueWorker\";\nimport { classToStorage, storageToClass } from \"./JobStorageConverters\";\n\n/**\n * Statistics tracked for the job queue\n */\nexport interface JobQueueStats {\n readonly totalJobs: number;\n readonly completedJobs: number;\n readonly failedJobs: number;\n readonly abortedJobs: number;\n readonly retriedJobs: number;\n readonly disabledJobs: number;\n readonly averageProcessingTime?: number;\n readonly lastUpdateTime: Date;\n}\n\n/**\n * Events emitted by JobQueueServer\n */\nexport type JobQueueServerEventListeners<Input, Output> = {\n server_start: (queueName: string) => void;\n server_stop: (queueName: string) => void;\n job_start: (queueName: string, jobId: unknown) => void;\n job_complete: (queueName: string, jobId: unknown, output: Output) => void;\n job_error: (queueName: string, jobId: unknown, error: string) => void;\n job_disabled: (queueName: string, jobId: unknown) => void;\n job_retry: (queueName: string, jobId: unknown, runAfter: Date) => void;\n job_progress: (\n queueName: string,\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ) => void;\n};\n\nexport type JobQueueServerEvents = keyof JobQueueServerEventListeners<unknown, unknown>;\n\n/**\n * Options for creating a JobQueueServer\n */\nexport interface JobQueueServerOptions<Input, Output> {\n readonly storage: IQueueStorage<Input, Output>;\n readonly queueName: string;\n readonly limiter?: ILimiter;\n readonly workerCount?: number;\n readonly pollIntervalMs?: number;\n readonly deleteAfterCompletionMs?: number;\n readonly deleteAfterFailureMs?: number;\n readonly deleteAfterDisabledMs?: number;\n readonly cleanupIntervalMs?: number;\n /**\n * Max time each worker's `stop()` waits for in-flight jobs before forcing aborts.\n * Defaults to 30s. Set to 0 to abort immediately.\n */\n readonly stopTimeoutMs?: number;\n}\n\n/**\n * Server that coordinates multiple workers and manages the job queue lifecycle.\n * Handles stuck job recovery, cleanup, and aggregates statistics.\n */\nexport class JobQueueServer<\n Input,\n Output,\n QueueJob extends Job<Input, Output> = Job<Input, Output>,\n> {\n public readonly queueName: string;\n protected readonly storage: IQueueStorage<Input, Output>;\n protected readonly jobClass: JobClass<Input, Output>;\n public readonly limiter: ILimiter;\n protected readonly workerCount: number;\n protected readonly pollIntervalMs: number;\n protected readonly deleteAfterCompletionMs?: number;\n protected readonly deleteAfterFailureMs?: number;\n protected readonly deleteAfterDisabledMs?: number;\n protected readonly cleanupIntervalMs: number;\n protected readonly stopTimeoutMs?: number;\n\n protected readonly events = new EventEmitter<JobQueueServerEventListeners<Input, Output>>();\n protected readonly workers: JobQueueWorker<Input, Output, QueueJob>[] = [];\n protected readonly clients: Set<JobQueueClient<Input, Output>> = new Set();\n\n protected running = false;\n protected cleanupTimer: ReturnType<typeof setTimeout> | null = null;\n protected storageUnsubscribe: (() => void) | null = null;\n\n protected stats: JobQueueStats = {\n totalJobs: 0,\n completedJobs: 0,\n failedJobs: 0,\n abortedJobs: 0,\n retriedJobs: 0,\n disabledJobs: 0,\n lastUpdateTime: new Date(),\n };\n\n constructor(jobClass: JobClass<Input, Output>, options: JobQueueServerOptions<Input, Output>) {\n this.queueName = options.queueName;\n this.storage = options.storage;\n this.jobClass = jobClass;\n this.limiter = options.limiter ?? new NullLimiter();\n this.workerCount = options.workerCount ?? 1;\n this.pollIntervalMs = options.pollIntervalMs ?? 100;\n this.deleteAfterCompletionMs = options.deleteAfterCompletionMs;\n this.deleteAfterFailureMs = options.deleteAfterFailureMs;\n this.deleteAfterDisabledMs = options.deleteAfterDisabledMs;\n this.cleanupIntervalMs = options.cleanupIntervalMs ?? 10000;\n this.stopTimeoutMs = options.stopTimeoutMs;\n\n this.initializeWorkers();\n }\n\n /**\n * Start the server and all workers\n */\n public async start(): Promise<this> {\n if (this.running) {\n return this;\n }\n\n this.running = true;\n this.events.emit(\"server_start\", this.queueName);\n\n // Warn when a process-scoped limiter is paired with a cluster-scoped\n // queue storage. The user almost certainly meant their configured limit\n // to be enforced cluster-wide; with a process-scoped limiter they get\n // N× the limit (one bucket per process).\n if (\n this.limiter.scope === \"process\" &&\n this.storage.scope === \"cluster\" &&\n !(this.limiter instanceof NullLimiter)\n ) {\n getLogger().warn(\n \"Process-scoped limiter on cluster-scoped queue storage — limit is enforced per-process, not cluster-wide. Use a cluster-scoped rate limiter storage (Postgres/Supabase) for global enforcement.\",\n {\n queueName: this.queueName,\n limiterScope: this.limiter.scope,\n storage: this.storage.constructor.name,\n }\n );\n }\n\n // Fix stuck jobs from previous runs\n await this.fixupJobs();\n\n // Subscribe to storage changes to wake workers when new work arrives.\n // - Cross-process deployments rely on this for wake-up.\n // - Same-process attached clients are also primarily woken by the direct\n // `handleJobAdded` path on submit, but we keep the subscription as a\n // correctness backstop: some async backends (e.g. fake-indexeddb under\n // bun) have read-after-write visibility lag where a just-committed job\n // isn't yet returned by `storage.next()`. The subscription fires only\n // after the write is visible to readers, providing a guaranteed wake.\n // - Sqlite/Postgres throw here; the try/catch falls through and direct\n // notify is the sole wake path on those backends.\n try {\n this.storageUnsubscribe = this.storage.subscribeToChanges(\n (change: QueueChangePayload<Input, Output>) => {\n if (\n change.type === \"INSERT\" ||\n change.type === \"RESYNC\" ||\n (change.type === \"UPDATE\" && change.new?.status === JobStatus.PENDING)\n ) {\n this.notifyWorkers();\n }\n }\n );\n } catch (err) {\n // Storage doesn't support change subscriptions — workers will poll.\n // Logged at debug because Sqlite/Postgres throw here by design; an\n // unexpected throw on a storage that *should* support subscriptions\n // (e.g. misconfigured Supabase realtime) is otherwise silent.\n getLogger().debug(\"subscribeToChanges unsupported on this storage\", {\n queueName: this.queueName,\n error: err,\n });\n }\n\n // Start all workers\n await Promise.all(this.workers.map((worker) => worker.start()));\n\n // Start cleanup loop only if at least one retention TTL is configured.\n if (this.hasRetentionTtls()) {\n this.startCleanupLoop();\n }\n\n return this;\n }\n\n /**\n * True if any delete-after-X TTL is set to a positive value.\n * When false, the cleanup loop would be a pure no-op and is skipped.\n */\n private hasRetentionTtls(): boolean {\n return (\n (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) ||\n (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) ||\n (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0)\n );\n }\n\n /**\n * Stop the server and all workers\n */\n public async stop(): Promise<this> {\n if (!this.running) {\n return this;\n }\n\n this.running = false;\n\n // Unsubscribe from storage changes\n if (this.storageUnsubscribe) {\n this.storageUnsubscribe();\n this.storageUnsubscribe = null;\n }\n\n // Stop cleanup loop\n if (this.cleanupTimer) {\n clearTimeout(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n\n // Stop all workers\n await Promise.all(this.workers.map((worker) => worker.stop()));\n\n this.events.emit(\"server_stop\", this.queueName);\n return this;\n }\n\n /**\n * Get the current queue statistics\n */\n public getStats(): JobQueueStats {\n return { ...this.stats };\n }\n\n /**\n * Get the storage instance (for client connection)\n */\n public getStorage(): IQueueStorage<Input, Output> {\n return this.storage;\n }\n\n /**\n * Scale the number of workers\n */\n public async scaleWorkers(count: number): Promise<void> {\n if (count < 1) {\n throw new Error(\"Worker count must be at least 1\");\n }\n\n const currentCount = this.workers.length;\n\n if (count > currentCount) {\n // Add more workers\n for (let i = currentCount; i < count; i++) {\n const worker = this.createWorker();\n this.workers.push(worker);\n if (this.running) {\n await worker.start();\n }\n }\n } else if (count < currentCount) {\n // Remove workers\n const toRemove = this.workers.splice(count);\n await Promise.all(toRemove.map((worker) => worker.stop()));\n }\n }\n\n /**\n * Check if the server is running\n */\n public isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Get the number of workers\n */\n public getWorkerCount(): number {\n return this.workers.length;\n }\n\n // ========================================================================\n // Client management\n // ========================================================================\n\n /**\n * Add a client for same-process event forwarding\n * @internal\n */\n public addClient(client: JobQueueClient<Input, Output>): void {\n this.clients.add(client);\n }\n\n /**\n * Remove a client\n * @internal\n */\n public removeClient(client: JobQueueClient<Input, Output>): void {\n this.clients.delete(client);\n }\n\n /**\n * Wake all idle workers so they check for new jobs immediately.\n * @internal\n */\n public notifyWorkers(): void {\n for (const worker of this.workers) {\n worker.notify();\n }\n }\n\n /**\n * Called by an attached client immediately after a job is inserted into storage,\n * so the worker can pick it up without waiting for the poll interval.\n * @internal\n */\n public handleJobAdded(_jobId: unknown): void {\n this.notifyWorkers();\n }\n\n /**\n * Fire the abort controller for the given job on whichever worker is running it.\n * Returns true if any worker handled the abort locally.\n * @internal\n */\n public abortJob(jobId: unknown): boolean {\n for (const worker of this.workers) {\n if (worker.requestAbort(jobId)) {\n return true;\n }\n }\n return false;\n }\n\n // ========================================================================\n // Event handling\n // ========================================================================\n\n public on<Event extends JobQueueServerEvents>(\n event: Event,\n listener: JobQueueServerEventListeners<Input, Output>[Event]\n ): void {\n this.events.on(event, listener);\n }\n\n public off<Event extends JobQueueServerEvents>(\n event: Event,\n listener: JobQueueServerEventListeners<Input, Output>[Event]\n ): void {\n this.events.off(event, listener);\n }\n\n // ========================================================================\n // Protected methods\n // ========================================================================\n\n /**\n * Initialize workers\n */\n protected initializeWorkers(): void {\n for (let i = 0; i < this.workerCount; i++) {\n const worker = this.createWorker();\n this.workers.push(worker);\n }\n }\n\n /**\n * Create a new worker and wire up event forwarding\n */\n protected createWorker(): JobQueueWorker<Input, Output, QueueJob> {\n const worker = new JobQueueWorker<Input, Output, QueueJob>(this.jobClass, {\n storage: this.storage,\n queueName: this.queueName,\n limiter: this.limiter,\n pollIntervalMs: this.pollIntervalMs,\n stopTimeoutMs: this.stopTimeoutMs,\n });\n\n // Forward worker events to server and clients\n worker.on(\"job_start\", (jobId) => {\n this.stats = { ...this.stats, totalJobs: this.stats.totalJobs + 1 };\n this.events.emit(\"job_start\", this.queueName, jobId);\n this.forwardToClients(\"handleJobStart\", jobId);\n });\n\n worker.on(\"job_complete\", (jobId, output) => {\n this.stats = { ...this.stats, completedJobs: this.stats.completedJobs + 1 };\n this.updateAverageProcessingTime();\n this.events.emit(\"job_complete\", this.queueName, jobId, output);\n this.forwardToClients(\"handleJobComplete\", jobId, output);\n\n // Immediate deletion when configured\n if (this.deleteAfterCompletionMs === 0) {\n this.storage.delete(jobId).catch((err) => {\n console.error(\"Error deleting job after completion:\", err);\n });\n }\n\n // A concurrency slot freed up — wake idle workers\n this.notifyWorkers();\n });\n\n worker.on(\"job_error\", (jobId, error, errorCode) => {\n this.stats = { ...this.stats, failedJobs: this.stats.failedJobs + 1 };\n this.events.emit(\"job_error\", this.queueName, jobId, error);\n this.forwardToClients(\"handleJobError\", jobId, error, errorCode);\n\n // Immediate deletion when configured\n if (this.deleteAfterFailureMs === 0) {\n this.storage.delete(jobId).catch((err) => {\n console.error(\"Error deleting job after error:\", err);\n });\n }\n\n // A concurrency slot freed up — wake idle workers\n this.notifyWorkers();\n });\n\n worker.on(\"job_disabled\", (jobId) => {\n this.stats = { ...this.stats, disabledJobs: this.stats.disabledJobs + 1 };\n this.events.emit(\"job_disabled\", this.queueName, jobId);\n this.forwardToClients(\"handleJobDisabled\", jobId);\n\n // Immediate deletion when configured\n if (this.deleteAfterDisabledMs === 0) {\n this.storage.delete(jobId).catch((err) => {\n console.error(\"Error deleting job after disabling:\", err);\n });\n }\n\n // A concurrency slot freed up — wake idle workers\n this.notifyWorkers();\n });\n\n worker.on(\"job_retry\", (jobId, runAfter) => {\n this.stats = { ...this.stats, retriedJobs: this.stats.retriedJobs + 1 };\n this.events.emit(\"job_retry\", this.queueName, jobId, runAfter);\n this.forwardToClients(\"handleJobRetry\", jobId, runAfter);\n });\n\n worker.on(\"job_progress\", (jobId, progress, message, details) => {\n this.events.emit(\"job_progress\", this.queueName, jobId, progress, message, details);\n this.forwardToClients(\"handleJobProgress\", jobId, progress, message, details);\n });\n\n return worker;\n }\n\n /**\n * Forward events to all attached clients\n */\n protected forwardToClients(method: \"handleJobStart\", jobId: unknown): void;\n protected forwardToClients(method: \"handleJobComplete\", jobId: unknown, output: Output): void;\n protected forwardToClients(\n method: \"handleJobError\",\n jobId: unknown,\n error: string,\n errorCode?: string\n ): void;\n protected forwardToClients(method: \"handleJobDisabled\", jobId: unknown): void;\n protected forwardToClients(method: \"handleJobRetry\", jobId: unknown, runAfter: Date): void;\n protected forwardToClients(\n method: \"handleJobProgress\",\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ): void;\n protected forwardToClients(method: string, ...args: unknown[]): void {\n for (const client of this.clients) {\n const fn = (client as any)[method];\n if (typeof fn === \"function\") {\n fn.apply(client, args);\n }\n }\n }\n\n /**\n * Update average processing time from all workers\n */\n protected updateAverageProcessingTime(): void {\n const times: number[] = [];\n for (const worker of this.workers) {\n const avgTime = worker.getAverageProcessingTime();\n if (avgTime !== undefined) {\n times.push(avgTime);\n }\n }\n if (times.length > 0) {\n const avg = times.reduce((a, b) => a + b, 0) / times.length;\n this.stats = {\n ...this.stats,\n averageProcessingTime: avg,\n lastUpdateTime: new Date(),\n };\n }\n }\n\n /**\n * Start the cleanup loop\n */\n protected startCleanupLoop(): void {\n if (!this.running) return;\n\n this.cleanupJobs().finally(() => {\n if (this.running) {\n this.cleanupTimer = setTimeout(() => this.startCleanupLoop(), this.cleanupIntervalMs);\n }\n });\n }\n\n /**\n * Clean up completed/failed jobs based on TTL settings\n */\n protected async cleanupJobs(): Promise<void> {\n try {\n // The workers will handle the abort via their abort controllers\n // We just need to ensure the jobs get marked as failed\n\n // Delete completed jobs after TTL\n if (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) {\n await this.storage.deleteJobsByStatusAndAge(\n JobStatus.COMPLETED,\n this.deleteAfterCompletionMs\n );\n }\n\n // Delete failed jobs after TTL\n if (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) {\n await this.storage.deleteJobsByStatusAndAge(JobStatus.FAILED, this.deleteAfterFailureMs);\n }\n\n // Delete disabled jobs after TTL\n if (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0) {\n await this.storage.deleteJobsByStatusAndAge(JobStatus.DISABLED, this.deleteAfterDisabledMs);\n }\n } catch (error) {\n console.error(\"Error in cleanup:\", error);\n }\n }\n\n /**\n * Fix stuck jobs from previous server runs.\n * Jobs in PROCESSING or ABORTING state that are not owned by any of the current\n * server's workers are considered orphaned and will be reset.\n */\n protected async fixupJobs(): Promise<void> {\n try {\n const stuckProcessingJobs = await this.storage.peek(JobStatus.PROCESSING);\n const stuckAbortingJobs = await this.storage.peek(JobStatus.ABORTING);\n const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];\n\n // Get the worker IDs of all workers managed by this server\n const currentWorkerIds = new Set(this.getWorkerIds());\n\n for (const jobData of stuckJobs) {\n // Skip jobs that belong to workers in this server (they may still be processing)\n if (jobData.worker_id && currentWorkerIds.has(jobData.worker_id)) {\n continue;\n }\n\n const job = this.storageToClass(jobData);\n if (job.runAttempts >= job.maxRetries) {\n job.status = JobStatus.FAILED;\n job.error = \"Max retries reached\";\n job.errorCode = \"MAX_RETRIES_REACHED\";\n // Clear worker_id since job is now failed\n job.workerId = null;\n } else {\n job.status = JobStatus.PENDING;\n job.runAfter = job.lastRanAt || new Date();\n job.progress = 0;\n job.progressMessage = \"\";\n job.progressDetails = null;\n job.error = \"Server restarted\";\n // Clear worker_id so a new worker can claim this job\n job.workerId = null;\n }\n\n await this.storage.complete(this.classToStorage(job));\n }\n } catch (error) {\n console.error(\"Error in fixupJobs:\", error);\n }\n }\n\n /**\n * Convert storage format to Job class\n */\n protected storageToClass(details: JobStorageFormat<Input, Output>): Job<Input, Output> {\n return storageToClass(details, this.jobClass);\n }\n\n /**\n * Convert Job class to storage format\n */\n protected classToStorage(job: Job<Input, Output>): JobStorageFormat<Input, Output> {\n return classToStorage(job, this.queueName);\n }\n\n /**\n * Get the worker IDs of all workers managed by this server\n */\n public getWorkerIds(): string[] {\n return this.workers.map((worker) => worker.workerId);\n }\n}\n",
|
|
9
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus } from \"../queue-storage/IQueueStorage\";\nimport type {\n IQueueStorage,\n JobStorageFormat,\n QueueChangePayload,\n} from \"../queue-storage/IQueueStorage\";\nimport { EventEmitter } from \"@workglow/util\";\nimport { Job } from \"./Job\";\nimport {\n AbortSignalJobError,\n JobDisabledError,\n JobError,\n JobNotFoundError,\n PermanentJobError,\n RetryableJobError,\n} from \"./JobError\";\nimport { applyPersistedDiagnosticsToStack } from \"./JobErrorDiagnostics\";\nimport {\n JobProgressListener,\n JobQueueEventListener,\n JobQueueEventListeners,\n JobQueueEventParameters,\n JobQueueEvents,\n} from \"./JobQueueEventListeners\";\nimport type { JobQueueServer } from \"./JobQueueServer\";\nimport { storageToClass } from \"./JobStorageConverters\";\n\n/**\n * Handle returned when submitting a job, providing methods to interact with the job\n */\nexport interface JobHandle<Output> {\n readonly id: unknown;\n waitFor(): Promise<Output>;\n abort(): Promise<void>;\n onProgress(callback: JobProgressListener): () => void;\n}\n\n/**\n * Options for creating a JobQueueClient\n */\nexport interface JobQueueClientOptions<Input, Output> {\n readonly storage: IQueueStorage<Input, Output>;\n readonly queueName: string;\n}\n\n/**\n * Client for submitting jobs and monitoring their progress.\n * Connect to a JobQueueServer for same-process optimization,\n * or use storage subscriptions for cross-process communication.\n */\nexport class JobQueueClient<Input, Output> {\n public readonly queueName: string;\n protected readonly storage: IQueueStorage<Input, Output>;\n protected readonly events = new EventEmitter<JobQueueEventListeners<Input, Output>>();\n protected server: JobQueueServer<Input, Output> | null = null;\n protected storageUnsubscribe: (() => void) | null = null;\n\n /**\n * Map of job IDs to their pending promise resolvers\n */\n protected readonly activeJobPromises: Map<\n unknown,\n Array<{\n resolve: (value: Output) => void;\n reject: (err: JobError) => void;\n }>\n > = new Map();\n\n /**\n * Map of job IDs to their progress listeners\n */\n protected readonly jobProgressListeners: Map<unknown, Set<JobProgressListener>> = new Map();\n\n /**\n * Last known progress state for each job\n */\n protected readonly lastKnownProgress: Map<\n unknown,\n {\n readonly progress: number;\n readonly message: string;\n readonly details: Record<string, unknown> | null;\n }\n > = new Map();\n\n constructor(options: JobQueueClientOptions<Input, Output>) {\n this.queueName = options.queueName;\n this.storage = options.storage;\n }\n\n /**\n * Attach to a local JobQueueServer for same-process event optimization.\n * When attached, events flow directly from server without storage polling.\n */\n public attach(server: JobQueueServer<Input, Output>): void {\n if (this.server) {\n this.detach();\n }\n this.server = server;\n server.addClient(this);\n\n // Unsubscribe from storage if we were using it\n if (this.storageUnsubscribe) {\n this.storageUnsubscribe();\n this.storageUnsubscribe = null;\n }\n }\n\n /**\n * Detach from the current server\n */\n public detach(): void {\n if (this.server) {\n this.server.removeClient(this);\n this.server = null;\n }\n }\n\n /**\n * Connect to storage for cross-process communication (when no local server).\n * Uses storage subscriptions to receive job updates.\n */\n public connect(): void {\n if (this.server) {\n return; // Already connected via server\n }\n\n if (this.storageUnsubscribe) {\n return; // Already subscribed\n }\n\n this.storageUnsubscribe = this.storage.subscribeToChanges(\n (change: QueueChangePayload<Input, Output>) => {\n this.handleStorageChange(change);\n }\n );\n }\n\n /**\n * Disconnect from storage subscriptions\n */\n public disconnect(): void {\n if (this.storageUnsubscribe) {\n this.storageUnsubscribe();\n this.storageUnsubscribe = null;\n }\n this.detach();\n }\n\n /**\n * Submit a job to the queue\n */\n public async submit(\n input: Input,\n options?: {\n readonly jobRunId?: string;\n readonly fingerprint?: string;\n readonly maxRetries?: number;\n readonly runAfter?: Date;\n readonly deadlineAt?: Date;\n }\n ): Promise<JobHandle<Output>> {\n const job: JobStorageFormat<Input, Output> = {\n queue: this.queueName,\n input,\n job_run_id: options?.jobRunId,\n fingerprint: options?.fingerprint,\n max_retries: options?.maxRetries ?? 10,\n run_after: options?.runAfter?.toISOString() ?? new Date().toISOString(),\n deadline_at: options?.deadlineAt?.toISOString() ?? null,\n completed_at: null,\n status: JobStatus.PENDING,\n };\n\n const id = await this.storage.add(job);\n\n // Same-process fast path: poke the worker directly so it doesn't have to\n // wait for the poll interval (crucial for Sqlite/Postgres, whose\n // subscribeToChanges throws).\n this.server?.handleJobAdded(id);\n\n return this.createJobHandle(id);\n }\n\n /**\n * Submit multiple jobs to the queue\n */\n public async submitBatch(\n inputs: readonly Input[],\n options?: {\n readonly jobRunId?: string;\n readonly maxRetries?: number;\n }\n ): Promise<readonly JobHandle<Output>[]> {\n const handles: JobHandle<Output>[] = [];\n for (const input of inputs) {\n const handle = await this.submit(input, options);\n handles.push(handle);\n }\n return handles;\n }\n\n /**\n * Get a job by ID\n */\n public async getJob(id: unknown): Promise<Job<Input, Output> | undefined> {\n if (!id) throw new JobNotFoundError(\"Cannot get undefined job\");\n const job = await this.storage.get(id);\n if (!job) return undefined;\n return this.storageToClass(job);\n }\n\n /**\n * Get jobs by run ID\n */\n public async getJobsByRunId(runId: string): Promise<readonly Job<Input, Output>[]> {\n if (!runId) throw new JobNotFoundError(\"Cannot get jobs by undefined runId\");\n const jobs = await this.storage.getByRunId(runId);\n return jobs.map((job) => this.storageToClass(job));\n }\n\n /**\n * Peek at jobs in the queue\n */\n public async peek(status?: JobStatus, num?: number): Promise<readonly Job<Input, Output>[]> {\n const jobs = await this.storage.peek(status, num);\n return jobs.map((job) => this.storageToClass(job));\n }\n\n /**\n * Get the size of the queue\n */\n public async size(status?: JobStatus): Promise<number> {\n return this.storage.size(status);\n }\n\n /**\n * Get the output for an input (if job completed)\n */\n public async outputForInput(input: Input): Promise<Output | null> {\n if (!input) throw new JobNotFoundError(\"Cannot get output for undefined input\");\n return this.storage.outputForInput(input);\n }\n\n /**\n * Wait for a job to complete.\n *\n * Registers the resolver BEFORE reading storage so that a `handleJobError`\n * / `handleJobComplete` event fired during the storage read isn't dropped\n * on the floor. The previous order (read first, register after) had a\n * TOCTOU window where a fast same-process abort could complete between the\n * read and the registration, leaving `waitFor` to register against an\n * already-finished job and hang forever.\n */\n public async waitFor(jobId: unknown): Promise<Output> {\n if (!jobId) throw new JobNotFoundError(\"Cannot wait for undefined job\");\n\n const { promise, resolve, reject } = Promise.withResolvers<Output>();\n promise.catch(() => {}); // Prevent unhandled rejection\n\n const promises = this.activeJobPromises.get(jobId) || [];\n promises.push({ resolve, reject });\n this.activeJobPromises.set(jobId, promises);\n\n // Now check storage — if the job is already terminal (raced us to it),\n // settle the promise ourselves and clean up the registration. The\n // handler paths (handleJobComplete/Error/Disabled) are idempotent on\n // already-settled promises.\n const job = await this.getJob(jobId);\n if (!job) {\n this.removePromise(jobId, resolve, reject);\n throw new JobNotFoundError(`Job ${jobId} not found`);\n }\n if (job.status === JobStatus.COMPLETED) {\n this.removePromise(jobId, resolve, reject);\n return job.output as Output;\n }\n if (job.status === JobStatus.DISABLED) {\n this.removePromise(jobId, resolve, reject);\n throw new JobDisabledError(`Job ${jobId} was disabled`);\n }\n if (job.status === JobStatus.FAILED) {\n this.removePromise(jobId, resolve, reject);\n throw this.buildErrorFromJob(job);\n }\n\n return promise;\n }\n\n private removePromise(\n jobId: unknown,\n resolve: (output: Output) => void,\n reject: (err: unknown) => void\n ): void {\n const list = this.activeJobPromises.get(jobId);\n if (!list) return;\n const idx = list.findIndex((p) => p.resolve === resolve && p.reject === reject);\n if (idx !== -1) list.splice(idx, 1);\n if (list.length === 0) this.activeJobPromises.delete(jobId);\n }\n\n /**\n * Abort a job.\n *\n * Same-process path: fires the in-memory abort controller on the attached\n * server — `handleAbort` will write FAILED directly, so we skip the\n * `storage.abort(…)` ABORTING write. Writing both would race (last-writer-\n * wins) and can leave the row stuck at ABORTING on async storages.\n *\n * Cross-process path (or job not currently running on any local worker):\n * write ABORTING to storage so the remote worker's poll picks it up.\n *\n * Crash window: if the process dies after the in-memory abort fires but\n * before `failJob` writes FAILED, the row stays PROCESSING. `fixupJobs()`\n * resets it to PENDING on next start and the job will re-run. Make handlers\n * idempotent (or use `uniquenessKey`) if that's not acceptable.\n */\n public async abort(jobId: unknown): Promise<void> {\n if (!jobId) throw new JobNotFoundError(\"Cannot abort undefined job\");\n const firedLocally = this.server?.abortJob(jobId) ?? false;\n if (!firedLocally) {\n try {\n await this.storage.abort(jobId);\n } finally {\n this.events.emit(\"job_aborting\", this.queueName, jobId);\n }\n return;\n }\n this.events.emit(\"job_aborting\", this.queueName, jobId);\n }\n\n /**\n * Abort all jobs in a job run\n */\n public async abortJobRun(jobRunId: string): Promise<void> {\n if (!jobRunId) throw new JobNotFoundError(\"Cannot abort job run with undefined jobRunId\");\n const jobs = await this.getJobsByRunId(jobRunId);\n await Promise.allSettled(\n jobs.map((job) => {\n if (job.status === JobStatus.PROCESSING || job.status === JobStatus.PENDING) {\n return this.abort(job.id);\n }\n })\n );\n }\n\n /**\n * Subscribe to progress updates for a specific job\n */\n public onJobProgress(jobId: unknown, listener: JobProgressListener): () => void {\n if (!this.jobProgressListeners.has(jobId)) {\n this.jobProgressListeners.set(jobId, new Set());\n }\n const listeners = this.jobProgressListeners.get(jobId)!;\n listeners.add(listener);\n\n return () => {\n const listeners = this.jobProgressListeners.get(jobId);\n if (listeners) {\n listeners.delete(listener);\n if (listeners.size === 0) {\n this.jobProgressListeners.delete(jobId);\n }\n }\n };\n }\n\n // ========================================================================\n // Event handling\n // ========================================================================\n\n public on<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): void {\n this.events.on(event, listener);\n }\n\n public off<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): void {\n this.events.off(event, listener);\n }\n\n public once<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): void {\n this.events.once(event, listener);\n }\n\n public waitOn<Event extends JobQueueEvents>(\n event: Event\n ): Promise<JobQueueEventParameters<Event, Input, Output>> {\n return this.events.waitOn(event) as Promise<JobQueueEventParameters<Event, Input, Output>>;\n }\n\n /**\n * Subscribes to an event and returns a function to unsubscribe\n * @param event - The event name to subscribe to\n * @param listener - The listener function to add\n * @returns a function to unsubscribe from the event\n */\n public subscribe<Event extends JobQueueEvents>(\n event: Event,\n listener: JobQueueEventListener<Event>\n ): () => void {\n return this.events.subscribe(event, listener);\n }\n\n // ========================================================================\n // Internal methods called by JobQueueServer for same-process optimization\n // ========================================================================\n\n /**\n * Called by server when a job starts processing\n * @internal\n */\n public handleJobStart(jobId: unknown): void {\n this.lastKnownProgress.set(jobId, {\n progress: 0,\n message: \"\",\n details: null,\n });\n this.events.emit(\"job_start\", this.queueName, jobId);\n }\n\n /**\n * Called by server when a job completes\n * @internal\n */\n public handleJobComplete(jobId: unknown, output: Output): void {\n this.events.emit(\"job_complete\", this.queueName, jobId, output);\n\n const promises = this.activeJobPromises.get(jobId);\n if (promises) {\n promises.forEach(({ resolve }) => resolve(output));\n }\n this.cleanupJob(jobId);\n }\n\n /**\n * Called by server when a job fails\n * @internal\n */\n public handleJobError(jobId: unknown, error: string, errorCode?: string): void {\n this.events.emit(\"job_error\", this.queueName, jobId, error);\n\n const promises = this.activeJobPromises.get(jobId);\n if (promises) {\n const jobError = this.buildErrorFromCode(error, errorCode);\n promises.forEach(({ reject }) => reject(jobError));\n }\n this.cleanupJob(jobId);\n }\n\n /**\n * Called by server when a job is disabled\n * @internal\n */\n public handleJobDisabled(jobId: unknown): void {\n this.events.emit(\"job_disabled\", this.queueName, jobId);\n\n const promises = this.activeJobPromises.get(jobId);\n if (promises) {\n promises.forEach(({ reject }) => reject(new JobDisabledError(\"Job was disabled\")));\n }\n this.cleanupJob(jobId);\n }\n\n /**\n * Called by server when a job is retried\n * @internal\n */\n public handleJobRetry(jobId: unknown, runAfter: Date): void {\n this.events.emit(\"job_retry\", this.queueName, jobId, runAfter);\n }\n\n /**\n * Called by server when job progress updates\n * @internal\n */\n public handleJobProgress(\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ): void {\n this.lastKnownProgress.set(jobId, { progress, message, details });\n this.events.emit(\"job_progress\", this.queueName, jobId, progress, message, details);\n\n const listeners = this.jobProgressListeners.get(jobId);\n if (listeners) {\n for (const listener of listeners) {\n listener(progress, message, details);\n }\n }\n }\n\n // ========================================================================\n // Private helpers\n // ========================================================================\n\n private createJobHandle(id: unknown): JobHandle<Output> {\n return {\n id,\n waitFor: () => this.waitFor(id),\n abort: () => this.abort(id),\n onProgress: (callback: JobProgressListener) => this.onJobProgress(id, callback),\n };\n }\n\n private cleanupJob(jobId: unknown): void {\n this.activeJobPromises.delete(jobId);\n this.lastKnownProgress.delete(jobId);\n this.jobProgressListeners.delete(jobId);\n }\n\n private handleStorageChange(change: QueueChangePayload<Input, Output>): void {\n if (!change.new && !change.old) return;\n\n const jobId = change.new?.id ?? change.old?.id;\n if (!jobId) return;\n\n // Only process changes for our queue\n const queueName = change.new?.queue ?? change.old?.queue;\n if (queueName !== this.queueName) return;\n\n if (change.type === \"UPDATE\" && change.new) {\n const newStatus = change.new.status;\n const oldStatus = change.old?.status;\n\n if (newStatus === JobStatus.PROCESSING && oldStatus === JobStatus.PENDING) {\n this.handleJobStart(jobId);\n } else if (newStatus === JobStatus.COMPLETED) {\n this.handleJobComplete(jobId, change.new.output as Output);\n } else if (newStatus === JobStatus.FAILED) {\n this.handleJobError(\n jobId,\n change.new.error ?? \"Job failed\",\n change.new.error_code ?? undefined\n );\n } else if (newStatus === JobStatus.DISABLED) {\n this.handleJobDisabled(jobId);\n } else if (newStatus === JobStatus.PENDING && oldStatus === JobStatus.PROCESSING) {\n // Retry\n const runAfter = change.new.run_after ? new Date(change.new.run_after) : new Date();\n this.handleJobRetry(jobId, runAfter);\n }\n\n // Progress update\n if (\n change.new.progress !== change.old?.progress ||\n change.new.progress_message !== change.old?.progress_message\n ) {\n this.handleJobProgress(\n jobId,\n change.new.progress ?? 0,\n change.new.progress_message ?? \"\",\n change.new.progress_details ?? null\n );\n }\n }\n }\n\n protected storageToClass(details: JobStorageFormat<Input, Output>): Job<Input, Output> {\n return storageToClass(details, Job, { includeWorkerId: true });\n }\n\n protected buildErrorFromJob(job: Job<Input, Output>): JobError {\n return this.buildErrorFromCode(job.error || \"Job failed\", job.errorCode ?? undefined);\n }\n\n protected buildErrorFromCode(message: string, errorCode?: string): JobError {\n if (errorCode === \"PermanentJobError\") {\n const err = new PermanentJobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n if (errorCode === \"RetryableJobError\") {\n const err = new RetryableJobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n if (errorCode === \"AbortSignalJobError\") {\n const err = new AbortSignalJobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n if (errorCode === \"JobDisabledError\") {\n const err = new JobDisabledError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n const err = new JobError(message);\n applyPersistedDiagnosticsToStack(err, message);\n return err;\n }\n}\n",
|
|
10
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus } from \"../queue-storage/IQueueStorage\";\nimport type { JobStorageFormat } from \"../queue-storage/IQueueStorage\";\nimport { Job, JobClass } from \"./Job\";\n\n/**\n * Convert a date string to a Date object, or null if invalid\n */\nfunction toDate(date: string | null | undefined): Date | null {\n if (!date) return null;\n const d = new Date(date);\n return isNaN(d.getTime()) ? null : d;\n}\n\n/**\n * Convert a Date object to an ISO string, or null if invalid\n */\nfunction dateToISOString(date: Date | null | undefined): string | null {\n if (!date) return null;\n return isNaN(date.getTime()) ? null : date.toISOString();\n}\n\n/**\n * Convert storage format to Job class\n */\nexport function storageToClass<Input, Output>(\n details: JobStorageFormat<Input, Output>,\n jobClass: JobClass<Input, Output>,\n options?: {\n readonly includeWorkerId?: boolean;\n }\n): Job<Input, Output> {\n const includeWorkerId = options?.includeWorkerId ?? true;\n return new jobClass({\n id: details.id,\n jobRunId: details.job_run_id,\n queueName: details.queue,\n fingerprint: details.fingerprint,\n input: details.input as Input,\n output: details.output as Output,\n runAfter: toDate(details.run_after),\n createdAt: toDate(details.created_at)!,\n deadlineAt: toDate(details.deadline_at),\n lastRanAt: toDate(details.last_ran_at),\n completedAt: toDate(details.completed_at),\n progress: details.progress || 0,\n progressMessage: details.progress_message || \"\",\n progressDetails: details.progress_details ?? null,\n status: details.status as JobStatus,\n error: details.error ?? null,\n errorCode: details.error_code ?? null,\n runAttempts: details.run_attempts ?? 0,\n maxRetries: details.max_retries ?? 10,\n ...(includeWorkerId ? { workerId: details.worker_id ?? null } : {}),\n });\n}\n\n/**\n * Convert Job class to storage format\n */\nexport function classToStorage<Input, Output>(\n job: Job<Input, Output>,\n queueName: string\n): JobStorageFormat<Input, Output> {\n const now = new Date().toISOString();\n return {\n id: job.id,\n job_run_id: job.jobRunId,\n queue: job.queueName || queueName,\n fingerprint: job.fingerprint,\n input: job.input,\n status: job.status,\n output: job.output ?? null,\n error: job.error === null ? null : String(job.error),\n error_code: job.errorCode || null,\n run_attempts: job.runAttempts ?? 0,\n max_retries: job.maxRetries ?? 10,\n run_after: dateToISOString(job.runAfter) ?? now,\n created_at: dateToISOString(job.createdAt) ?? now,\n deadline_at: dateToISOString(job.deadlineAt),\n last_ran_at: dateToISOString(job.lastRanAt),\n completed_at: dateToISOString(job.completedAt),\n progress: job.progress ?? 0,\n progress_message: job.progressMessage ?? \"\",\n progress_details: job.progressDetails ?? null,\n worker_id: job.workerId ?? null,\n };\n}\n",
|
|
11
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus } from \"../queue-storage/IQueueStorage\";\nimport type {\n IQueueStorage,\n JobStorageFormat,\n QueueChangePayload,\n} from \"../queue-storage/IQueueStorage\";\nimport { EventEmitter, getLogger } from \"@workglow/util\";\nimport { ILimiter } from \"../limiter/ILimiter\";\nimport { NullLimiter } from \"../limiter/NullLimiter\";\nimport { Job, JobClass } from \"./Job\";\nimport { JobQueueClient } from \"./JobQueueClient\";\nimport { JobQueueWorker } from \"./JobQueueWorker\";\nimport { classToStorage, storageToClass } from \"./JobStorageConverters\";\n\n/**\n * Statistics tracked for the job queue\n */\nexport interface JobQueueStats {\n readonly totalJobs: number;\n readonly completedJobs: number;\n readonly failedJobs: number;\n readonly abortedJobs: number;\n readonly retriedJobs: number;\n readonly disabledJobs: number;\n readonly averageProcessingTime?: number;\n readonly lastUpdateTime: Date;\n}\n\n/**\n * Events emitted by JobQueueServer\n */\nexport type JobQueueServerEventListeners<Input, Output> = {\n server_start: (queueName: string) => void;\n server_stop: (queueName: string) => void;\n job_start: (queueName: string, jobId: unknown) => void;\n job_complete: (queueName: string, jobId: unknown, output: Output) => void;\n job_error: (queueName: string, jobId: unknown, error: string) => void;\n job_disabled: (queueName: string, jobId: unknown) => void;\n job_retry: (queueName: string, jobId: unknown, runAfter: Date) => void;\n job_progress: (\n queueName: string,\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ) => void;\n};\n\nexport type JobQueueServerEvents = keyof JobQueueServerEventListeners<unknown, unknown>;\n\n/**\n * Options for creating a JobQueueServer\n */\nexport interface JobQueueServerOptions<Input, Output> {\n readonly storage: IQueueStorage<Input, Output>;\n readonly queueName: string;\n readonly limiter?: ILimiter;\n readonly workerCount?: number;\n readonly pollIntervalMs?: number;\n readonly deleteAfterCompletionMs?: number;\n readonly deleteAfterFailureMs?: number;\n readonly deleteAfterDisabledMs?: number;\n readonly cleanupIntervalMs?: number;\n /**\n * Max time each worker's `stop()` waits for in-flight jobs before forcing aborts.\n * Defaults to 30s. Set to 0 to abort immediately.\n */\n readonly stopTimeoutMs?: number;\n}\n\n/**\n * Server that coordinates multiple workers and manages the job queue lifecycle.\n * Handles stuck job recovery, cleanup, and aggregates statistics.\n */\nexport class JobQueueServer<\n Input,\n Output,\n QueueJob extends Job<Input, Output> = Job<Input, Output>,\n> {\n public readonly queueName: string;\n protected readonly storage: IQueueStorage<Input, Output>;\n protected readonly jobClass: JobClass<Input, Output>;\n public readonly limiter: ILimiter;\n protected readonly workerCount: number;\n protected readonly pollIntervalMs: number;\n protected readonly deleteAfterCompletionMs?: number;\n protected readonly deleteAfterFailureMs?: number;\n protected readonly deleteAfterDisabledMs?: number;\n protected readonly cleanupIntervalMs: number;\n protected readonly stopTimeoutMs?: number;\n\n protected readonly events = new EventEmitter<JobQueueServerEventListeners<Input, Output>>();\n protected readonly workers: JobQueueWorker<Input, Output, QueueJob>[] = [];\n protected readonly clients: Set<JobQueueClient<Input, Output>> = new Set();\n\n protected running = false;\n protected cleanupTimer: ReturnType<typeof setTimeout> | null = null;\n protected storageUnsubscribe: (() => void) | null = null;\n\n protected stats: JobQueueStats = {\n totalJobs: 0,\n completedJobs: 0,\n failedJobs: 0,\n abortedJobs: 0,\n retriedJobs: 0,\n disabledJobs: 0,\n lastUpdateTime: new Date(),\n };\n\n constructor(jobClass: JobClass<Input, Output>, options: JobQueueServerOptions<Input, Output>) {\n this.queueName = options.queueName;\n this.storage = options.storage;\n this.jobClass = jobClass;\n this.limiter = options.limiter ?? new NullLimiter();\n this.workerCount = options.workerCount ?? 1;\n this.pollIntervalMs = options.pollIntervalMs ?? 100;\n this.deleteAfterCompletionMs = options.deleteAfterCompletionMs;\n this.deleteAfterFailureMs = options.deleteAfterFailureMs;\n this.deleteAfterDisabledMs = options.deleteAfterDisabledMs;\n this.cleanupIntervalMs = options.cleanupIntervalMs ?? 10000;\n this.stopTimeoutMs = options.stopTimeoutMs;\n\n this.initializeWorkers();\n }\n\n /**\n * Start the server and all workers\n */\n public async start(): Promise<this> {\n if (this.running) {\n return this;\n }\n\n this.running = true;\n this.events.emit(\"server_start\", this.queueName);\n\n // Warn when a process-scoped limiter is paired with a cluster-scoped\n // queue storage. The user almost certainly meant their configured limit\n // to be enforced cluster-wide; with a process-scoped limiter they get\n // N× the limit (one bucket per process).\n if (\n this.limiter.scope === \"process\" &&\n this.storage.scope === \"cluster\" &&\n !(this.limiter instanceof NullLimiter)\n ) {\n getLogger().warn(\n \"Process-scoped limiter on cluster-scoped queue storage — limit is enforced per-process, not cluster-wide. Use a cluster-scoped rate limiter storage (Postgres/Supabase) for global enforcement.\",\n {\n queueName: this.queueName,\n limiterScope: this.limiter.scope,\n storage: this.storage.constructor.name,\n }\n );\n }\n\n // Fix stuck jobs from previous runs\n await this.fixupJobs();\n\n // Subscribe to storage changes to wake workers when new work arrives.\n // - Cross-process deployments rely on this for wake-up.\n // - Same-process attached clients are also primarily woken by the direct\n // `handleJobAdded` path on submit, but we keep the subscription as a\n // correctness backstop: some async backends (e.g. fake-indexeddb under\n // bun) have read-after-write visibility lag where a just-committed job\n // isn't yet returned by `storage.next()`. The subscription fires only\n // after the write is visible to readers, providing a guaranteed wake.\n // - Sqlite/Postgres throw here; the try/catch falls through and direct\n // notify is the sole wake path on those backends.\n try {\n this.storageUnsubscribe = this.storage.subscribeToChanges(\n (change: QueueChangePayload<Input, Output>) => {\n if (\n change.type === \"INSERT\" ||\n change.type === \"RESYNC\" ||\n (change.type === \"UPDATE\" && change.new?.status === JobStatus.PENDING)\n ) {\n this.notifyWorkers();\n }\n }\n );\n } catch (err) {\n // Storage doesn't support change subscriptions — workers will poll.\n // Logged at debug because Sqlite/Postgres throw here by design; an\n // unexpected throw on a storage that *should* support subscriptions\n // (e.g. misconfigured Supabase realtime) is otherwise silent.\n getLogger().debug(\"subscribeToChanges unsupported on this storage\", {\n queueName: this.queueName,\n error: err,\n });\n }\n\n // Start all workers\n await Promise.all(this.workers.map((worker) => worker.start()));\n\n // Start cleanup loop only if at least one retention TTL is configured.\n if (this.hasRetentionTtls()) {\n this.startCleanupLoop();\n }\n\n return this;\n }\n\n /**\n * True if any delete-after-X TTL is set to a positive value.\n * When false, the cleanup loop would be a pure no-op and is skipped.\n */\n private hasRetentionTtls(): boolean {\n return (\n (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) ||\n (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) ||\n (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0)\n );\n }\n\n /**\n * Stop the server and all workers\n */\n public async stop(): Promise<this> {\n if (!this.running) {\n return this;\n }\n\n this.running = false;\n\n // Unsubscribe from storage changes\n if (this.storageUnsubscribe) {\n this.storageUnsubscribe();\n this.storageUnsubscribe = null;\n }\n\n // Stop cleanup loop\n if (this.cleanupTimer) {\n clearTimeout(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n\n // Stop all workers\n await Promise.all(this.workers.map((worker) => worker.stop()));\n\n this.events.emit(\"server_stop\", this.queueName);\n return this;\n }\n\n /**\n * Get the current queue statistics\n */\n public getStats(): JobQueueStats {\n return { ...this.stats };\n }\n\n /**\n * Get the storage instance (for client connection)\n */\n public getStorage(): IQueueStorage<Input, Output> {\n return this.storage;\n }\n\n /**\n * Scale the number of workers\n */\n public async scaleWorkers(count: number): Promise<void> {\n if (count < 1) {\n throw new Error(\"Worker count must be at least 1\");\n }\n\n const currentCount = this.workers.length;\n\n if (count > currentCount) {\n // Add more workers\n for (let i = currentCount; i < count; i++) {\n const worker = this.createWorker();\n this.workers.push(worker);\n if (this.running) {\n await worker.start();\n }\n }\n } else if (count < currentCount) {\n // Remove workers\n const toRemove = this.workers.splice(count);\n await Promise.all(toRemove.map((worker) => worker.stop()));\n }\n }\n\n /**\n * Check if the server is running\n */\n public isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Get the number of workers\n */\n public getWorkerCount(): number {\n return this.workers.length;\n }\n\n // ========================================================================\n // Client management\n // ========================================================================\n\n /**\n * Add a client for same-process event forwarding\n * @internal\n */\n public addClient(client: JobQueueClient<Input, Output>): void {\n this.clients.add(client);\n }\n\n /**\n * Remove a client\n * @internal\n */\n public removeClient(client: JobQueueClient<Input, Output>): void {\n this.clients.delete(client);\n }\n\n /**\n * Wake all idle workers so they check for new jobs immediately.\n * @internal\n */\n public notifyWorkers(): void {\n for (const worker of this.workers) {\n worker.notify();\n }\n }\n\n /**\n * Called by an attached client immediately after a job is inserted into storage,\n * so the worker can pick it up without waiting for the poll interval.\n * @internal\n */\n public handleJobAdded(_jobId: unknown): void {\n this.notifyWorkers();\n }\n\n /**\n * Fire the abort controller for the given job on whichever worker is running it.\n * Returns true if any worker handled the abort locally.\n * @internal\n */\n public abortJob(jobId: unknown): boolean {\n for (const worker of this.workers) {\n if (worker.requestAbort(jobId)) {\n return true;\n }\n }\n return false;\n }\n\n // ========================================================================\n // Event handling\n // ========================================================================\n\n public on<Event extends JobQueueServerEvents>(\n event: Event,\n listener: JobQueueServerEventListeners<Input, Output>[Event]\n ): void {\n this.events.on(event, listener);\n }\n\n public off<Event extends JobQueueServerEvents>(\n event: Event,\n listener: JobQueueServerEventListeners<Input, Output>[Event]\n ): void {\n this.events.off(event, listener);\n }\n\n // ========================================================================\n // Protected methods\n // ========================================================================\n\n /**\n * Initialize workers\n */\n protected initializeWorkers(): void {\n for (let i = 0; i < this.workerCount; i++) {\n const worker = this.createWorker();\n this.workers.push(worker);\n }\n }\n\n /**\n * Create a new worker and wire up event forwarding\n */\n protected createWorker(): JobQueueWorker<Input, Output, QueueJob> {\n const worker = new JobQueueWorker<Input, Output, QueueJob>(this.jobClass, {\n storage: this.storage,\n queueName: this.queueName,\n limiter: this.limiter,\n pollIntervalMs: this.pollIntervalMs,\n stopTimeoutMs: this.stopTimeoutMs,\n });\n\n // Forward worker events to server and clients\n worker.on(\"job_start\", (jobId) => {\n this.stats = { ...this.stats, totalJobs: this.stats.totalJobs + 1 };\n this.events.emit(\"job_start\", this.queueName, jobId);\n this.forwardToClients(\"handleJobStart\", jobId);\n });\n\n worker.on(\"job_complete\", (jobId, output) => {\n this.stats = { ...this.stats, completedJobs: this.stats.completedJobs + 1 };\n this.updateAverageProcessingTime();\n this.events.emit(\"job_complete\", this.queueName, jobId, output);\n this.forwardToClients(\"handleJobComplete\", jobId, output);\n\n // Immediate deletion when configured\n if (this.deleteAfterCompletionMs === 0) {\n this.storage.delete(jobId).catch((err) => {\n console.error(\"Error deleting job after completion:\", err);\n });\n }\n\n // A concurrency slot freed up — wake idle workers\n this.notifyWorkers();\n });\n\n worker.on(\"job_error\", (jobId, error, errorCode) => {\n this.stats = { ...this.stats, failedJobs: this.stats.failedJobs + 1 };\n this.events.emit(\"job_error\", this.queueName, jobId, error);\n this.forwardToClients(\"handleJobError\", jobId, error, errorCode);\n\n // Immediate deletion when configured\n if (this.deleteAfterFailureMs === 0) {\n this.storage.delete(jobId).catch((err) => {\n console.error(\"Error deleting job after error:\", err);\n });\n }\n\n // A concurrency slot freed up — wake idle workers\n this.notifyWorkers();\n });\n\n worker.on(\"job_disabled\", (jobId) => {\n this.stats = { ...this.stats, disabledJobs: this.stats.disabledJobs + 1 };\n this.events.emit(\"job_disabled\", this.queueName, jobId);\n this.forwardToClients(\"handleJobDisabled\", jobId);\n\n // Immediate deletion when configured\n if (this.deleteAfterDisabledMs === 0) {\n this.storage.delete(jobId).catch((err) => {\n console.error(\"Error deleting job after disabling:\", err);\n });\n }\n\n // A concurrency slot freed up — wake idle workers\n this.notifyWorkers();\n });\n\n worker.on(\"job_retry\", (jobId, runAfter) => {\n this.stats = { ...this.stats, retriedJobs: this.stats.retriedJobs + 1 };\n this.events.emit(\"job_retry\", this.queueName, jobId, runAfter);\n this.forwardToClients(\"handleJobRetry\", jobId, runAfter);\n });\n\n worker.on(\"job_progress\", (jobId, progress, message, details) => {\n this.events.emit(\"job_progress\", this.queueName, jobId, progress, message, details);\n this.forwardToClients(\"handleJobProgress\", jobId, progress, message, details);\n });\n\n return worker;\n }\n\n /**\n * Forward events to all attached clients\n */\n protected forwardToClients(method: \"handleJobStart\", jobId: unknown): void;\n protected forwardToClients(method: \"handleJobComplete\", jobId: unknown, output: Output): void;\n protected forwardToClients(\n method: \"handleJobError\",\n jobId: unknown,\n error: string,\n errorCode?: string\n ): void;\n protected forwardToClients(method: \"handleJobDisabled\", jobId: unknown): void;\n protected forwardToClients(method: \"handleJobRetry\", jobId: unknown, runAfter: Date): void;\n protected forwardToClients(\n method: \"handleJobProgress\",\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ): void;\n protected forwardToClients(method: string, ...args: unknown[]): void {\n for (const client of this.clients) {\n const fn = (client as any)[method];\n if (typeof fn === \"function\") {\n fn.apply(client, args);\n }\n }\n }\n\n /**\n * Update average processing time from all workers\n */\n protected updateAverageProcessingTime(): void {\n const times: number[] = [];\n for (const worker of this.workers) {\n const avgTime = worker.getAverageProcessingTime();\n if (avgTime !== undefined) {\n times.push(avgTime);\n }\n }\n if (times.length > 0) {\n const avg = times.reduce((a, b) => a + b, 0) / times.length;\n this.stats = {\n ...this.stats,\n averageProcessingTime: avg,\n lastUpdateTime: new Date(),\n };\n }\n }\n\n /**\n * Start the cleanup loop\n */\n protected startCleanupLoop(): void {\n if (!this.running) return;\n\n this.cleanupJobs().finally(() => {\n if (this.running) {\n this.cleanupTimer = setTimeout(() => this.startCleanupLoop(), this.cleanupIntervalMs);\n }\n });\n }\n\n /**\n * Clean up completed/failed jobs based on TTL settings\n */\n protected async cleanupJobs(): Promise<void> {\n try {\n // The workers will handle the abort via their abort controllers\n // We just need to ensure the jobs get marked as failed\n\n // Delete completed jobs after TTL\n if (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) {\n await this.storage.deleteJobsByStatusAndAge(\n JobStatus.COMPLETED,\n this.deleteAfterCompletionMs\n );\n }\n\n // Delete failed jobs after TTL\n if (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) {\n await this.storage.deleteJobsByStatusAndAge(JobStatus.FAILED, this.deleteAfterFailureMs);\n }\n\n // Delete disabled jobs after TTL\n if (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0) {\n await this.storage.deleteJobsByStatusAndAge(JobStatus.DISABLED, this.deleteAfterDisabledMs);\n }\n } catch (error) {\n console.error(\"Error in cleanup:\", error);\n }\n }\n\n /**\n * Fix stuck jobs from previous server runs.\n * Jobs in PROCESSING or ABORTING state that are not owned by any of the current\n * server's workers are considered orphaned and will be reset.\n */\n protected async fixupJobs(): Promise<void> {\n try {\n const stuckProcessingJobs = await this.storage.peek(JobStatus.PROCESSING);\n const stuckAbortingJobs = await this.storage.peek(JobStatus.ABORTING);\n const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];\n\n // Get the worker IDs of all workers managed by this server\n const currentWorkerIds = new Set(this.getWorkerIds());\n\n for (const jobData of stuckJobs) {\n // Skip jobs that belong to workers in this server (they may still be processing)\n if (jobData.worker_id && currentWorkerIds.has(jobData.worker_id)) {\n continue;\n }\n\n const job = this.storageToClass(jobData);\n if (job.runAttempts >= job.maxRetries) {\n job.status = JobStatus.FAILED;\n job.error = \"Max retries reached\";\n job.errorCode = \"MAX_RETRIES_REACHED\";\n // Clear worker_id since job is now failed\n job.workerId = null;\n } else {\n job.status = JobStatus.PENDING;\n job.runAfter = job.lastRanAt || new Date();\n job.progress = 0;\n job.progressMessage = \"\";\n job.progressDetails = null;\n job.error = \"Server restarted\";\n // Clear worker_id so a new worker can claim this job\n job.workerId = null;\n }\n\n await this.storage.complete(this.classToStorage(job));\n }\n } catch (error) {\n console.error(\"Error in fixupJobs:\", error);\n }\n }\n\n /**\n * Convert storage format to Job class\n */\n protected storageToClass(details: JobStorageFormat<Input, Output>): Job<Input, Output> {\n return storageToClass(details, this.jobClass);\n }\n\n /**\n * Convert Job class to storage format\n */\n protected classToStorage(job: Job<Input, Output>): JobStorageFormat<Input, Output> {\n return classToStorage(job, this.queueName);\n }\n\n /**\n * Get the worker IDs of all workers managed by this server\n */\n public getWorkerIds(): string[] {\n return this.workers.map((worker) => worker.workerId);\n }\n}\n",
|
|
11
12
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken } from \"@workglow/util\";\nimport { ILimiter, LimiterScope } from \"./ILimiter\";\n\nexport const NULL_JOB_LIMITER = createServiceToken<ILimiter>(\"jobqueue.limiter.null\");\n\n/**\n * Null limiter that does nothing.\n */\nexport class NullLimiter implements ILimiter {\n public readonly scope: LimiterScope = \"process\";\n\n /** Sentinel token — non-null so callers' truthy checks see it as a success. */\n private static readonly SENTINEL = Symbol(\"NullLimiter.acquired\");\n\n async tryAcquire(): Promise<unknown | null> {\n return NullLimiter.SENTINEL;\n }\n\n async release(_token: unknown): Promise<void> {\n // Do nothing\n }\n\n async canProceed(): Promise<boolean> {\n return true;\n }\n\n async recordJobStart(): Promise<void> {\n // Do nothing\n }\n\n async recordJobCompletion(): Promise<void> {\n // Do nothing\n }\n\n async getNextAvailableTime(): Promise<Date> {\n return new Date();\n }\n\n async setNextAvailableTime(date: Date): Promise<void> {\n // Do nothing\n }\n\n async clear(): Promise<void> {\n // Do nothing\n }\n}\n",
|
|
12
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { IQueueStorage, JobStatus, JobStorageFormat } from \"@workglow/storage\";\nimport {\n EventEmitter,\n getLogger,\n getTelemetryProvider,\n sleep,\n SpanStatusCode,\n uuid4,\n} from \"@workglow/util\";\nimport { ILimiter } from \"../limiter/ILimiter\";\nimport { NullLimiter } from \"../limiter/NullLimiter\";\nimport { Job, JobClass } from \"./Job\";\nimport {\n AbortSignalJobError,\n JobDisabledError,\n JobError,\n JobNotFoundError,\n PermanentJobError,\n RetryableJobError,\n} from \"./JobError\";\nimport { withJobErrorDiagnostics } from \"./JobErrorDiagnostics\";\nimport { classToStorage, storageToClass } from \"./JobStorageConverters\";\n\n/**\n * Upper bound on {@link JobQueueWorker.getLimiterWakeDelay}. Prevents a\n * misconfigured or stuck limiter (e.g. one whose `getNextAvailableTime`\n * returns hours in the future) from making the worker unresponsive — it\n * wakes at least this often regardless of what the limiter says.\n */\nconst MAX_LIMITER_WAKE_MS = 30_000;\n\n/**\n * Events emitted by JobQueueWorker\n */\nexport type JobQueueWorkerEventListeners<Input, Output> = {\n job_start: (jobId: unknown) => void;\n job_complete: (jobId: unknown, output: Output) => void;\n job_error: (jobId: unknown, error: string, errorCode?: string) => void;\n job_disabled: (jobId: unknown) => void;\n job_retry: (jobId: unknown, runAfter: Date) => void;\n job_progress: (\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ) => void;\n worker_start: () => void;\n worker_stop: () => void;\n};\n\nexport type JobQueueWorkerEvents = keyof JobQueueWorkerEventListeners<unknown, unknown>;\n\n/**\n * Options for creating a JobQueueWorker\n */\nexport interface JobQueueWorkerOptions<Input, Output> {\n readonly storage: IQueueStorage<Input, Output>;\n readonly queueName: string;\n readonly limiter?: ILimiter;\n readonly pollIntervalMs?: number;\n /**\n * Optional worker ID. If not provided, a random UUID will be generated.\n * Use a persistent ID if you want the worker to reclaim its own jobs after restart.\n */\n readonly workerId?: string | null;\n /**\n * Max time `stop()` waits for in-flight jobs to finish before forcing aborts.\n * Defaults to 30s. Set to 0 to abort immediately.\n */\n readonly stopTimeoutMs?: number;\n}\n\n/**\n * Worker that processes jobs from the queue.\n * Reports progress and completion back to storage.\n */\nexport class JobQueueWorker<\n Input,\n Output,\n QueueJob extends Job<Input, Output> = Job<Input, Output>,\n> {\n public readonly queueName: string;\n public readonly workerId: string;\n protected readonly storage: IQueueStorage<Input, Output>;\n protected readonly jobClass: JobClass<Input, Output>;\n protected readonly limiter: ILimiter;\n protected readonly pollIntervalMs: number;\n protected readonly stopTimeoutMs: number;\n protected readonly events = new EventEmitter<JobQueueWorkerEventListeners<Input, Output>>();\n\n protected running = false;\n\n /**\n * Tracks in-flight job executions for drain-on-stop.\n * Each entry's promise resolves (never rejects) when the job settles\n * (complete / fail / retry / abort).\n */\n private readonly inFlight: Map<unknown, Promise<void>> = new Map();\n\n /**\n * Resolve function for the idle wait promise.\n * When set, the worker is idle and waiting for either a notification or poll timeout.\n */\n private wakeResolve: (() => void) | null = null;\n private wakeTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * Set when {@link notify} fires while the worker is not yet idle. The next\n * {@link waitForWakeOrTimeout} call consumes it and returns immediately\n * instead of sleeping. Without this flag, a notify that arrives during\n * `processJobs`'s pre-idle awaits (e.g. while `storage.next` and\n * `storage.peek` are in flight) would be dropped on the floor and the\n * worker would sleep for the full poll interval despite there being work\n * to do — observed under load on IndexedDb.\n */\n private wakePending = false;\n\n /**\n * Promise for the running `processJobs` loop. Captured in {@link start} so\n * {@link stop} can await actual loop exit instead of returning while the\n * loop is still suspended mid-iteration. Without this, a loop parked in\n * `await this.next()` could resume after stop returned and claim a job\n * that was submitted after stop — starting `processSingleJob` on a worker\n * that's no longer running.\n */\n private loopPromise: Promise<void> | null = null;\n\n /**\n * Abort controllers for active jobs\n */\n protected readonly activeJobAbortControllers: Map<unknown, AbortController> = new Map();\n\n /**\n * Processing times for statistics\n */\n protected readonly processingTimes: Map<unknown, number> = new Map();\n\n constructor(jobClass: JobClass<Input, Output>, options: JobQueueWorkerOptions<Input, Output>) {\n this.queueName = options.queueName;\n this.workerId = options.workerId ?? uuid4();\n this.storage = options.storage;\n this.jobClass = jobClass;\n this.limiter = options.limiter ?? new NullLimiter();\n this.pollIntervalMs = options.pollIntervalMs ?? 100;\n this.stopTimeoutMs = options.stopTimeoutMs ?? 30_000;\n }\n\n /**\n * Start the worker processing loop\n */\n public async start(): Promise<this> {\n if (this.running) {\n return this;\n }\n this.running = true;\n this.events.emit(\"worker_start\");\n this.loopPromise = this.processJobs();\n return this;\n }\n\n /**\n * If this worker is currently processing the given job, fire its abort controller\n * immediately and return true. Returns false if the job isn't active on this worker.\n * @internal\n */\n public requestAbort(jobId: unknown): boolean {\n const controller = this.activeJobAbortControllers.get(jobId);\n if (controller && !controller.signal.aborted) {\n controller.abort();\n return true;\n }\n return false;\n }\n\n /**\n * Wake the worker from idle sleep so it checks for jobs immediately.\n * If the worker is not yet idle, latches a pending-wake flag that the next\n * {@link waitForWakeOrTimeout} call consumes — so wake notifications that\n * race with worker startup or mid-iteration awaits are not lost.\n */\n public notify(): void {\n this.wakePending = true;\n if (this.wakeResolve) {\n if (this.wakeTimer) {\n clearTimeout(this.wakeTimer);\n this.wakeTimer = null;\n }\n const resolve = this.wakeResolve;\n this.wakeResolve = null;\n resolve();\n }\n }\n\n /**\n * Stop the worker, draining in-flight jobs up to {@link stopTimeoutMs}\n * before aborting anything still running.\n */\n public async stop(): Promise<this> {\n if (!this.running) {\n return this;\n }\n this.running = false;\n\n // Wake from idle sleep so the processing loop can exit.\n this.notify();\n\n // Wait for the processJobs loop to actually exit. Without this, a loop\n // suspended mid-iteration (e.g. inside `await this.next()`) could resume\n // after stop returned and claim/start a freshly-submitted job — leaving\n // a \"stopped\" worker running PROCESSING jobs.\n const loopPromise = this.loopPromise;\n this.loopPromise = null;\n if (loopPromise) {\n await loopPromise;\n }\n\n // Phase 1 — graceful drain.\n if (this.stopTimeoutMs > 0 && this.inFlight.size > 0) {\n const drain = Promise.allSettled([...this.inFlight.values()]);\n await Promise.race([drain, sleep(this.stopTimeoutMs)]);\n }\n\n // Phase 2 — anything still running gets aborted.\n if (this.inFlight.size > 0) {\n for (const controller of this.activeJobAbortControllers.values()) {\n if (!controller.signal.aborted) {\n controller.abort();\n }\n }\n const abortDrain = Promise.allSettled([...this.inFlight.values()]);\n await Promise.race([abortDrain, sleep(1000)]);\n }\n\n this.events.emit(\"worker_stop\");\n return this;\n }\n\n /**\n * Process a single job manually (useful for testing or manual control).\n *\n * Uses the atomic claim->acquire->release pattern: claim a job, then atomically\n * reserve a limiter slot. If the limiter rejects (e.g. raced another worker\n * to the last slot), the claimed job is released back to PENDING.\n */\n public async processNext(): Promise<boolean> {\n const job = await this.next();\n if (!job) {\n return false;\n }\n const limiterToken = await this.limiter.tryAcquire();\n if (limiterToken === null || limiterToken === undefined) {\n await this.releaseClaimedJob(job);\n return false;\n }\n await this.processSingleJob(job, limiterToken);\n return true;\n }\n\n /**\n * Check if the worker is currently running\n */\n public isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Get the number of active jobs being processed\n */\n public getActiveJobCount(): number {\n return this.activeJobAbortControllers.size;\n }\n\n /**\n * Get average processing time\n */\n public getAverageProcessingTime(): number | undefined {\n const times = Array.from(this.processingTimes.values());\n if (times.length === 0) return undefined;\n return times.reduce((a, b) => a + b, 0) / times.length;\n }\n\n // ========================================================================\n // Event handling\n // ========================================================================\n\n public on<Event extends JobQueueWorkerEvents>(\n event: Event,\n listener: JobQueueWorkerEventListeners<Input, Output>[Event]\n ): void {\n this.events.on(event, listener);\n }\n\n public off<Event extends JobQueueWorkerEvents>(\n event: Event,\n listener: JobQueueWorkerEventListeners<Input, Output>[Event]\n ): void {\n this.events.off(event, listener);\n }\n\n // ========================================================================\n // Protected methods\n // ========================================================================\n\n /**\n * Get the next job from the queue\n */\n protected async next(): Promise<QueueJob | undefined> {\n const job = await this.storage.next(this.workerId);\n if (!job) return undefined;\n return this.storageToClass(job) as QueueJob;\n }\n\n /**\n * Main job processing loop.\n *\n * Runs as a tight `while` loop (no recursive `setTimeout`) so that\n * back-to-back jobs are picked up with minimal inter-job latency.\n *\n * When no jobs are available the worker sleeps until either:\n * - {@link notify} is called (e.g. because the server saw a new job inserted\n * or a running job completed and freed a concurrency slot), or\n * - the poll-interval timeout expires (fallback for storages without push\n * events).\n */\n protected async processJobs(): Promise<void> {\n while (this.running) {\n try {\n // Check for aborting jobs\n await this.checkForAbortingJobs();\n\n // Claim first, then atomically reserve a limiter slot. Doing the claim\n // before the acquire avoids a race window: with the previous \"check\n // canProceed -> claim -> recordJobStart\" sequence, two workers could\n // both observe count < max, both claim, and both then increment —\n // overshooting the configured limit by exactly the worker concurrency.\n // The atomic tryAcquire guarantees only one of N concurrent acquirers\n // succeeds when there's one slot left.\n const job = await this.next();\n if (!job) {\n // Queue is empty — sleep until notified of new work or until the\n // next deferred job becomes ready.\n const delay = await this.getIdleDelay();\n await this.waitForWakeOrTimeout(delay);\n continue;\n }\n\n if (!this.running) {\n // Stopped during the await. Release the job back to PENDING.\n await this.releaseClaimedJob(job);\n return;\n }\n\n const limiterToken = await this.limiter.tryAcquire();\n if (limiterToken === null || limiterToken === undefined) {\n // Lost the race for the last slot, or hit the rate-limit window.\n // Give the job back and wait for capacity.\n await this.releaseClaimedJob(job);\n await this.waitForWakeOrTimeout(await this.getLimiterWakeDelay());\n continue;\n }\n\n if (!this.running) {\n // Stop fired while tryAcquire was in flight. Undo both the limiter\n // reservation (release THIS token, not \"the most recent\") and the\n // job claim so we don't start processing on a worker that's about\n // to exit.\n try {\n await this.limiter.release(limiterToken);\n } catch {\n // best-effort\n }\n await this.releaseClaimedJob(job);\n return;\n }\n\n // Don't await - process in background to allow concurrent jobs.\n // The loop will claim+acquire on the next iteration.\n this.processSingleJob(job, limiterToken);\n } catch {\n // Don't let transient errors kill the loop\n await sleep(this.pollIntervalMs);\n }\n }\n }\n\n /**\n * How long to sleep when the limiter rejected an acquire. Reads the limiter's\n * own next-available time so we wake exactly when capacity opens, instead of\n * polling every {@link pollIntervalMs}. Clamped to {@link pollIntervalMs}\n * (lower bound) and {@link MAX_LIMITER_WAKE_MS} (upper bound) so a\n * misconfigured/stuck limiter still wakes occasionally.\n */\n private async getLimiterWakeDelay(): Promise<number> {\n try {\n const next = await this.limiter.getNextAvailableTime();\n const delay = next.getTime() - Date.now();\n if (delay <= 0) return this.pollIntervalMs;\n return Math.min(Math.max(delay, this.pollIntervalMs), MAX_LIMITER_WAKE_MS);\n } catch {\n return this.pollIntervalMs;\n }\n }\n\n /**\n * Determine how long to sleep when idle.\n *\n * Peeks at the earliest PENDING job: if it has a future `run_after`,\n * returns the time until it becomes ready (clamped to `pollIntervalMs`);\n * otherwise returns `pollIntervalMs`.\n */\n private async getIdleDelay(): Promise<number> {\n try {\n const pending = await this.storage.peek(JobStatus.PENDING, 1);\n if (pending.length > 0 && pending[0].run_after) {\n const delay = new Date(pending[0].run_after).getTime() - Date.now();\n if (delay > 0) {\n return Math.min(delay, this.pollIntervalMs);\n }\n }\n } catch {\n // If peek fails, fall back to default\n }\n return this.pollIntervalMs;\n }\n\n /**\n * Wait for either a {@link notify} call or the given timeout,\n * whichever comes first. Consumes any pending wake latched while the worker\n * was not yet idle (see {@link wakePending}) — returns immediately in that\n * case rather than sleeping.\n */\n private waitForWakeOrTimeout(timeoutMs: number): Promise<void> {\n if (this.wakePending) {\n this.wakePending = false;\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n this.wakeTimer = setTimeout(() => {\n this.wakeTimer = null;\n this.wakeResolve = null;\n this.wakePending = false;\n resolve();\n }, timeoutMs);\n\n this.wakeResolve = () => {\n this.wakePending = false;\n resolve();\n };\n });\n }\n\n /**\n * Check for jobs that have been marked for abort and trigger their abort controllers.\n *\n * Only relevant for jobs running on THIS worker (we have an abort controller\n * registered for them). When no jobs are active, the peek result is irrelevant\n * — skip the storage round-trip entirely. Important for battery life on\n * same-process deployments (browser/mobile) where workers spend most time idle.\n */\n protected async checkForAbortingJobs(): Promise<void> {\n if (this.activeJobAbortControllers.size === 0) {\n return;\n }\n const abortingJobs = await this.storage.peek(JobStatus.ABORTING);\n for (const jobData of abortingJobs) {\n const controller = this.activeJobAbortControllers.get(jobData.id);\n if (controller && !controller.signal.aborted) {\n controller.abort();\n }\n }\n }\n\n /**\n * Process a single job\n */\n protected async processSingleJob(\n job: Job<Input, Output>,\n limiterToken: unknown\n ): Promise<void> {\n if (!job || !job.id) {\n throw new JobNotFoundError(\"Invalid job provided for processing\");\n }\n\n const { promise: inFlightPromise, resolve: resolveInFlight } = Promise.withResolvers<void>();\n this.inFlight.set(job.id, inFlightPromise);\n\n const startTime = Date.now();\n\n // Start telemetry span for job processing\n const telemetry = getTelemetryProvider();\n const span = telemetry.isEnabled\n ? telemetry.startSpan(\"workglow.job.process\", {\n attributes: {\n \"workglow.job.id\": String(job.id),\n \"workglow.job.queue\": this.queueName,\n \"workglow.job.worker_id\": this.workerId,\n \"workglow.job.run_attempt\": job.runAttempts,\n \"workglow.job.max_retries\": job.maxRetries,\n },\n })\n : undefined;\n\n // Set when validateJobState fails and we release() the limiter slot\n // ourselves — the finally block then skips recordJobCompletion to avoid\n // double-decrementing limiters where release() and recordJobCompletion()\n // both decrement (e.g. ConcurrencyLimiter).\n let slotReleased = false;\n try {\n // The limiter slot was already atomically reserved by tryAcquire() in\n // the main loop (or processNext), so we no longer call recordJobStart\n // here — doing so would double-count.\n try {\n await this.validateJobState(job);\n } catch (validationErr) {\n // Validation failed before we ran any actual work — release THIS\n // limiter slot (by token, not by recency) so it doesn't count toward\n // the rate limit and we don't accidentally release another worker's\n // slot.\n try {\n await this.limiter.release(limiterToken);\n slotReleased = true;\n } catch {\n // best-effort\n }\n throw validationErr;\n }\n\n const abortController = this.createAbortController(job.id);\n this.events.emit(\"job_start\", job.id);\n\n const output = await this.executeJob(job, abortController.signal);\n await this.completeJob(job, output);\n\n const elapsed = Date.now() - startTime;\n this.processingTimes.set(job.id, elapsed);\n\n if (span) {\n span.setAttributes({ \"workglow.job.duration_ms\": elapsed });\n span.setStatus(SpanStatusCode.OK);\n }\n } catch (err: unknown) {\n const error = this.normalizeError(err);\n let spanErrorMessage = error.message;\n if (error instanceof RetryableJobError) {\n const currentJob = await this.getJob(job.id);\n if (!currentJob) {\n throw new JobNotFoundError(`Job ${job.id} not found`);\n }\n\n if (currentJob.runAttempts >= currentJob.maxRetries) {\n spanErrorMessage = \"Max retries reached\";\n await this.failJob(currentJob, new PermanentJobError(spanErrorMessage));\n span?.setStatus(SpanStatusCode.ERROR, spanErrorMessage);\n } else {\n await this.rescheduleJob(currentJob, error.retryDate);\n span?.addEvent(\"workglow.job.retry\", {\n \"workglow.job.run_attempt\": currentJob.runAttempts,\n });\n span?.setStatus(SpanStatusCode.UNSET);\n }\n } else {\n await this.failJob(job, error);\n span?.setStatus(SpanStatusCode.ERROR, error.message);\n }\n span?.setAttributes({ \"workglow.job.error\": spanErrorMessage });\n } finally {\n span?.end();\n try {\n if (!slotReleased) {\n await this.limiter.recordJobCompletion();\n }\n } finally {\n this.inFlight.delete(job.id);\n resolveInFlight();\n }\n }\n }\n\n /**\n * Execute a job with the provided abort signal\n */\n protected async executeJob(job: Job<Input, Output>, signal: AbortSignal): Promise<Output> {\n if (!job) throw new JobNotFoundError(\"Cannot execute null or undefined job\");\n return await job.execute(job.input, {\n signal,\n updateProgress: this.updateProgress.bind(this, job.id),\n });\n }\n\n /**\n * Update progress for a job.\n *\n * Mid-job progress is delivered in-memory via the `job_progress` event;\n * storage is only touched at terminal transitions (complete / fail / retry).\n * Cross-process observers therefore see state transitions but not fine-grained\n * progress — subscribe to an attached `JobQueueClient` for that.\n */\n protected async updateProgress(\n jobId: unknown,\n progress: number,\n message: string = \"\",\n details: Record<string, unknown> | null = null\n ): Promise<void> {\n progress = Math.max(0, Math.min(100, progress));\n this.events.emit(\"job_progress\", jobId, progress, message, details);\n }\n\n /**\n * Mark a job as completed\n */\n protected async completeJob(job: Job<Input, Output>, output?: Output): Promise<void> {\n try {\n job.status = JobStatus.COMPLETED;\n job.progress = 100;\n job.progressMessage = \"\";\n job.progressDetails = null;\n job.completedAt = new Date();\n job.output = output ?? null;\n job.error = null;\n job.errorCode = null;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_complete\", job.id, output as Output);\n } catch (err) {\n getLogger().error(\"completeJob errored:\", { error: err });\n } finally {\n this.cleanupJob(job.id);\n }\n }\n\n /**\n * Mark a job as failed\n */\n protected async failJob(job: Job<Input, Output>, error: JobError): Promise<void> {\n try {\n job.status = JobStatus.FAILED;\n job.progress = 100;\n job.completedAt = new Date();\n job.progressMessage = \"\";\n job.progressDetails = null;\n job.error = error.message;\n job.errorCode = error?.constructor?.name ?? null;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_error\", job.id, error.message, error.constructor.name);\n } catch (err) {\n getLogger().error(\"failJob errored:\", { error: err });\n } finally {\n this.cleanupJob(job.id);\n }\n }\n\n /**\n * Mark a job as disabled\n */\n protected async disableJob(job: Job<Input, Output>): Promise<void> {\n try {\n job.status = JobStatus.DISABLED;\n job.progress = 100;\n job.completedAt = new Date();\n job.progressMessage = \"\";\n job.progressDetails = null;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_disabled\", job.id);\n } catch (err) {\n getLogger().error(\"disableJob errored:\", { error: err });\n } finally {\n this.cleanupJob(job.id);\n }\n }\n\n /**\n * Release a job that {@link next} just claimed but that we won't process\n * because the worker was stopped mid-claim. Resets the row to PENDING so\n * the next started worker can pick it up. `fixupJobs()` would otherwise\n * skip it (it ignores rows owned by current-server worker IDs).\n *\n * Uses `storage.release()` rather than `storage.complete()` so the retry\n * budget isn't burned: the worker never actually attempted execution.\n */\n protected async releaseClaimedJob(job: Job<Input, Output>): Promise<void> {\n try {\n await this.storage.release(job.id);\n } catch (err) {\n getLogger().error(\"releaseClaimedJob errored:\", { error: err });\n }\n }\n\n /**\n * Reschedule a job for retry\n */\n protected async rescheduleJob(job: Job<Input, Output>, retryDate?: Date): Promise<void> {\n try {\n job.status = JobStatus.PENDING;\n const nextAvailableTime = await this.limiter.getNextAvailableTime();\n job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;\n job.progress = 0;\n job.progressMessage = \"\";\n job.progressDetails = null;\n // Increment runAttempts to keep in-memory object in sync with storage\n // The storage layer will read from DB and increment, so this keeps them aligned\n job.runAttempts = (job.runAttempts ?? 0) + 1;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_retry\", job.id, job.runAfter);\n } catch (err) {\n getLogger().error(\"rescheduleJob errored:\", { error: err });\n }\n }\n\n /**\n * Create an abort controller for a job.\n *\n * The job MUST already be registered in {@link inFlight} — this enforces\n * the invariant that `activeJobAbortControllers ⊆ inFlight`, which\n * {@link handleAbort} relies on to decide whether `processSingleJob` is\n * still on the hook for the terminal write. Calling this from any path\n * other than `processSingleJob` (which registers `inFlight` first) is a\n * programming error.\n */\n protected createAbortController(jobId: unknown): AbortController {\n if (!jobId) throw new JobNotFoundError(\"Cannot create abort controller for undefined job\");\n\n if (!this.inFlight.has(jobId)) {\n throw new Error(\n `createAbortController invariant violated: jobId ${String(jobId)} is not in inFlight. ` +\n `Abort controllers must only be created from within processSingleJob.`\n );\n }\n\n if (this.activeJobAbortControllers.has(jobId)) {\n return this.activeJobAbortControllers.get(jobId)!;\n }\n\n const abortController = new AbortController();\n abortController.signal.addEventListener(\"abort\", () => this.handleAbort(jobId));\n this.activeJobAbortControllers.set(jobId, abortController);\n return abortController;\n }\n\n /**\n * Handle job abort.\n *\n * Two callers fire the controller and reach this listener:\n * 1. `requestAbort` — same-process abort while the job is in flight here.\n * 2. `checkForAbortingJobs` — cross-process abort observed via storage poll.\n *\n * In both cases, if processSingleJob is still running this job locally,\n * the abort signal will propagate into the user task and processSingleJob's\n * own catch path will write the terminal state. We must not race it: doing\n * so duplicates the `job_error` emit and, worse, can clobber a successful\n * `completeJob` that won the race (the COMPLETED→FAILED overwrite bug).\n *\n * If the job is no longer in flight here, it has already settled — recheck\n * storage and only write FAILED for non-terminal states (i.e. an ABORTING\n * row left over from a cross-process abort that this worker never picked up).\n */\n protected async handleAbort(jobId: unknown): Promise<void> {\n if (this.inFlight.has(jobId)) {\n return;\n }\n const job = await this.getJob(jobId);\n if (!job) {\n getLogger().error(\"handleAbort: job not found\", { jobId });\n return;\n }\n if (\n job.status === JobStatus.COMPLETED ||\n job.status === JobStatus.FAILED ||\n job.status === JobStatus.DISABLED\n ) {\n return;\n }\n await this.failJob(job, new AbortSignalJobError(\"Job Aborted\"));\n }\n\n /**\n * Get a job by ID\n */\n protected async getJob(id: unknown): Promise<Job<Input, Output> | undefined> {\n const job = await this.storage.get(id);\n if (!job) return undefined;\n return this.storageToClass(job);\n }\n\n /**\n * Validate job state before processing\n */\n protected async validateJobState(job: Job<Input, Output>): Promise<void> {\n if (job.status === JobStatus.COMPLETED) {\n throw new PermanentJobError(`Job ${job.id} is already completed`);\n }\n if (job.status === JobStatus.FAILED) {\n throw new PermanentJobError(`Job ${job.id} has failed`);\n }\n if (\n job.status === JobStatus.ABORTING ||\n this.activeJobAbortControllers.get(job.id)?.signal.aborted\n ) {\n throw new AbortSignalJobError(`Job ${job.id} is being aborted`);\n }\n if (job.deadlineAt && job.deadlineAt < new Date()) {\n throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);\n }\n if (job.status === JobStatus.DISABLED) {\n throw new JobDisabledError(`Job ${job.id} has been disabled`);\n }\n }\n\n /**\n * Normalize errors into JobError instances\n */\n protected normalizeError(err: unknown): JobError {\n if (err instanceof JobError) {\n return err;\n }\n if (err instanceof Error) {\n return new PermanentJobError(withJobErrorDiagnostics(err.message, err));\n }\n return new PermanentJobError(String(err));\n }\n\n /**\n * Clean up job state after completion/failure\n */\n protected cleanupJob(jobId: unknown): void {\n this.activeJobAbortControllers.delete(jobId);\n }\n\n /**\n * Convert storage format to Job class\n */\n protected storageToClass(details: JobStorageFormat<Input, Output>): Job<Input, Output> {\n return storageToClass(details, this.jobClass);\n }\n\n /**\n * Convert Job class to storage format\n */\n protected classToStorage(job: Job<Input, Output>): JobStorageFormat<Input, Output> {\n return classToStorage(job, this.queueName);\n }\n}\n",
|
|
13
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { ILimiter, LimiterScope } from \"./ILimiter\";\n\nexport class CompositeLimiter implements ILimiter {\n private limiters: ILimiter[] = [];\n\n constructor(limiters: ILimiter[] = []) {\n this.limiters = limiters;\n }\n\n /**\n * `\"cluster\"` only when EVERY child is cluster-scoped. A single process-scoped\n * child means the composite as a whole can't enforce a cluster-wide limit.\n */\n public get scope(): LimiterScope {\n return this.limiters.every((l) => l.scope === \"cluster\") && this.limiters.length > 0\n ? \"cluster\"\n : \"process\";\n }\n\n addLimiter(limiter: ILimiter): void {\n this.limiters.push(limiter);\n }\n\n async canProceed(): Promise<boolean> {\n for (const limiter of this.limiters) {\n if (!(await limiter.canProceed())) {\n return false; // If any limiter says \"no\", proceed no further\n }\n }\n return true; // All limiters agree\n }\n\n /**\n * Atomic against the composite: acquires children sequentially and rolls\n * back any successfully-acquired prefix if a later child rejects, so the\n * \"all-or-nothing\" semantics hold under concurrency.\n *\n * The returned token is an array of per-child tokens (parallel to\n * `this.limiters`) so {@link release} can free each child's specific slot\n * — never \"the most recent\", which would race other acquirers.\n */\n async tryAcquire(): Promise<unknown | null> {\n const tokens: unknown[] = [];\n for (const limiter of this.limiters) {\n const t = await limiter.tryAcquire();\n if (t === null || t === undefined) {\n // Roll back the partial acquisition. Iterate in reverse so children\n // are released in opposite order of acquisition.\n for (let i = tokens.length - 1; i >= 0; i--) {\n try {\n await this.limiters[i].release(tokens[i]);\n } catch {\n // best-effort\n }\n }\n return null;\n }\n tokens.push(t);\n }\n return tokens;\n }\n\n async release(token: unknown): Promise<void> {\n if (!Array.isArray(token)) return;\n await Promise.all(
|
|
13
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus } from \"../queue-storage/IQueueStorage\";\nimport type { IQueueStorage, JobStorageFormat } from \"../queue-storage/IQueueStorage\";\nimport {\n EventEmitter,\n getLogger,\n getTelemetryProvider,\n sleep,\n SpanStatusCode,\n uuid4,\n} from \"@workglow/util\";\nimport { ILimiter } from \"../limiter/ILimiter\";\nimport { NullLimiter } from \"../limiter/NullLimiter\";\nimport { Job, JobClass } from \"./Job\";\nimport {\n AbortSignalJobError,\n JobDisabledError,\n JobError,\n JobNotFoundError,\n PermanentJobError,\n RetryableJobError,\n} from \"./JobError\";\nimport { withJobErrorDiagnostics } from \"./JobErrorDiagnostics\";\nimport { classToStorage, storageToClass } from \"./JobStorageConverters\";\n\n/**\n * Upper bound on {@link JobQueueWorker.getLimiterWakeDelay}. Prevents a\n * misconfigured or stuck limiter (e.g. one whose `getNextAvailableTime`\n * returns hours in the future) from making the worker unresponsive — it\n * wakes at least this often regardless of what the limiter says.\n */\nconst MAX_LIMITER_WAKE_MS = 30_000;\n\n/**\n * Events emitted by JobQueueWorker\n */\nexport type JobQueueWorkerEventListeners<Input, Output> = {\n job_start: (jobId: unknown) => void;\n job_complete: (jobId: unknown, output: Output) => void;\n job_error: (jobId: unknown, error: string, errorCode?: string) => void;\n job_disabled: (jobId: unknown) => void;\n job_retry: (jobId: unknown, runAfter: Date) => void;\n job_progress: (\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, unknown> | null\n ) => void;\n worker_start: () => void;\n worker_stop: () => void;\n};\n\nexport type JobQueueWorkerEvents = keyof JobQueueWorkerEventListeners<unknown, unknown>;\n\n/**\n * Options for creating a JobQueueWorker\n */\nexport interface JobQueueWorkerOptions<Input, Output> {\n readonly storage: IQueueStorage<Input, Output>;\n readonly queueName: string;\n readonly limiter?: ILimiter;\n readonly pollIntervalMs?: number;\n /**\n * Optional worker ID. If not provided, a random UUID will be generated.\n * Use a persistent ID if you want the worker to reclaim its own jobs after restart.\n */\n readonly workerId?: string | null;\n /**\n * Max time `stop()` waits for in-flight jobs to finish before forcing aborts.\n * Defaults to 30s. Set to 0 to abort immediately.\n */\n readonly stopTimeoutMs?: number;\n}\n\n/**\n * Worker that processes jobs from the queue.\n * Reports progress and completion back to storage.\n */\nexport class JobQueueWorker<\n Input,\n Output,\n QueueJob extends Job<Input, Output> = Job<Input, Output>,\n> {\n public readonly queueName: string;\n public readonly workerId: string;\n protected readonly storage: IQueueStorage<Input, Output>;\n protected readonly jobClass: JobClass<Input, Output>;\n protected readonly limiter: ILimiter;\n protected readonly pollIntervalMs: number;\n protected readonly stopTimeoutMs: number;\n protected readonly events = new EventEmitter<JobQueueWorkerEventListeners<Input, Output>>();\n\n protected running = false;\n\n /**\n * Tracks in-flight job executions for drain-on-stop.\n * Each entry's promise resolves (never rejects) when the job settles\n * (complete / fail / retry / abort).\n */\n private readonly inFlight: Map<unknown, Promise<void>> = new Map();\n\n /**\n * Resolve function for the idle wait promise.\n * When set, the worker is idle and waiting for either a notification or poll timeout.\n */\n private wakeResolve: (() => void) | null = null;\n private wakeTimer: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * Set when {@link notify} fires while the worker is not yet idle. The next\n * {@link waitForWakeOrTimeout} call consumes it and returns immediately\n * instead of sleeping. Without this flag, a notify that arrives during\n * `processJobs`'s pre-idle awaits (e.g. while `storage.next` and\n * `storage.peek` are in flight) would be dropped on the floor and the\n * worker would sleep for the full poll interval despite there being work\n * to do — observed under load on IndexedDb.\n */\n private wakePending = false;\n\n /**\n * Promise for the running `processJobs` loop. Captured in {@link start} so\n * {@link stop} can await actual loop exit instead of returning while the\n * loop is still suspended mid-iteration. Without this, a loop parked in\n * `await this.next()` could resume after stop returned and claim a job\n * that was submitted after stop — starting `processSingleJob` on a worker\n * that's no longer running.\n */\n private loopPromise: Promise<void> | null = null;\n\n /**\n * Abort controllers for active jobs\n */\n protected readonly activeJobAbortControllers: Map<unknown, AbortController> = new Map();\n\n /**\n * Processing times for statistics\n */\n protected readonly processingTimes: Map<unknown, number> = new Map();\n\n constructor(jobClass: JobClass<Input, Output>, options: JobQueueWorkerOptions<Input, Output>) {\n this.queueName = options.queueName;\n this.workerId = options.workerId ?? uuid4();\n this.storage = options.storage;\n this.jobClass = jobClass;\n this.limiter = options.limiter ?? new NullLimiter();\n this.pollIntervalMs = options.pollIntervalMs ?? 100;\n this.stopTimeoutMs = options.stopTimeoutMs ?? 30_000;\n }\n\n /**\n * Start the worker processing loop\n */\n public async start(): Promise<this> {\n if (this.running) {\n return this;\n }\n this.running = true;\n this.events.emit(\"worker_start\");\n this.loopPromise = this.processJobs();\n return this;\n }\n\n /**\n * If this worker is currently processing the given job, fire its abort controller\n * immediately and return true. Returns false if the job isn't active on this worker.\n * @internal\n */\n public requestAbort(jobId: unknown): boolean {\n const controller = this.activeJobAbortControllers.get(jobId);\n if (controller && !controller.signal.aborted) {\n controller.abort();\n return true;\n }\n return false;\n }\n\n /**\n * Wake the worker from idle sleep so it checks for jobs immediately.\n * If the worker is not yet idle, latches a pending-wake flag that the next\n * {@link waitForWakeOrTimeout} call consumes — so wake notifications that\n * race with worker startup or mid-iteration awaits are not lost.\n */\n public notify(): void {\n this.wakePending = true;\n if (this.wakeResolve) {\n if (this.wakeTimer) {\n clearTimeout(this.wakeTimer);\n this.wakeTimer = null;\n }\n const resolve = this.wakeResolve;\n this.wakeResolve = null;\n resolve();\n }\n }\n\n /**\n * Stop the worker, draining in-flight jobs up to {@link stopTimeoutMs}\n * before aborting anything still running.\n */\n public async stop(): Promise<this> {\n if (!this.running) {\n return this;\n }\n this.running = false;\n\n // Wake from idle sleep so the processing loop can exit.\n this.notify();\n\n // Wait for the processJobs loop to actually exit. Without this, a loop\n // suspended mid-iteration (e.g. inside `await this.next()`) could resume\n // after stop returned and claim/start a freshly-submitted job — leaving\n // a \"stopped\" worker running PROCESSING jobs.\n const loopPromise = this.loopPromise;\n this.loopPromise = null;\n if (loopPromise) {\n await loopPromise;\n }\n\n // Phase 1 — graceful drain.\n if (this.stopTimeoutMs > 0 && this.inFlight.size > 0) {\n const drain = Promise.allSettled([...this.inFlight.values()]);\n await Promise.race([drain, sleep(this.stopTimeoutMs)]);\n }\n\n // Phase 2 — anything still running gets aborted.\n if (this.inFlight.size > 0) {\n for (const controller of this.activeJobAbortControllers.values()) {\n if (!controller.signal.aborted) {\n controller.abort();\n }\n }\n const abortDrain = Promise.allSettled([...this.inFlight.values()]);\n await Promise.race([abortDrain, sleep(1000)]);\n }\n\n this.events.emit(\"worker_stop\");\n return this;\n }\n\n /**\n * Process a single job manually (useful for testing or manual control).\n *\n * Uses the atomic claim->acquire->release pattern: claim a job, then atomically\n * reserve a limiter slot. If the limiter rejects (e.g. raced another worker\n * to the last slot), the claimed job is released back to PENDING.\n */\n public async processNext(): Promise<boolean> {\n const job = await this.next();\n if (!job) {\n return false;\n }\n const limiterToken = await this.limiter.tryAcquire();\n if (limiterToken === null || limiterToken === undefined) {\n await this.releaseClaimedJob(job);\n return false;\n }\n await this.processSingleJob(job, limiterToken);\n return true;\n }\n\n /**\n * Check if the worker is currently running\n */\n public isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Get the number of active jobs being processed\n */\n public getActiveJobCount(): number {\n return this.activeJobAbortControllers.size;\n }\n\n /**\n * Get average processing time\n */\n public getAverageProcessingTime(): number | undefined {\n const times = Array.from(this.processingTimes.values());\n if (times.length === 0) return undefined;\n return times.reduce((a, b) => a + b, 0) / times.length;\n }\n\n // ========================================================================\n // Event handling\n // ========================================================================\n\n public on<Event extends JobQueueWorkerEvents>(\n event: Event,\n listener: JobQueueWorkerEventListeners<Input, Output>[Event]\n ): void {\n this.events.on(event, listener);\n }\n\n public off<Event extends JobQueueWorkerEvents>(\n event: Event,\n listener: JobQueueWorkerEventListeners<Input, Output>[Event]\n ): void {\n this.events.off(event, listener);\n }\n\n // ========================================================================\n // Protected methods\n // ========================================================================\n\n /**\n * Get the next job from the queue\n */\n protected async next(): Promise<QueueJob | undefined> {\n const job = await this.storage.next(this.workerId);\n if (!job) return undefined;\n return this.storageToClass(job) as QueueJob;\n }\n\n /**\n * Main job processing loop.\n *\n * Runs as a tight `while` loop (no recursive `setTimeout`) so that\n * back-to-back jobs are picked up with minimal inter-job latency.\n *\n * When no jobs are available the worker sleeps until either:\n * - {@link notify} is called (e.g. because the server saw a new job inserted\n * or a running job completed and freed a concurrency slot), or\n * - the poll-interval timeout expires (fallback for storages without push\n * events).\n */\n protected async processJobs(): Promise<void> {\n while (this.running) {\n try {\n // Check for aborting jobs\n await this.checkForAbortingJobs();\n\n // Claim first, then atomically reserve a limiter slot. Doing the claim\n // before the acquire avoids a race window: with the previous \"check\n // canProceed -> claim -> recordJobStart\" sequence, two workers could\n // both observe count < max, both claim, and both then increment —\n // overshooting the configured limit by exactly the worker concurrency.\n // The atomic tryAcquire guarantees only one of N concurrent acquirers\n // succeeds when there's one slot left.\n const job = await this.next();\n if (!job) {\n // Queue is empty — sleep until notified of new work or until the\n // next deferred job becomes ready.\n const delay = await this.getIdleDelay();\n await this.waitForWakeOrTimeout(delay);\n continue;\n }\n\n if (!this.running) {\n // Stopped during the await. Release the job back to PENDING.\n await this.releaseClaimedJob(job);\n return;\n }\n\n const limiterToken = await this.limiter.tryAcquire();\n if (limiterToken === null || limiterToken === undefined) {\n // Lost the race for the last slot, or hit the rate-limit window.\n // Give the job back and wait for capacity.\n await this.releaseClaimedJob(job);\n await this.waitForWakeOrTimeout(await this.getLimiterWakeDelay());\n continue;\n }\n\n if (!this.running) {\n // Stop fired while tryAcquire was in flight. Undo both the limiter\n // reservation (release THIS token, not \"the most recent\") and the\n // job claim so we don't start processing on a worker that's about\n // to exit.\n try {\n await this.limiter.release(limiterToken);\n } catch {\n // best-effort\n }\n await this.releaseClaimedJob(job);\n return;\n }\n\n // Don't await - process in background to allow concurrent jobs.\n // The loop will claim+acquire on the next iteration.\n this.processSingleJob(job, limiterToken);\n } catch {\n // Don't let transient errors kill the loop\n await sleep(this.pollIntervalMs);\n }\n }\n }\n\n /**\n * How long to sleep when the limiter rejected an acquire. Reads the limiter's\n * own next-available time so we wake exactly when capacity opens, instead of\n * polling every {@link pollIntervalMs}. Clamped to {@link pollIntervalMs}\n * (lower bound) and {@link MAX_LIMITER_WAKE_MS} (upper bound) so a\n * misconfigured/stuck limiter still wakes occasionally.\n */\n private async getLimiterWakeDelay(): Promise<number> {\n try {\n const next = await this.limiter.getNextAvailableTime();\n const delay = next.getTime() - Date.now();\n if (delay <= 0) return this.pollIntervalMs;\n return Math.min(Math.max(delay, this.pollIntervalMs), MAX_LIMITER_WAKE_MS);\n } catch {\n return this.pollIntervalMs;\n }\n }\n\n /**\n * Determine how long to sleep when idle.\n *\n * Peeks at the earliest PENDING job: if it has a future `run_after`,\n * returns the time until it becomes ready (clamped to `pollIntervalMs`);\n * otherwise returns `pollIntervalMs`.\n */\n private async getIdleDelay(): Promise<number> {\n try {\n const pending = await this.storage.peek(JobStatus.PENDING, 1);\n if (pending.length > 0 && pending[0].run_after) {\n const delay = new Date(pending[0].run_after).getTime() - Date.now();\n if (delay > 0) {\n return Math.min(delay, this.pollIntervalMs);\n }\n }\n } catch {\n // If peek fails, fall back to default\n }\n return this.pollIntervalMs;\n }\n\n /**\n * Wait for either a {@link notify} call or the given timeout,\n * whichever comes first. Consumes any pending wake latched while the worker\n * was not yet idle (see {@link wakePending}) — returns immediately in that\n * case rather than sleeping.\n */\n private waitForWakeOrTimeout(timeoutMs: number): Promise<void> {\n if (this.wakePending) {\n this.wakePending = false;\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n this.wakeTimer = setTimeout(() => {\n this.wakeTimer = null;\n this.wakeResolve = null;\n this.wakePending = false;\n resolve();\n }, timeoutMs);\n\n this.wakeResolve = () => {\n this.wakePending = false;\n resolve();\n };\n });\n }\n\n /**\n * Check for jobs that have been marked for abort and trigger their abort controllers.\n *\n * Only relevant for jobs running on THIS worker (we have an abort controller\n * registered for them). When no jobs are active, the peek result is irrelevant\n * — skip the storage round-trip entirely. Important for battery life on\n * same-process deployments (browser/mobile) where workers spend most time idle.\n */\n protected async checkForAbortingJobs(): Promise<void> {\n if (this.activeJobAbortControllers.size === 0) {\n return;\n }\n const abortingJobs = await this.storage.peek(JobStatus.ABORTING);\n for (const jobData of abortingJobs) {\n const controller = this.activeJobAbortControllers.get(jobData.id);\n if (controller && !controller.signal.aborted) {\n controller.abort();\n }\n }\n }\n\n /**\n * Process a single job\n */\n protected async processSingleJob(job: Job<Input, Output>, limiterToken: unknown): Promise<void> {\n if (!job || !job.id) {\n throw new JobNotFoundError(\"Invalid job provided for processing\");\n }\n\n const { promise: inFlightPromise, resolve: resolveInFlight } = Promise.withResolvers<void>();\n this.inFlight.set(job.id, inFlightPromise);\n\n const startTime = Date.now();\n\n // Start telemetry span for job processing\n const telemetry = getTelemetryProvider();\n const span = telemetry.isEnabled\n ? telemetry.startSpan(\"workglow.job.process\", {\n attributes: {\n \"workglow.job.id\": String(job.id),\n \"workglow.job.queue\": this.queueName,\n \"workglow.job.worker_id\": this.workerId,\n \"workglow.job.run_attempt\": job.runAttempts,\n \"workglow.job.max_retries\": job.maxRetries,\n },\n })\n : undefined;\n\n // Set when validateJobState fails and we release() the limiter slot\n // ourselves — the finally block then skips recordJobCompletion to avoid\n // double-decrementing limiters where release() and recordJobCompletion()\n // both decrement (e.g. ConcurrencyLimiter).\n let slotReleased = false;\n try {\n // The limiter slot was already atomically reserved by tryAcquire() in\n // the main loop (or processNext), so we no longer call recordJobStart\n // here — doing so would double-count.\n try {\n await this.validateJobState(job);\n } catch (validationErr) {\n // Validation failed before we ran any actual work — release THIS\n // limiter slot (by token, not by recency) so it doesn't count toward\n // the rate limit and we don't accidentally release another worker's\n // slot.\n try {\n await this.limiter.release(limiterToken);\n slotReleased = true;\n } catch {\n // best-effort\n }\n throw validationErr;\n }\n\n const abortController = this.createAbortController(job.id);\n this.events.emit(\"job_start\", job.id);\n\n const output = await this.executeJob(job, abortController.signal);\n await this.completeJob(job, output);\n\n const elapsed = Date.now() - startTime;\n this.processingTimes.set(job.id, elapsed);\n\n if (span) {\n span.setAttributes({ \"workglow.job.duration_ms\": elapsed });\n span.setStatus(SpanStatusCode.OK);\n }\n } catch (err: unknown) {\n const error = this.normalizeError(err);\n let spanErrorMessage = error.message;\n if (error instanceof RetryableJobError) {\n const currentJob = await this.getJob(job.id);\n if (!currentJob) {\n throw new JobNotFoundError(`Job ${job.id} not found`);\n }\n\n if (currentJob.runAttempts >= currentJob.maxRetries) {\n spanErrorMessage = \"Max retries reached\";\n await this.failJob(currentJob, new PermanentJobError(spanErrorMessage));\n span?.setStatus(SpanStatusCode.ERROR, spanErrorMessage);\n } else {\n await this.rescheduleJob(currentJob, error.retryDate);\n span?.addEvent(\"workglow.job.retry\", {\n \"workglow.job.run_attempt\": currentJob.runAttempts,\n });\n span?.setStatus(SpanStatusCode.UNSET);\n }\n } else {\n await this.failJob(job, error);\n span?.setStatus(SpanStatusCode.ERROR, error.message);\n }\n span?.setAttributes({ \"workglow.job.error\": spanErrorMessage });\n } finally {\n span?.end();\n try {\n if (!slotReleased) {\n await this.limiter.recordJobCompletion();\n }\n } finally {\n this.inFlight.delete(job.id);\n resolveInFlight();\n }\n }\n }\n\n /**\n * Execute a job with the provided abort signal\n */\n protected async executeJob(job: Job<Input, Output>, signal: AbortSignal): Promise<Output> {\n if (!job) throw new JobNotFoundError(\"Cannot execute null or undefined job\");\n return await job.execute(job.input, {\n signal,\n updateProgress: this.updateProgress.bind(this, job.id),\n });\n }\n\n /**\n * Update progress for a job.\n *\n * Mid-job progress is delivered in-memory via the `job_progress` event;\n * storage is only touched at terminal transitions (complete / fail / retry).\n * Cross-process observers therefore see state transitions but not fine-grained\n * progress — subscribe to an attached `JobQueueClient` for that.\n */\n protected async updateProgress(\n jobId: unknown,\n progress: number,\n message: string = \"\",\n details: Record<string, unknown> | null = null\n ): Promise<void> {\n progress = Math.max(0, Math.min(100, progress));\n this.events.emit(\"job_progress\", jobId, progress, message, details);\n }\n\n /**\n * Mark a job as completed\n */\n protected async completeJob(job: Job<Input, Output>, output?: Output): Promise<void> {\n try {\n job.status = JobStatus.COMPLETED;\n job.progress = 100;\n job.progressMessage = \"\";\n job.progressDetails = null;\n job.completedAt = new Date();\n job.output = output ?? null;\n job.error = null;\n job.errorCode = null;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_complete\", job.id, output as Output);\n } catch (err) {\n getLogger().error(\"completeJob errored:\", { error: err });\n } finally {\n this.cleanupJob(job.id);\n }\n }\n\n /**\n * Mark a job as failed\n */\n protected async failJob(job: Job<Input, Output>, error: JobError): Promise<void> {\n try {\n job.status = JobStatus.FAILED;\n job.progress = 100;\n job.completedAt = new Date();\n job.progressMessage = \"\";\n job.progressDetails = null;\n job.error = error.message;\n job.errorCode = error?.constructor?.name ?? null;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_error\", job.id, error.message, error.constructor.name);\n } catch (err) {\n getLogger().error(\"failJob errored:\", { error: err });\n } finally {\n this.cleanupJob(job.id);\n }\n }\n\n /**\n * Mark a job as disabled\n */\n protected async disableJob(job: Job<Input, Output>): Promise<void> {\n try {\n job.status = JobStatus.DISABLED;\n job.progress = 100;\n job.completedAt = new Date();\n job.progressMessage = \"\";\n job.progressDetails = null;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_disabled\", job.id);\n } catch (err) {\n getLogger().error(\"disableJob errored:\", { error: err });\n } finally {\n this.cleanupJob(job.id);\n }\n }\n\n /**\n * Release a job that {@link next} just claimed but that we won't process\n * because the worker was stopped mid-claim. Resets the row to PENDING so\n * the next started worker can pick it up. `fixupJobs()` would otherwise\n * skip it (it ignores rows owned by current-server worker IDs).\n *\n * Uses `storage.release()` rather than `storage.complete()` so the retry\n * budget isn't burned: the worker never actually attempted execution.\n */\n protected async releaseClaimedJob(job: Job<Input, Output>): Promise<void> {\n try {\n await this.storage.release(job.id);\n } catch (err) {\n getLogger().error(\"releaseClaimedJob errored:\", { error: err });\n }\n }\n\n /**\n * Reschedule a job for retry\n */\n protected async rescheduleJob(job: Job<Input, Output>, retryDate?: Date): Promise<void> {\n try {\n job.status = JobStatus.PENDING;\n const nextAvailableTime = await this.limiter.getNextAvailableTime();\n job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;\n job.progress = 0;\n job.progressMessage = \"\";\n job.progressDetails = null;\n // Increment runAttempts to keep in-memory object in sync with storage\n // The storage layer will read from DB and increment, so this keeps them aligned\n job.runAttempts = (job.runAttempts ?? 0) + 1;\n\n await this.storage.complete(this.classToStorage(job));\n this.events.emit(\"job_retry\", job.id, job.runAfter);\n } catch (err) {\n getLogger().error(\"rescheduleJob errored:\", { error: err });\n }\n }\n\n /**\n * Create an abort controller for a job.\n *\n * The job MUST already be registered in {@link inFlight} — this enforces\n * the invariant that `activeJobAbortControllers ⊆ inFlight`, which\n * {@link handleAbort} relies on to decide whether `processSingleJob` is\n * still on the hook for the terminal write. Calling this from any path\n * other than `processSingleJob` (which registers `inFlight` first) is a\n * programming error.\n */\n protected createAbortController(jobId: unknown): AbortController {\n if (!jobId) throw new JobNotFoundError(\"Cannot create abort controller for undefined job\");\n\n if (!this.inFlight.has(jobId)) {\n throw new Error(\n `createAbortController invariant violated: jobId ${String(jobId)} is not in inFlight. ` +\n `Abort controllers must only be created from within processSingleJob.`\n );\n }\n\n if (this.activeJobAbortControllers.has(jobId)) {\n return this.activeJobAbortControllers.get(jobId)!;\n }\n\n const abortController = new AbortController();\n abortController.signal.addEventListener(\"abort\", () => this.handleAbort(jobId));\n this.activeJobAbortControllers.set(jobId, abortController);\n return abortController;\n }\n\n /**\n * Handle job abort.\n *\n * Two callers fire the controller and reach this listener:\n * 1. `requestAbort` — same-process abort while the job is in flight here.\n * 2. `checkForAbortingJobs` — cross-process abort observed via storage poll.\n *\n * In both cases, if processSingleJob is still running this job locally,\n * the abort signal will propagate into the user task and processSingleJob's\n * own catch path will write the terminal state. We must not race it: doing\n * so duplicates the `job_error` emit and, worse, can clobber a successful\n * `completeJob` that won the race (the COMPLETED→FAILED overwrite bug).\n *\n * If the job is no longer in flight here, it has already settled — recheck\n * storage and only write FAILED for non-terminal states (i.e. an ABORTING\n * row left over from a cross-process abort that this worker never picked up).\n */\n protected async handleAbort(jobId: unknown): Promise<void> {\n if (this.inFlight.has(jobId)) {\n return;\n }\n const job = await this.getJob(jobId);\n if (!job) {\n getLogger().error(\"handleAbort: job not found\", { jobId });\n return;\n }\n if (\n job.status === JobStatus.COMPLETED ||\n job.status === JobStatus.FAILED ||\n job.status === JobStatus.DISABLED\n ) {\n return;\n }\n await this.failJob(job, new AbortSignalJobError(\"Job Aborted\"));\n }\n\n /**\n * Get a job by ID\n */\n protected async getJob(id: unknown): Promise<Job<Input, Output> | undefined> {\n const job = await this.storage.get(id);\n if (!job) return undefined;\n return this.storageToClass(job);\n }\n\n /**\n * Validate job state before processing\n */\n protected async validateJobState(job: Job<Input, Output>): Promise<void> {\n if (job.status === JobStatus.COMPLETED) {\n throw new PermanentJobError(`Job ${job.id} is already completed`);\n }\n if (job.status === JobStatus.FAILED) {\n throw new PermanentJobError(`Job ${job.id} has failed`);\n }\n if (\n job.status === JobStatus.ABORTING ||\n this.activeJobAbortControllers.get(job.id)?.signal.aborted\n ) {\n throw new AbortSignalJobError(`Job ${job.id} is being aborted`);\n }\n if (job.deadlineAt && job.deadlineAt < new Date()) {\n throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);\n }\n if (job.status === JobStatus.DISABLED) {\n throw new JobDisabledError(`Job ${job.id} has been disabled`);\n }\n }\n\n /**\n * Normalize errors into JobError instances\n */\n protected normalizeError(err: unknown): JobError {\n if (err instanceof JobError) {\n return err;\n }\n if (err instanceof Error) {\n return new PermanentJobError(withJobErrorDiagnostics(err.message, err));\n }\n return new PermanentJobError(String(err));\n }\n\n /**\n * Clean up job state after completion/failure\n */\n protected cleanupJob(jobId: unknown): void {\n this.activeJobAbortControllers.delete(jobId);\n }\n\n /**\n * Convert storage format to Job class\n */\n protected storageToClass(details: JobStorageFormat<Input, Output>): Job<Input, Output> {\n return storageToClass(details, this.jobClass);\n }\n\n /**\n * Convert Job class to storage format\n */\n protected classToStorage(job: Job<Input, Output>): JobStorageFormat<Input, Output> {\n return classToStorage(job, this.queueName);\n }\n}\n",
|
|
14
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { ILimiter, LimiterScope } from \"./ILimiter\";\n\nexport class CompositeLimiter implements ILimiter {\n private limiters: ILimiter[] = [];\n\n constructor(limiters: ILimiter[] = []) {\n this.limiters = limiters;\n }\n\n /**\n * `\"cluster\"` only when EVERY child is cluster-scoped. A single process-scoped\n * child means the composite as a whole can't enforce a cluster-wide limit.\n */\n public get scope(): LimiterScope {\n return this.limiters.every((l) => l.scope === \"cluster\") && this.limiters.length > 0\n ? \"cluster\"\n : \"process\";\n }\n\n addLimiter(limiter: ILimiter): void {\n this.limiters.push(limiter);\n }\n\n async canProceed(): Promise<boolean> {\n for (const limiter of this.limiters) {\n if (!(await limiter.canProceed())) {\n return false; // If any limiter says \"no\", proceed no further\n }\n }\n return true; // All limiters agree\n }\n\n /**\n * Atomic against the composite: acquires children sequentially and rolls\n * back any successfully-acquired prefix if a later child rejects, so the\n * \"all-or-nothing\" semantics hold under concurrency.\n *\n * The returned token is an array of per-child tokens (parallel to\n * `this.limiters`) so {@link release} can free each child's specific slot\n * — never \"the most recent\", which would race other acquirers.\n */\n async tryAcquire(): Promise<unknown | null> {\n const tokens: unknown[] = [];\n for (const limiter of this.limiters) {\n const t = await limiter.tryAcquire();\n if (t === null || t === undefined) {\n // Roll back the partial acquisition. Iterate in reverse so children\n // are released in opposite order of acquisition.\n for (let i = tokens.length - 1; i >= 0; i--) {\n try {\n await this.limiters[i].release(tokens[i]);\n } catch {\n // best-effort\n }\n }\n return null;\n }\n tokens.push(t);\n }\n return tokens;\n }\n\n async release(token: unknown): Promise<void> {\n if (!Array.isArray(token)) return;\n await Promise.all(this.limiters.map((l, i) => l.release(token[i]).catch(() => {})));\n }\n\n async recordJobStart(): Promise<void> {\n await Promise.all(this.limiters.map((limiter) => limiter.recordJobStart()));\n }\n\n async recordJobCompletion(): Promise<void> {\n await Promise.all(this.limiters.map((limiter) => limiter.recordJobCompletion()));\n }\n\n async getNextAvailableTime(): Promise<Date> {\n let maxDate = new Date(); // Assume now as the default\n for (const limiter of this.limiters) {\n const limiterNextTime = await limiter.getNextAvailableTime();\n if (limiterNextTime > maxDate) {\n maxDate = limiterNextTime; // Find the latest time among limiters\n }\n }\n return maxDate;\n }\n\n async setNextAvailableTime(date: Date): Promise<void> {\n for (const limiter of this.limiters) {\n await limiter.setNextAvailableTime(date);\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(this.limiters.map((limiter) => limiter.clear()));\n }\n}\n",
|
|
14
15
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken } from \"@workglow/util\";\nimport { ILimiter, LimiterScope } from \"./ILimiter\";\n\nexport const CONCURRENT_JOB_LIMITER = createServiceToken<ILimiter>(\"jobqueue.limiter.concurrent\");\n\n/**\n * Concurrency limiter that limits the number of concurrent jobs.\n */\nexport class ConcurrencyLimiter implements ILimiter {\n /** In-memory counter — not shared across processes. */\n public readonly scope: LimiterScope = \"process\";\n private currentRunningJobs: number = 0;\n private readonly maxConcurrentJobs: number;\n private nextAllowedStartTime: Date = new Date();\n\n constructor(maxConcurrentJobs: number) {\n this.maxConcurrentJobs = maxConcurrentJobs;\n }\n\n async canProceed(): Promise<boolean> {\n return (\n this.currentRunningJobs < this.maxConcurrentJobs &&\n Date.now() >= this.nextAllowedStartTime.getTime()\n );\n }\n\n /** Sentinel token; ConcurrencyLimiter has no per-row identity, just a counter. */\n private static readonly SENTINEL = Symbol(\"ConcurrencyLimiter.acquired\");\n\n /**\n * Atomic in JS's single-threaded sense: the read-then-increment runs without\n * an `await` between them, so no other task can interleave.\n */\n async tryAcquire(): Promise<unknown | null> {\n if (\n this.currentRunningJobs >= this.maxConcurrentJobs ||\n Date.now() < this.nextAllowedStartTime.getTime()\n ) {\n return null;\n }\n this.currentRunningJobs++;\n return ConcurrencyLimiter.SENTINEL;\n }\n\n async release(token: unknown): Promise<void> {\n if (token !== ConcurrencyLimiter.SENTINEL) return;\n this.currentRunningJobs = Math.max(0, this.currentRunningJobs - 1);\n }\n\n async recordJobStart(): Promise<void> {\n this.currentRunningJobs++;\n }\n\n async recordJobCompletion(): Promise<void> {\n this.currentRunningJobs = Math.max(0, this.currentRunningJobs - 1);\n }\n\n async getNextAvailableTime(): Promise<Date> {\n return this.currentRunningJobs > this.maxConcurrentJobs ? new Date() : new Date(Date.now() - 1);\n }\n\n async setNextAvailableTime(date: Date): Promise<void> {\n if (date > this.nextAllowedStartTime) {\n this.nextAllowedStartTime = date;\n }\n }\n\n async clear(): Promise<void> {\n this.currentRunningJobs = 0;\n this.nextAllowedStartTime = new Date();\n }\n}\n",
|
|
15
16
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { ILimiter, LimiterScope } from \"./ILimiter\";\n\nexport class DelayLimiter implements ILimiter {\n /** In-memory state — not shared across processes. */\n public readonly scope: LimiterScope = \"process\";\n private nextAvailableTime: Date = new Date();\n /** Time of the most recent successful tryAcquire, used to undo on release. */\n private lastAcquireBaseline: number = 0;\n constructor(private delayInMilliseconds: number = 50) {}\n\n async canProceed(): Promise<boolean> {\n return Date.now() >= this.nextAvailableTime.getTime();\n }\n\n /**\n * Token records the previous nextAvailableTime so release can roll back to\n * exactly the state before this acquire — even if other acquires (or\n * setNextAvailableTime calls) ran in between.\n */\n async tryAcquire(): Promise<unknown | null> {\n const now = Date.now();\n if (now < this.nextAvailableTime.getTime()) {\n return null;\n }\n const previous = this.nextAvailableTime.getTime();\n this.lastAcquireBaseline = previous;\n this.nextAvailableTime = new Date(now + this.delayInMilliseconds);\n return previous;\n }\n\n async release(token: unknown): Promise<void> {\n if (typeof token !== \"number\") return;\n // Only roll back if no later acquire/setNextAvailableTime moved the\n // window forward — otherwise we'd undo someone else's reservation.\n if (this.nextAvailableTime.getTime() === this.lastAcquireBaseline + this.delayInMilliseconds) {\n this.nextAvailableTime = new Date(token);\n }\n }\n\n async recordJobStart(): Promise<void> {\n this.nextAvailableTime = new Date(Date.now() + this.delayInMilliseconds);\n }\n\n async recordJobCompletion(): Promise<void> {\n // No action needed.\n }\n\n async getNextAvailableTime(): Promise<Date> {\n return this.nextAvailableTime;\n }\n\n async setNextAvailableTime(date: Date): Promise<void> {\n if (date > this.nextAvailableTime) {\n this.nextAvailableTime = date;\n }\n }\n async clear(): Promise<void> {\n this.nextAvailableTime = new Date();\n }\n}\n",
|
|
16
17
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken } from \"@workglow/util\";\nimport { ILimiter, LimiterScope, RateLimiterOptions } from \"./ILimiter\";\n\nexport const EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken<ILimiter>(\n \"jobqueue.limiter.rate.evenlyspaced\"\n);\n\n/**\n * Rate limiter that spreads requests evenly across a time window.\n * Instead of allowing all requests up to the limit and then waiting,\n * this limiter spaces out the requests evenly across the window.\n */\nexport class EvenlySpacedRateLimiter implements ILimiter {\n /** In-memory per-instance state — not shared across processes. */\n public readonly scope: LimiterScope = \"process\";\n private readonly maxExecutions: number;\n private readonly windowSizeMs: number;\n private readonly idealInterval: number;\n private nextAvailableTime: number = Date.now();\n private lastStartTime: number = 0;\n private durations: number[] = [];\n /** Promise chain used to serialize concurrent {@link tryAcquire} callers. */\n private acquireChain: Promise<unknown> = Promise.resolve();\n\n constructor({ maxExecutions, windowSizeInSeconds }: RateLimiterOptions) {\n if (maxExecutions <= 0) {\n throw new Error(\"maxExecutions must be > 0\");\n }\n if (windowSizeInSeconds <= 0) {\n throw new Error(\"windowSizeInSeconds must be > 0\");\n }\n this.maxExecutions = maxExecutions;\n this.windowSizeMs = windowSizeInSeconds * 1_000;\n // If you want exactly maxExecutions in windowSize, start one every this many ms:\n this.idealInterval = this.windowSizeMs / this.maxExecutions;\n }\n\n /** Can we start a new job right now? */\n async canProceed(): Promise<boolean> {\n const now = Date.now();\n return now >= this.nextAvailableTime;\n }\n\n /**\n * Atomic acquire: serialized by an internal promise chain so two concurrent\n * acquirers cannot both observe `now >= nextAvailableTime` and both proceed.\n * Returns a token capturing the prior `nextAvailableTime` so {@link release}\n * can roll back to the exact state before this acquire.\n */\n async tryAcquire(): Promise<unknown | null> {\n const previous = this.acquireChain;\n let release!: (v: unknown) => void;\n const next = new Promise((r) => {\n release = r;\n });\n this.acquireChain = next;\n try {\n await previous;\n const now = Date.now();\n if (now < this.nextAvailableTime) {\n return null;\n }\n const priorNextAvailable = this.nextAvailableTime;\n // Reserve the slot by advancing nextAvailableTime now (recordJobStart-style)\n // so a follow-up tryAcquire from another caller in the same tick blocks.\n this.lastStartTime = now;\n if (this.durations.length === 0) {\n this.nextAvailableTime = now + this.idealInterval;\n } else {\n const sum = this.durations.reduce((a, b) => a + b, 0);\n const avgDuration = sum / this.durations.length;\n const waitMs = Math.max(0, this.idealInterval - avgDuration);\n this.nextAvailableTime = now + waitMs;\n }\n return { priorNextAvailable, advancedTo: this.nextAvailableTime };\n } finally {\n release(undefined);\n }\n }\n\n /**\n * Roll back the slot identified by `token`. Only undoes the advance if no\n * later acquire moved the window forward — otherwise we'd undo someone\n * else's reservation.\n */\n async release(token: unknown): Promise<void> {\n if (\n !token ||\n typeof token !== \"object\" ||\n typeof (token as { advancedTo?: unknown }).advancedTo !== \"number\"\n ) {\n return;\n }\n const t = token as { priorNextAvailable: number; advancedTo: number };\n if (this.nextAvailableTime === t.advancedTo) {\n this.nextAvailableTime = t.priorNextAvailable;\n }\n }\n\n /** Record that a job is starting now. */\n async recordJobStart(): Promise<void> {\n const now = Date.now();\n this.lastStartTime = now;\n\n // If no timing data yet, assume zero run-time (ideal interval)\n if (this.durations.length === 0) {\n this.nextAvailableTime = now + this.idealInterval;\n } else {\n // Compute average run duration\n const sum = this.durations.reduce((a, b) => a + b, 0);\n const avgDuration = sum / this.durations.length;\n // Schedule next start: ideal spacing minus average duration\n const waitMs = Math.max(0, this.idealInterval - avgDuration);\n this.nextAvailableTime = now + waitMs;\n }\n }\n\n /**\n * Call this when a job finishes.\n * We measure its duration, update our running-average,\n * and then compute how long to wait before the next job start.\n */\n async recordJobCompletion(): Promise<void> {\n const now = Date.now();\n const duration = now - this.lastStartTime;\n this.durations.push(duration);\n if (this.durations.length > this.maxExecutions) {\n this.durations.shift();\n }\n }\n\n async getNextAvailableTime(): Promise<Date> {\n return new Date(this.nextAvailableTime);\n }\n\n async setNextAvailableTime(date: Date): Promise<void> {\n const t = date.getTime();\n if (t > this.nextAvailableTime) {\n this.nextAvailableTime = t;\n }\n }\n\n async clear(): Promise<void> {\n this.durations = [];\n this.nextAvailableTime = Date.now();\n this.lastStartTime = 0;\n }\n}\n",
|
|
17
18
|
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken } from \"@workglow/util\";\n\nexport const JOB_LIMITER = createServiceToken<ILimiter>(\"jobqueue.limiter\");\n\n/**\n * Whether a limiter's state is shared across processes.\n *\n * - `\"process\"` — state lives in this process only. Multiple workers in the\n * same process share it, but separate processes do not. The configured limit\n * is multiplied by the number of processes.\n * - `\"cluster\"` — state lives in shared storage (Postgres, Supabase, etc.)\n * visible to every process in the cluster. The configured limit is enforced\n * globally.\n */\nexport type LimiterScope = \"process\" | \"cluster\";\n\n/**\n * Interface for a job limiter.\n *\n * The atomic primitive is {@link tryAcquire}: it both checks whether a job may\n * proceed and reserves the slot in a single uninterruptible step. Callers\n * MUST use {@link tryAcquire}/{@link release} (not the legacy\n * {@link canProceed}/{@link recordJobStart} pair) when correctness matters\n * under concurrency.\n */\nexport interface ILimiter {\n /**\n * Whether this limiter's state is shared across processes. See\n * {@link LimiterScope}. In-memory limiters MUST report `\"process\"` so users\n * don't mistake them for cluster-safe.\n */\n readonly scope: LimiterScope;\n\n /**\n * Atomic check-and-record. Returns an opaque, non-null token on success\n * (the caller may proceed AND the reservation has been recorded). Returns\n * `null` without side effects if the limiter is at capacity.\n *\n * The returned token MUST be passed to {@link release} to free the slot —\n * otherwise rolling back the reservation under contention would race to\n * delete some other worker's slot. Tokens are implementation-defined and\n * callers must treat them as opaque.\n *\n * Implementations must be safe under concurrent callers — two parallel\n * `tryAcquire()` calls with one slot remaining must result in exactly one\n * non-null token and one `null`.\n */\n tryAcquire(): Promise<unknown | null>;\n\n /**\n * Release a slot previously reserved by {@link tryAcquire}. The token must\n * be the value that {@link tryAcquire} returned for the slot being freed.\n * Used when the caller cannot actually use the slot it acquired (e.g.\n * claimed a job that vanished, executor failed before running, worker\n * shut down).\n */\n release(token: unknown): Promise<void>;\n\n /**\n * Legacy non-binding \"would tryAcquire succeed?\" probe. SUBJECT TO RACES —\n * do not use this followed by {@link recordJobStart} in production code; use\n * {@link tryAcquire} instead. Retained for observability and tests.\n */\n canProceed(): Promise<boolean>;\n\n /**\n * Legacy \"force-record an execution\" hook. SUBJECT TO RACES when paired with\n * {@link canProceed} — use {@link tryAcquire} instead. Retained for tests\n * and external bookkeeping.\n */\n recordJobStart(): Promise<void>;\n\n recordJobCompletion(): Promise<void>;\n getNextAvailableTime(): Promise<Date>;\n setNextAvailableTime(date: Date): Promise<void>;\n clear(): Promise<void>;\n}\n\nexport interface RateLimiterOptions {\n readonly maxExecutions: number;\n readonly windowSizeInSeconds: number;\n}\n\nexport interface RateLimiterWithBackoffOptions extends RateLimiterOptions {\n readonly initialBackoffDelay?: number;\n readonly backoffMultiplier?: number;\n readonly maxBackoffDelay?: number;\n}\n",
|
|
18
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { IRateLimiterStorage } from \"@workglow/storage\";\nimport { ILimiter, LimiterScope, RateLimiterWithBackoffOptions } from \"./ILimiter\";\n\n/**\n * Base rate limiter implementation that uses a storage backend.\n * Manages request counts and delays to control job execution.\n */\nexport class RateLimiter implements ILimiter {\n protected readonly windowSizeInMilliseconds: number;\n protected currentBackoffDelay: number;\n protected readonly maxExecutions: number;\n protected readonly initialBackoffDelay: number;\n protected readonly backoffMultiplier: number;\n protected readonly maxBackoffDelay: number;\n /**\n * Per-instance backoff hint set by the most recent failed {@link tryAcquire}.\n * Exposed via {@link getNextAvailableTime} so this process's worker sleeps\n * until the hint expires, but NOT written to storage so a parallel worker's\n * {@link release} can't clobber it. Cross-process workers each maintain\n * their own hint.\n */\n protected localBackoffUntilMs: number = 0;\n\n constructor(\n protected readonly storage: IRateLimiterStorage,\n protected readonly queueName: string,\n {\n maxExecutions,\n windowSizeInSeconds,\n initialBackoffDelay = 1_000,\n backoffMultiplier = 2,\n maxBackoffDelay = 600_000, // 10 minutes\n }: RateLimiterWithBackoffOptions\n ) {\n if (maxExecutions <= 0) {\n throw new Error(\"maxExecutions must be greater than 0\");\n }\n if (windowSizeInSeconds <= 0) {\n throw new Error(\"windowSizeInSeconds must be greater than 0\");\n }\n if (initialBackoffDelay <= 0) {\n throw new Error(\"initialBackoffDelay must be greater than 0\");\n }\n if (backoffMultiplier <= 1) {\n throw new Error(\"backoffMultiplier must be greater than 1\");\n }\n if (maxBackoffDelay <= initialBackoffDelay) {\n throw new Error(\"maxBackoffDelay must be greater than initialBackoffDelay\");\n }\n\n this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;\n this.maxExecutions = maxExecutions;\n this.initialBackoffDelay = initialBackoffDelay;\n this.backoffMultiplier = backoffMultiplier;\n this.maxBackoffDelay = maxBackoffDelay;\n this.currentBackoffDelay = initialBackoffDelay;\n }\n\n /**\n * Inherits its scope from the underlying storage. Storage-backed RateLimiter\n * is `\"cluster\"` for Postgres/Supabase, `\"process\"` for InMemory/IndexedDb/Sqlite.\n */\n public get scope(): LimiterScope {\n return this.storage.scope;\n }\n\n /**\n * Atomic check-and-record. Delegates to {@link IRateLimiterStorage.tryReserveExecution}\n * which performs the count, next-available, and insert under a single\n * critical section per backend (advisory xact lock for Postgres, BEGIN\n * IMMEDIATE for SQLite, per-key promise mutex for in-memory).\n *\n * Returns the storage-assigned row id as an opaque token on success, or\n * `null` on failure. The caller MUST pass the token to {@link release} to\n * roll back — releasing without a token would race to delete the wrong\n * worker's slot under contention.\n */\n async tryAcquire(): Promise<unknown | null> {\n const token = await this.storage.tryReserveExecution(\n this.queueName,\n this.maxExecutions,\n this.windowSizeInMilliseconds\n );\n if (token !== null && token !== undefined) {\n this.currentBackoffDelay = this.initialBackoffDelay;\n this.localBackoffUntilMs = 0;\n return token;\n }\n\n // Capacity reached — apply jittered exponential backoff. We hold this\n // hint on the instance only; writing to the shared storage sentinel\n // would race with parallel acquire/release calls (a peer's release()\n // could clobber our hint, and clobbering the storage sentinel is\n // dangerous because tryReserveExecution treats it as a hard gate).\n this.localBackoffUntilMs = Date.now() + this.addJitter(this.currentBackoffDelay);\n this.increaseBackoff();\n return null;\n }\n\n /**\n * Release the slot identified by `token` (a value returned from\n * {@link tryAcquire}). Deletes the specific row by id — NOT the most recent\n * — so two concurrent acquirers don't race to release each other's slots.\n * Does NOT touch the shared storage `nextAvailableAt` sentinel; that field\n * is reserved for explicit external pauses (e.g. cluster-wide rate-limit\n * cool-down set via {@link setNextAvailableTime}), and clearing it here\n * would clobber a parallel worker's failed-acquire backoff or a deliberate\n * cluster-wide pause. Local per-instance backoff is cleared via\n * {@link localBackoffUntilMs}.\n */\n async release(token: unknown): Promise<void> {\n if (token === null || token === undefined) return;\n await this.storage.releaseExecution(this.queueName, token);\n this.currentBackoffDelay = this.initialBackoffDelay;\n this.localBackoffUntilMs = 0;\n }\n\n protected addJitter(base: number): number {\n // full jitter in [base, 2*base)\n return base + Math.random() * base;\n }\n\n protected increaseBackoff(): void {\n this.currentBackoffDelay = Math.min(\n this.currentBackoffDelay * this.backoffMultiplier,\n this.maxBackoffDelay\n );\n }\n\n /**\n * Checks if a job can proceed based on rate limiting rules.\n * @returns True if the job can proceed, false otherwise\n */\n async canProceed(): Promise<boolean> {\n // First check if the window allows more executions\n const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();\n const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);\n const canProceedNow = attemptCount < this.maxExecutions;\n\n // If the window allows more executions, clear any backoff and proceed\n if (canProceedNow) {\n // Clear any existing nextAvailableTime backoff since the window allows more executions\n const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {\n // Clear the backoff by setting it to the past\n const pastTime = new Date(Date.now() - 1000);\n await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());\n }\n this.currentBackoffDelay = this.initialBackoffDelay;\n return true;\n }\n\n // Window is full, check if there's a backoff delay\n const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {\n this.increaseBackoff();\n return false;\n }\n\n // Window is full but no backoff delay, so we can't proceed\n this.increaseBackoff();\n return false;\n }\n\n /**\n * Records a new job attempt.\n */\n async recordJobStart(): Promise<void> {\n await this.storage.recordExecution(this.queueName);\n\n const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();\n const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);\n\n if (attemptCount >= this.maxExecutions) {\n const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));\n await this.setNextAvailableTime(backoffExpires);\n } else {\n // Window allows more executions, clear any existing nextAvailableTime by setting it to the past\n const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {\n // Clear the backoff since the window now allows more executions\n // Set to a time in the past to effectively clear it\n const pastTime = new Date(Date.now() - 1000);\n await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());\n }\n }\n }\n\n async recordJobCompletion(): Promise<void> {\n // Implementation can be no-op as completion doesn't affect rate limiting\n }\n\n /**\n * Retrieves the next available time for the specific queue. Returns the\n * latest of: the rate-limit wall (oldest execution + window), any externally\n * set storage sentinel (explicit pause), and this instance's local backoff\n * hint from the most recent failed {@link tryAcquire}.\n */\n async getNextAvailableTime(): Promise<Date> {\n // Get the time when the rate limit will allow the next job execution\n const oldestExecution = await this.storage.getOldestExecutionAtOffset(\n this.queueName,\n this.maxExecutions - 1\n );\n\n let latestMs = Date.now();\n if (oldestExecution) {\n latestMs = Math.max(\n latestMs,\n new Date(oldestExecution).getTime() + this.windowSizeInMilliseconds\n );\n }\n\n // External pause set via setNextAvailableTime (cluster-visible).\n const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableStr) {\n latestMs = Math.max(latestMs, new Date(nextAvailableStr).getTime());\n }\n\n // Local backoff hint from the most recent failed tryAcquire on this\n // instance — keeps this process's worker from re-acquiring before the\n // hint expires without polluting cluster state.\n if (this.localBackoffUntilMs > latestMs) {\n latestMs = this.localBackoffUntilMs;\n }\n\n return new Date(latestMs);\n }\n\n /**\n * Sets the next available time for the specific queue.\n * @param date - The new next available time\n */\n async setNextAvailableTime(date: Date): Promise<void> {\n await this.storage.setNextAvailableTime(this.queueName, date.toISOString());\n }\n\n /**\n * Clears all rate limit entries for this queue.\n */\n async clear(): Promise<void> {\n await this.storage.clear(this.queueName);\n this.currentBackoffDelay = this.initialBackoffDelay;\n this.localBackoffUntilMs = 0;\n }\n}\n"
|
|
19
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { IRateLimiterStorage } from \"../rate-limiter-storage/IRateLimiterStorage\";\nimport { ILimiter, LimiterScope, RateLimiterWithBackoffOptions } from \"./ILimiter\";\n\n/**\n * Base rate limiter implementation that uses a storage backend.\n * Manages request counts and delays to control job execution.\n */\nexport class RateLimiter implements ILimiter {\n protected readonly windowSizeInMilliseconds: number;\n protected currentBackoffDelay: number;\n protected readonly maxExecutions: number;\n protected readonly initialBackoffDelay: number;\n protected readonly backoffMultiplier: number;\n protected readonly maxBackoffDelay: number;\n /**\n * Per-instance backoff hint set by the most recent failed {@link tryAcquire}.\n * Exposed via {@link getNextAvailableTime} so this process's worker sleeps\n * until the hint expires, but NOT written to storage so a parallel worker's\n * {@link release} can't clobber it. Cross-process workers each maintain\n * their own hint.\n */\n protected localBackoffUntilMs: number = 0;\n\n constructor(\n protected readonly storage: IRateLimiterStorage,\n protected readonly queueName: string,\n {\n maxExecutions,\n windowSizeInSeconds,\n initialBackoffDelay = 1_000,\n backoffMultiplier = 2,\n maxBackoffDelay = 600_000, // 10 minutes\n }: RateLimiterWithBackoffOptions\n ) {\n if (maxExecutions <= 0) {\n throw new Error(\"maxExecutions must be greater than 0\");\n }\n if (windowSizeInSeconds <= 0) {\n throw new Error(\"windowSizeInSeconds must be greater than 0\");\n }\n if (initialBackoffDelay <= 0) {\n throw new Error(\"initialBackoffDelay must be greater than 0\");\n }\n if (backoffMultiplier <= 1) {\n throw new Error(\"backoffMultiplier must be greater than 1\");\n }\n if (maxBackoffDelay <= initialBackoffDelay) {\n throw new Error(\"maxBackoffDelay must be greater than initialBackoffDelay\");\n }\n\n this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;\n this.maxExecutions = maxExecutions;\n this.initialBackoffDelay = initialBackoffDelay;\n this.backoffMultiplier = backoffMultiplier;\n this.maxBackoffDelay = maxBackoffDelay;\n this.currentBackoffDelay = initialBackoffDelay;\n }\n\n /**\n * Inherits its scope from the underlying storage. Storage-backed RateLimiter\n * is `\"cluster\"` for Postgres/Supabase, `\"process\"` for InMemory/IndexedDb/Sqlite.\n */\n public get scope(): LimiterScope {\n return this.storage.scope;\n }\n\n /**\n * Atomic check-and-record. Delegates to {@link IRateLimiterStorage.tryReserveExecution}\n * which performs the count, next-available, and insert under a single\n * critical section per backend (advisory xact lock for Postgres, BEGIN\n * IMMEDIATE for SQLite, per-key promise mutex for in-memory).\n *\n * Returns the storage-assigned row id as an opaque token on success, or\n * `null` on failure. The caller MUST pass the token to {@link release} to\n * roll back — releasing without a token would race to delete the wrong\n * worker's slot under contention.\n */\n async tryAcquire(): Promise<unknown | null> {\n const token = await this.storage.tryReserveExecution(\n this.queueName,\n this.maxExecutions,\n this.windowSizeInMilliseconds\n );\n if (token !== null && token !== undefined) {\n this.currentBackoffDelay = this.initialBackoffDelay;\n this.localBackoffUntilMs = 0;\n return token;\n }\n\n // Capacity reached — apply jittered exponential backoff. We hold this\n // hint on the instance only; writing to the shared storage sentinel\n // would race with parallel acquire/release calls (a peer's release()\n // could clobber our hint, and clobbering the storage sentinel is\n // dangerous because tryReserveExecution treats it as a hard gate).\n this.localBackoffUntilMs = Date.now() + this.addJitter(this.currentBackoffDelay);\n this.increaseBackoff();\n return null;\n }\n\n /**\n * Release the slot identified by `token` (a value returned from\n * {@link tryAcquire}). Deletes the specific row by id — NOT the most recent\n * — so two concurrent acquirers don't race to release each other's slots.\n * Does NOT touch the shared storage `nextAvailableAt` sentinel; that field\n * is reserved for explicit external pauses (e.g. cluster-wide rate-limit\n * cool-down set via {@link setNextAvailableTime}), and clearing it here\n * would clobber a parallel worker's failed-acquire backoff or a deliberate\n * cluster-wide pause. Local per-instance backoff is cleared via\n * {@link localBackoffUntilMs}.\n */\n async release(token: unknown): Promise<void> {\n if (token === null || token === undefined) return;\n await this.storage.releaseExecution(this.queueName, token);\n this.currentBackoffDelay = this.initialBackoffDelay;\n this.localBackoffUntilMs = 0;\n }\n\n protected addJitter(base: number): number {\n // full jitter in [base, 2*base)\n return base + Math.random() * base;\n }\n\n protected increaseBackoff(): void {\n this.currentBackoffDelay = Math.min(\n this.currentBackoffDelay * this.backoffMultiplier,\n this.maxBackoffDelay\n );\n }\n\n /**\n * Checks if a job can proceed based on rate limiting rules.\n * @returns True if the job can proceed, false otherwise\n */\n async canProceed(): Promise<boolean> {\n // First check if the window allows more executions\n const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();\n const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);\n const canProceedNow = attemptCount < this.maxExecutions;\n\n // If the window allows more executions, clear any backoff and proceed\n if (canProceedNow) {\n // Clear any existing nextAvailableTime backoff since the window allows more executions\n const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {\n // Clear the backoff by setting it to the past\n const pastTime = new Date(Date.now() - 1000);\n await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());\n }\n this.currentBackoffDelay = this.initialBackoffDelay;\n return true;\n }\n\n // Window is full, check if there's a backoff delay\n const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {\n this.increaseBackoff();\n return false;\n }\n\n // Window is full but no backoff delay, so we can't proceed\n this.increaseBackoff();\n return false;\n }\n\n /**\n * Records a new job attempt.\n */\n async recordJobStart(): Promise<void> {\n await this.storage.recordExecution(this.queueName);\n\n const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();\n const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);\n\n if (attemptCount >= this.maxExecutions) {\n const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));\n await this.setNextAvailableTime(backoffExpires);\n } else {\n // Window allows more executions, clear any existing nextAvailableTime by setting it to the past\n const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {\n // Clear the backoff since the window now allows more executions\n // Set to a time in the past to effectively clear it\n const pastTime = new Date(Date.now() - 1000);\n await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());\n }\n }\n }\n\n async recordJobCompletion(): Promise<void> {\n // Implementation can be no-op as completion doesn't affect rate limiting\n }\n\n /**\n * Retrieves the next available time for the specific queue. Returns the\n * latest of: the rate-limit wall (oldest execution + window), any externally\n * set storage sentinel (explicit pause), and this instance's local backoff\n * hint from the most recent failed {@link tryAcquire}.\n */\n async getNextAvailableTime(): Promise<Date> {\n // Get the time when the rate limit will allow the next job execution\n const oldestExecution = await this.storage.getOldestExecutionAtOffset(\n this.queueName,\n this.maxExecutions - 1\n );\n\n let latestMs = Date.now();\n if (oldestExecution) {\n latestMs = Math.max(\n latestMs,\n new Date(oldestExecution).getTime() + this.windowSizeInMilliseconds\n );\n }\n\n // External pause set via setNextAvailableTime (cluster-visible).\n const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);\n if (nextAvailableStr) {\n latestMs = Math.max(latestMs, new Date(nextAvailableStr).getTime());\n }\n\n // Local backoff hint from the most recent failed tryAcquire on this\n // instance — keeps this process's worker from re-acquiring before the\n // hint expires without polluting cluster state.\n if (this.localBackoffUntilMs > latestMs) {\n latestMs = this.localBackoffUntilMs;\n }\n\n return new Date(latestMs);\n }\n\n /**\n * Sets the next available time for the specific queue.\n * @param date - The new next available time\n */\n async setNextAvailableTime(date: Date): Promise<void> {\n await this.storage.setNextAvailableTime(this.queueName, date.toISOString());\n }\n\n /**\n * Clears all rate limit entries for this queue.\n */\n async clear(): Promise<void> {\n await this.storage.clear(this.queueName);\n this.currentBackoffDelay = this.initialBackoffDelay;\n this.localBackoffUntilMs = 0;\n }\n}\n",
|
|
20
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n createServiceToken,\n EventEmitter,\n getLogger,\n makeFingerprint,\n sleep,\n uuid4,\n} from \"@workglow/util\";\nimport {\n IQueueStorage,\n JobStatus,\n JobStorageFormat,\n QueueChangePayload,\n QueueStorageOptions,\n QueueSubscribeOptions,\n} from \"./IQueueStorage\";\n\n/**\n * Event listeners for queue storage events\n */\ntype QueueEventListeners<Input, Output> = {\n change: (payload: QueueChangePayload<Input, Output>) => void;\n};\n\nexport const IN_MEMORY_QUEUE_STORAGE = createServiceToken<IQueueStorage<any, any>>(\n \"jobqueue.storage.inMemory\"\n);\n\n/**\n * In-memory implementation of a job queue that manages asynchronous tasks.\n * Supports job scheduling, status tracking, result caching, and prefix-based filtering.\n */\nexport class InMemoryQueueStorage<Input, Output> implements IQueueStorage<Input, Output> {\n public readonly scope = \"process\" as const;\n /** The prefix values for filtering jobs */\n protected readonly prefixValues: Readonly<Record<string, string | number>>;\n /** Event emitter for change notifications */\n protected readonly events = new EventEmitter<QueueEventListeners<Input, Output>>();\n\n /**\n * Creates a new in-memory job queue\n * @param queueName - Name of the queue\n * @param options - Optional configuration including prefix filters\n */\n constructor(\n public readonly queueName: string,\n options?: QueueStorageOptions\n ) {\n this.jobQueue = [];\n this.prefixValues = options?.prefixValues ?? {};\n }\n\n /** Internal array storing all jobs */\n public jobQueue: Array<JobStorageFormat<Input, Output> & Record<string, unknown>>;\n\n /**\n * Checks if a job matches the current prefix values\n */\n private matchesPrefixes(job: JobStorageFormat<Input, Output> & Record<string, unknown>): boolean {\n for (const [key, value] of Object.entries(this.prefixValues)) {\n if (job[key] !== value) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Returns a filtered and sorted list of pending jobs that are ready to run\n * Sorts by creation time to maintain FIFO order\n */\n private pendingQueue(): Array<JobStorageFormat<Input, Output> & Record<string, unknown>> {\n const now = new Date().toISOString();\n return this.jobQueue\n .filter((job) => this.matchesPrefixes(job))\n .filter((job) => job.status === JobStatus.PENDING)\n .filter((job) => !job.run_after || job.run_after <= now)\n .sort((a, b) => (a.run_after || \"\").localeCompare(b.run_after || \"\"));\n }\n\n /**\n * Adds a new job to the queue\n * Generates an ID and fingerprint if not provided\n */\n public async add(job: JobStorageFormat<Input, Output>): Promise<unknown> {\n await sleep(0);\n const now = new Date().toISOString();\n const jobWithPrefixes = job as JobStorageFormat<Input, Output> & Record<string, unknown>;\n jobWithPrefixes.id = jobWithPrefixes.id ?? uuid4();\n jobWithPrefixes.job_run_id = jobWithPrefixes.job_run_id ?? uuid4();\n jobWithPrefixes.queue = this.queueName;\n jobWithPrefixes.fingerprint = await makeFingerprint(jobWithPrefixes.input);\n jobWithPrefixes.status = JobStatus.PENDING;\n jobWithPrefixes.progress = 0;\n jobWithPrefixes.progress_message = \"\";\n jobWithPrefixes.progress_details = null;\n jobWithPrefixes.created_at = now;\n jobWithPrefixes.run_after = now;\n\n // Add prefix values to the job\n for (const [key, value] of Object.entries(this.prefixValues)) {\n jobWithPrefixes[key] = value;\n }\n\n this.jobQueue.push(jobWithPrefixes);\n this.events.emit(\"change\", { type: \"INSERT\", new: jobWithPrefixes });\n return jobWithPrefixes.id;\n }\n\n /**\n * Retrieves a job from the queue by its id.\n * @param id - The id of the job to retrieve.\n * @returns A promise that resolves to the job or undefined if the job is not found.\n */\n public async get(id: unknown): Promise<JobStorageFormat<Input, Output> | undefined> {\n await sleep(0);\n const job = this.jobQueue.find((j) => j.id === id);\n if (job && this.matchesPrefixes(job)) {\n return job;\n }\n return undefined;\n }\n\n /**\n * Retrieves a slice of jobs from the queue.\n * @param status - The status of the jobs to retrieve.\n * @param num - The number of jobs to retrieve.\n * @returns A promise that resolves to an array of jobs.\n */\n public async peek(\n status: JobStatus = JobStatus.PENDING,\n num: number = 100\n ): Promise<Array<JobStorageFormat<Input, Output>>> {\n await sleep(0);\n num = Number(num) || 100;\n return this.jobQueue\n .filter((j) => this.matchesPrefixes(j))\n .sort((a, b) => (a.run_after || \"\").localeCompare(b.run_after || \"\"))\n .filter((j) => j.status === status)\n .slice(0, num);\n }\n\n /**\n * Retrieves the next available job that is ready to be processed\n * Updates the job status to PROCESSING before returning\n * @param workerId - Worker ID to associate with the job\n * @returns The next job or undefined if no job is available\n */\n public async next(workerId: string): Promise<JobStorageFormat<Input, Output> | undefined> {\n await sleep(0);\n const top = this.pendingQueue();\n\n const job = top[0];\n if (job) {\n const oldJob = { ...job };\n job.status = JobStatus.PROCESSING;\n job.last_ran_at = new Date().toISOString();\n job.worker_id = workerId;\n this.events.emit(\"change\", { type: \"UPDATE\", old: oldJob, new: job });\n return job;\n }\n }\n\n /**\n * Retrieves the size of the queue for a given status\n * @param status - The status of the jobs to retrieve.\n * @returns A promise that resolves to the number of jobs.\n */\n public async size(status = JobStatus.PENDING): Promise<number> {\n await sleep(0);\n return this.jobQueue.filter((j) => this.matchesPrefixes(j) && j.status === status).length;\n }\n\n /**\n * Saves the progress of a job\n * @param jobId - The id of the job to save the progress for.\n * @param progress - The progress of the job.\n * @param message - The message of the job.\n * @param details - The details of the job.\n */\n public async saveProgress(\n id: unknown,\n progress: number,\n message: string,\n details: Record<string, any> | null\n ): Promise<void> {\n await sleep(0);\n const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));\n if (!job) {\n // Job not found - this can happen if the job was already completed/removed\n // or if there's a race condition. Silently ignore progress updates for missing jobs.\n const jobWithAnyPrefix = this.jobQueue.find((j) => j.id === id);\n getLogger().warn(\"Job not found for progress update\", {\n id,\n reason: jobWithAnyPrefix ? \"prefix_mismatch\" : \"missing\",\n existingStatus: jobWithAnyPrefix?.status,\n queueName: this.queueName,\n prefixValues: this.prefixValues,\n });\n return;\n }\n\n // Skip progress updates for jobs that are already completed or failed\n // to avoid unnecessary updates and potential race conditions\n if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {\n getLogger().warn(\"Job already completed or failed for progress update\", {\n id,\n status: job.status,\n completedAt: job.completed_at,\n error: job.error,\n });\n return;\n }\n\n const oldJob = { ...job };\n job.progress = progress;\n job.progress_message = message;\n job.progress_details = details;\n this.events.emit(\"change\", { type: \"UPDATE\", old: oldJob, new: job });\n }\n\n /**\n * Marks a job as complete with its output or error\n * Handles run_attempts for failed jobs and triggers completion callbacks\n * @param id - ID of the job to complete\n * @param output - Result of the job execution\n * @param error - Optional error message if job failed\n */\n public async complete(job: JobStorageFormat<Input, Output>) {\n await sleep(0);\n const jobWithPrefixes = job as JobStorageFormat<Input, Output> & Record<string, unknown>;\n const index = this.jobQueue.findIndex((j) => j.id === job.id && this.matchesPrefixes(j));\n if (index !== -1) {\n const existing = this.jobQueue[index];\n const currentAttempts = existing?.run_attempts ?? 0;\n jobWithPrefixes.run_attempts = currentAttempts + 1;\n // Preserve prefix values from the existing job\n for (const [key, value] of Object.entries(this.prefixValues)) {\n jobWithPrefixes[key] = value;\n }\n this.jobQueue[index] = jobWithPrefixes;\n this.events.emit(\"change\", { type: \"UPDATE\", old: existing, new: jobWithPrefixes });\n }\n }\n\n /**\n * Releases a claimed job back to PENDING without incrementing run_attempts.\n * @param id - The id of the claimed job to release.\n */\n public async release(id: unknown): Promise<void> {\n await sleep(0);\n const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));\n if (job) {\n const oldJob = { ...job };\n job.status = JobStatus.PENDING;\n job.worker_id = null;\n job.progress = 0;\n job.progress_message = \"\";\n job.progress_details = null;\n this.events.emit(\"change\", { type: \"UPDATE\", old: oldJob, new: job });\n }\n }\n\n /**\n * Aborts a job\n * @param id - The id of the job to abort.\n */\n public async abort(id: unknown): Promise<void> {\n await sleep(0);\n const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));\n if (job) {\n const oldJob = { ...job };\n job.status = JobStatus.ABORTING;\n this.events.emit(\"change\", { type: \"UPDATE\", old: oldJob, new: job });\n }\n }\n\n /**\n * Retrieves all jobs by their job_run_id.\n * @param job_run_id - The job_run_id of the jobs to retrieve.\n * @returns A promise that resolves to an array of jobs.\n */\n public async getByRunId(runId: string): Promise<Array<JobStorageFormat<Input, Output>>> {\n await sleep(0);\n return this.jobQueue.filter((job) => this.matchesPrefixes(job) && job.job_run_id === runId);\n }\n\n /**\n * Deletes all jobs from the queue that match the current prefix values.\n */\n public async deleteAll(): Promise<void> {\n await sleep(0);\n const deletedJobs = this.jobQueue.filter((job) => this.matchesPrefixes(job));\n this.jobQueue = this.jobQueue.filter((job) => !this.matchesPrefixes(job));\n for (const job of deletedJobs) {\n this.events.emit(\"change\", { type: \"DELETE\", old: job });\n }\n }\n\n /**\n * Looks up cached output for a given input\n * Uses input fingerprinting for efficient matching\n * @param input - The input to look up the cached output for.\n * @returns The cached output or null if not found\n */\n public async outputForInput(input: Input): Promise<Output | null> {\n await sleep(0);\n const fingerprint = await makeFingerprint(input);\n return (\n this.jobQueue.find(\n (j) =>\n this.matchesPrefixes(j) &&\n j.fingerprint === fingerprint &&\n j.status === JobStatus.COMPLETED\n )?.output ?? null\n );\n }\n\n /**\n * Deletes a job by its ID\n */\n public async delete(id: unknown): Promise<void> {\n await sleep(0);\n const deletedJob = this.jobQueue.find((job) => job.id === id && this.matchesPrefixes(job));\n this.jobQueue = this.jobQueue.filter((job) => !(job.id === id && this.matchesPrefixes(job)));\n if (deletedJob) {\n this.events.emit(\"change\", { type: \"DELETE\", old: deletedJob });\n }\n }\n\n /**\n * Delete jobs with a specific status older than a cutoff date\n * @param status - Status of jobs to delete\n * @param olderThanMs - Delete jobs completed more than this many milliseconds ago\n */\n public async deleteJobsByStatusAndAge(status: JobStatus, olderThanMs: number): Promise<void> {\n await sleep(0);\n const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();\n const deletedJobs = this.jobQueue.filter(\n (job) =>\n this.matchesPrefixes(job) &&\n job.status === status &&\n job.completed_at &&\n job.completed_at <= cutoffDate\n );\n this.jobQueue = this.jobQueue.filter(\n (job) =>\n !this.matchesPrefixes(job) ||\n job.status !== status ||\n !job.completed_at ||\n job.completed_at > cutoffDate\n );\n for (const job of deletedJobs) {\n this.events.emit(\"change\", { type: \"DELETE\", old: job });\n }\n }\n\n /**\n * Sets up the database schema and tables.\n * No-op for in-memory storage as it doesn't require database setup.\n */\n public async setupDatabase(): Promise<void> {\n // No-op for in-memory storage\n }\n\n /**\n * Checks if a job matches the specified prefix filter\n * @param job - The job to check\n * @param prefixFilter - The prefix filter (undefined = use instance prefixes, {} = no filter)\n */\n private matchesPrefixFilter(\n job: JobStorageFormat<Input, Output>,\n prefixFilter?: Readonly<Record<string, string | number>>\n ): boolean {\n // If prefixFilter is explicitly an empty object, no prefix filtering\n if (prefixFilter && Object.keys(prefixFilter).length === 0) {\n return true;\n }\n\n // Use provided prefixFilter or fall back to instance's prefixValues\n const filterValues = prefixFilter ?? this.prefixValues;\n\n // If no filter values, match all\n if (Object.keys(filterValues).length === 0) {\n return true;\n }\n\n // Check each filter value\n const jobWithPrefixes = job as JobStorageFormat<Input, Output> & Record<string, unknown>;\n for (const [key, value] of Object.entries(filterValues)) {\n if (jobWithPrefixes[key] !== value) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Subscribes to changes in the queue.\n * Since InMemory is both client and server, changes are detected via local events.\n *\n * @param callback - Function called when a change occurs\n * @param options - Subscription options including prefix filter\n * @returns Unsubscribe function\n */\n public subscribeToChanges(\n callback: (change: QueueChangePayload<Input, Output>) => void,\n options?: QueueSubscribeOptions\n ): () => void {\n const prefixFilter = options?.prefixFilter;\n\n // Create a filtered callback wrapper\n const filteredCallback = (change: QueueChangePayload<Input, Output>) => {\n // Check if either old or new job matches the filter\n const newMatches = change.new ? this.matchesPrefixFilter(change.new, prefixFilter) : false;\n const oldMatches = change.old ? this.matchesPrefixFilter(change.old, prefixFilter) : false;\n\n if (!newMatches && !oldMatches) {\n return;\n }\n\n callback(change);\n };\n\n return this.events.subscribe(\"change\", filteredCallback);\n }\n}\n",
|
|
21
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { traced } from \"@workglow/util\";\nimport type {\n IQueueStorage,\n JobStatus,\n JobStorageFormat,\n QueueChangePayload,\n QueueStorageScope,\n QueueSubscribeOptions,\n} from \"./IQueueStorage\";\n\n/**\n * Telemetry wrapper for any IQueueStorage implementation.\n * Creates spans for all queue storage operations.\n */\nexport class TelemetryQueueStorage<Input, Output> implements IQueueStorage<Input, Output> {\n constructor(\n private readonly storageName: string,\n private readonly inner: IQueueStorage<Input, Output>\n ) {}\n\n public get scope(): QueueStorageScope {\n return this.inner.scope;\n }\n\n add(job: JobStorageFormat<Input, Output>): Promise<unknown> {\n return traced(\"workglow.storage.queue.add\", this.storageName, () => this.inner.add(job));\n }\n get(id: unknown): Promise<JobStorageFormat<Input, Output> | undefined> {\n return traced(\"workglow.storage.queue.get\", this.storageName, () => this.inner.get(id));\n }\n next(workerId: string): Promise<JobStorageFormat<Input, Output> | undefined> {\n return traced(\"workglow.storage.queue.next\", this.storageName, () => this.inner.next(workerId));\n }\n peek(status?: JobStatus, num?: number): Promise<Array<JobStorageFormat<Input, Output>>> {\n return traced(\"workglow.storage.queue.peek\", this.storageName, () =>\n this.inner.peek(status, num)\n );\n }\n size(status?: JobStatus): Promise<number> {\n return traced(\"workglow.storage.queue.size\", this.storageName, () => this.inner.size(status));\n }\n complete(job: JobStorageFormat<Input, Output>): Promise<void> {\n return traced(\"workglow.storage.queue.complete\", this.storageName, () =>\n this.inner.complete(job)\n );\n }\n release(id: unknown): Promise<void> {\n return traced(\"workglow.storage.queue.release\", this.storageName, () => this.inner.release(id));\n }\n deleteAll(): Promise<void> {\n return traced(\"workglow.storage.queue.deleteAll\", this.storageName, () =>\n this.inner.deleteAll()\n );\n }\n outputForInput(input: Input): Promise<Output | null> {\n return traced(\"workglow.storage.queue.outputForInput\", this.storageName, () =>\n this.inner.outputForInput(input)\n );\n }\n abort(id: unknown): Promise<void> {\n return traced(\"workglow.storage.queue.abort\", this.storageName, () => this.inner.abort(id));\n }\n getByRunId(runId: string): Promise<Array<JobStorageFormat<Input, Output>>> {\n return traced(\"workglow.storage.queue.getByRunId\", this.storageName, () =>\n this.inner.getByRunId(runId)\n );\n }\n saveProgress(\n id: unknown,\n progress: number,\n message: string,\n details: Record<string, any> | null\n ): Promise<void> {\n return traced(\"workglow.storage.queue.saveProgress\", this.storageName, () =>\n this.inner.saveProgress(id, progress, message, details)\n );\n }\n delete(id: unknown): Promise<void> {\n return traced(\"workglow.storage.queue.delete\", this.storageName, () => this.inner.delete(id));\n }\n deleteJobsByStatusAndAge(status: JobStatus, olderThanMs: number): Promise<void> {\n return traced(\"workglow.storage.queue.deleteJobsByStatusAndAge\", this.storageName, () =>\n this.inner.deleteJobsByStatusAndAge(status, olderThanMs)\n );\n }\n setupDatabase(): Promise<void> {\n return this.inner.setupDatabase();\n }\n subscribeToChanges(\n callback: (change: QueueChangePayload<Input, Output>) => void,\n options?: QueueSubscribeOptions\n ): () => void {\n return this.inner.subscribeToChanges(callback, options);\n }\n}\n",
|
|
22
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken } from \"@workglow/util\";\nimport type { PrefixColumn } from \"../queue-storage/IQueueStorage\";\n\nexport const RATE_LIMITER_STORAGE = createServiceToken<IRateLimiterStorage>(\"ratelimiter.storage\");\n\n/**\n * Whether a rate-limiter storage's state is shared across processes.\n *\n * - `\"process\"` — in-memory / per-process state. Multiple workers in the same\n * process share it, but separate processes do not.\n * - `\"cluster\"` — state lives in shared external storage (Postgres, Supabase,\n * etc.) visible to every process.\n */\nexport type RateLimiterStorageScope = \"process\" | \"cluster\";\n\n/**\n * Options for configuring rate limiter storage with prefix filters.\n */\nexport interface RateLimiterStorageOptions {\n /** The prefix column definitions for this storage */\n readonly prefixes?: readonly PrefixColumn[];\n /** The values for each prefix column */\n readonly prefixValues?: Readonly<Record<string, string | number>>;\n}\n\n/**\n * Record of a job execution for rate limiting tracking.\n */\nexport interface ExecutionRecord {\n readonly id?: unknown;\n readonly queue_name: string;\n readonly executed_at: string;\n}\n\n/**\n * Record of the next available time for a queue.\n */\nexport interface NextAvailableRecord {\n readonly queue_name: string;\n readonly next_available_at: string;\n}\n\n/**\n * Interface defining the storage operations for a rate limiter.\n * This separates the storage concerns from the rate limiting logic.\n */\nexport interface IRateLimiterStorage {\n /**\n * Whether this storage is shared across processes. In-memory backends MUST\n * report `\"process\"`. Shared databases (Postgres, Supabase) report\n * `\"cluster\"`.\n */\n readonly scope: RateLimiterStorageScope;\n\n /**\n * Sets up the database schema and tables.\n * This method should be called before using the storage.\n * For production use, database setup should be done via migrations.\n */\n setupDatabase(): Promise<void>;\n\n /**\n * Atomic check-and-record. Inserts an execution row and returns the\n * inserted row's id iff BOTH (a) fewer than `maxExecutions` rows have\n * `executed_at > (now - windowMs)` AND (b) any persisted `nextAvailableAt`\n * is in the past or absent. Returns `null` without writing anything\n * otherwise.\n *\n * The returned id MUST be passed to {@link releaseExecution} to free the\n * slot — otherwise concurrent acquirers would race to delete the wrong\n * worker's row when one of them rolls back.\n *\n * Implementations MUST serialize concurrent callers (advisory locks,\n * `BEGIN IMMEDIATE`, per-key mutex, etc.) so the count-then-insert window\n * is uninterruptible.\n *\n * @param queueName - The name of the queue\n * @param maxExecutions - Max allowed executions in the window\n * @param windowMs - Window size in milliseconds\n * @returns the inserted row's id on success, or `null` on failure\n */\n tryReserveExecution(\n queueName: string,\n maxExecutions: number,\n windowMs: number\n ): Promise<unknown | null>;\n\n /**\n * Release the execution row identified by `token` (the value previously\n * returned from {@link tryReserveExecution}). No-op if the row no longer\n * exists.\n *\n * Critical: implementations MUST delete by id, NOT by recency or position.\n * Two concurrent workers can hold tokens for different rows; deleting the\n * \"most recent\" row would release another worker's reservation.\n */\n releaseExecution(queueName: string, token: unknown): Promise<void>;\n\n /**\n * Records a job execution for rate limiting tracking.\n * @param queueName - The name of the queue\n */\n recordExecution(queueName: string): Promise<void>;\n\n /**\n * Gets the count of executions within a time window.\n * @param queueName - The name of the queue\n * @param windowStartTime - The start of the time window (ISO string)\n * @returns The count of executions within the window\n */\n getExecutionCount(queueName: string, windowStartTime: string): Promise<number>;\n\n /**\n * Gets the oldest execution time within the window, offset by a count.\n * Used to calculate when the rate limit will allow the next execution.\n * @param queueName - The name of the queue\n * @param offset - The offset (typically maxExecutions - 1)\n * @returns The execution time or undefined if not enough executions\n */\n getOldestExecutionAtOffset(queueName: string, offset: number): Promise<string | undefined>;\n\n /**\n * Gets the next available time for a queue.\n * @param queueName - The name of the queue\n * @returns The next available time or undefined if not set\n */\n getNextAvailableTime(queueName: string): Promise<string | undefined>;\n\n /**\n * Sets the next available time for a queue.\n * @param queueName - The name of the queue\n * @param nextAvailableAt - The next available time (ISO string)\n */\n setNextAvailableTime(queueName: string, nextAvailableAt: string): Promise<void>;\n\n /**\n * Clears all rate limit entries for a queue.\n * @param queueName - The name of the queue\n */\n clear(queueName: string): Promise<void>;\n}\n",
|
|
23
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { createServiceToken, sleep, uuid4 } from \"@workglow/util\";\nimport {\n IRateLimiterStorage,\n RateLimiterStorageOptions,\n RateLimiterStorageScope,\n} from \"./IRateLimiterStorage\";\n\nexport const IN_MEMORY_RATE_LIMITER_STORAGE = createServiceToken<IRateLimiterStorage>(\n \"ratelimiter.storage.inMemory\"\n);\n\n/**\n * Record of a job execution stored in memory.\n */\ninterface ExecutionEntry {\n /** Per-row id; serves as the opaque token returned by tryReserveExecution. */\n readonly id: string;\n readonly queueName: string;\n readonly executedAt: Date;\n}\n\n/**\n * In-memory implementation of rate limiter storage.\n * Manages execution records and next available times for rate limiting.\n */\nexport class InMemoryRateLimiterStorage implements IRateLimiterStorage {\n public readonly scope: RateLimiterStorageScope = \"process\";\n\n /** The prefix values for filtering */\n protected readonly prefixValues: Readonly<Record<string, string | number>>;\n\n /** Execution records keyed by a composite of prefix values and queue name */\n private readonly executions: Map<string, ExecutionEntry[]> = new Map();\n\n /** Next available times keyed by a composite of prefix values and queue name */\n private readonly nextAvailableTimes: Map<string, Date> = new Map();\n\n /**\n * Per-key promise chain used to serialize {@link tryReserveExecution} so\n * concurrent callers cannot both observe `count < max` before either\n * inserts. Each key's chain is replaced with the next pending operation\n * before the current one returns, giving FIFO mutex semantics.\n */\n private readonly reserveChains: Map<string, Promise<unknown>> = new Map();\n\n constructor(options?: RateLimiterStorageOptions) {\n this.prefixValues = options?.prefixValues ?? {};\n }\n\n /**\n * Creates a storage key from the queue name and prefix values.\n */\n private makeKey(queueName: string): string {\n const prefixPart = Object.entries(this.prefixValues)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join(\"|\");\n return prefixPart ? `${prefixPart}|${queueName}` : queueName;\n }\n\n public async setupDatabase(): Promise<void> {\n // No-op for in-memory storage\n }\n\n /**\n * Run `fn` under a per-key mutex. Without this, two concurrent\n * `tryReserveExecution` calls would both `await sleep(0)`, both observe the\n * same execution count, and both insert — overshooting the configured limit.\n */\n private async withKeyLock<T>(key: string, fn: () => T | Promise<T>): Promise<T> {\n const previous = this.reserveChains.get(key) ?? Promise.resolve();\n let release!: (value: unknown) => void;\n const next = new Promise((resolve) => {\n release = resolve;\n });\n this.reserveChains.set(key, next);\n try {\n await previous;\n return await fn();\n } finally {\n release(undefined);\n if (this.reserveChains.get(key) === next) {\n this.reserveChains.delete(key);\n }\n }\n }\n\n public async tryReserveExecution(\n queueName: string,\n maxExecutions: number,\n windowMs: number\n ): Promise<unknown | null> {\n const key = this.makeKey(queueName);\n return this.withKeyLock(key, () => {\n const now = Date.now();\n const windowStart = new Date(now - windowMs);\n const executions = this.executions.get(key) ?? [];\n const live = executions.filter((e) => e.executedAt > windowStart);\n if (live.length >= maxExecutions) {\n // Compact while we're here.\n if (live.length !== executions.length) {\n this.executions.set(key, live);\n }\n return null;\n }\n const next = this.nextAvailableTimes.get(key);\n if (next && next.getTime() > now) {\n return null;\n }\n const id = uuid4();\n live.push({ id, queueName, executedAt: new Date(now) });\n this.executions.set(key, live);\n return id;\n });\n }\n\n public async releaseExecution(queueName: string, token: unknown): Promise<void> {\n if (token === null || token === undefined) return;\n const key = this.makeKey(queueName);\n await this.withKeyLock(key, () => {\n const executions = this.executions.get(key);\n if (!executions || executions.length === 0) return;\n // Delete by id — NEVER by recency. Two concurrent acquirers can hold\n // tokens for different rows; popping the most recent would release the\n // other worker's slot.\n const idx = executions.findIndex((e) => e.id === token);\n if (idx === -1) return;\n executions.splice(idx, 1);\n this.executions.set(key, executions);\n });\n }\n\n public async recordExecution(queueName: string): Promise<void> {\n await sleep(0);\n const key = this.makeKey(queueName);\n const executions = this.executions.get(key) ?? [];\n executions.push({\n id: uuid4(),\n queueName,\n executedAt: new Date(),\n });\n this.executions.set(key, executions);\n }\n\n public async getExecutionCount(queueName: string, windowStartTime: string): Promise<number> {\n await sleep(0);\n const key = this.makeKey(queueName);\n const executions = this.executions.get(key) ?? [];\n const windowStart = new Date(windowStartTime);\n return executions.filter((e) => e.executedAt > windowStart).length;\n }\n\n public async getOldestExecutionAtOffset(\n queueName: string,\n offset: number\n ): Promise<string | undefined> {\n await sleep(0);\n const key = this.makeKey(queueName);\n const executions = this.executions.get(key) ?? [];\n const sorted = [...executions].sort((a, b) => a.executedAt.getTime() - b.executedAt.getTime());\n const execution = sorted[offset];\n return execution?.executedAt.toISOString();\n }\n\n public async getNextAvailableTime(queueName: string): Promise<string | undefined> {\n await sleep(0);\n const key = this.makeKey(queueName);\n const time = this.nextAvailableTimes.get(key);\n return time?.toISOString();\n }\n\n public async setNextAvailableTime(queueName: string, nextAvailableAt: string): Promise<void> {\n await sleep(0);\n const key = this.makeKey(queueName);\n this.nextAvailableTimes.set(key, new Date(nextAvailableAt));\n }\n\n public async clear(queueName: string): Promise<void> {\n await sleep(0);\n const key = this.makeKey(queueName);\n this.executions.delete(key);\n this.nextAvailableTimes.delete(key);\n }\n}\n"
|
|
19
24
|
],
|
|
20
|
-
"mappings": "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",
|
|
21
|
-
"debugId": "
|
|
25
|
+
"mappings": "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",
|
|
26
|
+
"debugId": "F5B8A6C859447D5F64756E2164756E21",
|
|
22
27
|
"names": []
|
|
23
28
|
}
|