import { pruefeNow, pruefeText } from './util.js' /** * Grenzen des Terminplaners (Spezifikation 2026-10-02-terminplaner-design.md, Abschnitt 8): Zaehler im Speicher dieses * Prozesses, je Schluessel ein festes Fenster ab dem ersten gezaehlten Ereignis. Die Schluessel enthalten Adressen oder * IPs und verlassen den Speicher nie (kein Log, keine Datenbank). Hoechstens 10 000 Schluessel: ist die Map voll, * faellt der aelteste (zuerst begonnene) Zaehler weg; ein abgelaufenes Fenster wird beim naechsten Zaehlen neu begonnen. * Ein Neustart des Prozesses leert alles (die Grenzen sind Missbrauchsschutz, kein Buchungsrecht). * * Allgemein: zaehle / zaehleAlle (true = Ereignis erlaubt und gezaehlt, false = Grenze erreicht, nichts gezaehlt). * Mails des Widerrufs (Aufgabe 13): darfG08 und darfK21NichtZugeordnet; sie gelten nur fuer Erklaerungen ohne passende * Buchung, ein Treffer bekommt seine Mails immer. Die Grenzen je IP, siteKey und G01 ergaenzt * Aufgabe 16 in diesem Modul. */ const MAX_SCHLUESSEL = 10000 const STUNDE_MS = 60 * 60 * 1000 const TAG_MS = 24 * STUNDE_MS /** G08 (Eingangsbestaetigung des Widerrufs): je Empfaengeradresse und Tag, je Funktion und Stunde. */ const G08_JE_ADRESSE = 3 const G08_JE_FUNKTION = 20 /** K21 "nicht zugeordnet": je Funktion und Stunde. */ const K21_NICHT_ZUGEORDNET_JE_FUNKTION = 30 /** schluessel -> { start (ms), n }; die Einfuegereihenfolge ist die Reihenfolge der Fensterbeginne. */ const zaehler = new Map() /** Der Zaehler des Schluessels, wenn sein Fenster zum Zeitpunkt nowMs noch laeuft; sonst undefined. */ function laufend(schluessel, fensterMs, nowMs) { const eintrag = zaehler.get(schluessel) return eintrag && nowMs < eintrag.start + fensterMs ? eintrag : undefined } /** * Zaehlt ein Ereignis je Regel, aber nur, wenn keine Regel ihre Grenze erreicht hat (alle oder keine). * @param {{ schluessel: string, max: number, fensterMs: number }[]} regeln * @param {Date} now * @returns {boolean} true = erlaubt (gezaehlt), false = mindestens eine Grenze erreicht */ export function zaehleAlle(regeln, now) { const nowMs = pruefeNow(now, 'zaehleAlle') if (!Array.isArray(regeln) || regeln.length === 0) throw new TypeError('zaehleAlle: regeln fehlen') for (const { schluessel, max, fensterMs } of regeln) { pruefeText(schluessel, 'schluessel') if (!Number.isInteger(max) || max < 0 || !(fensterMs > 0)) throw new TypeError('zaehleAlle: max oder fensterMs ungueltig') if ((laufend(schluessel, fensterMs, nowMs)?.n ?? 0) >= max) return false } for (const { schluessel, fensterMs } of regeln) { const eintrag = laufend(schluessel, fensterMs, nowMs) if (eintrag) { eintrag.n += 1 continue } zaehler.delete(schluessel) // neu einfuegen = ans Ende der Reihenfolge if (zaehler.size >= MAX_SCHLUESSEL) zaehler.delete(zaehler.keys().next().value) zaehler.set(schluessel, { start: nowMs, n: 1 }) } return true } /** Ein Ereignis zaehlen: hoechstens max je Fenster. => true = erlaubt (gezaehlt), false = Grenze erreicht. */ export function zaehle(schluessel, max, fensterMs, now) { return zaehleAlle([{ schluessel, max, fensterMs }], now) } /** Darf eine G08 an diese (normalisierte) Adresse gehen? Hoechstens 3 je Adresse und Tag, 20 je Funktion und Stunde. */ export function darfG08(featureId, adresse, now) { return zaehleAlle( [ { schluessel: `g08-adresse:${adresse}`, max: G08_JE_ADRESSE, fensterMs: TAG_MS }, { schluessel: `g08-funktion:${featureId}`, max: G08_JE_FUNKTION, fensterMs: STUNDE_MS }, ], now, ) } /** Darf eine K21 "nicht zugeordnet" an den Betrieb gehen? Hoechstens 30 je Funktion und Stunde. */ export function darfK21NichtZugeordnet(featureId, now) { return zaehle(`k21-nicht-zugeordnet:${featureId}`, K21_NICHT_ZUGEORDNET_JE_FUNKTION, STUNDE_MS, now) } /** Leert alle Zaehler (Tests). */ export function resetTermineLimitsForTests() { zaehler.clear() }