Files

62 lines
2.6 KiB
JavaScript

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