feat: read projection sourced from the register in Objecten (closes #153) #155
@@ -8,7 +8,8 @@ namespace EventSubscriber.Application;
|
||||
public sealed class NotificationProjector(INotificationLog log, IProjectionStore store, IAclClient acl)
|
||||
{
|
||||
/// <summary>Handle one inbound notification. Reacts to a register record being written to
|
||||
/// Objecten (S-19b-2, ADR-0030) and ignores everything else.</summary>
|
||||
/// Objecten (S-19b-2, ADR-0030) and ignores everything else. The notification carries only the
|
||||
/// object URL, so the record is read back through the ACL (§8.1) and becomes the row verbatim.</summary>
|
||||
public async Task HandleAsync(Notification notification, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
@@ -16,11 +17,36 @@ public sealed class NotificationProjector(INotificationLog log, IProjectionStore
|
||||
if (!notification.IsRegisterRecordWritten)
|
||||
return;
|
||||
|
||||
// S-19b-2: reading the record back through the ACL and projecting it lands with the
|
||||
// implementation; today nothing reaches the store.
|
||||
await Task.CompletedTask;
|
||||
var record = await acl.GetRegisterRecordAsync(notification.ObjectUrl, ct);
|
||||
// The object is gone, or holds no register record — nothing to project (§8.6).
|
||||
if (record is null)
|
||||
return;
|
||||
|
||||
var recorded = new RecordedNotification(
|
||||
KeyFor(notification.ObjectUrl, record), record.Id, record.Status, record.Reference);
|
||||
|
||||
// Atomic record-or-skip: a duplicate (or concurrent) delivery is recognised and dropped
|
||||
// before it touches the projection, so the projection stays a faithful derived artefact.
|
||||
if (!await log.TryRecordAsync(recorded, ct))
|
||||
return;
|
||||
|
||||
await store.UpsertAsync(ToEntry(recorded), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A deterministic dedup key: the object, plus the state that write puts in the projection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Open Notificaties carries no notification id and may redeliver, so the key is derived from
|
||||
/// content. It cannot be the object URL alone — the ACL upserts one object per registration, so
|
||||
/// submit and approval both notify about the *same* URL and the approval would be swallowed as a
|
||||
/// duplicate. Nor can it include the actie: a retried approval would be a second `update`. Keying
|
||||
/// on the projected row means a redelivery collapses and a genuine state change does not, which
|
||||
/// is exactly the property §8.6 asks for.
|
||||
/// </remarks>
|
||||
private static string KeyFor(Uri objectUrl, RegisterRecord record)
|
||||
=> $"objecten:object:{objectUrl}:{record.Status}:{record.Reference}";
|
||||
|
||||
/// <summary>Rebuild the projection from the durable notification log (PRD §8.4).</summary>
|
||||
public async Task RebuildAsync(CancellationToken ct = default)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user