Bausteine: slugify mit Umlauten, deutsche Zeit, Sperren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a3faa1aff1
commit
679f996597
6 files changed
+144
No files matched your search
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Datum und Uhrzeit in der Berliner Zeitzone (beruecksichtigt MESZ/MEZ automatisch),
|
||||
* fuer Anzeige und Dateinamen. date: 'tt.mm.jjjj', time: 'hhmm' (24h, '0030' statt '2430').
|
||||
*/
|
||||
export function berlinDateTime(date = new Date()) {
|
||||
const parts = new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
}).formatToParts(date)
|
||||
|
||||
const value = (type) => parts.find((part) => part.type === type)?.value
|
||||
|
||||
return {
|
||||
date: `${value('day')}.${value('month')}.${value('year')}`,
|
||||
time: `${value('hour')}${value('minute')}`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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, wie lange auf die aktuellen Inhaber gewartet wird (Default: unbegrenzt);
|
||||
* nach Ablauf laeuft fn trotzdem los, ohne weiter zu warten.
|
||||
*
|
||||
* Umsetzung: je Schluessel eine Promise-Kette (das "tail") in einer modulweiten Map.
|
||||
*/
|
||||
const tailsByKey = new Map()
|
||||
|
||||
// setTimeout(fn, Infinity) wuerde Node zu 1 ms clampen (samt Warnung) - hier bewusst
|
||||
// eine Promise, die nie aufloest, damit "unbegrenzt warten" auch wirklich unbegrenzt ist.
|
||||
const sleep = (ms) =>
|
||||
ms === Infinity ? new Promise(() => {}) : new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
export async function withLocks(keys, fn, { waitMs = Infinity } = {}) {
|
||||
const previousTails = keys.map((key) => tailsByKey.get(key)).filter(Boolean)
|
||||
|
||||
let releaseTail
|
||||
const tail = new Promise((resolve) => {
|
||||
releaseTail = resolve
|
||||
})
|
||||
for (const key of keys) tailsByKey.set(key, tail)
|
||||
|
||||
await Promise.race([Promise.allSettled(previousTails), sleep(waitMs)])
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
releaseTail()
|
||||
for (const key of keys) {
|
||||
if (tailsByKey.get(key) === tail) tailsByKey.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* URL-/Dateiname-taugliches Kuerzel aus beliebigem Text: klein geschrieben, deutsche
|
||||
* Umlaute ausgeschrieben (ä→ae, ö→oe, ü→ue, ß→ss), Akzente entfernt, Apostrophe entfernt,
|
||||
* alles ausser a-z/0-9/- wird zu einem einzelnen '-', Rand-Bindestriche entfernt, auf
|
||||
* 100 Zeichen gekuerzt. Kein Text (z. B. null/undefined) ergibt ''.
|
||||
*/
|
||||
export function slugify(value) {
|
||||
const input = value === null || value === undefined ? '' : String(value)
|
||||
|
||||
return input
|
||||
.toLowerCase()
|
||||
.replace(/ä/g, 'ae')
|
||||
.replace(/ö/g, 'oe')
|
||||
.replace(/ü/g, 'ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
.normalize('NFD')
|
||||
.replace(/\p{M}/gu, '')
|
||||
.replace(/['’‘`]/g, '')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 100)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { berlinDateTime } from '../server/services/berlinTime.js'
|
||||
|
||||
test('Sommerzeit', () => assert.deepEqual(berlinDateTime(new Date('2026-09-27T14:46:34Z')), { date: '27.09.2026', time: '1646' }))
|
||||
test('Winterzeit', () => assert.deepEqual(berlinDateTime(new Date('2026-01-15T10:05:00Z')), { date: '15.01.2026', time: '1105' }))
|
||||
test('nach Mitternacht UTC', () => assert.deepEqual(berlinDateTime(new Date('2026-09-27T22:30:00Z')), { date: '28.09.2026', time: '0030' }))
|
||||
@@ -0,0 +1,39 @@
|
||||
import { test, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { withLocks } from '../server/services/keyedLock.js'
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
let log
|
||||
beforeEach(() => {
|
||||
log = []
|
||||
})
|
||||
|
||||
const job = (name, ms) => async () => {
|
||||
log.push(name + ':start')
|
||||
await sleep(ms)
|
||||
log.push(name + ':end')
|
||||
}
|
||||
|
||||
test('gleicher Schlüssel läuft nacheinander', async () => {
|
||||
await Promise.all([withLocks(['a'], job('1', 50)), withLocks(['a'], job('2', 10))])
|
||||
assert.deepEqual(log, ['1:start', '1:end', '2:start', '2:end'])
|
||||
})
|
||||
test('verschiedene Schlüssel laufen parallel', async () => {
|
||||
await Promise.all([withLocks(['a'], job('1', 50)), withLocks(['b'], job('2', 10))])
|
||||
assert.deepEqual(log.slice(0, 2), ['1:start', '2:start'])
|
||||
})
|
||||
test('mehrere Schlüssel sperren alle', async () => {
|
||||
await Promise.all([withLocks(['a', 'b'], job('1', 50)), withLocks(['b'], job('2', 10))])
|
||||
assert.deepEqual(log, ['1:start', '1:end', '2:start', '2:end'])
|
||||
})
|
||||
test('waitMs begrenzt das Warten', async () => {
|
||||
const t0 = Date.now()
|
||||
let started
|
||||
await Promise.all([withLocks(['a'], job('1', 300)), withLocks(['a'], async () => { started = Date.now() - t0 }, { waitMs: 50 })])
|
||||
assert.ok(started >= 45 && started < 250, `gestartet nach ${started} ms`)
|
||||
})
|
||||
test('Fehler geben die Sperre frei', async () => {
|
||||
await assert.rejects(withLocks(['a'], async () => { throw new Error('x') }))
|
||||
assert.equal(await withLocks(['a'], async () => 42), 42)
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { slugify } from '../server/services/slug.js'
|
||||
|
||||
test('Umlaute', () => {
|
||||
assert.equal(slugify('Döner Nation'), 'doener-nation')
|
||||
assert.equal(slugify('Größe Überall Ärger'), 'groesse-ueberall-aerger')
|
||||
})
|
||||
test('Apostrophe entfallen', () => {
|
||||
assert.equal(slugify("Emma's Schatzkiste"), 'emmas-schatzkiste')
|
||||
assert.equal(slugify('Emma’s'), 'emmas')
|
||||
})
|
||||
test('Akzente', () => assert.equal(slugify('Café Élan'), 'cafe-elan'))
|
||||
test('Sonderzeichen, Ränder, Länge', () => {
|
||||
assert.equal(slugify('--A&B--'), 'a-b')
|
||||
assert.equal(slugify('x'.repeat(150)).length, 100)
|
||||
assert.equal(slugify(null), '')
|
||||
})
|
||||
Reference in new issue
Block a user