2.5 KiB
name, description, metadata
| name | description | metadata | ||||||
|---|---|---|---|---|---|---|---|---|
| project-optimistic-locking | Server-seitige optimistische Sperre für Customer-Writes (VERSION_CONFLICT) — Fix gegen Last-Write-Wins bei zwei Sales am selben Kunden, eingebaut 2026-06-29. |
|
Vor Go-live (Donnerstag 2026-07-02) das Concurrency-Verhalten der Customer-Writes gefixt.
Ausgangslage (charakterisiert): @VersionColumn war im Schreibpfad wirkungslos — persistCustomer machte repo.merge(existing, scalars), das die version aus dem Client-Payload übernahm. Folge: Last-Write-Wins, version lief sogar rückwärts (2→1), stiller Datenverlust bei parallelen Editoren. Jeder Sales hat prinzipiell alle Kunden des Landes → der Fall ist real.
Fix (rein BE, kein FE-Eingriff):
libs/shared/src/lib/errors/error-catalog.ts: neuer CodeVERSION_CONFLICT(HTTP 409).libs/customer/customer-be/src/lib/customer-core.service.ts:enforceVersion()inpersistCustomervor dem Merge —incoming.version !== existing.version→DomainException('VERSION_CONFLICT', …, forcedState: existing). Danach zählt@VersionColumnwieder hoch.- Kernregel: Sperre greift nur, wenn
incoming.version != null. Ein nie synchronisierter Neuanlage-Stand (version undefined), der nach Crash erneut gesendet wird, ist ein idempotenter Re-Send — sonst wäre at-least-once / Crash-Recovery (S8b) kaputtgegangen. Ein bereits synchronisierter Datensatz trägt clientseitig immer eine version (reconcile schreibt den Server-Stand lokal zurück).
Verhalten: stale Edit → 409 → FE rollt über den vorhandenen forcedState-Mechanismus (wie SAP-Readonly) auf den Server-Stand zurück, Fehler sichtbar. Verliererseite verliert ihre Änderung (mit Meldung); ein Merge-Dialog wäre eine spätere fachliche Erweiterung.
Wichtig für künftige Fixes: version ist als „wer ist neuer"-Vergleichsbasis untauglich (client-steuerbar). lastUpdateTs ist server-autoritativ (@BeforeUpdate → Date.now()) — das ist die verlässliche Basis. Belegt in den Tests.
Tests: customer-core.concurrency.spec.ts (5, BE-direkt gegen In-Memory-SQLite) + sync-loop.integration.spec.ts S10 (End-to-End Multi-Sales). Gesamt 8 Projekte grün. Siehe project-sync-testing für den Test-Harness.
Lokaler Testlauf: node 22 via fnm nötig (eval "$(/opt/homebrew/bin/fnm env)" && fnm use 22), better-sqlite3 baut nicht gegen node 26. node_modules waren nicht installiert → npm ci. Repo: ~/projects/fahrzeugeinrichtung-plato-backend.