import { request } from "node:http";
import { DatabaseSync } from "node:sqlite";
import { until } from "./net";
const onPc = process.env.E2E_STACK === "pc";
const stateDir = () => {
const state = process.env.E2E_PC_STATE;
if (!onPc || !state) throw new Error("this needs the pc stack, run it with E2E_STACK=pc");
return state;
};
interface Processor {
status: string;
is_ready: string;
}
const pc = (method: string, path: string) =>
new Promise<{ status: number; body: string }>((done, fail) => {
request({ socketPath: `${stateDir()}/run/pc.sock`, method, path }, (res) => {
let body = "";
res.on("data", (chunk) => (body += chunk));
res.on("end", () => done({ status: res.statusCode ?? 0, body }));
})
.on("error", fail)
.end();
});
const processOf = async (name: string): Promise<Processor> => {
const { status, body } = await pc("GET", `/process/${name}`);
if (status !== 200) throw new Error(`process-compose answered ${status} for ${name}: ${body}`);
return JSON.parse(body);
};
const command = async (method: string, verb: string, name: string) => {
const { status, body } = await pc(method, `/process/${verb}/${name}`);
if (status !== 200)
throw new Error(`process-compose could not ${verb} ${name}: ${status} ${body}`);
};
export const stopProcess = async (name: string) => {
await command("PATCH", "stop", name);
await until(`${name} should have stopped`, 30_000, async () => {
const { status } = await processOf(name);
return [
status !== "Running" && status !== "Launching" && status !== "Terminating",
`it is ${status}`
];
});
};
// use started when the next step has to kill it mid-startup
export const startProcess = async (name: string, upTo: "started" | "ready" = "ready") => {
await command("POST", "start", name);
await until(`${name} should be ${upTo}`, 90_000, async () => {
const { status, is_ready } = await processOf(name);
const there = upTo === "ready" ? is_ready === "Ready" : status === "Running";
return [there, `it is ${status}, ${is_ready}`];
});
};
export const isReady = async (name: string) => (await processOf(name)).is_ready === "Ready";
// deliberi has no resend key here, so the token comes from its db instead of a mail
export const signupToken = (email: string) => {
const db = new DatabaseSync(`${stateDir()}/data/deliberi/deliberi.db`, { readOnly: true });
try {
const row = db
.prepare("select verification_token as token from signups_inflight where email = ?")
.get(email);
if (typeof row?.token !== "string") throw new Error(`deliberi has no signup for ${email}`);
return row.token;
} finally {
db.close();
}
};
// stop spindle first, it reads this at boot and keeps writing it
export const rewindJetstreamCursor = (microseconds: number) => {
const db = new DatabaseSync(`${stateDir()}/data/spindle/spindle.db`);
try {
db.exec("pragma busy_timeout = 10000");
db.prepare("update _jetstream set last_time_us = ? where id = 1").run(microseconds);
const moved = db.prepare("select last_time_us as at from _jetstream where id = 1").get();
if (moved?.at !== microseconds)
throw new Error(`spindle has no saved cursor to rewind: ${JSON.stringify(moved)}`);
} finally {
db.close();
}
};