feat(alerting): InAppInboxQuery with 5s unread-count memoization
listInbox resolves user groups+roles via RbacService.getEffectiveGroupsForUser / getEffectiveRolesForUser then delegates to AlertInstanceRepository. countUnread memoized per (envId, userId) with 5s TTL via ConcurrentHashMap using a controllable Clock. 6 unit tests covering delegation, cache hit, TTL expiry, and isolation between users/envs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package com.cameleer.server.app.alerting.notify;
|
||||
|
||||
import com.cameleer.server.core.alerting.AlertInstance;
|
||||
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
||||
import com.cameleer.server.core.rbac.RbacService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Server-side query helper for the in-app alert inbox.
|
||||
* <p>
|
||||
* {@link #listInbox} returns alerts the user is allowed to see (targeted directly or via group/role).
|
||||
* {@link #countUnread} is memoized per {@code (envId, userId)} for 5 seconds to avoid hammering
|
||||
* the database on every page render.
|
||||
*/
|
||||
@Component
|
||||
public class InAppInboxQuery {
|
||||
|
||||
private static final long MEMO_TTL_MS = 5_000L;
|
||||
|
||||
private final AlertInstanceRepository instanceRepo;
|
||||
private final RbacService rbacService;
|
||||
private final Clock clock;
|
||||
|
||||
/** Cache key for the unread count memo. */
|
||||
private record Key(UUID envId, String userId) {}
|
||||
|
||||
/** Cache entry: cached count + expiry timestamp. */
|
||||
private record Entry(long count, Instant expiresAt) {}
|
||||
|
||||
private final ConcurrentHashMap<Key, Entry> memo = new ConcurrentHashMap<>();
|
||||
|
||||
public InAppInboxQuery(AlertInstanceRepository instanceRepo,
|
||||
RbacService rbacService,
|
||||
Clock alertingClock) {
|
||||
this.instanceRepo = instanceRepo;
|
||||
this.rbacService = rbacService;
|
||||
this.clock = alertingClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent {@code limit} alert instances visible to the given user.
|
||||
* <p>
|
||||
* Visibility: the instance must target this user directly, or target a group the user belongs to,
|
||||
* or target a role the user holds. Empty target lists mean "broadcast to all".
|
||||
*/
|
||||
public List<AlertInstance> listInbox(UUID envId, String userId, int limit) {
|
||||
List<String> groupIds = resolveGroupIds(userId);
|
||||
List<String> roleNames = resolveRoleNames(userId);
|
||||
return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of unread (un-acked) alert instances visible to the user.
|
||||
* <p>
|
||||
* The result is memoized for 5 seconds per {@code (envId, userId)}.
|
||||
*/
|
||||
public long countUnread(UUID envId, String userId) {
|
||||
Key key = new Key(envId, userId);
|
||||
Instant now = Instant.now(clock);
|
||||
Entry cached = memo.get(key);
|
||||
if (cached != null && now.isBefore(cached.expiresAt())) {
|
||||
return cached.count();
|
||||
}
|
||||
long count = instanceRepo.countUnreadForUser(envId, userId);
|
||||
memo.put(key, new Entry(count, now.plusMillis(MEMO_TTL_MS)));
|
||||
return count;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private List<String> resolveGroupIds(String userId) {
|
||||
return rbacService.getEffectiveGroupsForUser(userId)
|
||||
.stream()
|
||||
.map(g -> g.id().toString())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<String> resolveRoleNames(String userId) {
|
||||
return rbacService.getEffectiveRolesForUser(userId)
|
||||
.stream()
|
||||
.map(r -> r.name())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user