Files
ditto.site/packages/api/src/store.ts
T
2026-06-29 15:11:48 -07:00

77 lines
2.0 KiB
TypeScript

import { rmSync } from "node:fs";
import type { CloneJobResult, CloneOptions } from "@cloner/core";
/** A completed (or failed) clone held in memory for the M1 sync API. The `base` is
* the temp dir holding the run's files (kept alive so /files/* can stream them);
* it is removed on eviction. M2/M4 replace this with DB rows + S3 objects. */
export type JobRecord = {
id: string;
status: "succeeded" | "failed";
url: string;
kind: "clone" | "clone_site";
options: CloneOptions;
createdAt: number;
result?: CloneJobResult;
base?: string;
error?: string;
};
/** In-memory result store with TTL eviction. Swapped for a DB-backed store in M2. */
export class InMemoryStore {
private jobs = new Map<string, JobRecord>();
private timer: NodeJS.Timeout | null = null;
constructor(private ttlMs: number) {}
put(rec: JobRecord): void {
this.jobs.set(rec.id, rec);
}
get(id: string): JobRecord | undefined {
return this.jobs.get(id);
}
list(): JobRecord[] {
return [...this.jobs.values()].sort((a, b) => b.createdAt - a.createdAt);
}
remove(id: string): boolean {
const rec = this.jobs.get(id);
if (!rec) return false;
if (rec.base) {
try {
rmSync(rec.base, { recursive: true, force: true });
} catch {
/* best effort */
}
}
return this.jobs.delete(id);
}
sweep(): void {
const now = Date.now();
for (const rec of [...this.jobs.values()]) {
if (now - rec.createdAt > this.ttlMs) this.remove(rec.id);
}
}
/** Start a periodic sweep (unref'd so it never holds the process open). */
startSweeper(intervalMs = 60_000): void {
if (this.timer) return;
this.timer = setInterval(() => this.sweep(), intervalMs);
this.timer.unref?.();
}
stopSweeper(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
/** Evict everything (used by tests / shutdown). */
clear(): void {
for (const id of [...this.jobs.keys()]) this.remove(id);
}
}