@workglow/postgres 0.2.36 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/job-queue/PostgresJobStore.d.ts +42 -0
- package/dist/job-queue/PostgresJobStore.d.ts.map +1 -0
- package/dist/job-queue/PostgresMessageQueue.d.ts +38 -0
- package/dist/job-queue/PostgresMessageQueue.d.ts.map +1 -0
- package/dist/job-queue/PostgresQueueStorage.d.ts +65 -10
- package/dist/job-queue/PostgresQueueStorage.d.ts.map +1 -1
- package/dist/job-queue/PostgresRateLimiterStorage.d.ts +1 -2
- package/dist/job-queue/PostgresRateLimiterStorage.d.ts.map +1 -1
- package/dist/job-queue/browser.js +546 -55
- package/dist/job-queue/browser.js.map +11 -8
- package/dist/job-queue/common.d.ts +3 -0
- package/dist/job-queue/common.d.ts.map +1 -1
- package/dist/job-queue/createPostgresQueue.d.ts +22 -0
- package/dist/job-queue/createPostgresQueue.d.ts.map +1 -0
- package/dist/job-queue/node.js +546 -55
- package/dist/job-queue/node.js.map +11 -8
- package/dist/migrations/PostgresMigrationRunner.d.ts +6 -1
- package/dist/migrations/PostgresMigrationRunner.d.ts.map +1 -1
- package/dist/migrations/postgresQueueMigrations.d.ts +9 -1
- package/dist/migrations/postgresQueueMigrations.d.ts.map +1 -1
- package/dist/migrations/postgresRateLimiterMigrations.d.ts +1 -1
- package/dist/migrations/postgresRateLimiterMigrations.d.ts.map +1 -1
- package/dist/storage/PostgresKvStorage.d.ts +1 -1
- package/dist/storage/PostgresKvStorage.d.ts.map +1 -1
- package/dist/storage/PostgresTabularStorage.d.ts +30 -1
- package/dist/storage/PostgresTabularStorage.d.ts.map +1 -1
- package/dist/storage/PostgresVectorStorage.d.ts +5 -1
- package/dist/storage/PostgresVectorStorage.d.ts.map +1 -1
- package/dist/storage/browser.js +28 -12
- package/dist/storage/browser.js.map +5 -5
- package/dist/storage/node.js +28 -12
- package/dist/storage/node.js.map +6 -6
- package/dist/text/PostgresFtsTextIndex.d.ts +10 -0
- package/dist/text/PostgresFtsTextIndex.d.ts.map +1 -1
- package/dist/text/browser.js.map +2 -2
- package/dist/text/node.js.map +2 -2
- package/package.json +8 -8
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/job-queue/PostgresQueueStorage.ts", "../../src/migrations/PostgresMigrationRunner.ts", "../../src/migrations/postgresQueueMigrations.ts", "../../src/job-queue/PostgresRateLimiterStorage.ts", "../../src/migrations/postgresRateLimiterMigrations.ts"],
|
|
3
|
+
"sources": ["../../src/job-queue/PostgresQueueStorage.ts", "../../src/migrations/PostgresMigrationRunner.ts", "../../src/migrations/postgresQueueMigrations.ts", "../../src/job-queue/PostgresRateLimiterStorage.ts", "../../src/migrations/postgresRateLimiterMigrations.ts", "../../src/job-queue/PostgresMessageQueue.ts", "../../src/job-queue/PostgresJobStore.ts", "../../src/job-queue/createPostgresQueue.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 { createHash } from \"node:crypto\";\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport { createServiceToken, getLogger, makeFingerprint, uuid4 } from \"@workglow/util\";\nimport { JobStatus } from \"@workglow/job-queue\";\nimport type {\n IQueueStorage,\n JobStorageFormat,\n PrefixColumn,\n QueueChangePayload,\n QueueStorageOptions,\n QueueSubscribeOptions,\n} from \"@workglow/job-queue\";\nimport {\n assertPrefixesSafe,\n buildPrefixInsertFragments,\n buildPrefixWhereClause,\n getPrefixColumnNames,\n getPrefixParamValues,\n PostgresDialect,\n} from \"@workglow/storage\";\nimport { PostgresMigrationRunner } from \"../migrations/PostgresMigrationRunner\";\nimport { postgresQueueMigrations } from \"../migrations/postgresQueueMigrations\";\n\nexport const POSTGRES_QUEUE_STORAGE = createServiceToken<IQueueStorage<any, any>>(\n \"jobqueue.storage.postgres\"\n);\n\n/**\n * Subset of pg.PoolClient that {@link PostgresQueueStorage.subscribeToChanges}\n * needs. Typed locally so we don't require `pg` types at the storage layer.\n */\ninterface ListenClient {\n query: (sql: string) => Promise<unknown>;\n release: () => void;\n removeAllListeners?: (event: string) => void;\n on: (event: string, listener: (...args: any[]) => void) => void;\n}\n\n/**\n * PostgreSQL implementation of a job queue.\n * Provides storage and retrieval for job execution states using PostgreSQL.\n */\nexport class PostgresQueueStorage<Input, Output> implements IQueueStorage<Input, Output> {\n public readonly scope = \"cluster\" as const;\n /** The prefix column definitions */\n protected readonly prefixes: readonly PrefixColumn[];\n /** The prefix values for filtering */\n protected readonly prefixValues: Readonly<Record<string, string | number>>;\n /** The table name for the job queue */\n protected readonly tableName: string;\n\n constructor(\n protected readonly db: Pool,\n protected readonly queueName: string,\n options?: QueueStorageOptions\n ) {\n this.prefixes = options?.prefixes ?? [];\n this.prefixValues = options?.prefixValues ?? {};\n\n // Validate prefix column names to prevent SQL injection in DDL statements\n assertPrefixesSafe(this.prefixes);\n\n // Generate table name based on prefix configuration to avoid column conflicts\n if (this.prefixes.length > 0) {\n const prefixNames = this.prefixes.map((p) => p.name).join(\"_\");\n this.tableName = `job_queue_${prefixNames}`;\n } else {\n this.tableName = \"job_queue\";\n }\n }\n\n /** WHERE-clause helper specialized for this instance's dialect + prefix values. */\n private buildPrefixWhereClause(startParam: number) {\n return buildPrefixWhereClause(PostgresDialect, this.prefixes, this.prefixValues, startParam);\n }\n\n /** Returns prefix values in column order. */\n private getPrefixParamValues(): Array<string | number> {\n return getPrefixParamValues(this.prefixes, this.prefixValues);\n }\n\n /**\n * Returns the versioned migrations that this storage's table layout depends\n * on. Callers can compose them with other storages' migrations under a\n * shared {@link PostgresMigrationRunner}; otherwise call {@link migrate}.\n */\n public getMigrations() {\n return postgresQueueMigrations(this.tableName, this.prefixes);\n }\n\n /**\n * Applies any pending migrations for this queue's table. Idempotent —\n * already-applied versions are recorded in `_storage_migrations` and\n * skipped on subsequent calls.\n */\n public async migrate(): Promise<void> {\n await new PostgresMigrationRunner(this.db).run(this.getMigrations());\n }\n\n /**\n * Channel name for this storage's LISTEN/NOTIFY. Mirrors the trigger's\n * computation so subscriber and notifier agree.\n */\n private notifyChannelName(): string {\n // md5() returns 32 hex chars; combined with the 7-char prefix this is well\n // under Postgres's 63-byte identifier limit.\n const tableAndQueue = `${this.tableName}${this.queueName}`;\n const hash = createHash(\"md5\").update(tableAndQueue).digest(\"hex\");\n return `wglw_q_${hash}`;\n }\n\n /**\n * Adds a new job to the queue.\n * @param job - The job to add\n * @returns The ID of the added job\n */\n public async add(job: JobStorageFormat<Input, Output>): Promise<unknown> {\n const now = new Date().toISOString();\n job.queue = this.queueName;\n job.job_run_id = job.job_run_id ?? uuid4();\n job.fingerprint = await makeFingerprint(job.input);\n job.status = JobStatus.PENDING;\n job.progress = 0;\n job.progress_message = \"\";\n job.progress_details = null;\n job.created_at = now;\n job.run_after = now;\n\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const { columns: prefixColumnsInsert, placeholders: prefixParamPlaceholders } =\n buildPrefixInsertFragments(PostgresDialect, this.prefixes, 1);\n const prefixParamValues = this.getPrefixParamValues();\n const baseParamStart = prefixColumnNames.length + 1;\n\n const sql = `\n INSERT INTO ${this.tableName}(\n ${prefixColumnsInsert}queue, \n fingerprint, \n input, \n run_after,\n created_at,\n deadline_at,\n max_retries, \n job_run_id, \n progress, \n progress_message, \n progress_details\n )\n VALUES \n (${prefixParamPlaceholders}$${baseParamStart},$${baseParamStart + 1},$${baseParamStart + 2},$${baseParamStart + 3},$${baseParamStart + 4},$${baseParamStart + 5},$${baseParamStart + 6},$${baseParamStart + 7},$${baseParamStart + 8},$${baseParamStart + 9},$${baseParamStart + 10})\n RETURNING id`;\n const params = [\n ...prefixParamValues,\n job.queue,\n job.fingerprint,\n JSON.stringify(job.input),\n job.run_after,\n job.created_at,\n job.deadline_at,\n job.max_retries,\n job.job_run_id,\n job.progress,\n job.progress_message,\n job.progress_details ? JSON.stringify(job.progress_details) : null,\n ];\n const result = await this.db.query(sql, params);\n\n if (!result) throw new Error(\"Failed to add to queue\");\n job.id = result.rows[0].id;\n return job.id;\n }\n\n /**\n * Retrieves a job by its ID.\n * @param id - The ID of the job to retrieve\n * @returns The job if found, undefined otherwise\n */\n public async get(id: unknown): Promise<JobStorageFormat<Input, Output> | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query(\n `SELECT *\n FROM ${this.tableName}\n WHERE id = $1 AND queue = $2${prefixConditions}\n FOR UPDATE SKIP LOCKED\n LIMIT 1`,\n [id, this.queueName, ...prefixParams]\n );\n\n if (!result || result.rows.length === 0) return undefined;\n return result.rows[0];\n }\n\n /**\n * Retrieves a slice of jobs from the queue.\n * @param num - Maximum number of jobs to return\n * @returns 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 num = Number(num) || 100; // TS does not validate, so ensure it is a number\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);\n const result = await this.db.query<\n JobStorageFormat<Input, Output>,\n Array<string | number | JobStatus>\n >(\n `\n SELECT *\n FROM ${this.tableName}\n WHERE queue = $1\n AND status = $2${prefixConditions}\n ORDER BY run_after ASC\n LIMIT $3\n FOR UPDATE SKIP LOCKED`,\n [this.queueName, status, num, ...prefixParams]\n );\n if (!result) return [];\n return result.rows;\n }\n\n /**\n * Retrieves the next available job that is ready to be processed.\n * @param workerId - Worker ID to associate with the job (required)\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 // Parameters: $1=status, $2=queue, $3=status, $4=worker_id, $5+=prefix params\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(5);\n const result = await this.db.query<\n JobStorageFormat<Input, Output>,\n Array<string | number | JobStatus | null>\n >(\n `\n UPDATE ${this.tableName} \n SET status = $1, last_ran_at = NOW() AT TIME ZONE 'UTC', worker_id = $4\n WHERE id = (\n SELECT id \n FROM ${this.tableName} \n WHERE queue = $2 \n AND status = $3\n ${prefixConditions}\n AND run_after <= NOW() AT TIME ZONE 'UTC'\n ORDER BY run_after ASC \n FOR UPDATE SKIP LOCKED \n LIMIT 1\n )\n RETURNING *`,\n [JobStatus.PROCESSING, this.queueName, JobStatus.PENDING, workerId, ...prefixParams]\n );\n\n return result?.rows?.[0] ?? undefined;\n }\n\n /**\n * Retrieves the number of jobs in the queue with a specific status.\n * @param status - The status of the jobs to count\n * @returns The count of jobs with the specified status\n */\n public async size(status = JobStatus.PENDING): Promise<number> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query<{ count: string }, Array<string | number | JobStatus>>(\n `\n SELECT COUNT(*) as count\n FROM ${this.tableName}\n WHERE queue = $1\n AND status = $2${prefixConditions}`,\n [this.queueName, status, ...prefixParams]\n );\n if (!result) return 0;\n return parseInt(result.rows[0].count, 10);\n }\n\n /**\n * Marks a job as complete with its output or error.\n * Enhanced error handling:\n * - For a retryable error, increments run_attempts and updates run_after.\n * - Marks a job as FAILED immediately for permanent or generic errors.\n */\n public async complete(jobDetails: JobStorageFormat<Input, Output>): Promise<void> {\n const prefixParams = this.getPrefixParamValues();\n\n if (jobDetails.status === JobStatus.DISABLED) {\n const { conditions: prefixConditions } = this.buildPrefixWhereClause(4);\n await this.db.query(\n `UPDATE ${this.tableName} \n SET \n status = $1, \n progress = 100,\n progress_message = '',\n progress_details = NULL,\n completed_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $2 AND queue = $3${prefixConditions}`,\n [jobDetails.status, jobDetails.id, this.queueName, ...prefixParams]\n );\n } else if (jobDetails.status === JobStatus.PENDING) {\n const { conditions: prefixConditions } = this.buildPrefixWhereClause(7);\n await this.db.query(\n `UPDATE ${this.tableName} \n SET \n error = $1, \n error_code = $2,\n status = $3, \n run_after = $4, \n progress = 0,\n progress_message = '',\n progress_details = NULL,\n run_attempts = run_attempts + 1, \n last_ran_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $5 AND queue = $6${prefixConditions}`,\n [\n jobDetails.error,\n jobDetails.error_code,\n jobDetails.status,\n jobDetails.run_after,\n jobDetails.id,\n this.queueName,\n ...prefixParams,\n ]\n );\n } else {\n const { conditions: prefixConditions } = this.buildPrefixWhereClause(7);\n await this.db.query(\n `\n UPDATE ${this.tableName} \n SET \n output = $1, \n error = $2, \n error_code = $3,\n status = $4, \n progress = 100,\n progress_message = '',\n progress_details = NULL,\n run_attempts = run_attempts + 1, \n completed_at = NOW() AT TIME ZONE 'UTC',\n last_ran_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $5 AND queue = $6${prefixConditions}`,\n [\n jobDetails.output ? JSON.stringify(jobDetails.output) : null,\n jobDetails.error ?? null,\n jobDetails.error_code ?? null,\n jobDetails.status,\n jobDetails.id,\n this.queueName,\n ...prefixParams,\n ]\n );\n }\n }\n\n /**\n * Clears all jobs from the queue.\n */\n public async deleteAll(): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);\n await this.db.query(\n `\n DELETE FROM ${this.tableName}\n WHERE queue = $1${prefixConditions}`,\n [this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Looks up cached output for a given input\n * Uses input fingerprinting for efficient matching\n * @returns The cached output or null if not found\n */\n public async outputForInput(input: Input): Promise<Output | null> {\n const fingerprint = await makeFingerprint(input);\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query(\n `\n SELECT output\n FROM ${this.tableName}\n WHERE fingerprint = $1 AND queue = $2 AND status = 'COMPLETED'${prefixConditions}`,\n [fingerprint, this.queueName, ...prefixParams]\n );\n if (!result || result.rows.length === 0) return null;\n return result.rows[0].output;\n }\n\n /**\n * Aborts a job by setting its status to \"ABORTING\".\n * This method will signal the corresponding AbortController so that\n * the job's execute() method (if it supports an AbortSignal parameter)\n * can clean up and exit.\n */\n public async abort(jobId: unknown): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n await this.db.query(\n `\n UPDATE ${this.tableName} \n SET status = 'ABORTING' \n WHERE id = $1 AND queue = $2${prefixConditions}`,\n [jobId, this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Releases a claimed job back to PENDING without incrementing run_attempts.\n * @param jobId - The id of the claimed job to release.\n */\n public async release(jobId: unknown): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n await this.db.query(\n `\n UPDATE ${this.tableName}\n SET status = 'PENDING',\n worker_id = NULL,\n progress = 0,\n progress_message = '',\n progress_details = NULL\n WHERE id = $1 AND queue = $2${prefixConditions}`,\n [jobId, this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Retrieves all jobs for a given job run ID.\n * @param job_run_id - The ID of the job run to retrieve\n * @returns An array of jobs\n */\n public async getByRunId(job_run_id: string): Promise<Array<JobStorageFormat<Input, Output>>> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query(\n `\n SELECT * FROM ${this.tableName} WHERE job_run_id = $1 AND queue = $2${prefixConditions}`,\n [job_run_id, this.queueName, ...prefixParams]\n );\n if (!result) return [];\n return result.rows;\n }\n\n /**\n * Implements the abstract saveProgress method from JobQueue\n */\n public async saveProgress(\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, any>\n ): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(6);\n await this.db.query(\n `\n UPDATE ${this.tableName} \n SET progress = $1,\n progress_message = $2,\n progress_details = $3\n WHERE id = $4 AND queue = $5${prefixConditions}`,\n [\n progress,\n message,\n details ? JSON.stringify(details) : null,\n jobId,\n this.queueName,\n ...prefixParams,\n ]\n );\n }\n\n /**\n * Deletes a job by its ID\n */\n public async delete(jobId: unknown): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n await this.db.query(\n `DELETE FROM ${this.tableName} WHERE id = $1 AND queue = $2${prefixConditions}`,\n [jobId, this.queueName, ...prefixParams]\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 const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);\n await this.db.query(\n `DELETE FROM ${this.tableName} \n WHERE queue = $1 \n AND status = $2 \n AND completed_at IS NOT NULL \n AND completed_at <= $3${prefixConditions}`,\n [this.queueName, status, cutoffDate, ...prefixParams]\n );\n }\n\n /**\n * Subscribe to INSERT/UPDATE notifications via PostgreSQL LISTEN/NOTIFY.\n * Replaces 100ms-poll fallback for cluster Postgres deployments — workers\n * wake within network-latency of an actual change rather than on a timer.\n *\n * Acquires a dedicated client from the pool (LISTEN occupies a connection\n * for its lifetime). On unsubscribe, runs UNLISTEN and releases the client.\n * On connection loss, attempts to reconnect with bounded backoff and\n * re-LISTEN. After every successful (re)connect — including the initial\n * one — emits a synthetic `{ type: \"RESYNC\" }` event so subscribers can\n * re-poll state and pick up any rows inserted during the disconnect window\n * (or between subscribe and the first NOTIFY landing).\n *\n * Throws synchronously when the underlying pool lacks `connect()`\n * (single-connection wrappers like PGLite) so the caller's try/catch can\n * fall back to polling. JobQueueServer.start does this and logs at debug.\n *\n * `options.prefixFilter` follows {@link QueueSubscribeOptions.prefixFilter}:\n * `undefined` means \"use the storage instance's configured prefixValues\",\n * `{}` means \"receive all changes regardless of prefix\".\n */\n public subscribeToChanges(\n callback: (change: QueueChangePayload<Input, Output>) => void,\n options?: QueueSubscribeOptions\n ): () => void {\n type PoolWithConnect = { connect: () => Promise<ListenClient> };\n const poolMaybe = this.db as unknown as Partial<PoolWithConnect>;\n if (typeof poolMaybe.connect !== \"function\") {\n // Detect synchronously so callers can catch and fall back to polling\n // without leaving a dangling unhandled promise rejection behind.\n throw new Error(\n \"PostgresQueueStorage.subscribeToChanges requires a pg.Pool (got a single-connection wrapper)\"\n );\n }\n const pool = poolMaybe as PoolWithConnect;\n\n const channel = this.notifyChannelName();\n // undefined -> default to instance prefixValues; {} -> no filtering;\n // partial -> filter by the provided subset (matches QueueSubscribeOptions docs).\n const effectivePrefixFilter: Readonly<Record<string, string | number>> | null =\n options?.prefixFilter === undefined\n ? this.prefixes.length > 0\n ? this.prefixValues\n : null\n : Object.keys(options.prefixFilter).length === 0\n ? null\n : options.prefixFilter;\n\n let unsubscribed = false;\n let activeClient: ListenClient | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let backoffMs = 250;\n\n const dispatch = (change: QueueChangePayload<Input, Output>): void => {\n try {\n callback(change);\n } catch (err) {\n // Never let user callbacks tear down the listener loop.\n getLogger().debug(\"PostgresQueueStorage subscribe callback threw\", {\n channel,\n changeType: change.type,\n error: err,\n });\n }\n };\n\n const matchesPrefix = (\n change: QueueChangePayload<Input, Output>,\n filter: Readonly<Record<string, string | number>>\n ): boolean => {\n const row = (change.new ?? change.old) as Record<string, unknown> | undefined;\n if (!row) return false;\n for (const [k, v] of Object.entries(filter)) {\n if (row[k] !== v) return false;\n }\n return true;\n };\n\n // Hydrate the row referenced by a notification. Postgres NOTIFY is capped\n // at 8 KB so we ship only {id, queue, status} in the payload and fetch the\n // full row here — keeps subscriber payloads consistent with other backends\n // (e.g. Supabase realtime, IndexedDb) and lets prefixFilter inspect the\n // prefix columns the row actually has.\n const hydrate = async (id: unknown): Promise<JobStorageFormat<Input, Output> | undefined> => {\n try {\n return await this.get(id);\n } catch {\n return undefined;\n }\n };\n\n const handleNotification = (msg: { channel: string; payload?: string }): void => {\n if (msg.channel !== channel || !msg.payload) return;\n let parsed: { op: string; id: number; queue: string; status: string };\n try {\n parsed = JSON.parse(msg.payload);\n } catch {\n return;\n }\n void (async () => {\n const op = parsed.op;\n const fallback = {\n id: parsed.id,\n queue: parsed.queue,\n status: parsed.status,\n } as unknown as JobStorageFormat<Input, Output>;\n const fullRow = op === \"DELETE\" ? undefined : await hydrate(parsed.id);\n const change: QueueChangePayload<Input, Output> = {\n type: op === \"INSERT\" ? \"INSERT\" : op === \"DELETE\" ? \"DELETE\" : \"UPDATE\",\n new: op === \"DELETE\" ? undefined : (fullRow ?? fallback),\n old: op === \"DELETE\" ? fallback : undefined,\n };\n if (effectivePrefixFilter && !matchesPrefix(change, effectivePrefixFilter)) return;\n dispatch(change);\n })();\n };\n\n const connect = async (): Promise<void> => {\n if (unsubscribed) return;\n try {\n const client = await pool.connect();\n if (unsubscribed) {\n client.release();\n return;\n }\n activeClient = client;\n client.on(\"notification\", handleNotification);\n client.on(\"error\", () => scheduleReconnect());\n await client.query(`LISTEN ${channel}`);\n backoffMs = 250; // reset on successful (re)connect\n // Synthetic resync: any rows inserted between subscribe and LISTEN\n // landing (or during a reconnect window) have no NOTIFY backing.\n // Fire a no-payload event so subscribers can re-poll state.\n dispatch({ type: \"RESYNC\" });\n } catch {\n scheduleReconnect();\n }\n };\n\n const scheduleReconnect = (): void => {\n if (unsubscribed || reconnectTimer) return;\n const c = activeClient;\n activeClient = null;\n try {\n c?.removeAllListeners?.(\"notification\");\n c?.removeAllListeners?.(\"error\");\n c?.release();\n } catch {\n // best-effort\n }\n const delay = Math.min(backoffMs, 30_000);\n backoffMs = Math.min(backoffMs * 2, 30_000);\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n void connect();\n }, delay);\n };\n\n void connect();\n\n return () => {\n unsubscribed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n const c = activeClient;\n activeClient = null;\n if (c) {\n // Best-effort UNLISTEN; ignore errors (connection may already be dead).\n c.query(`UNLISTEN ${channel}`)\n .catch(() => {})\n .finally(() => {\n try {\n c.removeAllListeners?.(\"notification\");\n c.removeAllListeners?.(\"error\");\n c.release();\n } catch {\n // best-effort\n }\n });\n }\n };\n }\n}\n",
|
|
6
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport
|
|
7
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Pool } from \"../storage/_postgres/node-bun\";\nimport { JobStatus, type PrefixColumn } from \"@workglow/job-queue\";\nimport {\n buildPrefixColumnsSql,\n getPrefixIndexPrefix,\n getPrefixIndexSuffix,\n type IMigration,\n PostgresDialect,\n} from \"@workglow/storage\";\n\n/**\n * Frozen v1 set of `job_status` enum values, captured at migration creation\n * time. Migration bodies are historical artifacts and MUST NOT read the\n * mutable {@link JobStatus} const directly: a fresh DB created after a value\n * is added to the const would receive the new value, while a DB already at v1\n * would not — silently producing version-skewed enums and runtime errors on\n * insert. Adding a status requires a NEW migration that runs\n * `ALTER TYPE job_status ADD VALUE IF NOT EXISTS '...'`.\n */\nconst JOB_STATUS_V1: readonly string[] = [\n \"PENDING\",\n \"PROCESSING\",\n \"COMPLETED\",\n \"ABORTING\",\n \"FAILED\",\n \"DISABLED\",\n];\n\n/**\n * Sanity check: if a developer adds a status to {@link JobStatus} without\n * also writing a follow-up migration that ALTER TYPE-adds it, queries that\n * insert the new status will fail at runtime against any DB still on v1.\n *\n * Run lazily from {@link postgresQueueMigrations} (NOT at module import) so\n * that consumers re-exporting this module via barrel files don't crash on\n * import when they have no intention of running migrations.\n */\nfunction assertJobStatusMatchesV1(): void {\n const current = new Set(Object.values(JobStatus));\n for (const v of JOB_STATUS_V1) {\n if (!current.has(v as JobStatus)) {\n throw new Error(\n `JobStatus const is missing v1 enum value \"${v}\"; v1 migration values are frozen.`\n );\n }\n }\n for (const v of current) {\n if (!JOB_STATUS_V1.includes(v)) {\n throw new Error(\n `JobStatus contains \"${v}\" which is not in JOB_STATUS_V1. ` +\n `Add a new migration that runs \"ALTER TYPE job_status ADD VALUE IF NOT EXISTS '${v}'\" ` +\n `instead of mutating the v1 enum literal.`\n );\n }\n }\n}\n\n/**\n * Initial migration set for the Postgres queue table identified by `tableName`.\n *\n * Component name is `queue:postgres:<tableName>` so two queues with different\n * table names get tracked independently in `_storage_migrations`. The v1\n * payload covers schema + indexes + LISTEN/NOTIFY plumbing; the trigger is\n * idempotent (`CREATE OR REPLACE FUNCTION` + `DROP TRIGGER IF EXISTS`).\n */\nexport function postgresQueueMigrations(\n tableName: string,\n prefixes: readonly PrefixColumn[]\n): IMigration<Pool>[] {\n assertJobStatusMatchesV1();\n const component = `queue:postgres:${tableName}`;\n const prefixColumnsSql = buildPrefixColumnsSql(PostgresDialect, prefixes);\n const prefixIndexPrefix = getPrefixIndexPrefix(prefixes);\n const indexSuffix = getPrefixIndexSuffix(prefixes);\n\n return [\n {\n component,\n version: 1,\n description: \"Create job_status enum + queue table + indexes + notify trigger\",\n async up(db: Pool) {\n // Enum literal is the frozen v1 set, NOT Object.values(JobStatus).\n // See JOB_STATUS_V1 for why.\n const enumLiteral = JOB_STATUS_V1.map((v) => `'${v}'`).join(\",\");\n // DO block so the existence check + CREATE TYPE happen in one\n // statement. A bare `CREATE TYPE ...` raising duplicate_object inside\n // a transaction would leave it aborted (Postgres state 25P02), and\n // the runner's BEGIN/COMMIT would reject every subsequent statement.\n await db.query(`\n DO $$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_status') THEN\n CREATE TYPE job_status AS ENUM (${enumLiteral});\n END IF;\n END $$;\n `);\n\n await db.query(`\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id SERIAL NOT NULL,\n ${prefixColumnsSql}fingerprint text NOT NULL,\n queue text NOT NULL,\n job_run_id text NOT NULL,\n status job_status NOT NULL default 'PENDING',\n input jsonb NOT NULL,\n output jsonb,\n run_attempts integer default 0,\n max_retries integer default 20,\n run_after timestamp with time zone DEFAULT now(),\n last_ran_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now(),\n deadline_at timestamp with time zone,\n completed_at timestamp with time zone,\n error text,\n error_code text,\n progress real DEFAULT 0,\n progress_message text DEFAULT '',\n progress_details jsonb,\n worker_id text\n )\n `);\n\n await db.query(`\n CREATE INDEX IF NOT EXISTS job_fetcher${indexSuffix}_idx\n ON ${tableName} (${prefixIndexPrefix}id, status, run_after)\n `);\n await db.query(`\n CREATE INDEX IF NOT EXISTS job_queue_fetcher${indexSuffix}_idx\n ON ${tableName} (${prefixIndexPrefix}queue, status, run_after)\n `);\n await db.query(`\n CREATE INDEX IF NOT EXISTS jobs_fingerprint${indexSuffix}_unique_idx\n ON ${tableName} (${prefixIndexPrefix}queue, fingerprint, status)\n `);\n\n // Install LISTEN/NOTIFY plumbing so subscribers can wake on\n // INSERT/UPDATE without polling. Best-effort: in-process\n // Postgres-compatible engines like PGLite may not implement pg_notify\n // or plpgsql. Skip trigger installation in that case — the queue's\n // subscribeToChanges throws synchronously for those engines anyway.\n //\n // Wrapped in a SAVEPOINT because this migration runs inside the\n // runner's BEGIN/COMMIT. Without the savepoint, any error in the\n // trigger DDL would put the outer transaction into aborted state\n // (Postgres 25P02), poisoning the runner's bookkeeping INSERT and\n // failing the whole migration — even though the trigger itself is\n // optional. ROLLBACK TO SAVEPOINT lets us discard just the failed\n // trigger work and keep the rest of the migration committable.\n const fnName = `${tableName}_notify`;\n const trgName = `${tableName}_notify_trg`;\n await db.query(\"SAVEPOINT install_notify_trigger\");\n try {\n await db.query(`\n CREATE OR REPLACE FUNCTION ${fnName}() RETURNS trigger AS $fn$\n DECLARE\n channel TEXT := 'wglw_q_' || md5('${tableName}' || COALESCE(NEW.queue, OLD.queue));\n payload TEXT;\n BEGIN\n payload := json_build_object(\n 'op', TG_OP,\n 'id', COALESCE(NEW.id, OLD.id),\n 'queue', COALESCE(NEW.queue, OLD.queue),\n 'status', COALESCE(NEW.status::text, OLD.status::text)\n )::text;\n PERFORM pg_notify(channel, payload);\n RETURN NULL;\n END;\n $fn$ LANGUAGE plpgsql;\n `);\n await db.query(`DROP TRIGGER IF EXISTS ${trgName} ON ${tableName}`);\n await db.query(`\n CREATE TRIGGER ${trgName}\n AFTER INSERT OR UPDATE ON ${tableName}\n FOR EACH ROW EXECUTE FUNCTION ${fnName}();\n `);\n await db.query(\"RELEASE SAVEPOINT install_notify_trigger\");\n } catch {\n // Engine doesn't support LISTEN/NOTIFY; rewind to the savepoint so\n // the outer transaction stays usable, and let subscribers fall\n // back to polling.\n await db.query(\"ROLLBACK TO SAVEPOINT install_notify_trigger\").catch(() => undefined);\n await db.query(\"RELEASE SAVEPOINT install_notify_trigger\").catch(() => undefined);\n }\n },\n },\n ];\n}\n",
|
|
8
|
-
"/**\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 { Pool } from \"@workglow/postgres/storage\";\nimport type { PrefixColumn } from \"@workglow/job-queue\";\nimport type {\n IRateLimiterStorage,\n RateLimiterStorageOptions,\n RateLimiterStorageScope,\n} from \"@workglow/job-queue\";\nimport {\n assertPrefixesSafe,\n buildPrefixWhereClause,\n getPrefixColumnNames,\n getPrefixParamValues,\n PostgresDialect,\n} from \"@workglow/storage\";\nimport { PostgresMigrationRunner } from \"../migrations/PostgresMigrationRunner\";\nimport { postgresRateLimiterMigrations } from \"../migrations/postgresRateLimiterMigrations\";\n\nexport const POSTGRES_RATE_LIMITER_STORAGE = createServiceToken<IRateLimiterStorage>(\n \"ratelimiter.storage.postgres\"\n);\n\n/**\n * PostgreSQL implementation of rate limiter storage.\n * Manages execution records and next available times for rate limiting.\n */\nexport class PostgresRateLimiterStorage implements IRateLimiterStorage {\n public readonly scope: RateLimiterStorageScope = \"cluster\";\n /** The prefix column definitions */\n protected readonly prefixes: readonly PrefixColumn[];\n /** The prefix values for filtering */\n protected readonly prefixValues: Readonly<Record<string, string | number>>;\n /** The table name for execution tracking */\n protected readonly executionTableName: string;\n /** The table name for next available times */\n protected readonly nextAvailableTableName: string;\n\n constructor(\n protected readonly db: Pool,\n options?: RateLimiterStorageOptions\n ) {\n this.prefixes = options?.prefixes ?? [];\n this.prefixValues = options?.prefixValues ?? {};\n // Validate prefix column names before deriving table names from them.\n assertPrefixesSafe(this.prefixes);\n\n // Generate table names based on prefix configuration\n if (this.prefixes.length > 0) {\n const prefixNames = this.prefixes.map((p) => p.name).join(\"_\");\n this.executionTableName = `rate_limit_executions_${prefixNames}`;\n this.nextAvailableTableName = `rate_limit_next_available_${prefixNames}`;\n } else {\n this.executionTableName = \"rate_limit_executions\";\n this.nextAvailableTableName = \"rate_limit_next_available\";\n }\n }\n\n /** WHERE-clause helper specialized for the Postgres dialect. */\n private buildPrefixWhereClause(startParam: number) {\n return buildPrefixWhereClause(PostgresDialect, this.prefixes, this.prefixValues, startParam);\n }\n\n /** Returns prefix values in column order. */\n private getPrefixParamValues(): Array<string | number> {\n return getPrefixParamValues(this.prefixes, this.prefixValues);\n }\n\n /**\n * Returns the versioned migrations that this storage's tables depend on.\n * Callers can compose them with other storages' migrations under a shared\n * {@link PostgresMigrationRunner}; otherwise call {@link migrate}.\n */\n public getMigrations() {\n return postgresRateLimiterMigrations(\n this.executionTableName,\n this.nextAvailableTableName,\n this.prefixes\n );\n }\n\n /** Applies any pending migrations for this rate limiter's tables. */\n public async migrate(): Promise<void> {\n await new PostgresMigrationRunner(this.db).run(this.getMigrations());\n }\n\n public async tryReserveExecution(\n queueName: string,\n maxExecutions: number,\n windowMs: number\n ): Promise<unknown | null> {\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const prefixParamValues = this.getPrefixParamValues();\n const prefixCount = prefixColumnNames.length;\n\n // Parameter layout:\n // $1..$N prefix values (used in lock-key, count, next-available, and INSERT)\n // $(N+1) queueName\n // $(N+2) windowStart timestamp\n // $(N+3) maxExecutions\n const queueParam = `$${prefixCount + 1}`;\n const windowStartParam = `$${prefixCount + 2}`;\n\n // For the advisory lock we hash table-name + prefix values + queue-name into a bigint.\n // Use hashtextextended which returns int8 (advisory_xact_lock takes bigint).\n const lockKeyParts: string[] = [`'${this.executionTableName}'`];\n for (let i = 0; i < prefixCount; i++) {\n lockKeyParts.push(`$${i + 1}::text`);\n }\n lockKeyParts.push(`${queueParam}::text`);\n const lockKeyExpr = `hashtextextended(${lockKeyParts.join(\" || '|' || \")}, 0)`;\n\n const prefixWhere =\n prefixCount > 0\n ? \" AND \" + prefixColumnNames.map((p, i) => `${p} = $${i + 1}`).join(\" AND \")\n : \"\";\n\n const prefixInsertCols = prefixCount > 0 ? prefixColumnNames.join(\", \") + \", \" : \"\";\n const prefixInsertPlaceholders =\n prefixCount > 0 ? prefixColumnNames.map((_, i) => `$${i + 1}`).join(\", \") + \", \" : \"\";\n\n const windowStart = new Date(Date.now() - windowMs).toISOString();\n\n // Use a dedicated client when the pool supports connect() (real pg.Pool)\n // — the advisory xact lock + transaction MUST run on the same connection\n // for atomicity. Without connect(), only known single-connection wrappers\n // (PGLitePool, raw PGlite) are safe, since they implicitly serialize all\n // queries through one underlying connection. Anything else (e.g. a custom\n // multi-connection adapter without connect()) would dispatch the lock and\n // the INSERT to different sessions, defeating the lock entirely.\n const supportsConnect =\n typeof (this.db as unknown as { connect?: unknown }).connect === \"function\";\n if (!supportsConnect) {\n // Without connect() we run BEGIN/advisory_lock/INSERT/COMMIT directly on\n // `db.query`. That's only safe if `db` is a single-connection wrapper\n // (every query goes through the same underlying session). Recognize:\n // - PGLitePool — our own wrapper class; constructor name preserved.\n // - PGlite — third-party, ships minified so `constructor.name`\n // can be obfuscated (observed: \"q\"). Detect via duck-typing on\n // methods PGlite uniquely exposes (`waitReady` Promise + `exec`).\n // Any other no-connect() pool would dispatch the lock and the INSERT\n // to potentially different sessions, breaking atomicity.\n const dbAny = this.db as unknown as {\n waitReady?: unknown;\n exec?: unknown;\n constructor?: { name?: string };\n };\n const ctorName = dbAny.constructor?.name;\n const looksLikePGlite = typeof dbAny.exec === \"function\" && dbAny.waitReady !== undefined;\n const looksLikePGLitePool = ctorName === \"PGLitePool\";\n if (!looksLikePGlite && !looksLikePGLitePool) {\n throw new Error(\n `PostgresRateLimiterStorage.tryReserveExecution requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${ctorName ?? typeof this.db}. A multi-connection pool without connect() would dispatch the advisory lock and the INSERT to different sessions, breaking atomicity.`\n );\n }\n }\n const conn = supportsConnect\n ? await (\n this.db as unknown as {\n connect: () => Promise<{\n query: Pool[\"query\"];\n release: () => void;\n }>;\n }\n ).connect()\n : { query: this.db.query.bind(this.db), release: () => {} };\n\n try {\n await conn.query(\"BEGIN\");\n try {\n await conn.query(`SELECT pg_advisory_xact_lock(${lockKeyExpr})`, [\n ...prefixParamValues,\n queueName,\n ]);\n\n const countResult = await conn.query(\n `\n SELECT COUNT(*)::int AS n\n FROM ${this.executionTableName}\n WHERE queue_name = ${queueParam} AND executed_at > ${windowStartParam}${prefixWhere}\n `,\n [...prefixParamValues, queueName, windowStart]\n );\n const n: number = countResult.rows[0]?.n ?? 0;\n if (n >= maxExecutions) {\n await conn.query(\"COMMIT\");\n return null;\n }\n\n const naResult = await conn.query(\n `\n SELECT next_available_at\n FROM ${this.nextAvailableTableName}\n WHERE queue_name = ${queueParam}${prefixWhere}\n `,\n [...prefixParamValues, queueName]\n );\n const nextAvailableAt: Date | null = naResult.rows[0]?.next_available_at ?? null;\n if (nextAvailableAt && new Date(nextAvailableAt).getTime() > Date.now()) {\n await conn.query(\"COMMIT\");\n return null;\n }\n\n const insertResult = await conn.query(\n `\n INSERT INTO ${this.executionTableName} (${prefixInsertCols}queue_name)\n VALUES (${prefixInsertPlaceholders}${queueParam})\n RETURNING id\n `,\n [...prefixParamValues, queueName]\n );\n await conn.query(\"COMMIT\");\n return insertResult.rows[0]?.id ?? null;\n } catch (err) {\n try {\n await conn.query(\"ROLLBACK\");\n } catch {\n // best-effort\n }\n throw err;\n }\n } finally {\n conn.release();\n }\n }\n\n public async releaseExecution(queueName: string, token: unknown): Promise<void> {\n if (token === null || token === undefined) return;\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n // Delete by id — NEVER by recency. Two concurrent acquirers can hold\n // tokens for different rows; deleting \"the most recent\" would release the\n // other worker's slot.\n await this.db.query(\n `DELETE FROM ${this.executionTableName} WHERE id = $1 AND queue_name = $2${prefixConditions}`,\n [token as string | number, queueName, ...prefixParams]\n );\n }\n\n public async recordExecution(queueName: string): Promise<void> {\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const prefixColumnsInsert =\n prefixColumnNames.length > 0 ? prefixColumnNames.join(\", \") + \", \" : \"\";\n const prefixParamValues = this.getPrefixParamValues();\n const prefixParamPlaceholders =\n prefixColumnNames.length > 0\n ? prefixColumnNames.map((_, i) => `$${i + 1}`).join(\", \") + \", \"\n : \"\";\n const queueParamNum = prefixColumnNames.length + 1;\n\n await this.db.query(\n `\n INSERT INTO ${this.executionTableName} (${prefixColumnsInsert}queue_name)\n VALUES (${prefixParamPlaceholders}$${queueParamNum})\n `,\n [...prefixParamValues, queueName]\n );\n }\n\n public async getExecutionCount(queueName: string, windowStartTime: string): Promise<number> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n\n const result = await this.db.query(\n `\n SELECT COUNT(*) AS count\n FROM ${this.executionTableName}\n WHERE queue_name = $1 AND executed_at > $2${prefixConditions}\n `,\n [queueName, windowStartTime, ...prefixParams]\n );\n\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n public async getOldestExecutionAtOffset(\n queueName: string,\n offset: number\n ): Promise<string | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n\n const result = await this.db.query(\n `\n SELECT executed_at\n FROM ${this.executionTableName}\n WHERE queue_name = $1${prefixConditions}\n ORDER BY executed_at ASC\n LIMIT 1 OFFSET $2\n `,\n [queueName, offset, ...prefixParams]\n );\n\n const executedAt = result.rows[0]?.executed_at;\n if (!executedAt) return undefined;\n return new Date(executedAt).toISOString();\n }\n\n public async getNextAvailableTime(queueName: string): Promise<string | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);\n\n const result = await this.db.query(\n `\n SELECT next_available_at\n FROM ${this.nextAvailableTableName}\n WHERE queue_name = $1${prefixConditions}\n `,\n [queueName, ...prefixParams]\n );\n\n const nextAvailableAt = result.rows[0]?.next_available_at;\n if (!nextAvailableAt) return undefined;\n return new Date(nextAvailableAt).toISOString();\n }\n\n public async setNextAvailableTime(queueName: string, nextAvailableAt: string): Promise<void> {\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const prefixColumnsInsert =\n prefixColumnNames.length > 0 ? prefixColumnNames.join(\", \") + \", \" : \"\";\n const prefixParamValues = this.getPrefixParamValues();\n const prefixParamPlaceholders =\n prefixColumnNames.length > 0\n ? prefixColumnNames.map((_, i) => `$${i + 1}`).join(\", \") + \", \"\n : \"\";\n const baseParamStart = prefixColumnNames.length + 1;\n\n // Build the conflict columns for upsert\n const conflictColumns =\n prefixColumnNames.length > 0 ? `${prefixColumnNames.join(\", \")}, queue_name` : \"queue_name\";\n\n await this.db.query(\n `\n INSERT INTO ${this.nextAvailableTableName} (${prefixColumnsInsert}queue_name, next_available_at)\n VALUES (${prefixParamPlaceholders}$${baseParamStart}, $${baseParamStart + 1})\n ON CONFLICT (${conflictColumns})\n DO UPDATE SET next_available_at = EXCLUDED.next_available_at\n `,\n [...prefixParamValues, queueName, nextAvailableAt]\n );\n }\n\n public async clear(queueName: string): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);\n\n await this.db.query(\n `DELETE FROM ${this.executionTableName} WHERE queue_name = $1${prefixConditions}`,\n [queueName, ...prefixParams]\n );\n await this.db.query(\n `DELETE FROM ${this.nextAvailableTableName} WHERE queue_name = $1${prefixConditions}`,\n [queueName, ...prefixParams]\n );\n }\n}\n",
|
|
9
|
-
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type {
|
|
5
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type {\n IQueueStorage,\n JobStorageFormat,\n PrefixColumn,\n QueueChangePayload,\n QueueStorageOptions,\n QueueSubscribeOptions,\n} from \"@workglow/job-queue\";\nimport { JobStatus, validateLeaseMs } from \"@workglow/job-queue\";\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport {\n assertPrefixesSafe,\n buildPrefixInsertFragments,\n buildPrefixWhereClause,\n getPrefixColumnNames,\n getPrefixParamValues,\n PostgresDialect,\n} from \"@workglow/storage\";\nimport { createServiceToken, getLogger, makeFingerprint, uuid4 } from \"@workglow/util\";\nimport { createHash } from \"node:crypto\";\nimport { PostgresMigrationRunner } from \"../migrations/PostgresMigrationRunner\";\nimport { postgresQueueMigrations } from \"../migrations/postgresQueueMigrations\";\n\nexport const POSTGRES_QUEUE_STORAGE = createServiceToken<IQueueStorage<any, any>>(\n \"jobqueue.storage.postgres\"\n);\n\n/**\n * Subset of pg.PoolClient that {@link PostgresQueueStorage.subscribeToChanges}\n * needs. Typed locally so we don't require `pg` types at the storage layer.\n */\ninterface ListenClient {\n query: (sql: string) => Promise<unknown>;\n release: () => void;\n removeAllListeners?: (event: string) => void;\n on: (event: string, listener: (...args: any[]) => void) => void;\n}\n\n/**\n * PostgreSQL implementation of a job queue.\n * Provides storage and retrieval for job execution states using PostgreSQL.\n */\nexport class PostgresQueueStorage<Input, Output> implements IQueueStorage<Input, Output> {\n public readonly scope = \"cluster\" as const;\n /** The prefix column definitions */\n protected readonly prefixes: readonly PrefixColumn[];\n /** The prefix values for filtering */\n protected readonly prefixValues: Readonly<Record<string, string | number>>;\n /** The table name for the job queue */\n protected readonly tableName: string;\n\n constructor(\n protected readonly db: Pool,\n protected readonly queueName: string,\n options?: QueueStorageOptions\n ) {\n this.prefixes = options?.prefixes ?? [];\n this.prefixValues = options?.prefixValues ?? {};\n\n // Validate prefix column names to prevent SQL injection in DDL statements\n assertPrefixesSafe(this.prefixes);\n\n // Generate table name based on prefix configuration to avoid column conflicts\n if (this.prefixes.length > 0) {\n const prefixNames = this.prefixes.map((p) => p.name).join(\"_\");\n this.tableName = `job_queue_${prefixNames}`;\n } else {\n this.tableName = \"job_queue\";\n }\n }\n\n /** WHERE-clause helper specialized for this instance's dialect + prefix values. */\n private buildPrefixWhereClause(startParam: number) {\n return buildPrefixWhereClause(PostgresDialect, this.prefixes, this.prefixValues, startParam);\n }\n\n /** Returns prefix values in column order. */\n private getPrefixParamValues(): Array<string | number> {\n return getPrefixParamValues(this.prefixes, this.prefixValues);\n }\n\n /**\n * Returns the versioned migrations that this storage's table layout depends\n * on. Callers can compose them with other storages' migrations under a\n * shared {@link PostgresMigrationRunner}; otherwise call {@link migrate}.\n */\n public getMigrations() {\n return postgresQueueMigrations(this.tableName, this.prefixes);\n }\n\n /**\n * Applies any pending migrations for this queue's table. Idempotent —\n * already-applied versions are recorded in `_storage_migrations` and\n * skipped on subsequent calls.\n */\n public async migrate(): Promise<void> {\n await new PostgresMigrationRunner(this.db).run(this.getMigrations());\n }\n\n /**\n * Channel name for this storage's LISTEN/NOTIFY. Mirrors the trigger's\n * computation so subscriber and notifier agree.\n */\n private notifyChannelName(): string {\n // md5() returns 32 hex chars; combined with the 7-char prefix this is well\n // under Postgres's 63-byte identifier limit.\n const tableAndQueue = `${this.tableName}${this.queueName}`;\n const hash = createHash(\"md5\").update(tableAndQueue).digest(\"hex\");\n return `wglw_q_${hash}`;\n }\n\n /**\n * Adds a new job to the queue.\n * @param job - The job to add\n * @returns The ID of the added job\n */\n public async add(job: JobStorageFormat<Input, Output>): Promise<unknown> {\n const now = new Date().toISOString();\n job.queue = this.queueName;\n job.job_run_id = job.job_run_id ?? uuid4();\n job.fingerprint = job.fingerprint ?? (await makeFingerprint(job.input));\n job.status = JobStatus.PENDING;\n job.progress = 0;\n job.progress_message = \"\";\n job.progress_details = null;\n job.created_at = now;\n job.visible_at = now;\n\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const { columns: prefixColumnsInsert, placeholders: prefixParamPlaceholders } =\n buildPrefixInsertFragments(PostgresDialect, this.prefixes, 1);\n const prefixParamValues = this.getPrefixParamValues();\n const baseParamStart = prefixColumnNames.length + 1;\n\n const sql = `\n INSERT INTO ${this.tableName}(\n ${prefixColumnsInsert}queue,\n fingerprint,\n input,\n visible_at,\n created_at,\n deadline_at,\n max_attempts,\n job_run_id,\n progress,\n progress_message,\n progress_details\n )\n VALUES\n (${prefixParamPlaceholders}$${baseParamStart},$${baseParamStart + 1},$${baseParamStart + 2},$${baseParamStart + 3},$${baseParamStart + 4},$${baseParamStart + 5},$${baseParamStart + 6},$${baseParamStart + 7},$${baseParamStart + 8},$${baseParamStart + 9},$${baseParamStart + 10})\n RETURNING id`;\n const params = [\n ...prefixParamValues,\n job.queue,\n job.fingerprint,\n JSON.stringify(job.input),\n job.visible_at,\n job.created_at,\n job.deadline_at,\n job.max_attempts,\n job.job_run_id,\n job.progress,\n job.progress_message,\n job.progress_details ? JSON.stringify(job.progress_details) : null,\n ];\n let result: { rows: Array<{ id: unknown }> } | undefined;\n try {\n result = (await this.db.query(sql, params)) as { rows: Array<{ id: unknown }> } | undefined;\n } catch (err) {\n // H2: race-safety for fingerprint dedup. With the v4 UNIQUE partial\n // index in place, two concurrent inserts for the same (queue,\n // fingerprint) where one row is PENDING/PROCESSING raise a 23505\n // unique_violation. We resolve the race by returning the winner's id\n // (the row that's already PENDING/PROCESSING). Any other error\n // re-throws.\n const e = err as { code?: string; constraint?: string; message?: string };\n const isUniqueViolation = e?.code === \"23505\";\n const involvesFingerprint =\n typeof e?.constraint === \"string\"\n ? e.constraint.includes(\"fingerprint\")\n : typeof e?.message === \"string\"\n ? e.message.includes(\"fingerprint\")\n : false;\n if (isUniqueViolation && involvesFingerprint && job.fingerprint) {\n const winner = await this.findActiveByFingerprint(job.fingerprint, this.queueName);\n if (winner?.id != null) {\n job.id = winner.id;\n return winner.id;\n }\n }\n throw err;\n }\n\n if (!result) throw new Error(\"Failed to add to queue\");\n job.id = result.rows[0].id;\n return job.id;\n }\n\n /**\n * Retrieves a job by its ID.\n * @param id - The ID of the job to retrieve\n * @returns The job if found, undefined otherwise\n */\n public async get(id: unknown): Promise<JobStorageFormat<Input, Output> | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query(\n `SELECT *\n FROM ${this.tableName}\n WHERE id = $1 AND queue = $2${prefixConditions}\n FOR UPDATE SKIP LOCKED\n LIMIT 1`,\n [id, this.queueName, ...prefixParams]\n );\n\n if (!result || result.rows.length === 0) return undefined;\n return result.rows[0];\n }\n\n /**\n * Retrieves a slice of jobs from the queue.\n * @param num - Maximum number of jobs to return\n * @returns 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 num = Number(num) || 100; // TS does not validate, so ensure it is a number\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);\n const result = await this.db.query<\n JobStorageFormat<Input, Output>,\n Array<string | number | JobStatus>\n >(\n `\n SELECT *\n FROM ${this.tableName}\n WHERE queue = $1\n AND status = $2${prefixConditions}\n ORDER BY visible_at ASC\n LIMIT $3\n FOR UPDATE SKIP LOCKED`,\n [this.queueName, status, num, ...prefixParams]\n );\n if (!result) return [];\n return result.rows;\n }\n\n /**\n * Retrieves the next available job that is ready to be processed.\n * Claims PENDING jobs ready to run, and also reclaims PROCESSING jobs whose\n * lease has expired (crash recovery). Sets lease_expires_at on the claimed row.\n * @param workerId - Worker ID to associate with the job (required)\n * @param opts - Optional options including leaseMs (default 30000)\n * @returns The next job or undefined if no job is available\n */\n public async next(\n workerId: string,\n opts?: { leaseMs?: number }\n ): Promise<JobStorageFormat<Input, Output> | undefined> {\n const leaseMs = opts?.leaseMs ?? 30000;\n validateLeaseMs(leaseMs, \"leaseMs\");\n // Parameters: $1=PROCESSING, $2=now+leaseMs interval, $3=queue, $4=workerId, $5=PENDING, $6=PROCESSING, $7+=prefix params\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(7);\n const result = await this.db.query<\n JobStorageFormat<Input, Output>,\n Array<string | number | JobStatus | null>\n >(\n `\n UPDATE ${this.tableName}\n SET status = $1,\n last_attempted_at = NOW() AT TIME ZONE 'UTC',\n lease_owner = $4,\n lease_expires_at = NOW() AT TIME ZONE 'UTC' + ($2 * INTERVAL '1 millisecond'),\n -- A reclaimed PROCESSING row was claimed by a now-crashed worker;\n -- that constitutes one used-up attempt against max_attempts.\n -- PENDING claims must not be charged here — JobQueueWorker's\n -- existing validateJobState() will FAIL the job in the next-step\n -- branch when attempts >= max_attempts.\n attempts = CASE WHEN status = $6 THEN attempts + 1 ELSE attempts END,\n -- Always clear any stale abort_requested_at on (re)claim. A PROCESSING\n -- row may have had abort_requested_at set before the worker crashed;\n -- the new owner must start with a clean slate or the worker will see\n -- the abort flag immediately and never run user code.\n abort_requested_at = NULL\n WHERE id = (\n SELECT id\n FROM ${this.tableName}\n WHERE queue = $3\n AND (\n (status = $5 AND visible_at <= NOW() AT TIME ZONE 'UTC')\n OR (status = $6 AND (lease_expires_at IS NULL OR lease_expires_at < NOW() AT TIME ZONE 'UTC'))\n )\n ${prefixConditions}\n ORDER BY visible_at ASC\n FOR UPDATE SKIP LOCKED\n LIMIT 1\n )\n RETURNING *`,\n [\n JobStatus.PROCESSING,\n leaseMs,\n this.queueName,\n workerId,\n JobStatus.PENDING,\n JobStatus.PROCESSING,\n ...prefixParams,\n ]\n );\n\n return result?.rows?.[0] ?? undefined;\n }\n\n /**\n * Extend the lease on a currently PROCESSING job.\n * @param id - The ID of the job to extend the lease for\n * @param workerId - Worker ID that must match the current lease owner (lease_owner)\n * @param ms - Number of milliseconds to extend the lease by\n */\n public async extendLease(id: unknown, workerId: string, ms: number): Promise<void> {\n validateLeaseMs(ms, \"ms\");\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(5);\n const result = await this.db.query(\n `UPDATE ${this.tableName}\n SET lease_expires_at = NOW() AT TIME ZONE 'UTC' + ($1 * INTERVAL '1 millisecond')\n WHERE id = $2 AND queue = $3 AND lease_owner = $4 AND status = 'PROCESSING'${prefixConditions}`,\n [ms, id, this.queueName, workerId, ...prefixParams]\n );\n if (!result || result.rowCount === 0) {\n throw new Error(\n `extendLease failed: job ${String(id)} is not PROCESSING or lease is not owned by worker ${workerId}`\n );\n }\n }\n\n /**\n * Retrieves the number of jobs in the queue with a specific status.\n * @param status - The status of the jobs to count\n * @returns The count of jobs with the specified status\n */\n public async size(status = JobStatus.PENDING): Promise<number> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query<{ count: string }, Array<string | number | JobStatus>>(\n `\n SELECT COUNT(*) as count\n FROM ${this.tableName}\n WHERE queue = $1\n AND status = $2${prefixConditions}`,\n [this.queueName, status, ...prefixParams]\n );\n if (!result) return 0;\n return parseInt(result.rows[0].count, 10);\n }\n\n /**\n * Marks a job as complete with its output or error.\n * Enhanced error handling:\n * - For a retryable error, increments attempts and updates visible_at.\n * - Marks a job as FAILED immediately for permanent or generic errors.\n */\n public async complete(jobDetails: JobStorageFormat<Input, Output>): Promise<void> {\n const prefixParams = this.getPrefixParamValues();\n\n if (jobDetails.status === JobStatus.DISABLED) {\n const { conditions: prefixConditions } = this.buildPrefixWhereClause(4);\n await this.db.query(\n `UPDATE ${this.tableName} \n SET \n status = $1, \n progress = 100,\n progress_message = '',\n progress_details = NULL,\n completed_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $2 AND queue = $3${prefixConditions}`,\n [jobDetails.status, jobDetails.id, this.queueName, ...prefixParams]\n );\n } else if (jobDetails.status === JobStatus.PENDING) {\n const { conditions: prefixConditions } = this.buildPrefixWhereClause(7);\n // PENDING-retry branch: the worker hit a retryable error and the row is\n // returning to PENDING for another attempt. Clear abort_requested_at so\n // an abort that was requested DURING the previous attempt does not\n // immediately cancel the retry.\n await this.db.query(\n `UPDATE ${this.tableName}\n SET\n error = $1,\n error_code = $2,\n status = $3,\n visible_at = $4,\n progress = 0,\n progress_message = '',\n progress_details = NULL,\n attempts = attempts + 1,\n last_attempted_at = NOW() AT TIME ZONE 'UTC',\n abort_requested_at = NULL\n WHERE id = $5 AND queue = $6${prefixConditions}`,\n [\n jobDetails.error,\n jobDetails.error_code,\n jobDetails.status,\n jobDetails.visible_at,\n jobDetails.id,\n this.queueName,\n ...prefixParams,\n ]\n );\n } else {\n const { conditions: prefixConditions } = this.buildPrefixWhereClause(7);\n await this.db.query(\n `\n UPDATE ${this.tableName} \n SET \n output = $1, \n error = $2, \n error_code = $3,\n status = $4, \n progress = 100,\n progress_message = '',\n progress_details = NULL,\n attempts = attempts + 1,\n completed_at = NOW() AT TIME ZONE 'UTC',\n last_attempted_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $5 AND queue = $6${prefixConditions}`,\n [\n jobDetails.output ? JSON.stringify(jobDetails.output) : null,\n jobDetails.error ?? null,\n jobDetails.error_code ?? null,\n jobDetails.status,\n jobDetails.id,\n this.queueName,\n ...prefixParams,\n ]\n );\n }\n }\n\n /**\n * Terminal write that does NOT bump `attempts`. See IQueueStorage.finalize\n * for the rationale (avoids double-counting on ack/fail because the lease\n * reclaim path already charged the attempt at next() time).\n */\n public async finalize(\n id: unknown,\n fields: {\n output?: Output | null;\n error?: string | null;\n error_code?: string | null;\n status?: JobStatus;\n completed_at?: string | null;\n abort_requested_at?: string | null;\n lease_owner?: string | null;\n progress?: number;\n progress_message?: string;\n progress_details?: Record<string, any> | null;\n visible_at?: string | null;\n }\n ): Promise<void> {\n // Build a dynamic SET clause covering only the fields the caller supplied —\n // a partial overwrite. Everything else (in particular `attempts`,\n // `visible_at`, `lease_expires_at`) is untouched.\n const sets: string[] = [];\n const params: Array<unknown> = [];\n let nextParam = 1;\n const push = (col: string, value: unknown): void => {\n sets.push(`${col} = $${nextParam}`);\n params.push(value);\n nextParam += 1;\n };\n if (\"output\" in fields) {\n push(\"output\", fields.output != null ? JSON.stringify(fields.output) : null);\n }\n if (\"error\" in fields) push(\"error\", fields.error ?? null);\n if (\"error_code\" in fields) push(\"error_code\", fields.error_code ?? null);\n if (\"status\" in fields) push(\"status\", fields.status);\n if (\"completed_at\" in fields) push(\"completed_at\", fields.completed_at ?? null);\n if (\"abort_requested_at\" in fields) {\n push(\"abort_requested_at\", fields.abort_requested_at ?? null);\n }\n if (\"lease_owner\" in fields) push(\"lease_owner\", fields.lease_owner ?? null);\n if (\"progress\" in fields) push(\"progress\", fields.progress ?? 0);\n if (\"progress_message\" in fields) push(\"progress_message\", fields.progress_message ?? \"\");\n if (\"progress_details\" in fields) {\n push(\n \"progress_details\",\n fields.progress_details != null ? JSON.stringify(fields.progress_details) : null\n );\n }\n if (\"visible_at\" in fields) push(\"visible_at\", fields.visible_at ?? null);\n if (sets.length === 0) return; // nothing to write\n const idParam = nextParam;\n nextParam += 1;\n const queueParam = nextParam;\n nextParam += 1;\n const { conditions: prefixConditions, params: prefixParams } =\n this.buildPrefixWhereClause(nextParam);\n await this.db.query(\n `UPDATE ${this.tableName}\n SET ${sets.join(\", \")}\n WHERE id = $${idParam} AND queue = $${queueParam}${prefixConditions}`,\n [...params, id, this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Clears all jobs from the queue.\n */\n public async deleteAll(): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);\n await this.db.query(\n `\n DELETE FROM ${this.tableName}\n WHERE queue = $1${prefixConditions}`,\n [this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Looks up cached output for a given input\n * Uses input fingerprinting for efficient matching\n * @returns The cached output or null if not found\n */\n public async outputForInput(input: Input): Promise<Output | null> {\n const fingerprint = await makeFingerprint(input);\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query(\n `\n SELECT output\n FROM ${this.tableName}\n WHERE fingerprint = $1 AND queue = $2 AND status = 'COMPLETED'${prefixConditions}`,\n [fingerprint, this.queueName, ...prefixParams]\n );\n if (!result || result.rows.length === 0) return null;\n return result.rows[0].output;\n }\n\n /**\n * Aborts a job.\n * - If PENDING: immediately mark as FAILED with abort_requested_at set.\n * - If PROCESSING: set abort_requested_at only (leave status as PROCESSING).\n * - Otherwise: no-op.\n */\n public async abort(jobId: unknown): Promise<void> {\n // Abort PENDING → FAILED immediately\n {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);\n await this.db.query(\n `UPDATE ${this.tableName}\n SET status = 'FAILED',\n abort_requested_at = NOW() AT TIME ZONE 'UTC',\n completed_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $1 AND queue = $2 AND status = $3${prefixConditions}`,\n [jobId, this.queueName, JobStatus.PENDING, ...prefixParams]\n );\n }\n // Abort PROCESSING → set abort_requested_at only\n {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);\n await this.db.query(\n `UPDATE ${this.tableName}\n SET abort_requested_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $1 AND queue = $2 AND status = $3${prefixConditions}`,\n [jobId, this.queueName, JobStatus.PROCESSING, ...prefixParams]\n );\n }\n }\n\n /**\n * Releases a claimed job back to PENDING without incrementing attempts.\n * @param jobId - The id of the claimed job to release.\n */\n public async releaseClaim(jobId: unknown): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n // releaseClaim returns the row to PENDING without consuming an attempt.\n // Clear abort_requested_at so an abort that was requested mid-claim does\n // not survive the release and cancel the next worker that picks the row up.\n await this.db.query(\n `\n UPDATE ${this.tableName}\n SET status = 'PENDING',\n lease_owner = NULL,\n progress = 0,\n progress_message = '',\n progress_details = NULL,\n abort_requested_at = NULL\n WHERE id = $1 AND queue = $2${prefixConditions}`,\n [jobId, this.queueName, ...prefixParams]\n );\n }\n\n /** Force-overwrite status without touching attempts (used to persist DISABLED after lease release). */\n public async saveStatus(jobId: unknown, status: string): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n await this.db.query(\n `UPDATE ${this.tableName} SET status = $1 WHERE id = $2 AND queue = $3${prefixConditions}`,\n [status, jobId, this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Retrieves all jobs for a given job run ID.\n * @param job_run_id - The ID of the job run to retrieve\n * @returns An array of jobs\n */\n public async getByRunId(job_run_id: string): Promise<Array<JobStorageFormat<Input, Output>>> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query(\n `\n SELECT * FROM ${this.tableName} WHERE job_run_id = $1 AND queue = $2${prefixConditions}`,\n [job_run_id, this.queueName, ...prefixParams]\n );\n if (!result) return [];\n return result.rows;\n }\n\n /**\n * Implements the abstract saveProgress method from JobQueue\n */\n public async saveProgress(\n jobId: unknown,\n progress: number,\n message: string,\n details: Record<string, any>\n ): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(6);\n await this.db.query(\n `\n UPDATE ${this.tableName} \n SET progress = $1,\n progress_message = $2,\n progress_details = $3\n WHERE id = $4 AND queue = $5${prefixConditions}`,\n [\n progress,\n message,\n details ? JSON.stringify(details) : null,\n jobId,\n this.queueName,\n ...prefixParams,\n ]\n );\n }\n\n /**\n * Deletes a job by its ID\n */\n public async delete(jobId: unknown): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n await this.db.query(\n `DELETE FROM ${this.tableName} WHERE id = $1 AND queue = $2${prefixConditions}`,\n [jobId, this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Finds the most recent active (PENDING or PROCESSING) job with the given fingerprint in this queue.\n * Uses the partial index idx_<table>_fingerprint_active for O(1) lookup.\n */\n public async findActiveByFingerprint(\n fingerprint: string,\n queueName: string\n ): Promise<JobStorageFormat<Input, Output> | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query<JobStorageFormat<Input, Output>>(\n `SELECT * FROM ${this.tableName}\n WHERE fingerprint = $1 AND queue = $2\n AND status IN ('PENDING','PROCESSING')${prefixConditions}\n ORDER BY created_at DESC\n LIMIT 1`,\n [fingerprint, queueName, ...prefixParams]\n );\n if (!result || result.rows.length === 0) return undefined;\n return result.rows[0];\n }\n\n /**\n * Retrieves multiple jobs by their IDs in a single query.\n * Returns results in the same order as the input ids array, with undefined for missing ids.\n */\n public async getMany(\n ids: readonly unknown[]\n ): Promise<Array<JobStorageFormat<Input, Output> | undefined>> {\n if (ids.length === 0) return [];\n // Filter to only integer-coercible IDs — Postgres SERIAL IDs are integers.\n // Non-integer ids (e.g. UUIDs passed from tests) will never match a row.\n const numericIds = ids.map((id) => Number(id)).filter((n) => Number.isInteger(n) && n > 0);\n if (numericIds.length === 0) return ids.map(() => undefined);\n // $1 = ids array, $2 = queue, $3+ = prefix params\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n const result = await this.db.query<JobStorageFormat<Input, Output>>(\n `SELECT * FROM ${this.tableName}\n WHERE id = ANY($1::int[]) AND queue = $2${prefixConditions}`,\n [numericIds, this.queueName, ...prefixParams]\n );\n if (!result) return ids.map(() => undefined);\n const map = new Map<number, JobStorageFormat<Input, Output>>();\n for (const row of result.rows) {\n map.set(Number(row.id), row);\n }\n return ids.map((id) => map.get(Number(id)));\n }\n\n /**\n * Atomically writes output and sets status=COMPLETED.\n */\n public async completeWithResult(id: unknown, result: Output): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n await this.db.query(\n `UPDATE ${this.tableName}\n SET output = $1,\n status = 'COMPLETED',\n progress = 100,\n progress_message = '',\n progress_details = NULL,\n completed_at = NOW() AT TIME ZONE 'UTC'\n WHERE id = $2 AND queue = $3${prefixConditions}`,\n [result != null ? JSON.stringify(result) : null, id, this.queueName, ...prefixParams]\n );\n }\n\n /**\n * Atomically writes error fields and sets status=FAILED.\n * COALESCEs preserve existing error/error_code when opts fields are absent.\n */\n public async failWithError(\n id: unknown,\n opts: {\n readonly error?: string | null;\n readonly errorCode?: string | null;\n readonly abortRequested?: boolean;\n }\n ): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(5);\n await this.db.query(\n `UPDATE ${this.tableName}\n SET error = COALESCE($1, error),\n error_code = COALESCE($2, error_code),\n abort_requested_at = CASE WHEN $3 THEN NOW() AT TIME ZONE 'UTC' ELSE abort_requested_at END,\n status = 'FAILED',\n completed_at = COALESCE(completed_at, NOW() AT TIME ZONE 'UTC')\n WHERE id = $4 AND queue = $5${prefixConditions}`,\n [\n \"error\" in opts ? (opts.error ?? null) : null,\n \"errorCode\" in opts ? (opts.errorCode ?? null) : null,\n opts.abortRequested === true,\n id,\n this.queueName,\n ...prefixParams,\n ]\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 const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);\n await this.db.query(\n `DELETE FROM ${this.tableName} \n WHERE queue = $1 \n AND status = $2 \n AND completed_at IS NOT NULL \n AND completed_at <= $3${prefixConditions}`,\n [this.queueName, status, cutoffDate, ...prefixParams]\n );\n }\n\n /**\n * Subscribe to INSERT/UPDATE notifications via PostgreSQL LISTEN/NOTIFY.\n * Replaces 100ms-poll fallback for cluster Postgres deployments — workers\n * wake within network-latency of an actual change rather than on a timer.\n *\n * Acquires a dedicated client from the pool (LISTEN occupies a connection\n * for its lifetime). On unsubscribe, runs UNLISTEN and releases the client.\n * On connection loss, attempts to reconnect with bounded backoff and\n * re-LISTEN. After every successful (re)connect — including the initial\n * one — emits a synthetic `{ type: \"RESYNC\" }` event so subscribers can\n * re-poll state and pick up any rows inserted during the disconnect window\n * (or between subscribe and the first NOTIFY landing).\n *\n * Throws synchronously when the underlying pool lacks `connect()`\n * (single-connection wrappers like PGLite) so the caller's try/catch can\n * fall back to polling. JobQueueServer.start does this and logs at debug.\n *\n * `options.prefixFilter` follows {@link QueueSubscribeOptions.prefixFilter}:\n * `undefined` means \"use the storage instance's configured prefixValues\",\n * `{}` means \"receive all changes regardless of prefix\".\n */\n public subscribeToChanges(\n callback: (change: QueueChangePayload<Input, Output>) => void,\n options?: QueueSubscribeOptions\n ): () => void {\n type PoolWithConnect = { connect: () => Promise<ListenClient> };\n const poolMaybe = this.db as unknown as Partial<PoolWithConnect>;\n if (typeof poolMaybe.connect !== \"function\") {\n // Detect synchronously so callers can catch and fall back to polling\n // without leaving a dangling unhandled promise rejection behind.\n throw new Error(\n \"PostgresQueueStorage.subscribeToChanges requires a pg.Pool (got a single-connection wrapper)\"\n );\n }\n const pool = poolMaybe as PoolWithConnect;\n\n const channel = this.notifyChannelName();\n // undefined -> default to instance prefixValues; {} -> no filtering;\n // partial -> filter by the provided subset (matches QueueSubscribeOptions docs).\n const effectivePrefixFilter: Readonly<Record<string, string | number>> | null =\n options?.prefixFilter === undefined\n ? this.prefixes.length > 0\n ? this.prefixValues\n : null\n : Object.keys(options.prefixFilter).length === 0\n ? null\n : options.prefixFilter;\n\n let unsubscribed = false;\n let activeClient: ListenClient | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let backoffMs = 250;\n\n const dispatch = (change: QueueChangePayload<Input, Output>): void => {\n try {\n callback(change);\n } catch (err) {\n // Never let user callbacks tear down the listener loop.\n getLogger().debug(\"PostgresQueueStorage subscribe callback threw\", {\n channel,\n changeType: change.type,\n error: err,\n });\n }\n };\n\n const matchesPrefix = (\n change: QueueChangePayload<Input, Output>,\n filter: Readonly<Record<string, string | number>>\n ): boolean => {\n const row = (change.new ?? change.old) as Record<string, unknown> | undefined;\n if (!row) return false;\n for (const [k, v] of Object.entries(filter)) {\n if (row[k] !== v) return false;\n }\n return true;\n };\n\n // Hydrate the row referenced by a notification. Postgres NOTIFY is capped\n // at 8 KB so we ship only {id, queue, status} in the payload and fetch the\n // full row here — keeps subscriber payloads consistent with other backends\n // (e.g. Supabase realtime, IndexedDb) and lets prefixFilter inspect the\n // prefix columns the row actually has.\n const hydrate = async (id: unknown): Promise<JobStorageFormat<Input, Output> | undefined> => {\n try {\n return await this.get(id);\n } catch {\n return undefined;\n }\n };\n\n const handleNotification = (msg: { channel: string; payload?: string }): void => {\n if (msg.channel !== channel || !msg.payload) return;\n let parsed: { op: string; id: number; queue: string; status: string };\n try {\n parsed = JSON.parse(msg.payload);\n } catch {\n return;\n }\n void (async () => {\n const op = parsed.op;\n const fallback = {\n id: parsed.id,\n queue: parsed.queue,\n status: parsed.status,\n } as unknown as JobStorageFormat<Input, Output>;\n const fullRow = op === \"DELETE\" ? undefined : await hydrate(parsed.id);\n const change: QueueChangePayload<Input, Output> = {\n type: op === \"INSERT\" ? \"INSERT\" : op === \"DELETE\" ? \"DELETE\" : \"UPDATE\",\n new: op === \"DELETE\" ? undefined : (fullRow ?? fallback),\n old: op === \"DELETE\" ? fallback : undefined,\n };\n if (effectivePrefixFilter && !matchesPrefix(change, effectivePrefixFilter)) return;\n dispatch(change);\n })();\n };\n\n const connect = async (): Promise<void> => {\n if (unsubscribed) return;\n try {\n const client = await pool.connect();\n if (unsubscribed) {\n client.release();\n return;\n }\n activeClient = client;\n client.on(\"notification\", handleNotification);\n client.on(\"error\", () => scheduleReconnect());\n await client.query(`LISTEN ${channel}`);\n backoffMs = 250; // reset on successful (re)connect\n // Synthetic resync: any rows inserted between subscribe and LISTEN\n // landing (or during a reconnect window) have no NOTIFY backing.\n // Fire a no-payload event so subscribers can re-poll state.\n dispatch({ type: \"RESYNC\" });\n } catch {\n scheduleReconnect();\n }\n };\n\n const scheduleReconnect = (): void => {\n if (unsubscribed || reconnectTimer) return;\n const c = activeClient;\n activeClient = null;\n try {\n c?.removeAllListeners?.(\"notification\");\n c?.removeAllListeners?.(\"error\");\n c?.release();\n } catch {\n // best-effort\n }\n const delay = Math.min(backoffMs, 30_000);\n backoffMs = Math.min(backoffMs * 2, 30_000);\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n void connect();\n }, delay);\n };\n\n void connect();\n\n return () => {\n unsubscribed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n const c = activeClient;\n activeClient = null;\n if (c) {\n // Best-effort UNLISTEN; ignore errors (connection may already be dead).\n c.query(`UNLISTEN ${channel}`)\n .catch(() => {})\n .finally(() => {\n try {\n c.removeAllListeners?.(\"notification\");\n c.removeAllListeners?.(\"error\");\n c.release();\n } catch {\n // best-effort\n }\n });\n }\n };\n }\n}\n",
|
|
6
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n type IMigration,\n type IMigrationRunner,\n type RunMigrationsOptions,\n MIGRATIONS_TABLE,\n sortMigrations,\n} from \"@workglow/storage\";\nimport type { Pool } from \"../storage/_postgres/node-bun\";\n\n/**\n * Minimal \"queryable\" shape implemented by both a checked-out node-postgres\n * client and the PGlite-backed Pool we use in browser/test contexts.\n *\n * The runner programs against this so it can run BEGIN / up() / INSERT /\n * COMMIT through whichever path is available without dragging in pg's full\n * `PoolClient` type.\n */\ninterface PgQueryable {\n query<T = unknown, P extends unknown[] = unknown[]>(\n sql: string,\n params?: P\n ): Promise<{ rows: T[] }>;\n}\n\n/**\n * `Pool.connect()` shape — present on real node-postgres pools, absent on\n * the embedded PGlite pool used in browser/tests. Detected at runtime.\n */\ntype ConnectablePool = Pool & {\n connect?: () => Promise<PgQueryable & { release: () => void }>;\n};\n\n/**\n * In-process serialization of `run()` calls per `Pool` wrapper. Keyed by\n * the wrapper object (not the underlying database), so two separate pools\n * pointing at the same database — or runners in different processes — do\n * not contend through this map. Cross-instance races are handled by the\n * bookkeeping PK check below (a 23505 from a concurrent INSERT is caught\n * and treated as \"already applied\"). Without this in-process mutex,\n * multiple runners sharing one pool would each invoke `up()` before the\n * 23505 was raised. The map is a WeakMap so disposing the pool releases\n * the entry.\n */\nconst runLocks = new WeakMap<object, Promise<unknown>>();\n\n/**\n * Runs versioned migrations against a PostgreSQL pool.\n *\n * Each migration runs inside a single connection's transaction so the\n * bookkeeping INSERT and the migration's DDL commit together. node-postgres\n * pools don't guarantee that two consecutive `pool.query()` calls hit the\n * same client, so we check out a dedicated connection via `pool.connect()`\n * when available. The embedded PGlite pool used in browser/tests has no\n * `connect()` (it is single-client by construction), so we fall back to\n * the raw pool — BEGIN/COMMIT on it still hit the same backing connection.\n *\n * Concurrent `run()` calls against the same pool are serialized through a\n * JS-layer mutex so racing runners see each others' bookkeeping rows before\n * deciding to invoke `up()`. The unique constraint on `(component, version)`\n * remains a defense-in-depth check — a 23505 from a separate process (or a\n * different pool instance backed by the same database) is still treated as\n * \"already applied\".\n */\nexport class PostgresMigrationRunner implements IMigrationRunner<Pool> {\n constructor(private readonly db: Pool) {}\n\n async ensureBookkeepingTable(): Promise<void> {\n await this.db.query(`\n CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (\n component TEXT NOT NULL,\n version INTEGER NOT NULL,\n description TEXT,\n applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),\n PRIMARY KEY (component, version)\n )\n `);\n }\n\n async appliedVersions(component: string): Promise<Set<number>> {\n const result = await this.db.query<{ version: number }, [string]>(\n `SELECT version FROM ${MIGRATIONS_TABLE} WHERE component = $1`,\n [component]\n );\n return new Set(result.rows.map((r) => Number(r.version)));\n }\n\n /**\n * Acquires a transaction-scoped queryable. Returns the raw pool when the\n * underlying driver doesn't expose `connect()` (PGlite); the `release()`\n * callback is then a no-op.\n */\n private async acquireClient(): Promise<{ client: PgQueryable; release: () => void }> {\n const pool = this.db as ConnectablePool;\n if (typeof pool.connect === \"function\") {\n const client = await pool.connect();\n return { client, release: () => client.release() };\n }\n return { client: this.db as unknown as PgQueryable, release: () => undefined };\n }\n\n async run(\n migrations: ReadonlyArray<IMigration<Pool>>,\n options: RunMigrationsOptions = {}\n ): Promise<ReadonlyArray<IMigration<Pool>>> {\n const key = this.db as unknown as object;\n const prev = runLocks.get(key) ?? Promise.resolve();\n const result = prev.then(() => this.runInternal(migrations, options));\n runLocks.set(\n key,\n result.catch(() => undefined)\n );\n return result;\n }\n\n private async runInternal(\n migrations: ReadonlyArray<IMigration<Pool>>,\n options: RunMigrationsOptions\n ): Promise<ReadonlyArray<IMigration<Pool>>> {\n await this.ensureBookkeepingTable();\n const sorted = sortMigrations(migrations);\n const applied: IMigration<Pool>[] = [];\n const cache = new Map<string, Set<number>>();\n const onProgress = options.onProgress;\n\n for (const m of sorted) {\n let seen = cache.get(m.component);\n if (!seen) {\n seen = await this.appliedVersions(m.component);\n cache.set(m.component, seen);\n }\n if (seen.has(m.version)) continue;\n\n onProgress?.({\n component: m.component,\n version: m.version,\n phase: \"starting\",\n description: m.description,\n });\n\n const { client, release } = await this.acquireClient();\n try {\n await client.query(\"BEGIN\");\n // Migrations are written against the `Pool` type but only need a\n // queryable surface. Casting keeps the public signature stable while\n // routing the migration's queries through the dedicated client.\n await m.up(client as unknown as Pool, (fraction) => {\n onProgress?.({\n component: m.component,\n version: m.version,\n phase: \"running\",\n description: m.description,\n fraction,\n });\n });\n await client.query(\n `INSERT INTO ${MIGRATIONS_TABLE}(component, version, description) VALUES ($1, $2, $3)`,\n [m.component, m.version, m.description ?? null]\n );\n await client.query(\"COMMIT\");\n seen.add(m.version);\n applied.push(m);\n onProgress?.({\n component: m.component,\n version: m.version,\n phase: \"completed\",\n description: m.description,\n fraction: 1,\n });\n } catch (err: unknown) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n // Concurrent runner already inserted — treat as success.\n if ((err as { code?: string })?.code === \"23505\") {\n seen.add(m.version);\n onProgress?.({\n component: m.component,\n version: m.version,\n phase: \"completed\",\n description: m.description,\n fraction: 1,\n });\n continue;\n }\n onProgress?.({\n component: m.component,\n version: m.version,\n phase: \"failed\",\n description: m.description,\n error: err,\n });\n throw err;\n } finally {\n release();\n }\n }\n\n return applied;\n }\n}\n",
|
|
7
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { JobStatus, type PrefixColumn } from \"@workglow/job-queue\";\nimport {\n buildPrefixColumnsSql,\n getPrefixIndexPrefix,\n getPrefixIndexSuffix,\n type IMigration,\n PostgresDialect,\n} from \"@workglow/storage\";\nimport type { Pool } from \"../storage/_postgres/node-bun\";\n\n/**\n * Frozen v1 set of `job_status` enum values, captured at migration creation\n * time. Migration bodies are historical artifacts and MUST NOT read the\n * mutable {@link JobStatus} const directly: a fresh DB created after a value\n * is added to the const would receive the new value, while a DB already at v1\n * would not — silently producing version-skewed enums and runtime errors on\n * insert. Adding a status requires a NEW migration that runs\n * `ALTER TYPE job_status ADD VALUE IF NOT EXISTS '...'`.\n *\n * ABORTING was present in v1 and removed from the application model in PR 2.\n * It remains in the v1 enum literal so existing databases are not broken;\n * the application simply no longer writes that value.\n */\nconst JOB_STATUS_V1: readonly string[] = [\n \"PENDING\",\n \"PROCESSING\",\n \"COMPLETED\",\n \"ABORTING\",\n \"FAILED\",\n \"DISABLED\",\n];\n\n/**\n * Sanity check: every current {@link JobStatus} value must be covered by the\n * v1 enum (or a subsequent ALTER TYPE migration). ABORTING was intentionally\n * removed from the application model; it is still legal in the DB schema but\n * we skip it here so the check does not reject a valid removal.\n *\n * Run lazily from {@link postgresQueueMigrations} (NOT at module import) so\n * that consumers re-exporting this module via barrel files don't crash on\n * import when they have no intention of running migrations.\n */\nfunction assertJobStatusMatchesV1(): void {\n const current = new Set(Object.values(JobStatus));\n // Every current status must be present in the v1 enum (or added by a later migration).\n for (const v of current) {\n if (!JOB_STATUS_V1.includes(v)) {\n throw new Error(\n `JobStatus contains \"${v}\" which is not in JOB_STATUS_V1. ` +\n `Add a new migration that runs \"ALTER TYPE job_status ADD VALUE IF NOT EXISTS '${v}'\" ` +\n `instead of mutating the v1 enum literal.`\n );\n }\n }\n}\n\n/**\n * Initial migration set for the Postgres queue table identified by `tableName`.\n *\n * Component name is `queue:postgres:<tableName>` so two queues with different\n * table names get tracked independently in `_storage_migrations`. The v1\n * payload covers schema + indexes + LISTEN/NOTIFY plumbing; the trigger is\n * idempotent (`CREATE OR REPLACE FUNCTION` + `DROP TRIGGER IF EXISTS`).\n *\n * v1 is FROZEN byte-for-byte against the pre-PR shape — it MUST keep\n * creating the `run_after`/`run_attempts`/`max_retries`/`last_ran_at`/\n * `worker_id` columns and the corresponding `run_after`-keyed indexes.\n * Renames and index swaps live in v3 (with `IF EXISTS` guards so a fresh\n * install — which still goes through v1 — can apply v3 without errors).\n * Mutating v1 would silently produce divergent schemas between fresh and\n * already-migrated DBs and break older deployments mid-rollout.\n */\nexport function postgresQueueMigrations(\n tableName: string,\n prefixes: readonly PrefixColumn[]\n): IMigration<Pool>[] {\n assertJobStatusMatchesV1();\n const component = `queue:postgres:${tableName}`;\n const prefixColumnsSql = buildPrefixColumnsSql(PostgresDialect, prefixes);\n const prefixIndexPrefix = getPrefixIndexPrefix(prefixes);\n const indexSuffix = getPrefixIndexSuffix(prefixes);\n\n return [\n {\n component,\n version: 1,\n description: \"Create job_status enum + queue table + indexes + notify trigger\",\n async up(db: Pool) {\n // Enum literal is the frozen v1 set, NOT Object.values(JobStatus).\n // See JOB_STATUS_V1 for why.\n const enumLiteral = JOB_STATUS_V1.map((v) => `'${v}'`).join(\",\");\n // DO block so the existence check + CREATE TYPE happen in one\n // statement. A bare `CREATE TYPE ...` raising duplicate_object inside\n // a transaction would leave it aborted (Postgres state 25P02), and\n // the runner's BEGIN/COMMIT would reject every subsequent statement.\n await db.query(`\n DO $$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_status') THEN\n CREATE TYPE job_status AS ENUM (${enumLiteral});\n END IF;\n END $$;\n `);\n\n await db.query(`\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id SERIAL NOT NULL,\n ${prefixColumnsSql}fingerprint text NOT NULL,\n queue text NOT NULL,\n job_run_id text NOT NULL,\n status job_status NOT NULL default 'PENDING',\n input jsonb NOT NULL,\n output jsonb,\n run_attempts integer default 0,\n max_retries integer default 20,\n run_after timestamp with time zone DEFAULT now(),\n last_ran_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now(),\n deadline_at timestamp with time zone,\n completed_at timestamp with time zone,\n error text,\n error_code text,\n progress real DEFAULT 0,\n progress_message text DEFAULT '',\n progress_details jsonb,\n worker_id text\n )\n `);\n\n await db.query(`\n CREATE INDEX IF NOT EXISTS job_fetcher${indexSuffix}_idx\n ON ${tableName} (${prefixIndexPrefix}id, status, run_after)\n `);\n await db.query(`\n CREATE INDEX IF NOT EXISTS job_queue_fetcher${indexSuffix}_idx\n ON ${tableName} (${prefixIndexPrefix}queue, status, run_after)\n `);\n await db.query(`\n CREATE INDEX IF NOT EXISTS jobs_fingerprint${indexSuffix}_unique_idx\n ON ${tableName} (${prefixIndexPrefix}queue, fingerprint, status)\n `);\n\n // Install LISTEN/NOTIFY plumbing so subscribers can wake on\n // INSERT/UPDATE without polling. Best-effort: in-process\n // Postgres-compatible engines like PGLite may not implement pg_notify\n // or plpgsql. Skip trigger installation in that case — the queue's\n // subscribeToChanges throws synchronously for those engines anyway.\n //\n // Wrapped in a SAVEPOINT because this migration runs inside the\n // runner's BEGIN/COMMIT. Without the savepoint, any error in the\n // trigger DDL would put the outer transaction into aborted state\n // (Postgres 25P02), poisoning the runner's bookkeeping INSERT and\n // failing the whole migration — even though the trigger itself is\n // optional. ROLLBACK TO SAVEPOINT lets us discard just the failed\n // trigger work and keep the rest of the migration committable.\n const fnName = `${tableName}_notify`;\n const trgName = `${tableName}_notify_trg`;\n await db.query(\"SAVEPOINT install_notify_trigger\");\n try {\n await db.query(`\n CREATE OR REPLACE FUNCTION ${fnName}() RETURNS trigger AS $fn$\n DECLARE\n channel TEXT := 'wglw_q_' || md5('${tableName}' || COALESCE(NEW.queue, OLD.queue));\n payload TEXT;\n BEGIN\n payload := json_build_object(\n 'op', TG_OP,\n 'id', COALESCE(NEW.id, OLD.id),\n 'queue', COALESCE(NEW.queue, OLD.queue),\n 'status', COALESCE(NEW.status::text, OLD.status::text)\n )::text;\n PERFORM pg_notify(channel, payload);\n RETURN NULL;\n END;\n $fn$ LANGUAGE plpgsql;\n `);\n await db.query(`DROP TRIGGER IF EXISTS ${trgName} ON ${tableName}`);\n await db.query(`\n CREATE TRIGGER ${trgName}\n AFTER INSERT OR UPDATE ON ${tableName}\n FOR EACH ROW EXECUTE FUNCTION ${fnName}();\n `);\n await db.query(\"RELEASE SAVEPOINT install_notify_trigger\");\n } catch {\n // Engine doesn't support LISTEN/NOTIFY; rewind to the savepoint so\n // the outer transaction stays usable, and let subscribers fall\n // back to polling.\n await db.query(\"ROLLBACK TO SAVEPOINT install_notify_trigger\").catch(() => undefined);\n await db.query(\"RELEASE SAVEPOINT install_notify_trigger\").catch(() => undefined);\n }\n },\n },\n {\n component,\n version: 2,\n description: \"Add abort_requested_at and lease_expires_at columns\",\n async up(db: Pool) {\n await db.query(`\n ALTER TABLE ${tableName}\n ADD COLUMN IF NOT EXISTS abort_requested_at timestamp with time zone,\n ADD COLUMN IF NOT EXISTS lease_expires_at timestamp with time zone\n `);\n },\n },\n {\n component,\n version: 3,\n description:\n \"Rename run_after→visible_at, last_ran_at→last_attempted_at, run_attempts→attempts, max_retries→max_attempts, worker_id→lease_owner; drop run_after-keyed indexes and recreate visible_at-keyed\",\n async up(db: Pool) {\n // Each rename is guarded by IF EXISTS — fresh installs (which still\n // run v1 → v2 → v3) skip every branch and end up with the v1 schema\n // exactly. Existing installs from before this PR get renamed in place.\n await db.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='run_after' AND table_schema=current_schema()) THEN\n EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN run_after TO visible_at';\n END IF;\n IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='last_ran_at' AND table_schema=current_schema()) THEN\n EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN last_ran_at TO last_attempted_at';\n END IF;\n IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='run_attempts' AND table_schema=current_schema()) THEN\n EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN run_attempts TO attempts';\n END IF;\n IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='max_retries' AND table_schema=current_schema()) THEN\n EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN max_retries TO max_attempts';\n EXECUTE 'ALTER TABLE ${tableName} ALTER COLUMN max_attempts SET DEFAULT 10';\n END IF;\n IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='worker_id' AND table_schema=current_schema()) THEN\n EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN worker_id TO lease_owner';\n END IF;\n END $$\n `);\n\n // Drop the v1 run_after-keyed indexes and recreate them keyed on\n // visible_at. CREATE INDEX cannot be wrapped in CONCURRENTLY here\n // because the migration runs inside a transaction. The old indexes\n // are useless after the rename — PostgreSQL automatically retargets\n // them onto `visible_at` post-rename, but their NAMES still encode\n // the old column, which is confusing for operators and slot-binds\n // against the wrong stats; doing an explicit DROP + CREATE swap\n // keeps names and schemas consistent.\n await db.query(`DROP INDEX IF EXISTS job_fetcher${indexSuffix}_idx`);\n await db.query(`DROP INDEX IF EXISTS job_queue_fetcher${indexSuffix}_idx`);\n await db.query(`\n CREATE INDEX IF NOT EXISTS job_fetcher${indexSuffix}_idx\n ON ${tableName} (${prefixIndexPrefix}id, status, visible_at)\n `);\n await db.query(`\n CREATE INDEX IF NOT EXISTS job_queue_fetcher${indexSuffix}_idx\n ON ${tableName} (${prefixIndexPrefix}queue, status, visible_at)\n `);\n },\n },\n {\n component,\n version: 4,\n description:\n \"Add UNIQUE partial index for findActiveByFingerprint O(1) lookup + fingerprint dedup at the DB layer (H2)\",\n async up(db: Pool) {\n // H2: UNIQUE so concurrent send() calls with the same fingerprint can\n // race to INSERT and have the DB resolve the winner via a 23505 unique-\n // violation. The cloud-queue adapters used to optimistically check\n // findActiveByFingerprint then insert, leaving a TOCTOU window. With\n // UNIQUE in place, the insert path can catch the violation and\n // re-resolve via findActiveByFingerprint. Edited in place rather than\n // a v5 because PR #516 is still unmerged at the time of this fixup,\n // so no consumer has applied v4 yet — confirmed via PR state at the\n // top of the worktree session.\n await db.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS idx_${tableName}_fingerprint_active\n ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)\n WHERE status IN ('PENDING','PROCESSING')\n `);\n },\n },\n {\n component,\n version: 5,\n description:\n \"Converge idx_<table>_fingerprint_active to UNIQUE for DBs that applied the pre-edit v4 (non-unique) variant\",\n async up(db: Pool) {\n // PR #516 reviewers flagged that v4 was edited in place after some\n // pull-request consumers may already have applied the pre-edit\n // (non-unique) variant. The migration runner keys on (component,\n // version) alone, so an already-recorded v4 is never re-run — those\n // DBs would stay non-unique forever. v5 inspects the existing index\n // and converts it if needed, idempotently.\n const indexName = `idx_${tableName}_fingerprint_active`;\n const result = await db.query<{ indisunique: boolean }>(\n `SELECT i.indisunique\n FROM pg_class c\n JOIN pg_index i ON i.indexrelid = c.oid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relname = $1\n AND n.nspname = current_schema()`,\n [indexName]\n );\n const existing = result.rows[0];\n if (existing && existing.indisunique) {\n // Already UNIQUE — DB applied the post-edit v4 (or this v5 ran\n // previously). No-op.\n return;\n }\n // Either the index is missing entirely or it's the pre-edit\n // non-unique variant. Drop and recreate as UNIQUE. We use the same\n // canonical name so downstream code (PostgresQueueStorage,\n // SupabaseQueueStorage, the H2 race tests) keeps finding it by name.\n await db.query(`DROP INDEX IF EXISTS ${indexName}`);\n await db.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS ${indexName}\n ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)\n WHERE status IN ('PENDING','PROCESSING')\n `);\n },\n },\n ];\n}\n",
|
|
8
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type {\n IRateLimiterStorage,\n PrefixColumn,\n RateLimiterStorageOptions,\n RateLimiterStorageScope,\n} from \"@workglow/job-queue\";\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport {\n assertPrefixesSafe,\n buildPrefixWhereClause,\n getPrefixColumnNames,\n getPrefixParamValues,\n PostgresDialect,\n} from \"@workglow/storage\";\nimport { createServiceToken } from \"@workglow/util\";\nimport { PostgresMigrationRunner } from \"../migrations/PostgresMigrationRunner\";\nimport { postgresRateLimiterMigrations } from \"../migrations/postgresRateLimiterMigrations\";\n\nexport const POSTGRES_RATE_LIMITER_STORAGE = createServiceToken<IRateLimiterStorage>(\n \"ratelimiter.storage.postgres\"\n);\n\n/**\n * PostgreSQL implementation of rate limiter storage.\n * Manages execution records and next available times for rate limiting.\n */\nexport class PostgresRateLimiterStorage implements IRateLimiterStorage {\n public readonly scope: RateLimiterStorageScope = \"cluster\";\n /** The prefix column definitions */\n protected readonly prefixes: readonly PrefixColumn[];\n /** The prefix values for filtering */\n protected readonly prefixValues: Readonly<Record<string, string | number>>;\n /** The table name for execution tracking */\n protected readonly executionTableName: string;\n /** The table name for next available times */\n protected readonly nextAvailableTableName: string;\n\n constructor(\n protected readonly db: Pool,\n options?: RateLimiterStorageOptions\n ) {\n this.prefixes = options?.prefixes ?? [];\n this.prefixValues = options?.prefixValues ?? {};\n // Validate prefix column names before deriving table names from them.\n assertPrefixesSafe(this.prefixes);\n\n // Generate table names based on prefix configuration\n if (this.prefixes.length > 0) {\n const prefixNames = this.prefixes.map((p) => p.name).join(\"_\");\n this.executionTableName = `rate_limit_executions_${prefixNames}`;\n this.nextAvailableTableName = `rate_limit_next_available_${prefixNames}`;\n } else {\n this.executionTableName = \"rate_limit_executions\";\n this.nextAvailableTableName = \"rate_limit_next_available\";\n }\n }\n\n /** WHERE-clause helper specialized for the Postgres dialect. */\n private buildPrefixWhereClause(startParam: number) {\n return buildPrefixWhereClause(PostgresDialect, this.prefixes, this.prefixValues, startParam);\n }\n\n /** Returns prefix values in column order. */\n private getPrefixParamValues(): Array<string | number> {\n return getPrefixParamValues(this.prefixes, this.prefixValues);\n }\n\n /**\n * Returns the versioned migrations that this storage's tables depend on.\n * Callers can compose them with other storages' migrations under a shared\n * {@link PostgresMigrationRunner}; otherwise call {@link migrate}.\n */\n public getMigrations() {\n return postgresRateLimiterMigrations(\n this.executionTableName,\n this.nextAvailableTableName,\n this.prefixes\n );\n }\n\n /** Applies any pending migrations for this rate limiter's tables. */\n public async migrate(): Promise<void> {\n await new PostgresMigrationRunner(this.db).run(this.getMigrations());\n }\n\n public async tryReserveExecution(\n queueName: string,\n maxExecutions: number,\n windowMs: number\n ): Promise<unknown | null> {\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const prefixParamValues = this.getPrefixParamValues();\n const prefixCount = prefixColumnNames.length;\n\n // Parameter layout:\n // $1..$N prefix values (used in lock-key, count, next-available, and INSERT)\n // $(N+1) queueName\n // $(N+2) windowStart timestamp\n // $(N+3) maxExecutions\n const queueParam = `$${prefixCount + 1}`;\n const windowStartParam = `$${prefixCount + 2}`;\n\n // For the advisory lock we hash table-name + prefix values + queue-name into a bigint.\n // Use hashtextextended which returns int8 (advisory_xact_lock takes bigint).\n const lockKeyParts: string[] = [`'${this.executionTableName}'`];\n for (let i = 0; i < prefixCount; i++) {\n lockKeyParts.push(`$${i + 1}::text`);\n }\n lockKeyParts.push(`${queueParam}::text`);\n const lockKeyExpr = `hashtextextended(${lockKeyParts.join(\" || '|' || \")}, 0)`;\n\n const prefixWhere =\n prefixCount > 0\n ? \" AND \" + prefixColumnNames.map((p, i) => `${p} = $${i + 1}`).join(\" AND \")\n : \"\";\n\n const prefixInsertCols = prefixCount > 0 ? prefixColumnNames.join(\", \") + \", \" : \"\";\n const prefixInsertPlaceholders =\n prefixCount > 0 ? prefixColumnNames.map((_, i) => `$${i + 1}`).join(\", \") + \", \" : \"\";\n\n const windowStart = new Date(Date.now() - windowMs).toISOString();\n\n // Use a dedicated client when the pool supports connect() (real pg.Pool)\n // — the advisory xact lock + transaction MUST run on the same connection\n // for atomicity. Without connect(), only known single-connection wrappers\n // (PGLitePool, raw PGlite) are safe, since they implicitly serialize all\n // queries through one underlying connection. Anything else (e.g. a custom\n // multi-connection adapter without connect()) would dispatch the lock and\n // the INSERT to different sessions, defeating the lock entirely.\n const supportsConnect =\n typeof (this.db as unknown as { connect?: unknown }).connect === \"function\";\n if (!supportsConnect) {\n // Without connect() we run BEGIN/advisory_lock/INSERT/COMMIT directly on\n // `db.query`. That's only safe if `db` is a single-connection wrapper\n // (every query goes through the same underlying session). Recognize:\n // - PGLitePool — our own wrapper class; constructor name preserved.\n // - PGlite — third-party, ships minified so `constructor.name`\n // can be obfuscated (observed: \"q\"). Detect via duck-typing on\n // methods PGlite uniquely exposes (`waitReady` Promise + `exec`).\n // Any other no-connect() pool would dispatch the lock and the INSERT\n // to potentially different sessions, breaking atomicity.\n const dbAny = this.db as unknown as {\n waitReady?: unknown;\n exec?: unknown;\n constructor?: { name?: string };\n };\n const ctorName = dbAny.constructor?.name;\n const looksLikePGlite = typeof dbAny.exec === \"function\" && dbAny.waitReady !== undefined;\n const looksLikePGLitePool = ctorName === \"PGLitePool\";\n if (!looksLikePGlite && !looksLikePGLitePool) {\n throw new Error(\n `PostgresRateLimiterStorage.tryReserveExecution requires a pg.Pool with connect() or a known single-connection wrapper (PGLitePool, PGlite); got ${ctorName ?? typeof this.db}. A multi-connection pool without connect() would dispatch the advisory lock and the INSERT to different sessions, breaking atomicity.`\n );\n }\n }\n const conn = supportsConnect\n ? await (\n this.db as unknown as {\n connect: () => Promise<{\n query: Pool[\"query\"];\n release: () => void;\n }>;\n }\n ).connect()\n : { query: this.db.query.bind(this.db), release: () => {} };\n\n try {\n await conn.query(\"BEGIN\");\n try {\n await conn.query(`SELECT pg_advisory_xact_lock(${lockKeyExpr})`, [\n ...prefixParamValues,\n queueName,\n ]);\n\n const countResult = await conn.query(\n `\n SELECT COUNT(*)::int AS n\n FROM ${this.executionTableName}\n WHERE queue_name = ${queueParam} AND executed_at > ${windowStartParam}${prefixWhere}\n `,\n [...prefixParamValues, queueName, windowStart]\n );\n const n: number = countResult.rows[0]?.n ?? 0;\n if (n >= maxExecutions) {\n await conn.query(\"COMMIT\");\n return null;\n }\n\n const naResult = await conn.query(\n `\n SELECT next_available_at\n FROM ${this.nextAvailableTableName}\n WHERE queue_name = ${queueParam}${prefixWhere}\n `,\n [...prefixParamValues, queueName]\n );\n const nextAvailableAt: Date | null = naResult.rows[0]?.next_available_at ?? null;\n if (nextAvailableAt && new Date(nextAvailableAt).getTime() > Date.now()) {\n await conn.query(\"COMMIT\");\n return null;\n }\n\n const insertResult = await conn.query(\n `\n INSERT INTO ${this.executionTableName} (${prefixInsertCols}queue_name)\n VALUES (${prefixInsertPlaceholders}${queueParam})\n RETURNING id\n `,\n [...prefixParamValues, queueName]\n );\n await conn.query(\"COMMIT\");\n return insertResult.rows[0]?.id ?? null;\n } catch (err) {\n try {\n await conn.query(\"ROLLBACK\");\n } catch {\n // best-effort\n }\n throw err;\n }\n } finally {\n conn.release();\n }\n }\n\n public async releaseExecution(queueName: string, token: unknown): Promise<void> {\n if (token === null || token === undefined) return;\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n // Delete by id — NEVER by recency. Two concurrent acquirers can hold\n // tokens for different rows; deleting \"the most recent\" would release the\n // other worker's slot.\n await this.db.query(\n `DELETE FROM ${this.executionTableName} WHERE id = $1 AND queue_name = $2${prefixConditions}`,\n [token as string | number, queueName, ...prefixParams]\n );\n }\n\n public async recordExecution(queueName: string): Promise<void> {\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const prefixColumnsInsert =\n prefixColumnNames.length > 0 ? prefixColumnNames.join(\", \") + \", \" : \"\";\n const prefixParamValues = this.getPrefixParamValues();\n const prefixParamPlaceholders =\n prefixColumnNames.length > 0\n ? prefixColumnNames.map((_, i) => `$${i + 1}`).join(\", \") + \", \"\n : \"\";\n const queueParamNum = prefixColumnNames.length + 1;\n\n await this.db.query(\n `\n INSERT INTO ${this.executionTableName} (${prefixColumnsInsert}queue_name)\n VALUES (${prefixParamPlaceholders}$${queueParamNum})\n `,\n [...prefixParamValues, queueName]\n );\n }\n\n public async getExecutionCount(queueName: string, windowStartTime: string): Promise<number> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n\n const result = await this.db.query(\n `\n SELECT COUNT(*) AS count\n FROM ${this.executionTableName}\n WHERE queue_name = $1 AND executed_at > $2${prefixConditions}\n `,\n [queueName, windowStartTime, ...prefixParams]\n );\n\n return parseInt(result.rows[0]?.count ?? \"0\", 10);\n }\n\n public async getOldestExecutionAtOffset(\n queueName: string,\n offset: number\n ): Promise<string | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);\n\n const result = await this.db.query(\n `\n SELECT executed_at\n FROM ${this.executionTableName}\n WHERE queue_name = $1${prefixConditions}\n ORDER BY executed_at ASC\n LIMIT 1 OFFSET $2\n `,\n [queueName, offset, ...prefixParams]\n );\n\n const executedAt = result.rows[0]?.executed_at;\n if (!executedAt) return undefined;\n return new Date(executedAt).toISOString();\n }\n\n public async getNextAvailableTime(queueName: string): Promise<string | undefined> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);\n\n const result = await this.db.query(\n `\n SELECT next_available_at\n FROM ${this.nextAvailableTableName}\n WHERE queue_name = $1${prefixConditions}\n `,\n [queueName, ...prefixParams]\n );\n\n const nextAvailableAt = result.rows[0]?.next_available_at;\n if (!nextAvailableAt) return undefined;\n return new Date(nextAvailableAt).toISOString();\n }\n\n public async setNextAvailableTime(queueName: string, nextAvailableAt: string): Promise<void> {\n const prefixColumnNames = getPrefixColumnNames(this.prefixes);\n const prefixColumnsInsert =\n prefixColumnNames.length > 0 ? prefixColumnNames.join(\", \") + \", \" : \"\";\n const prefixParamValues = this.getPrefixParamValues();\n const prefixParamPlaceholders =\n prefixColumnNames.length > 0\n ? prefixColumnNames.map((_, i) => `$${i + 1}`).join(\", \") + \", \"\n : \"\";\n const baseParamStart = prefixColumnNames.length + 1;\n\n // Build the conflict columns for upsert\n const conflictColumns =\n prefixColumnNames.length > 0 ? `${prefixColumnNames.join(\", \")}, queue_name` : \"queue_name\";\n\n await this.db.query(\n `\n INSERT INTO ${this.nextAvailableTableName} (${prefixColumnsInsert}queue_name, next_available_at)\n VALUES (${prefixParamPlaceholders}$${baseParamStart}, $${baseParamStart + 1})\n ON CONFLICT (${conflictColumns})\n DO UPDATE SET next_available_at = EXCLUDED.next_available_at\n `,\n [...prefixParamValues, queueName, nextAvailableAt]\n );\n }\n\n public async clear(queueName: string): Promise<void> {\n const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);\n\n await this.db.query(\n `DELETE FROM ${this.executionTableName} WHERE queue_name = $1${prefixConditions}`,\n [queueName, ...prefixParams]\n );\n await this.db.query(\n `DELETE FROM ${this.nextAvailableTableName} WHERE queue_name = $1${prefixConditions}`,\n [queueName, ...prefixParams]\n );\n }\n}\n",
|
|
9
|
+
"/**\n * @license\n * Copyright 2025 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { PrefixColumn } from \"@workglow/job-queue\";\nimport {\n buildPrefixColumnsSql,\n getPrefixColumnNames,\n getPrefixIndexPrefix,\n getPrefixIndexSuffix,\n type IMigration,\n PostgresDialect,\n} from \"@workglow/storage\";\nimport type { Pool } from \"../storage/_postgres/node-bun\";\n\n/** Initial migration set for the Postgres rate-limiter tables. */\nexport function postgresRateLimiterMigrations(\n executionTableName: string,\n nextAvailableTableName: string,\n prefixes: readonly PrefixColumn[]\n): IMigration<Pool>[] {\n const component = `rate-limiter:postgres:${executionTableName}`;\n const prefixColumnsSql = buildPrefixColumnsSql(PostgresDialect, prefixes);\n const prefixColumnNames = getPrefixColumnNames(prefixes);\n const prefixIndexPrefix = getPrefixIndexPrefix(prefixes);\n const indexSuffix = getPrefixIndexSuffix(prefixes);\n const primaryKeyColumns =\n prefixColumnNames.length > 0 ? `${prefixColumnNames.join(\", \")}, queue_name` : \"queue_name\";\n\n return [\n {\n component,\n version: 1,\n description: \"Create rate-limiter execution + next_available tables\",\n async up(db: Pool) {\n await db.query(`\n CREATE TABLE IF NOT EXISTS ${executionTableName} (\n id SERIAL PRIMARY KEY,\n ${prefixColumnsSql}queue_name TEXT NOT NULL,\n executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n await db.query(`\n CREATE INDEX IF NOT EXISTS rate_limit_exec_queue${indexSuffix}_idx\n ON ${executionTableName} (${prefixIndexPrefix}queue_name, executed_at)\n `);\n await db.query(`\n CREATE TABLE IF NOT EXISTS ${nextAvailableTableName} (\n ${prefixColumnsSql}queue_name TEXT NOT NULL,\n next_available_at TIMESTAMP WITH TIME ZONE,\n PRIMARY KEY (${primaryKeyColumns})\n )\n `);\n },\n },\n ];\n}\n",
|
|
10
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type {\n IClaim,\n IMessageQueue,\n JobStorageFormat,\n MessageId,\n QueueChangePayload,\n QueueStorageScope,\n QueueSubscribeOptions,\n SendOptions,\n} from \"@workglow/job-queue\";\nimport type { PostgresQueueStorage } from \"./PostgresQueueStorage\";\n\n/**\n * Per-id buffer that lets {@link IJobStore.saveResult}/{@link IJobStore.saveError}\n * stage output/error until the terminal claim.ack()/fail() persists them in\n * a single complete() call (avoids double-bumping `attempts`).\n */\nexport type PostgresPendingWrite<Output> = {\n output?: Output | null;\n error?: string | null;\n errorCode?: string | null;\n abortRequested?: boolean;\n};\n\nclass PostgresClaim<Input, Output> implements IClaim<JobStorageFormat<Input, Output>> {\n constructor(\n private readonly core: PostgresQueueStorage<Input, Output>,\n private readonly pending: Map<unknown, PostgresPendingWrite<Output>>,\n public readonly id: MessageId,\n public readonly body: JobStorageFormat<Input, Output>,\n public readonly attempts: number,\n private readonly workerId: string\n ) {}\n\n async ack(result?: unknown): Promise<void> {\n const buf = this.pending.get(this.id);\n this.pending.delete(this.id);\n const current = (await this.core.get(this.id)) ?? this.body;\n const output =\n result !== undefined\n ? result\n : buf?.output !== undefined\n ? buf.output\n : (current.output ?? null);\n await this.core.finalize(this.id, {\n output: output as Output | null,\n error: null,\n error_code: null,\n status: \"COMPLETED\",\n completed_at: current.completed_at ?? new Date().toISOString(),\n });\n }\n\n async retry(opts?: { delaySeconds?: number }): Promise<void> {\n this.pending.delete(this.id);\n const delay = opts?.delaySeconds ?? 0;\n const current = (await this.core.get(this.id)) ?? this.body;\n await this.core.complete({\n ...current,\n status: \"PENDING\",\n lease_owner: null,\n lease_expires_at: null,\n visible_at: new Date(Date.now() + delay * 1000).toISOString(),\n progress: 0,\n progress_message: \"\",\n progress_details: null,\n });\n }\n\n async fail(opts?: {\n error?: string | null;\n errorCode?: string | null;\n abortRequested?: boolean;\n permanent?: boolean;\n }): Promise<void> {\n void opts?.permanent;\n const buf = this.pending.get(this.id);\n this.pending.delete(this.id);\n const current = (await this.core.get(this.id)) ?? this.body;\n const error =\n opts?.error !== undefined\n ? opts.error\n : buf?.error !== undefined\n ? buf.error\n : (current.error ?? null);\n const errorCode =\n opts?.errorCode !== undefined\n ? opts.errorCode\n : buf?.errorCode !== undefined\n ? buf.errorCode\n : (current.error_code ?? null);\n const abortRequested =\n opts?.abortRequested !== undefined ? opts.abortRequested : (buf?.abortRequested ?? false);\n await this.core.finalize(this.id, {\n error,\n error_code: errorCode,\n abort_requested_at: abortRequested\n ? (current.abort_requested_at ?? new Date().toISOString())\n : (current.abort_requested_at ?? null),\n status: \"FAILED\",\n completed_at: current.completed_at ?? new Date().toISOString(),\n });\n }\n\n async extendLease(ms: number): Promise<void> {\n await this.core.extendLease(this.id, this.workerId, ms);\n }\n\n async disable(): Promise<void> {\n this.pending.delete(this.id);\n const current = await this.core.get(this.id);\n const completedAt = current?.completed_at ?? new Date().toISOString();\n await this.core.finalize(this.id, {\n status: \"DISABLED\",\n completed_at: completedAt,\n lease_owner: null,\n progress: 0,\n progress_message: \"\",\n progress_details: null,\n });\n }\n}\n\nexport class PostgresMessageQueue<Input, Output> implements IMessageQueue<\n JobStorageFormat<Input, Output>\n> {\n public readonly scope: QueueStorageScope;\n\n /** @internal — shared with the paired job store */\n public readonly core: PostgresQueueStorage<Input, Output>;\n\n /** @internal — shared transient buffer for saveResult/saveError. */\n private readonly pending: Map<unknown, PostgresPendingWrite<Output>>;\n\n constructor(\n core: PostgresQueueStorage<Input, Output>,\n pending: Map<unknown, PostgresPendingWrite<Output>>\n ) {\n this.core = core;\n this.pending = pending;\n this.scope = core.scope;\n }\n\n async send(body: JobStorageFormat<Input, Output>, opts?: SendOptions): Promise<MessageId> {\n return this.core.add(applySendOptions(body, opts));\n }\n\n async sendBatch(\n bodies: readonly JobStorageFormat<Input, Output>[],\n opts?: SendOptions\n ): Promise<readonly MessageId[]> {\n const ids: MessageId[] = [];\n for (const body of bodies) {\n ids.push(await this.send(body, opts));\n }\n return ids;\n }\n\n async receive(opts: {\n workerId: string;\n leaseMs: number;\n max?: number;\n }): Promise<readonly IClaim<JobStorageFormat<Input, Output>>[]> {\n const max = Math.max(1, opts.max ?? 1);\n const claims: IClaim<JobStorageFormat<Input, Output>>[] = [];\n while (claims.length < max) {\n const job = await this.core.next(opts.workerId, { leaseMs: opts.leaseMs });\n if (!job) break;\n claims.push(\n new PostgresClaim<Input, Output>(\n this.core,\n this.pending,\n job.id,\n job,\n job.attempts ?? 0,\n opts.workerId\n )\n );\n }\n return claims;\n }\n\n async releaseClaim(id: MessageId): Promise<void> {\n this.pending.delete(id);\n await this.core.releaseClaim(id);\n }\n\n async migrate(): Promise<void> {\n await this.core.migrate();\n }\n\n getMigrations(): ReadonlyArray<unknown> {\n return this.core.getMigrations();\n }\n\n subscribeToChanges(\n callback: (change: QueueChangePayload<any, any>) => void,\n options?: QueueSubscribeOptions\n ): () => void {\n return this.core.subscribeToChanges(callback, options);\n }\n}\n\nfunction applySendOptions<Input, Output>(\n body: JobStorageFormat<Input, Output>,\n opts?: SendOptions\n): JobStorageFormat<Input, Output> {\n if (!opts) return body;\n const out: JobStorageFormat<Input, Output> = { ...body };\n if (opts.delaySeconds != null) {\n out.visible_at = new Date(Date.now() + opts.delaySeconds * 1000).toISOString();\n }\n if (opts.timeoutSeconds != null) {\n out.deadline_at = new Date(Date.now() + opts.timeoutSeconds * 1000).toISOString();\n }\n if (opts.fingerprint != null) out.fingerprint = opts.fingerprint;\n if (opts.jobRunId != null) out.job_run_id = opts.jobRunId;\n if (opts.maxAttempts != null) out.max_attempts = opts.maxAttempts;\n return out;\n}\n",
|
|
11
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type {\n IJobStore,\n JobRecord,\n JobStatus,\n JobStorageFormat,\n MessageId,\n SendOptions,\n} from \"@workglow/job-queue\";\nimport type { PostgresPendingWrite } from \"./PostgresMessageQueue\";\nimport type { PostgresQueueStorage } from \"./PostgresQueueStorage\";\n\nexport class PostgresJobStore<Input, Output> implements IJobStore<Input, Output> {\n /** @internal — shared with the paired message queue */\n public readonly core: PostgresQueueStorage<Input, Output>;\n\n /** @internal — shared transient buffer for saveResult/saveError. */\n private readonly pending: Map<unknown, PostgresPendingWrite<Output>>;\n\n constructor(\n core: PostgresQueueStorage<Input, Output>,\n pending: Map<unknown, PostgresPendingWrite<Output>>\n ) {\n this.core = core;\n this.pending = pending;\n }\n\n get(id: MessageId): Promise<JobRecord<Input, Output> | undefined> {\n return this.core.get(id);\n }\n\n async peek(status?: JobStatus, num?: number): Promise<readonly JobRecord<Input, Output>[]> {\n return this.core.peek(status as any, num);\n }\n\n size(status?: JobStatus): Promise<number> {\n return this.core.size(status as any);\n }\n\n async getByRunId(runId: string): Promise<readonly JobRecord<Input, Output>[]> {\n return this.core.getByRunId(runId);\n }\n\n outputForInput(input: Input): Promise<Output | null> {\n return this.core.outputForInput(input);\n }\n\n async saveProgress(\n id: MessageId,\n progress: number,\n message: string,\n details: Record<string, any> | null\n ): Promise<void> {\n await this.core.saveProgress(id, progress, message, details as Record<string, any>);\n }\n\n async saveResult(id: MessageId, output: Output): Promise<void> {\n const buf = this.pending.get(id) ?? {};\n buf.output = output ?? null;\n this.pending.set(id, buf);\n }\n\n async saveError(\n id: MessageId,\n error: string,\n errorCode: string | null,\n abortRequested: boolean\n ): Promise<void> {\n const buf = this.pending.get(id) ?? {};\n buf.error = error;\n buf.errorCode = errorCode;\n buf.abortRequested = abortRequested;\n this.pending.set(id, buf);\n }\n\n async deleteByStatusAndAge(status: JobStatus, olderThanMs: number): Promise<void> {\n await this.core.deleteJobsByStatusAndAge(status, olderThanMs);\n }\n\n async delete(id: MessageId): Promise<void> {\n this.pending.delete(id);\n await this.core.delete(id);\n }\n\n async deleteAll(): Promise<void> {\n this.pending.clear();\n await this.core.deleteAll();\n }\n\n async abort(id: MessageId): Promise<void> {\n await this.core.abort(id);\n }\n\n async saveStatus(id: MessageId, status: JobStatus): Promise<void> {\n await this.core.saveStatus(id, status);\n }\n\n async create(body: JobStorageFormat<Input, Output>, opts: SendOptions): Promise<MessageId> {\n const enriched = {\n ...body,\n fingerprint: opts.fingerprint ?? body.fingerprint,\n job_run_id: opts.jobRunId ?? body.job_run_id,\n max_attempts: opts.maxAttempts ?? body.max_attempts,\n deadline_at:\n opts.timeoutSeconds != null\n ? new Date(Date.now() + opts.timeoutSeconds * 1000).toISOString()\n : body.deadline_at,\n } as JobStorageFormat<Input, Output>;\n return this.core.add(enriched);\n }\n\n async findActiveByFingerprint(\n fingerprint: string,\n queueName: string\n ): Promise<JobRecord<Input, Output> | undefined> {\n return this.core.findActiveByFingerprint(fingerprint, queueName);\n }\n\n async getMany(\n ids: readonly MessageId[]\n ): Promise<readonly (JobRecord<Input, Output> | undefined)[]> {\n return this.core.getMany(ids);\n }\n\n async completeWithResult(id: MessageId, result: Output): Promise<void> {\n this.pending.delete(id);\n await this.core.completeWithResult(id, result);\n }\n\n async failWithError(\n id: MessageId,\n opts: {\n readonly error?: string | null;\n readonly errorCode?: string | null;\n readonly abortRequested?: boolean;\n }\n ): Promise<void> {\n this.pending.delete(id);\n await this.core.failWithError(id, opts);\n }\n\n async markEnqueueDeferred(\n id: MessageId,\n opts: { readonly visible_at: Date; readonly errorCode: string }\n ): Promise<void> {\n await this.core.finalize(id, {\n visible_at: opts.visible_at.toISOString(),\n error_code: opts.errorCode,\n });\n }\n}\n",
|
|
12
|
+
"/**\n * @license\n * Copyright 2026 Steven Roussey <sroussey@gmail.com>\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { QueueStorageOptions } from \"@workglow/job-queue\";\nimport type { Pool } from \"@workglow/postgres/storage\";\nimport { PostgresJobStore } from \"./PostgresJobStore\";\nimport { PostgresMessageQueue, type PostgresPendingWrite } from \"./PostgresMessageQueue\";\nimport { PostgresQueueStorage } from \"./PostgresQueueStorage\";\n\n/**\n * Factory for the paired Postgres message queue and job store. Both\n * facades share a single underlying {@link PostgresQueueStorage} so writes\n * through one are observable through the other.\n */\nexport function createPostgresQueue<Input, Output>(\n queueName: string,\n pool: Pool,\n opts?: QueueStorageOptions\n): {\n messageQueue: PostgresMessageQueue<Input, Output>;\n jobStore: PostgresJobStore<Input, Output>;\n /** @internal — exposed for callers that still need the legacy storage object. */\n core: PostgresQueueStorage<Input, Output>;\n} {\n const core = new PostgresQueueStorage<Input, Output>(pool, queueName, opts);\n const pending = new Map<unknown, PostgresPendingWrite<Output>>();\n return {\n messageQueue: new PostgresMessageQueue<Input, Output>(core, pending),\n jobStore: new PostgresJobStore<Input, Output>(core, pending),\n core,\n };\n}\n"
|
|
10
13
|
],
|
|
11
|
-
"mappings": "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",
|
|
12
|
-
"debugId": "
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "D9E27F175B26F25364756E2164756E21",
|
|
13
16
|
"names": []
|
|
14
17
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import type { Pool } from "../storage/_postgres/node-bun";
|
|
7
6
|
import { type IMigration, type IMigrationRunner, type RunMigrationsOptions } from "@workglow/storage";
|
|
7
|
+
import type { Pool } from "../storage/_postgres/node-bun";
|
|
8
8
|
/**
|
|
9
9
|
* Runs versioned migrations against a PostgreSQL pool.
|
|
10
10
|
*
|
|
@@ -28,6 +28,11 @@ export declare class PostgresMigrationRunner implements IMigrationRunner<Pool> {
|
|
|
28
28
|
constructor(db: Pool);
|
|
29
29
|
ensureBookkeepingTable(): Promise<void>;
|
|
30
30
|
appliedVersions(component: string): Promise<Set<number>>;
|
|
31
|
+
/**
|
|
32
|
+
* Acquires a transaction-scoped queryable. Returns the raw pool when the
|
|
33
|
+
* underlying driver doesn't expose `connect()` (PGlite); the `release()`
|
|
34
|
+
* callback is then a no-op.
|
|
35
|
+
*/
|
|
31
36
|
private acquireClient;
|
|
32
37
|
run(migrations: ReadonlyArray<IMigration<Pool>>, options?: RunMigrationsOptions): Promise<ReadonlyArray<IMigration<Pool>>>;
|
|
33
38
|
private runInternal;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostgresMigrationRunner.d.ts","sourceRoot":"","sources":["../../src/migrations/PostgresMigrationRunner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"PostgresMigrationRunner.d.ts","sourceRoot":"","sources":["../../src/migrations/PostgresMigrationRunner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAG1B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AAsC1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,uBAAwB,YAAW,gBAAgB,CAAC,IAAI,CAAC;IACxD,OAAO,CAAC,QAAQ,CAAC,EAAE;IAA/B,YAA6B,EAAE,EAAE,IAAI,EAAI;IAEnC,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAU5C;IAEK,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAM7D;IAED;;;;OAIG;YACW,aAAa;IASrB,GAAG,CACP,UAAU,EAAE,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAC3C,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAS1C;YAEa,WAAW;CAmF1B"}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import type { Pool } from "../storage/_postgres/node-bun";
|
|
7
6
|
import { type PrefixColumn } from "@workglow/job-queue";
|
|
8
7
|
import { type IMigration } from "@workglow/storage";
|
|
8
|
+
import type { Pool } from "../storage/_postgres/node-bun";
|
|
9
9
|
/**
|
|
10
10
|
* Initial migration set for the Postgres queue table identified by `tableName`.
|
|
11
11
|
*
|
|
@@ -13,6 +13,14 @@ import { type IMigration } from "@workglow/storage";
|
|
|
13
13
|
* table names get tracked independently in `_storage_migrations`. The v1
|
|
14
14
|
* payload covers schema + indexes + LISTEN/NOTIFY plumbing; the trigger is
|
|
15
15
|
* idempotent (`CREATE OR REPLACE FUNCTION` + `DROP TRIGGER IF EXISTS`).
|
|
16
|
+
*
|
|
17
|
+
* v1 is FROZEN byte-for-byte against the pre-PR shape — it MUST keep
|
|
18
|
+
* creating the `run_after`/`run_attempts`/`max_retries`/`last_ran_at`/
|
|
19
|
+
* `worker_id` columns and the corresponding `run_after`-keyed indexes.
|
|
20
|
+
* Renames and index swaps live in v3 (with `IF EXISTS` guards so a fresh
|
|
21
|
+
* install — which still goes through v1 — can apply v3 without errors).
|
|
22
|
+
* Mutating v1 would silently produce divergent schemas between fresh and
|
|
23
|
+
* already-migrated DBs and break older deployments mid-rollout.
|
|
16
24
|
*/
|
|
17
25
|
export declare function postgresQueueMigrations(tableName: string, prefixes: readonly PrefixColumn[]): IMigration<Pool>[];
|
|
18
26
|
//# sourceMappingURL=postgresQueueMigrations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgresQueueMigrations.d.ts","sourceRoot":"","sources":["../../src/migrations/postgresQueueMigrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"postgresQueueMigrations.d.ts","sourceRoot":"","sources":["../../src/migrations/postgresQueueMigrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAIL,KAAK,UAAU,EAEhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AAgD1D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,SAAS,YAAY,EAAE,GAChC,UAAU,CAAC,IAAI,CAAC,EAAE,CAmPpB"}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import type { Pool } from "../storage/_postgres/node-bun";
|
|
7
6
|
import type { PrefixColumn } from "@workglow/job-queue";
|
|
8
7
|
import { type IMigration } from "@workglow/storage";
|
|
8
|
+
import type { Pool } from "../storage/_postgres/node-bun";
|
|
9
9
|
/** Initial migration set for the Postgres rate-limiter tables. */
|
|
10
10
|
export declare function postgresRateLimiterMigrations(executionTableName: string, nextAvailableTableName: string, prefixes: readonly PrefixColumn[]): IMigration<Pool>[];
|
|
11
11
|
//# sourceMappingURL=postgresRateLimiterMigrations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgresRateLimiterMigrations.d.ts","sourceRoot":"","sources":["../../src/migrations/postgresRateLimiterMigrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"postgresRateLimiterMigrations.d.ts","sourceRoot":"","sources":["../../src/migrations/postgresRateLimiterMigrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AAE1D,kEAAkE;AAClE,wBAAgB,6BAA6B,CAC3C,kBAAkB,EAAE,MAAM,EAC1B,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,EAAE,SAAS,YAAY,EAAE,GAChC,UAAU,CAAC,IAAI,CAAC,EAAE,CAoCpB"}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { DefaultKeyValueKey, DefaultKeyValueSchema, IKvStorage, KvViaTabularStorage } from "@workglow/storage";
|
|
6
7
|
import { JsonSchema } from "@workglow/util/schema";
|
|
7
8
|
import { PostgresTabularStorage } from "./PostgresTabularStorage";
|
|
8
|
-
import { DefaultKeyValueKey, DefaultKeyValueSchema, IKvStorage, KvViaTabularStorage } from "@workglow/storage";
|
|
9
9
|
export declare const POSTGRES_KV_REPOSITORY: import("@workglow/util").ServiceToken<IKvStorage<string, any, any>>;
|
|
10
10
|
/**
|
|
11
11
|
* A key-value repository implementation that uses PostgreSQL for persistent storage.
|