startBackground

tcmts.cli.web.startBackground · #sqit7cle · term · runs on the server

uses node:url

Background work for an app: a worker for the durable job queue (functions loaded by hash, so a job runs the code that enqueued it), and the recurring jobs (every(...) values) found in the app's namespace.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { isSchedule } from "#codebase/tcmts/cli/pages";
import { codebaseRoles } from "#codebase/tcmts/cli/roles";
import { dim, red } from "#codebase/tcmts/cli/style";
import { dataDir, serverApiHost } from "#codebase/tcmts/cli/web";
import type { Fn } from "#codebase/tcmts/cli/web";
import { exportId, shortHash } from "#codebase/tcmts/core/hash";
import { materialize } from "#codebase/tcmts/loader";
import { pathToFileURL } from "node:url";

export async function startBackground(cb: Codebase, appName: string) {
  const api = await serverApiHost(cb)();
  const data = dataDir(cb);
  // `<hash>` is a function; `<hash>#fn` is the function inside the recurring job (schedule) with that hash
  const load = async (ref: string): Promise<Fn> => {
    const [hash, part] = ref.split("#") as [string, string | undefined];
    const rec = cb.store.getDef(hash);
    if (!rec) throw new Error(`no definition ${shortHash(hash)} in this codebase`);
    materialize([hash], cb.source(), cb.lay);
    const v = (
      (await import(pathToFileURL(cb.modulePath(rec.componentHash)).href)) as Record<
        string,
        unknown
      >
    )[exportId(hash)];
    const fn = part === "fn" ? (v as { fn?: unknown } | undefined)?.fn : v;
    if (typeof fn !== "function") throw new Error(`${cb.displayRef(`#${hash}`)} isn't a function`);
    return fn as Fn;
  };
  const stopWorker = api.startWorker({
    dataDir: data,
    load,
    log: (line) => console.log(`  ${dim("→")} ${line}`),
  });
  const ns = appName.includes(".") ? appName.slice(0, appName.lastIndexOf(".")) : "";
  let described = "";
  const refresh = async (quiet = false) => {
    // found from the code itself: definitions in the app's namespace whose value is made by `every(...)`
    const roles = codebaseRoles(cb);
    const candidates = cb.store
      .names()
      .filter((n) => n.kind === "term" && (!ns || n.path.startsWith(ns + ".")))
      .filter((n) => isSchedule(cb.store.getDef(n.hash)!, roles));
    const jobs: { name: string; spec: string; fn: Fn; hash: string }[] = [];
    for (const n of candidates) {
      const rec = cb.store.getDef(n.hash)!;
      materialize([n.hash], cb.source(), cb.lay);
      const v = (
        (await import(pathToFileURL(cb.modulePath(rec.componentHash)).href)) as Record<
          string,
          unknown
        >
      )[exportId(n.hash)] as { [k: symbol]: unknown; spec: string; fn: Fn } | undefined;
      if (v && v[Symbol.for("tcm.job")])
        jobs.push({ name: n.path, spec: v.spec, fn: v.fn, hash: n.hash });
    }
    api.setSchedules({ dataDir: data, jobs, log: (line) => console.log(red(`  ${line}`)) });
    const now = jobs.map((j) => `${j.name} (${j.spec})`).join(", ");
    if (!quiet && now !== described)
      console.log(
        `  jobs     ${jobs.length ? `recurring: ${now}` : dim("no recurring jobs; enqueue() from server functions runs here too")}`,
      );
    described = now;
  };
  await refresh();
  return { refresh, stop: stopWorker };
}

uses

used by