Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
/**
|
|
* UUIDv7, zeitlich sortiert.
|
|
*
|
|
* Selbst geschrieben statt als Abhängigkeit: das Paket wog 2,6 kB im Bündel
|
|
* der Fallakte, und die eine Eigenschaft, auf die sich das System verlässt,
|
|
* steht so ausdrücklich hier statt in fremdem Code.
|
|
*
|
|
* Die Eigenschaft ist: **die Kennungen sind lexikografisch sortierbar wie die
|
|
* Zeit.** Darauf beruhen zwei Dinge - die Nachlieferung beim Wiederverbinden
|
|
* (`id > seit`) und die Reihenfolge im Chat, ohne dass eine Sortierspalte
|
|
* nötig wäre. Siehe docs/06-chat-und-realtime.md.
|
|
*
|
|
* Aufbau nach RFC 9562:
|
|
*
|
|
* 48 Bit Zeitstempel (ms) | 4 Bit Version 7 | 12 Bit Zähler
|
|
* 2 Bit Variante | 62 Bit Zufall
|
|
*
|
|
* Der Zähler in `rand_a` sichert die Reihenfolge auch innerhalb derselben
|
|
* Millisekunde. Ohne ihn kämen zwei schnell hintereinander gesendete
|
|
* Nachrichten in zufälliger Reihenfolge an.
|
|
*/
|
|
|
|
let letzteZeit = 0
|
|
let zaehler = 0
|
|
|
|
/** 12 Bit, danach wird auf die nächste Millisekunde gewartet. */
|
|
const ZAEHLER_MAX = 0xfff
|
|
|
|
export function uuidv7(now: number = Date.now()): string {
|
|
if (now === letzteZeit) {
|
|
zaehler += 1
|
|
if (zaehler > ZAEHLER_MAX) {
|
|
// Mehr als 4096 Kennungen in einer Millisekunde: lieber eine
|
|
// Millisekunde vorgreifen als die Sortierung verlieren.
|
|
letzteZeit += 1
|
|
zaehler = 0
|
|
now = letzteZeit
|
|
}
|
|
} else if (now > letzteZeit) {
|
|
letzteZeit = now
|
|
zaehler = 0
|
|
} else {
|
|
// Die Uhr ist zurückgesprungen. Die Reihenfolge ist wichtiger als der
|
|
// genaue Zeitpunkt, also weiterzählen statt zurückfallen.
|
|
letzteZeit += 1
|
|
zaehler = 0
|
|
now = letzteZeit
|
|
}
|
|
|
|
const bytes = new Uint8Array(16)
|
|
|
|
// 48 Bit Zeitstempel, big endian.
|
|
const zeit = BigInt(now)
|
|
for (let i = 0; i < 6; i++) {
|
|
bytes[i] = Number((zeit >> BigInt(8 * (5 - i))) & 0xffn)
|
|
}
|
|
|
|
// Version 7 in den oberen vier Bit von Byte 6, darunter der Zähler.
|
|
bytes[6] = 0x70 | ((zaehler >> 8) & 0x0f)
|
|
bytes[7] = zaehler & 0xff
|
|
|
|
const zufall = new Uint8Array(8)
|
|
crypto.getRandomValues(zufall)
|
|
bytes.set(zufall, 8)
|
|
|
|
// Variante 10xx in den oberen zwei Bit von Byte 8.
|
|
bytes[8] = (bytes[8]! & 0x3f) | 0x80
|
|
|
|
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
}
|