feat: client ensurePeered API, connection flow swap, WS event handlers for peer rejection
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user