UNPKG

@cadence-mq/driver-libsql

Version:
172 lines (165 loc) 5.34 kB
import { createJobNotFoundError } from "@cadence-mq/core"; import { sleepMs } from "@cadence-mq/core/utils/time"; //#region src/driver.constants.ts const DEFAULT_POLL_INTERVAL_MS = 1e3; //#endregion //#region src/driver.models.ts function buildUpdateJobSetClause({ values }) { const fields = { status: "status", error: "error", result: "result", startedAt: "started_at", completedAt: "completed_at", maxRetries: "max_retries", data: "data", cron: "cron", scheduledAt: "scheduled_at", deleteJobOnCompletion: "delete_job_on_completion" }; const fieldsKeys = Object.keys(fields); const valuesEntries = Object.entries(values).filter(([key]) => fieldsKeys.includes(key)).filter(([, value]) => value !== void 0).sort(([keyA], [keyB]) => fields[keyA].localeCompare(fields[keyB])); if (valuesEntries.length === 0) throw new Error("No fields to update"); const setClause = valuesEntries.map(([key]) => `${fields[key]} = ?`).join(", "); const args = valuesEntries.map(([, value]) => value).map((value) => { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value; if (value instanceof Date) return value; return JSON.stringify(value); }); return { setClause, args }; } //#endregion //#region src/driver.ts async function getAndMarkJobAsProcessing({ client, now = /* @__PURE__ */ new Date() }) { const { rows } = await client.execute({ sql: ` UPDATE jobs SET status = 'processing', started_at = ? WHERE id = ( SELECT id FROM jobs WHERE status = 'pending' AND scheduled_at <= ? ORDER BY scheduled_at ASC LIMIT 1 ) AND status = 'pending' RETURNING * `, args: [now, now] }); const [jobRow] = rows; if (!jobRow) return { job: null }; return { job: toJob(jobRow) }; } function toJob(row) { return { id: String(row.id), taskName: String(row.task_name), status: row.status, startedAt: row.started_at ? new Date(row.started_at) : void 0, completedAt: row.completed_at ? new Date(row.completed_at) : void 0, maxRetries: row.max_retries ? Number(row.max_retries) : void 0, data: row.data ? JSON.parse(String(row.data)) : void 0, result: row.result ? JSON.parse(String(row.result)) : void 0, error: row.error ? String(row.error) : void 0, cron: row.cron ? String(row.cron) : void 0, scheduledAt: new Date(row.scheduled_at), createdAt: new Date(row.created_at), deleteJobOnCompletion: Boolean(row.delete_job_on_completion) }; } function createLibSqlDriver({ client, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS }) { return { getNextJobAndMarkAsProcessing: async () => { while (true) { const { job } = await getAndMarkJobAsProcessing({ client }); if (!job) { await sleepMs(pollIntervalMs); continue; } return { job }; } }, saveJob: async ({ job }) => { await client.batch([{ sql: "INSERT INTO jobs (id, task_name, status, created_at, max_retries, data, scheduled_at, cron, delete_job_on_completion) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", args: [ job.id, job.taskName, job.status, job.createdAt, job.maxRetries ?? null, job.data ? JSON.stringify(job.data) : null, job.scheduledAt, job.cron ?? null, job.deleteJobOnCompletion ?? false ] }], "write"); }, getJob: async ({ jobId }) => { const { rows } = await client.execute({ sql: "SELECT * FROM jobs WHERE id = ?", args: [jobId] }); const [jobRow] = rows; return { job: jobRow ? toJob(jobRow) : null }; }, getJobCount: async ({ filter = {} } = {}) => { const fields = { status: "status" }; const filterEntries = Object.entries(filter); const whereClause = filterEntries.map(([key]) => `${fields[key]} = ?`).join(" AND "); const { rows } = await client.execute({ sql: `SELECT COUNT(*) AS count FROM jobs ${whereClause ? `WHERE ${whereClause}` : ""}`, args: filterEntries.map(([, value]) => String(value)) }); const [{ count } = { count: 0 }] = rows ?? []; return { count: Number(count) }; }, updateJob: async ({ jobId, values }) => { const { setClause, args } = buildUpdateJobSetClause({ values }); await client.batch([{ sql: `UPDATE jobs SET ${setClause} WHERE id = ?`, args: [...args, jobId] }]); }, deleteJob: async ({ jobId }) => { const { rowsAffected } = await client.execute({ sql: "DELETE FROM jobs WHERE id = ?", args: [jobId] }); if (rowsAffected === 0) throw createJobNotFoundError(); } }; } //#endregion //#region src/migrations.ts function getSchema() { return ` PRAGMA journal_mode = WAL; PRAGMA synchronous = 1; PRAGMA busy_timeout = 5000; CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, task_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at DATETIME NOT NULL, started_at DATETIME, completed_at DATETIME, max_retries INTEGER, error TEXT, data TEXT, result TEXT, scheduled_at DATETIME NOT NULL, cron TEXT, delete_job_on_completion BOOLEAN DEFAULT FALSE ); CREATE INDEX IF NOT EXISTS jobs_status_scheduled_at_started_at_idx ON jobs (status, scheduled_at, started_at); `.trim(); } async function setupSchema({ client }) { await client.executeMultiple(getSchema()); } //#endregion export { createLibSqlDriver, getSchema, setupSchema };