/** * Sperren je Schluessel (z. B. Kundennummer, Ticket-Id), damit parallele Anfragen fuer * dieselbe Ressource nacheinander laufen, waehrend Anfragen zu anderen Schluesseln parallel * bleiben. Ein Aufruf kann mehrere Schluessel auf einmal sperren (alle muessen frei werden). * waitMs begrenzt nur, wie lange DIESER Aufruf auf die aktuellen Inhaber wartet, bevor er * trotzdem losläuft (Default: unbegrenzt) - spaetere Aufrufe warten trotzdem auf die gesamte * Kette bis hierhin, einschliesslich eines Inhabers, den dieser Aufruf per waitMs uebersprungen * hat. * * Umsetzung: je Schluessel eine Promise-Kette (das "tail") in einer modulweiten Map. Der * gespeicherte Wert ist immer "die gesamte Kette bis einschliesslich dieses Aufrufs ist fertig" * (Promise.allSettled der vorherigen tails + der eigene Abschluss) - nie nur der eigene * Abschluss allein. Sonst wuerde ein Aufruf, der per waitMs vorzeitig losgelaufen ist, beim * Aufraeumen die Spur eines noch laufenden, unbegrenzt wartenden frueheren Inhabers loeschen, * und ein dritter Aufruf wuerde faelschlich gar nicht mehr warten. */ const tailsByKey = new Map() // setTimeout(fn, Infinity) wuerde Node zu 1 ms clampen (samt Warnung) - hier bewusst eine // Promise ohne echten Timer, damit "unbegrenzt warten" auch wirklich unbegrenzt ist und nichts // aufgeraeumt werden muss. Bei endlichem waitMs wird der Timer ueber cancel() wieder entfernt, // sobald das Rennen entschieden ist - sonst haelt ein liegen gebliebener Timer z. B. // `node --test` unnoetig am Leben. function waitMsTimeout(waitMs) { if (waitMs === Infinity) return { promise: new Promise(() => {}), cancel: () => {} } let timer const promise = new Promise((resolve) => { timer = setTimeout(resolve, waitMs) }) return { promise, cancel: () => clearTimeout(timer) } } export async function withLocks(keys, fn, { waitMs = Infinity } = {}) { const previousTails = keys.map((key) => tailsByKey.get(key)).filter(Boolean) let resolveOwnDone const ownDone = new Promise((resolve) => { resolveOwnDone = resolve }) // Gespeichert wird die gesamte Kette (siehe Kommentar oben), nicht nur ownDone. const tail = Promise.allSettled([...previousTails, ownDone]) for (const key of keys) tailsByKey.set(key, tail) tail.then(() => { for (const key of keys) { if (tailsByKey.get(key) === tail) tailsByKey.delete(key) } }) const { promise: timeout, cancel: cancelTimeout } = waitMsTimeout(waitMs) try { await Promise.race([Promise.allSettled(previousTails), timeout]) } finally { cancelTimeout() } try { return await fn() } finally { resolveOwnDone() } }