86 lines
3.8 KiB
JavaScript
86 lines
3.8 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. 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 = 10
|
|
|
|
/** 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 10 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()
|
|
}
|