fix(federation): broadcast peering_subscription_changed on terminal state

Live verification caught a stale-UI bug: when admin denial / approval
fanout / janitor expiry cascaded subscriber rows away, only the
peering_notification_received WS event fired (which only refetches
the notification list). The user's pending-subscriptions section
stayed stale until manual refresh.

Each terminal-state path now also fires peering_subscription_changed
to affected users so their pending list refreshes alongside the new
notification. Fixed in:
- onPeerActivated.fanoutOutboundSubscribers (approval path)
- handleOutboundDeny (admin denial)
- cleanupExpiredApprovalRequests (janitor expiry; also adds the
  notification-received broadcast that was previously deferred to
  next-page-load only)
This commit is contained in:
Jannis Braun
2026-04-26 23:20:31 +02:00
parent e35b44c05f
commit 6ba8b9256b
3 changed files with 20 additions and 0 deletions
+4
View File
@@ -775,6 +775,10 @@ async function handleOutboundDeny(
type: 'peering_notification_received' as const,
kind: 'denied',
});
// Subscriber row is about to cascade-delete; refresh the user's pending list.
connectionManager.sendToUser(sub.userId, {
type: 'peering_subscription_changed' as const,
});
}
// Cascade-delete clears subscribers via onDelete: 'cascade'.
@@ -246,6 +246,11 @@ async function fanoutOutboundSubscribers(peerId: string): Promise<void> {
type: 'peering_notification_received' as const,
kind: 'approved',
});
// The subscriber row is about to cascade-delete; tell the user's UI to
// refetch its pending list so the now-stale row disappears.
connectionManager.sendToUser(sub.userId, {
type: 'peering_subscription_changed' as const,
});
}
// Cascade-deletes subscriber rows via onDelete: 'cascade'.
@@ -460,6 +460,7 @@ export async function cleanupExpiredApprovalRequests(): Promise<number> {
if (expiredRequests.length === 0) return 0;
const { getOurOrigin, buildFederationHeaders } = await import('./federationAuth.js');
const { connectionManager } = await import('../ws/handler.js');
const ourOrigin = getOurOrigin();
const now = Date.now();
let deletedCount = 0;
@@ -486,6 +487,16 @@ export async function cleanupExpiredApprovalRequests(): Promise<number> {
readAt: null,
})
.run();
// Subscriber row is about to cascade-delete; if the user is online,
// refresh their pending list AND notification list. Offline users see
// both on next page load via the GET endpoints (persistence guarantee).
connectionManager.sendToUser(sub.userId, {
type: 'peering_notification_received' as const,
kind: 'expired',
});
connectionManager.sendToUser(sub.userId, {
type: 'peering_subscription_changed' as const,
});
}
db.delete(schema.peerApprovalRequests)
.where(eq(schema.peerApprovalRequests.id, req.id))