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
@@ -60,6 +60,24 @@ public sealed class InvoiceDraftSession
|
||||
/// <summary>Automatic change history, appended on every applied patch. Cache-only (never persisted).</summary>
|
||||
public List<ChangeHistoryEntry> History { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Per-session lock. The editor fires patches, state fetches and previews concurrently for the
|
||||
/// same token, and the payload (<see cref="Newtonsoft.Json.Linq.JArray"/>/<c>JObject</c>, the
|
||||
/// history list) is not thread-safe — the draft service takes this lock around every read and
|
||||
/// write of the payload, and hands out deep-cloned snapshots for anything serialised later.
|
||||
/// </summary>
|
||||
public object SyncRoot { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Serialises flushes ("Zwischenspeichern") of this session. A flush reads <see cref="InvId"/>,
|
||||
/// awaits the DB registration and then writes the id back; <see cref="SyncRoot"/> cannot be held
|
||||
/// across that await, so without this gate two overlapping saves of a not-yet-registered draft
|
||||
/// both see an empty id and both create a DB row. Only ever awaited (never held while taking
|
||||
/// <see cref="SyncRoot"/>), so there is no lock-order inversion. Not disposed with the session on
|
||||
/// purpose: a <see cref="SemaphoreSlim"/> owns no kernel handle unless its wait handle is touched.
|
||||
/// </summary>
|
||||
public SemaphoreSlim FlushGate { get; } = new(1, 1);
|
||||
|
||||
public void Touch() => LastAccessUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Fuchs.intranet;
|
||||
|
||||
|
||||
@@ -60,6 +60,23 @@ public sealed class ReminderDraftSession
|
||||
/// <summary>Automatic change history, appended on every applied patch. Cache-only (never persisted).</summary>
|
||||
public List<ChangeHistoryEntry> History { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Per-session lock (see <see cref="InvoiceDraftSession.SyncRoot"/>): the draft service takes it
|
||||
/// around every read and write of the non-thread-safe payload, so concurrent patch/state/preview
|
||||
/// requests for the same token cannot interleave.
|
||||
/// </summary>
|
||||
public object SyncRoot { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Serialises flushes ("Zwischenspeichern") of this session. A flush reads <see cref="RemId"/>,
|
||||
/// awaits the DB registration and then writes the id back; <see cref="SyncRoot"/> cannot be held
|
||||
/// across that await, so without this gate two overlapping saves of a not-yet-registered draft
|
||||
/// both see an empty id and both create a DB row. Only ever awaited (never held while taking
|
||||
/// <see cref="SyncRoot"/>), so there is no lock-order inversion. Not disposed with the session on
|
||||
/// purpose: a <see cref="SemaphoreSlim"/> owns no kernel handle unless its wait handle is touched.
|
||||
/// </summary>
|
||||
public SemaphoreSlim FlushGate { get; } = new(1, 1);
|
||||
|
||||
public void Touch() => LastAccessUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user