Files
agentdock/runner/session-store.js
T

104 lines
2.9 KiB
JavaScript

import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
export class SessionStore {
constructor(directory) {
this.directory = directory;
this.sessions = new Map();
}
async init() {
await fsp.mkdir(this.directory, { recursive: true, mode: 0o700 });
const files = await fsp.readdir(this.directory);
for (const file of files.filter((name) => name.endsWith(".json"))) {
try {
const record = JSON.parse(await fsp.readFile(path.join(this.directory, file), "utf8"));
if (record.status === "running") {
record.status = "interrupted";
record.endedAt = new Date().toISOString();
}
this.sessions.set(record.id, { ...record, archived: true, clients: new Set(), buffer: "" });
} catch {
// A partial metadata file must not prevent the runner from starting.
}
}
}
add(record) {
const session = { ...record, clients: new Set(), buffer: "", errorBuffer: "", archived: false };
this.sessions.set(session.id, session);
this.persist(session);
return session;
}
get(id) {
return this.sessions.get(id);
}
publicRecord(session) {
const {
clients,
pty,
childProcess,
buffer,
errorBuffer,
logStream,
environment,
archived,
...record
} = session;
return { ...record, connectedClients: clients?.size || 0, archived: Boolean(archived) };
}
list() {
return [...this.sessions.values()]
.map((session) => this.publicRecord(session))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
append(session, data) {
session.buffer += data;
if (Buffer.byteLength(session.buffer) > MAX_BUFFER_BYTES) {
session.buffer = session.buffer.slice(-MAX_BUFFER_BYTES);
}
if (!session.logStream) {
session.logStream = fs.createWriteStream(path.join(this.directory, `${session.id}.log`), {
flags: "a",
mode: 0o600,
});
}
session.logStream.write(data);
}
async readOutput(session) {
if (session.buffer) return session.buffer;
try {
return await fsp.readFile(path.join(this.directory, `${session.id}.log`), "utf8");
} catch {
return "";
}
}
finish(session, status, exitCode = null, signal = null) {
session.status = status;
session.exitCode = exitCode;
session.signal = signal;
session.endedAt = new Date().toISOString();
session.logStream?.end();
session.logStream = undefined;
this.persist(session);
}
persist(session) {
const target = path.join(this.directory, `${session.id}.json`);
const temp = `${target}.tmp`;
const body = `${JSON.stringify(this.publicRecord(session), null, 2)}\n`;
fsp.writeFile(temp, body, { mode: 0o600 })
.then(() => fsp.rename(temp, target))
.catch(() => {});
}
}