Skip to content

fix(notifications): archive the oldest active notification at the per-source cap instead of dropping the newest - #685

Merged
ryceg merged 1 commit into
mainfrom
fix/notification-cap-evicts-oldest
Aug 12, 2026
Merged

fix(notifications): archive the oldest active notification at the per-source cap instead of dropping the newest#685
ryceg merged 1 commit into
mainfrom
fix/notification-cap-evicts-oldest

Conversation

@ryceg

@ryceg ryceg commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

InAppNotificationService.CreateNotificationAsync threw once a (userId, source) pair held 10 active notifications (inline literal). The cap is per source, not per subject, so a user with 11+ simultaneously pending items from one source lost everything past the tenth — and since MealMatchingService catches per entry, the drop was invisible outside an error log. NotificationsController additionally caught the throw by message substring (Contains("Rate limit")) to return 429.

Fix

  • The limit is a named public constant, MaxActiveNotificationsPerSource = 10.
  • A create that would exceed it archives the source's oldest active notifications (new NotificationArchiveReason.Superseded; the column is string-converted, so no migration) through the existing ArchiveNotificationAsync path — archive broadcast and ownership guard included — and then always succeeds. The newest item is kept; the stalest is displaced.
  • GetActiveCountBySourceAsync (single caller, now useless) is replaced by GetActiveBySourceAsync, ordered CreatedAt then Id for a deterministic oldest.
  • The controller's message-sniffing 429 catch and its ProducesResponseType(429) are removed; nothing throws for the cap any more.

The constant is a public const rather than an options binding: no notification options surface exists today (NotificationCleanupService hardcodes its intervals the same way). Happy to bind it to configuration if preferred.

Tests

Three new tests: the eleventh create supersedes exactly the oldest and keeps the newest; one-below-cap supersedes nothing; over-cap (12 active) archives three to land on the cap. Mutation-proven: off-by-one flips in the overflow arithmetic fail 2–3 of them. ~Notifications suite: 103 passed / 0 failed.

Follow-up from #571.

https://claude.ai/code/session_01NwnN3ND2eSXKAxJteNWSXb

…of dropping the newest

A create for a (user, source) pair already holding ten active notifications threw,
so the eleventh and every later item was lost while the ten stale ones stayed put.
Callers that swallow the failure per item made the loss invisible outside a log.

The cap now archives the source's oldest active notifications as Superseded, and the
new one is always written. The repository returns the source's active rows oldest
first in place of a bare count, and the create endpoint's 429 branch goes with the
throw it caught.

Claude-Session: https://claude.ai/code/session_01NwnN3ND2eSXKAxJteNWSXb
@github-actions

Copy link
Copy Markdown
Contributor

Preview Container Images

Published for commit b3d2db2 with tag pr-685-b3d2db2.

Image Status Package URI
nocturne-api ✅ Published package ghcr.io/nightscout/nocturne/nocturne-api:pr-685-b3d2db2
nocturne-demo ✅ Published package ghcr.io/nightscout/nocturne/nocturne-demo:pr-685-b3d2db2
nocturne-web ✅ Published package ghcr.io/nightscout/nocturne/nocturne-web:pr-685-b3d2db2

This comment is updated on each push to this PR.

@ryceg
ryceg merged commit ef6a5e1 into main Aug 12, 2026
16 checks passed
@ryceg
ryceg deleted the fix/notification-cap-evicts-oldest branch August 12, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant