Files
growqr-backend/scripts/rivet-actors.ts

180 lines
5.3 KiB
TypeScript

import "dotenv/config";
import { config } from "../src/config.js";
type ActorRecord = {
actor_id: string;
name: string;
key?: string | null;
namespace_id?: string;
runner_name_selector?: string;
crash_policy?: string;
create_ts?: number;
start_ts?: number | null;
connectable_ts?: number | null;
sleep_ts?: number | null;
destroy_ts?: number | null;
error?: unknown;
};
type ListActorsResponse = {
actors?: ActorRecord[];
pagination?: { cursor?: string | null };
};
type Options = {
command: "list" | "flush";
endpoint: string;
namespace: string;
name?: string;
key?: string;
crashedOnly: boolean;
dryRun: boolean;
};
function parseArgs(): Options {
const args = process.argv.slice(2);
const command = args[0] === "flush" ? "flush" : "list";
const options: Options = {
command,
endpoint: process.env.RIVET_ENDPOINT ?? config.rivetEndpoint,
namespace: process.env.RIVET_NAMESPACE ?? "default",
crashedOnly: false,
dryRun: false,
};
for (let i = command === "flush" || args[0] === "list" ? 1 : 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === "--") continue;
else if (arg === "--endpoint") options.endpoint = requireValue(args, ++i, arg);
else if (arg === "--namespace") options.namespace = requireValue(args, ++i, arg);
else if (arg === "--name") options.name = requireValue(args, ++i, arg);
else if (arg === "--key") options.key = requireValue(args, ++i, arg);
else if (arg === "--crashed-only") options.crashedOnly = true;
else if (arg === "--dry-run") options.dryRun = true;
else if (arg === "--help" || arg === "-h") printHelpAndExit();
else throw new Error(`Unknown argument: ${arg}`);
}
return options;
}
function requireValue(args: string[], index: number, flag: string) {
const value = args[index];
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${flag}`);
return value;
}
function printHelpAndExit(): never {
console.log(`Usage:
pnpm rivet:actors:list [--name growActor] [--key user_123] [--crashed-only]
pnpm rivet:actors:flush -- --name growActor [--key user_123] [--crashed-only] [--dry-run]
Environment:
RIVET_ENDPOINT Defaults to config.rivetEndpoint (${config.rivetEndpoint})
RIVET_NAMESPACE Defaults to default
Examples:
pnpm rivet:actors:list
pnpm rivet:actors:flush:crashed
pnpm rivet:actors:flush:grow
pnpm rivet:actors:flush -- --name growActor --key user_123
`);
process.exit(0);
}
function buildEndpoint(rawEndpoint: string) {
const url = new URL(rawEndpoint);
const token = url.password || process.env.RIVET_TOKEN || process.env.RIVET_ADMIN_TOKEN || "dev-admin-token";
url.username = "";
url.password = "";
return {
baseUrl: url.toString().replace(/\/$/, ""),
headers: token ? { Authorization: `Bearer ${token}`, "x-rivet-token": token } : {},
};
}
function actorHasError(actor: ActorRecord) {
return Boolean(actor.error);
}
async function listActors(options: Options) {
const { baseUrl, headers } = buildEndpoint(options.endpoint);
const actors: ActorRecord[] = [];
let cursor: string | null | undefined;
do {
const url = new URL(`${baseUrl}/actors`);
url.searchParams.set("namespace", options.namespace);
if (options.name) url.searchParams.set("name", options.name);
if (options.key) url.searchParams.set("key", options.key);
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`Failed to list actors: ${response.status} ${response.statusText}\n${await response.text()}`);
}
const body = (await response.json()) as ListActorsResponse;
actors.push(...(body.actors ?? []));
cursor = body.pagination?.cursor;
} while (cursor);
return options.crashedOnly ? actors.filter(actorHasError) : actors;
}
async function deleteActor(options: Options, actor: ActorRecord) {
const { baseUrl, headers } = buildEndpoint(options.endpoint);
const url = new URL(`${baseUrl}/actors/${encodeURIComponent(actor.actor_id)}`);
url.searchParams.set("namespace", options.namespace);
if (options.dryRun) return;
const response = await fetch(url, { method: "DELETE", headers });
if (!response.ok) {
throw new Error(`Failed to delete ${actor.actor_id}: ${response.status} ${response.statusText}\n${await response.text()}`);
}
}
function printActors(actors: ActorRecord[]) {
if (!actors.length) {
console.log("No actors matched.");
return;
}
for (const actor of actors) {
console.log(
[
actor.actor_id,
actor.name,
`key=${actor.key ?? ""}`,
actor.error ? `error=${JSON.stringify(actor.error)}` : "ok",
].join("\t"),
);
}
}
async function main() {
const options = parseArgs();
const actors = await listActors(options);
if (options.command === "list") {
printActors(actors);
return;
}
if (!options.name && !options.crashedOnly && !options.key) {
throw new Error("Refusing to flush every actor without a filter. Pass --name, --key, or --crashed-only.");
}
printActors(actors);
for (const actor of actors) {
await deleteActor(options, actor);
console.log(`${options.dryRun ? "Would delete" : "Deleted"} ${actor.name} ${actor.actor_id} key=${actor.key ?? ""}`);
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});