perf/test: Build-Coalescing teilen, syncLibrary drosseln, API-Tests

- coalesce.js: generisches Serialisieren+Koaleszieren je Key; buildSite() in
  hugo.js nutzt es → Publish/Preview/Profil starten nie überlappende Hugo-
  Prozesse, schnelle Folge-Aufrufe lösen nur einen Trailing-Build aus
- dialog-store: syncLibrary() gedrosselt (60s-TTL) statt bei jedem Forum-Read
  Filesystem-Walk + Upsert; Publish forciert Sync (force:true)
- test/: node:test-Suite (19 Tests) für safeRel/normAuthors/urlFor/hasAccess,
  roleOf + lokale JWT-Verifikation, Rate-Limiter, Coalescing; npm test

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:14:36 +02:00
parent 8404165f5c
commit f97999c3c0
12 changed files with 269 additions and 22 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import { Hono } from 'hono';
import { urlFor, safeRel } from '../files.js';
import { hugoBuild } from '../hugo.js';
import { buildSite } from '../hugo.js';
// Echte Hugo-Vorschau: ganze Site mit --buildDrafts nach preview/ bauen und die
// URL des Eintrags zurückgeben (so erscheinen auch draft:true-Einträge).
@@ -10,7 +10,7 @@ preview.post('/', async (c) => {
const { path: rel } = await c.req.json();
try {
const safe = safeRel(rel);
const build = await hugoBuild({ dest: 'preview', drafts: true });
const build = await buildSite({ dest: 'preview', drafts: true });
return c.json({ ok: true, url: `/_preview${urlFor(safe)}`, hugo: build.stdout });
} catch (e) {
return c.json({ error: String(e.message || e) }, 500);
+2 -16
View File
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
import path from 'node:path';
import matter from 'gray-matter';
import { hugoBuild } from '../hugo.js';
import { buildSite } from '../hugo.js';
// Profile als Hugo-Data-Datei (data/authors.json) + öffentliche Autor-Seite
// (content/authors/<slug>.md), gerendert von layouts/authors/single.html.
@@ -19,20 +19,6 @@ function slugify(s) {
}
async function exists(p) { try { await stat(p); return true; } catch { return false; } }
// Hugo-Build koaleszieren: nie zwei parallel, und schnelle Folge-Speicherungen
// lösen nur EINEN nachgelagerten Build aus (verhindert Build-Sturm/DoS).
let building = false, rerun = false;
async function rebuildSite() {
if (building) { rerun = true; return; }
building = true;
try {
do {
rerun = false;
await hugoBuild({ dest: 'public', drafts: false }).catch((e) => console.error('[profile] build:', e?.message || e));
} while (rerun);
} finally { building = false; }
}
const profile = new Hono();
profile.get('/', async (c) => {
@@ -61,7 +47,7 @@ profile.put('/', async (c) => {
const page = matter.stringify(bio || '', { title: name, avatar: avatar || '' });
await writeFile(path.join(AUTHORS_DIR, `${slug}.md`), page, 'utf8');
// Live bauen (koalesziert), damit die Seite + Byline-Links sofort wirken.
await rebuildSite();
await buildSite({ dest: 'public', drafts: false }).catch((e) => console.error('[profile] build:', e?.message || e));
}
return c.json({ ok: true, slug });
+5 -2
View File
@@ -1,6 +1,7 @@
import { Hono } from 'hono';
import { urlFor, safeRel } from '../files.js';
import { hugoBuild, gitCommit } from '../hugo.js';
import { buildSite, gitCommit } from '../hugo.js';
import { syncLibrary } from '../dialog-store.js';
// Publizieren: public/ neu bauen (ohne Drafts) → live. Optional git-commit.
const publish = new Hono();
@@ -9,7 +10,9 @@ publish.post('/', async (c) => {
const { path: rel } = await c.req.json();
try {
const safe = safeRel(rel);
const build = await hugoBuild({ dest: 'public', drafts: false });
const build = await buildSite({ dest: 'public', drafts: false });
// Neue/aktualisierte Library-Beiträge sofort als Dialog-Threads spiegeln.
await syncLibrary({ force: true }).catch(() => {});
const git = await gitCommit(`cms: publish ${safe}`).catch((e) => ({ error: String(e.message || e) }));
return c.json({ ok: true, url: urlFor(safe), git, hugo: build.stdout });
} catch (e) {