feat(cms): improve catalog, editorial recovery and operations
This commit is contained in:
1 parent
2fead134e6
commit
c389c3893d
122 files changed
+8137
-703
No files matched your search
@@ -4,6 +4,7 @@ import {
|
||||
mkdir,
|
||||
readdir,
|
||||
readFile,
|
||||
rename,
|
||||
stat,
|
||||
unlink,
|
||||
writeFile,
|
||||
@@ -27,6 +28,7 @@ export function redactErrorText(value: string): string {
|
||||
export interface CmsErrorRecord {
|
||||
id: string;
|
||||
resolved?: boolean;
|
||||
assignment?: { userId: number; at: string } | null;
|
||||
at: string;
|
||||
source: "server" | "browser";
|
||||
event: string;
|
||||
@@ -111,7 +113,9 @@ export class ErrorStore {
|
||||
.slice(0, 10);
|
||||
for (const name of files)
|
||||
if (
|
||||
/^\d{4}-\d{2}-\d{2}(\.jsonl|\.[a-f0-9]{16}\.resolved)$/.test(name) &&
|
||||
/^\d{4}-\d{2}-\d{2}(\.jsonl|\.[a-f0-9]{16}\.(?:resolved|assignment)(?:\.[a-f0-9-]+\.tmp)?)$/.test(
|
||||
name,
|
||||
) &&
|
||||
name.slice(0, 10) < cutoff
|
||||
)
|
||||
await unlink(path.join(this.directory, name)).catch(() => {});
|
||||
@@ -135,6 +139,43 @@ export class ErrorStore {
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
async assign(fingerprint: string, userId: number | null) {
|
||||
if (!/^[a-f0-9]{16}$/.test(fingerprint))
|
||||
throw new Error("Invalid fingerprint");
|
||||
if (userId !== null && (!Number.isSafeInteger(userId) || userId <= 0))
|
||||
throw new Error("Invalid assignee");
|
||||
if (ErrorStore.pending >= 100) throw new Error("Error storage queue full");
|
||||
ErrorStore.pending++;
|
||||
const write = ErrorStore.queue.then(async () => {
|
||||
const { records } = await this.read();
|
||||
if (!records.some((record) => record.fingerprint === fingerprint))
|
||||
throw new Error("Error group no longer available");
|
||||
const at = new Date().toISOString();
|
||||
const file = path.join(
|
||||
this.directory,
|
||||
`${at.slice(0, 10)}.${fingerprint}.assignment`,
|
||||
);
|
||||
const metadata = (await readdir(this.directory)).filter(
|
||||
(name) =>
|
||||
name.startsWith(at.slice(0, 10)) && name.endsWith(".assignment"),
|
||||
);
|
||||
if (metadata.length >= 1000 && !metadata.includes(path.basename(file)))
|
||||
throw new Error("Daily assignment storage limit reached");
|
||||
const temp = `${file}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
await writeFile(temp, JSON.stringify({ userId, at }), { mode: 0o600 });
|
||||
await rename(temp, file);
|
||||
} finally {
|
||||
await unlink(temp).catch(() => {});
|
||||
}
|
||||
});
|
||||
ErrorStore.queue = write.catch(() => {});
|
||||
try {
|
||||
await write;
|
||||
} finally {
|
||||
ErrorStore.pending--;
|
||||
}
|
||||
}
|
||||
async read(): Promise<{ records: CmsErrorRecord[]; truncated: boolean }> {
|
||||
const files = await readdir(this.directory).catch(
|
||||
(e: NodeJS.ErrnoException) => {
|
||||
@@ -142,17 +183,48 @@ export class ErrorStore {
|
||||
throw e;
|
||||
},
|
||||
);
|
||||
const cutoff = new Date(Date.now() - RETENTION_DAYS * 86400000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
const assignments = new Map<string, CmsErrorRecord["assignment"]>();
|
||||
for (const name of files
|
||||
.filter(
|
||||
(f) =>
|
||||
/^\d{4}-\d{2}-\d{2}\.[a-f0-9]{16}\.assignment$/.test(f) &&
|
||||
f.slice(0, 10) >= cutoff,
|
||||
)
|
||||
.sort()) {
|
||||
const file = path.join(this.directory, name);
|
||||
if ((await stat(file)).size > 512) continue;
|
||||
try {
|
||||
const value = JSON.parse(await readFile(file, "utf8"));
|
||||
if (
|
||||
typeof value.at !== "string" ||
|
||||
!Number.isFinite(Date.parse(value.at))
|
||||
)
|
||||
continue;
|
||||
if (value.userId === null) assignments.set(name.split(".")[1], null);
|
||||
else if (Number.isSafeInteger(value.userId) && value.userId > 0)
|
||||
assignments.set(name.split(".")[1], {
|
||||
userId: value.userId,
|
||||
at: value.at,
|
||||
});
|
||||
} catch {
|
||||
/* Ignore interrupted or malformed metadata. */
|
||||
}
|
||||
}
|
||||
const resolved = new Map<string, string>();
|
||||
for (const name of files
|
||||
.filter((f) => /^\d{4}-\d{2}-\d{2}\.[a-f0-9]{16}\.resolved$/.test(f))
|
||||
.filter(
|
||||
(f) =>
|
||||
/^\d{4}-\d{2}-\d{2}\.[a-f0-9]{16}\.resolved$/.test(f) &&
|
||||
f.slice(0, 10) >= cutoff,
|
||||
)
|
||||
.sort()) {
|
||||
const at = await readFile(path.join(this.directory, name), "utf8");
|
||||
resolved.set(name.split(".")[1], at);
|
||||
}
|
||||
const records: CmsErrorRecord[] = [];
|
||||
const cutoff = new Date(Date.now() - RETENTION_DAYS * 86400000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
for (const name of files
|
||||
.filter(
|
||||
(f) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f) && f.slice(0, 10) >= cutoff,
|
||||
@@ -169,6 +241,7 @@ export class ErrorStore {
|
||||
records.push({
|
||||
...r,
|
||||
resolved: (resolved.get(r.fingerprint) ?? "") >= r.at,
|
||||
assignment: assignments.get(r.fingerprint) ?? null,
|
||||
});
|
||||
} catch {
|
||||
/* Ignore an interrupted final write. */
|
||||
|
||||
Reference in new issue
Block a user