feat: client ensurePeered API, connection flow swap, WS event handlers for peer rejection

This commit is contained in:
Jannis Braun
2026-04-09 13:56:18 +02:00
parent 5f700c1a51
commit 4285e44d2d
4 changed files with 58 additions and 5 deletions
+1 -1
View File
@@ -390,7 +390,7 @@ export type ClientEvent =
// Server → Client Events
export type ServerEvent =
| { type: 'ready'; user: User; spaces: SpaceWithChannelsAndMembers[]; dmChannels: DmChannel[]; folders?: SpaceFolder[]; spaceLayout?: SpaceLayoutItem[] | null; layoutUpdatedAt?: number; voiceStates?: Record<string, string[]>; voiceUserStates?: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>; readStates?: ReadState[]; activeCalls?: ActiveCallInfo[]; spaceVoiceStates?: Record<string, { spaceMuted: boolean; spaceDeafened: boolean }>; userActivities?: Record<string, Activity[]> }
| { type: 'ready'; user: User; spaces: SpaceWithChannelsAndMembers[]; dmChannels: DmChannel[]; folders?: SpaceFolder[]; spaceLayout?: SpaceLayoutItem[] | null; layoutUpdatedAt?: number; voiceStates?: Record<string, string[]>; voiceUserStates?: Record<string, { isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; isScreenSharing: boolean }>; readStates?: ReadState[]; activeCalls?: ActiveCallInfo[]; spaceVoiceStates?: Record<string, { spaceMuted: boolean; spaceDeafened: boolean }>; userActivities?: Record<string, Activity[]>; rejectedPeerOrigins?: string[] }
| { type: 'message_created'; message: MessageWithUser }
| { type: 'message_updated'; message: MessageWithUser }
| { type: 'message_deleted'; messageId: string; channelId: string }
+5
View File
@@ -224,6 +224,7 @@ export class BackspaceApiClient {
readonly federation: {
initiatePeering: (data: { remoteOrigin: string }) => Promise<{ peer: FederationPeer }>;
ensurePeered: (data: { remoteOrigin: string }) => Promise<{ peeringStatus: string; peerId?: string; error?: string }>;
peers: () => Promise<{ peers: FederationPeer[] }>;
revokePeer: (id: string) => Promise<{ success: boolean }>;
rotatePeerSecret: (id: string) => Promise<{ success: boolean; gracePeriodMs: number }>;
@@ -666,6 +667,10 @@ export class BackspaceApiClient {
request<{ peer: FederationPeer }>(
'POST', '/federation/peer/initiate', data
),
ensurePeered: (data: { remoteOrigin: string }) =>
request<{ peeringStatus: string; peerId?: string; error?: string }>(
'POST', '/federation/peer/ensure', data
),
peers: () =>
request<{ peers: FederationPeer[] }>(
'GET', '/federation/peers'
+34
View File
@@ -15,6 +15,13 @@ import { useUIStore } from '../stores/uiStore';
import { useActivityStore } from '../stores/activityStore';
import { useDiscoverStore } from '../stores/discoverStore';
// ─── Rejected peer origins (for unreachable member indicators) ───────────────
const rejectedPeerOrigins = new Set<string>();
export function getRejectedPeerOrigins(): Set<string> {
return rejectedPeerOrigins;
}
// ─── Connection state ─────────────────────────────────────────────────────────
interface ConnectionState {
@@ -364,6 +371,16 @@ function handleEvent(origin: string, event: ServerEvent): void {
loadFriends();
loadRequests();
}
// Populate rejected peer origins for DM member indicators
if (isHome) {
rejectedPeerOrigins.clear();
if (Array.isArray(event.rejectedPeerOrigins)) {
for (const o of event.rejectedPeerOrigins) {
rejectedPeerOrigins.add(o);
}
}
}
break;
case 'message_created':
@@ -638,6 +655,23 @@ function handleEvent(origin: string, event: ServerEvent): void {
break;
}
case 'federation_peer_rejected': {
const { addToast } = useUIStore.getState();
rejectedPeerOrigins.add(event.peerOrigin);
const label = event.peerLabel || event.peerOrigin;
addToast(
`Cannot relay messages to ${label} — their server requires manual peering approval. Contact their admin to set up peering.`,
'warning',
10000,
);
break;
}
case 'federation_peer_active': {
rejectedPeerOrigins.delete(event.peerOrigin);
break;
}
case 'dm_message_deleted':
removeMessage(event.messageId, event.dmChannelId);
break;
+18 -4
View File
@@ -341,11 +341,25 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
// Open WebSocket connection to the remote instance
connectInstance(origin, response.token);
// Initiate server-to-server peering for DM relay (non-fatal)
// Ensure server-to-server peering for DM relay (non-fatal)
try {
await api.federation.initiatePeering({ remoteOrigin: origin });
const peerResult = await api.federation.ensurePeered({ remoteOrigin: origin });
if (peerResult.peeringStatus === 'rejected') {
const { addToast } = useUIStore.getState();
addToast(
`Cross-instance messaging unavailable — ${instance.label} requires manual peering approval`,
'warning',
10000,
);
} else if (peerResult.peeringStatus === 'pending') {
const { addToast } = useUIStore.getState();
addToast(
`Peering with ${instance.label} in progress — cross-instance messaging will be available shortly`,
'info',
);
}
} catch (err) {
console.warn('[federation] Peering initiation failed (non-fatal):', err);
console.warn('[federation] Peering attempt failed (non-fatal):', err);
}
// Sync instance list to all instances (fire-and-forget)
@@ -977,7 +991,7 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
connectInstance(origin, cachedEntry.token);
// Initiate server-to-server peering for DM relay (non-fatal, idempotent)
api.federation.initiatePeering({ remoteOrigin: origin }).catch(() => {});
api.federation.ensurePeered({ remoteOrigin: origin }).catch(() => {});
} catch (err) {
if (isNetworkError(err)) {
// Instance unreachable (NAT hairpinning, DNS, server down) — token may still be valid