jobs

tcmts.cli.main.jobs · #tq72zaim · term · runs on the server

uses node:path

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import type { routeJobs } from "#codebase/tcmts/cli/commands";
import { cyan, dim, green, red, yellow } from "#codebase/tcmts/cli/style";
import { UsageError } from "#codebase/tcmts/cli/ui";
import type { parse } from "@bomb.sh/router";
import fs from "node:fs";
import path_2 from "node:path";
import { DatabaseSync } from "node:sqlite";

export function jobs(
  cb: Codebase,
  r: Extract<ReturnType<typeof parse<ReturnType<typeof routeJobs>>>, { method: "execute" }>,
): number {
  const file = path_2.join(cb.lay.tcmDir, "data", "jobs.sqlite");
  if (!fs.existsSync(file))
    return (
      console.log(
        dim("no jobs yet (enqueue() from a server function, or every() for recurring ones)"),
      ),
      0
    );
  const db = new DatabaseSync(file);
  try {
    const find = (prefix: string) => {
      const rows = db.prepare("SELECT id FROM jobs WHERE id LIKE ?").all(`${prefix}%`) as {
        id: string;
      }[];
      if (rows.length !== 1)
        throw new UsageError(
          rows.length ? `'${prefix}' matches ${rows.length} jobs` : `no job '${prefix}'`,
        );
      return rows[0]!.id;
    };
    if (r.route === "/retry") {
      db.prepare(
        "UPDATE jobs SET status = 'queued', run_at = ?, attempts = 0, last_error = NULL, finished = NULL WHERE id = ?",
      ).run(Date.now(), find(r.model.id));
      return (console.log("queued again"), 0);
    }
    if (r.route === "/cancel") {
      const c = db
        .prepare(
          "UPDATE jobs SET status = 'cancelled', finished = ? WHERE id = ? AND status = 'queued'",
        )
        .run(Date.now(), find(r.model.id));
      return (
        console.log(c.changes ? "cancelled" : dim("not queued (already running or finished)")), 0
      );
    }
    const rows = db
      .prepare("SELECT * FROM jobs ORDER BY created DESC LIMIT ?")
      .all(r.model.n) as Record<string, string | number | null>[];
    const colour: Record<string, (s: string) => string> = {
      done: green,
      failed: red,
      queued: yellow,
      running: cyan,
      cancelled: dim,
    };
    for (const row of rows) {
      const when = new Date(
        Number(row.status === "queued" ? row.run_at : (row.finished ?? row.run_at)),
      )
        .toISOString()
        .replace("T", " ")
        .slice(0, 19);
      // a finished job's last error is history; a failed or retrying one's is the news
      const err =
        row.last_error && row.status !== "done"
          ? dim(`  ${String(row.last_error).split("\n")[0]}`)
          : "";
      console.log(
        `${dim(String(row.id).slice(0, 8))}  ${(colour[String(row.status)] ?? dim)(String(row.status).padEnd(9))} ${String(row.fn_name).padEnd(24)} ${dim(`${row.attempts}/${row.max_attempts}  ${when}`)}${err}`,
      );
    }
    return 0;
  } finally {
    db.close();
  }
}

uses

used by