fix(drafts): serialize draft session flushes and register reminders via fds__setReminder
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
Make invoice/reminder live-draft edits concurrency-safe with an explicit DraftFlushResult, add the fds__setReminder stored procedure, and harden the date helper functions. Includes concurrency and SQL contract tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d0002ccdad
commit
546648533b
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: Active
|
||||
lastUpdated: 2026-09-09
|
||||
lastUpdated: 2026-10-01
|
||||
applyTo:
|
||||
- "Fuchs/Services/InvoiceDraft*"
|
||||
- "Fuchs/Services/IInvoiceDraft*"
|
||||
@@ -99,6 +99,21 @@ Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bit
|
||||
(`RecomputeLevels`), rendered as an indent class in the editor and a per-level indent in the
|
||||
PDF. `item.setprice` sums a header's **transitive** descendants. Structure deltas never
|
||||
change values — only order, membership, levels and therefore position numbers.
|
||||
- **Concurrency.** The editor fires patches, state fetches, previews and saves for one token
|
||||
concurrently, and the session payload (`JObject`/`JArray`, the history list) is not thread-safe.
|
||||
Every read and write of the payload runs under the session's `SyncRoot`, and everything the
|
||||
controller serialises afterwards (`BuildState`, `GetHistory`, the `FdsInvoiceData` a preview or
|
||||
flush uses) is a deep-cloned snapshot taken under that lock — never a live reference. Flushes are
|
||||
additionally serialised per session on `FlushGate` (a `SemaphoreSlim`, awaited — `SyncRoot`
|
||||
cannot be held across the registration's `await`): the create/update decision in
|
||||
`FlushToDbAsync` is read-then-write across that await, so two overlapping saves of a
|
||||
not-yet-registered draft (double-click on *Zwischenspeichern*, or *Zwischenspeichern* followed
|
||||
by preview → confirm) would otherwise both call `fds__createInvoice` and leave an orphaned
|
||||
invoice. The waiting save snapshots **after** it acquires the gate, so it persists the newer
|
||||
state as an update of the row the first save created. `FlushToDbAsync` reports that decision
|
||||
(`DraftFlushResult.Updated`) and the controller publishes `InvoiceDraftCreated`/`-Updated` from
|
||||
it rather than from the session state it saw before the flush. Patches are never blocked by a
|
||||
running save. `DraftSessionConcurrencyTests` pins both the lock and the gate.
|
||||
- **Cache** (`InvoiceDraftCache`, singleton) stores sessions by token with an idle sliding
|
||||
TTL; `InvoiceDraftExpiryService` (a `BackgroundService`) warns before, and evicts after,
|
||||
the TTL. TTL/warn-lead are configurable under `Fuchs:DraftEditing`.
|
||||
@@ -146,9 +161,12 @@ amount pair.
|
||||
- **Orchestration** (`ReminderDraftEditService`, scoped) flushes to the DB by reusing
|
||||
`IReminderService.RegisterReminderAsync`, and renders previews from a synthesised
|
||||
`ReminderRegistration` (including the single-invoice `invoices` row the reminder PDF table
|
||||
renders) so a preview needs no DB round-trip. **Note:** `RegisterReminderAsync` is create-only
|
||||
(there is no `fds__setReminder` update proc), so a re-saved reminder draft does not update the
|
||||
prior DB row — the primary flow (preview → confirm) flushes once immediately before finalising.
|
||||
renders) so a preview needs no DB round-trip. A first save runs `fds__createReminder`; every
|
||||
later save of the same session runs `fds__setReminder`, which updates the row in place (version
|
||||
bump, same result shape) and refuses — `@Id` NULL → the batch throws → "nicht gespeichert" — once
|
||||
the reminder is finalised or has a stored file. Flushes are serialised on the session's
|
||||
`FlushGate` exactly like invoice flushes (see *Concurrency* above), so overlapping saves cannot
|
||||
create two reminder rows either.
|
||||
- **Cache/expiry** (`ReminderDraftCache` singleton + `ReminderDraftExpiryService`) mirror the
|
||||
invoice ones and share the same `Fuchs:DraftEditing` TTL config.
|
||||
- **Signals** reuse the shared `DraftPreviewHub` + `IDraftNotifier` unchanged — the token-keyed
|
||||
|
||||
Reference in New Issue
Block a user