Replaces the B-T1 stub WriteAsync with the production hot-path: - Bounded Channel<PendingAuditEvent> (BoundedChannelFullMode.Wait, capacity from options) feeds a background ProcessWriteQueueAsync loop that drains up to BatchSize events per transaction. - The loop INSERTs each event with explicit parameter binding (enums and DateTime stored as text); duplicate EventIds (SqliteException with ErrorCode 19 SQLITE_CONSTRAINT) are swallowed as first-write-wins per alog.md §11, and the pending TCS is still completed successfully so callers see idempotent semantics. - Site rows force ForwardState = Pending on enqueue when the inbound event leaves it null — site-side default per the M2 design. - ReadPendingAsync(limit) returns oldest-first pending rows for the Bundle D telemetry actor; EventId is the deterministic tiebreaker on identical OccurredAtUtc timestamps. MarkForwardedAsync(ids) flips a batch to Forwarded in one UPDATE with a parameterised IN list. - IAsyncDisposable graceful shutdown: TryComplete the writer, await the drain (5s budget), then dispose the connection. Tests (7 new, total 16 -> 23): - WriteAsync_FreshEvent_PersistsWithForwardStatePending - WriteAsync_Concurrent_1000Calls_All_Persist_NoExceptions - WriteAsync_DuplicateEventId_FirstWriteWins_NoException - WriteAsync_ForcesForwardStatePending_IfNull - ReadPendingAsync_Returns_OldestFirst_LimitedToN - MarkForwardedAsync_FlipsRowsToForwarded - MarkForwardedAsync_NonExistentId_NoThrow
7.7 KiB
7.7 KiB