@cadence-mq/driver-libsql
Version:
LibSQL driver for CadenceMQ
172 lines (165 loc) • 5.34 kB
JavaScript
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 };