Files
Webklar-Kundenbereich/server/services/termine/limits.js
T

87 lines
3.9 KiB
JavaScript

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()
}