/** * Schreib-Begrenzung je Schluessel (meist Kunde) im Speicher: hoechstens `max` Aufrufe je Fenster (Standard eine Stunde), * gezaehlt ab dem Aufruf. Jeder Aufruf zaehlt, auch ein abgelehnter. Wie beim Chat und bei den News nur im Prozess, ein * Neustart setzt die Zaehler zurueck; Schluessel ohne Treffer im Fenster werden bei der naechsten Abfrage vergessen. * Genutzt von routes/news.js (30 je Stunde) und routes/termine.js (120 je Stunde). * * @param {{ max: number, fensterMs?: number, now?: () => number }} optionen now liefert Millisekunden (Tests: feste Uhr) * @returns {{ * istUeberschritten: (key: string) => boolean, * middleware: (keyOf: (req: object) => string, message: string) => (req: object, res: object, next: () => void) => void, * reset: () => void, * }} istUeberschritten zaehlt diesen Aufruf mit und sagt, ob damit mehr als `max` im Fenster liegen; * middleware antwortet dann 429 { error: message }, sonst next(); reset leert alles (Tests) */ export function createWriteLimiter({ max, fensterMs = 60 * 60 * 1000, now = () => Date.now() } = {}) { if (!Number.isInteger(max) || max < 1) throw new TypeError('createWriteLimiter: max muss eine ganze Zahl ab 1 sein') if (!Number.isFinite(fensterMs) || fensterMs <= 0) throw new TypeError('createWriteLimiter: fensterMs muss groesser als 0 sein') if (typeof now !== 'function') throw new TypeError('createWriteLimiter: now muss eine Funktion sein') const treffer = new Map() // Schluessel -> Zeitpunkte (ms) im Fenster function istUeberschritten(key) { const jetzt = now() for (const [schluessel, zeiten] of treffer) { const frisch = zeiten.filter((t) => jetzt - t < fensterMs) if (frisch.length === 0) treffer.delete(schluessel) else if (frisch.length !== zeiten.length) treffer.set(schluessel, frisch) } const liste = treffer.get(key) || [] liste.push(jetzt) treffer.set(key, liste) return liste.length > max } const middleware = (keyOf, message) => (req, res, next) => { if (istUeberschritten(keyOf(req))) return res.status(429).json({ error: message }) return next() } return { istUeberschritten, middleware, reset: () => treffer.clear() } }