feat(federation): queue S2S presence_update on auth/disconnect/status/activity changes

New FederationPresenceUpdatePayload + queuePresenceRelay() helper. Five WS
sites now project the native user's status (and optional activities) to all
active peers via the outbox: WS auth-success, finalizeDisconnect,
manual presence_update, activity_update, showActivity-toggle clear.

Outbox-only (no mutation-log entry) — presence is ephemeral; the upcoming
peer-activation hook re-emits a fresh snapshot so peers recovering from
unreachable converge without history replay. No-op for replicated users.
This commit is contained in:
Jannis Braun
2026-05-05 16:01:28 +02:00
parent bdbc90ebd2
commit 613424e1c7
6 changed files with 230 additions and 1 deletions
@@ -0,0 +1,61 @@
import { eq } from 'drizzle-orm';
import type { Activity, FederationRelayEvent, FederationPresenceUpdatePayload } from '@backspace/shared';
import { getDb, schema } from '../db/index.js';
import { getOurOrigin } from './federationAuth.js';
import { isFederationRelayEnabled, queueOutboxEvent } from './federationOutbox.js';
export type PresenceStatus = 'online' | 'idle' | 'dnd' | 'offline';
/**
* Queue a presence_update event for the given native user. Broadcast to all
* active peers (mirrors profile_update). Outbox-only — presence is ephemeral;
* stale replays from a mutation log are wrong, so we never call
* appendMutationLog. The peer-activation hook re-emits a fresh snapshot for
* peer-related online natives, so a peer recovering from unreachable converges
* without history replay.
*
* No-op for replicated users (their home instance owns presence projection).
*/
export function queuePresenceRelay(
userId: string,
status: PresenceStatus,
activities: Activity[],
): void {
if (!isFederationRelayEnabled()) return;
const db = getDb();
const user = db.select().from(schema.users).where(eq(schema.users.id, userId)).get();
if (!user) return;
if (user.homeInstance) return; // replicated — not our authority
const ts = Date.now();
const payload: FederationPresenceUpdatePayload = {
homeUserId: user.id,
homeInstance: getOurOrigin(),
status,
ts,
...(activities.length > 0 ? { activities } : {}),
};
const event: FederationRelayEvent = {
eventType: 'presence_update',
contextType: 'profile',
messageId: `presence:${user.id}:${ts}`,
encryptionVersion: 0,
timestamp: ts,
presenceUpdate: payload,
};
// entityId = userId so the outbox coalesces rapid status flaps into the latest.
// contextId = userId, contextType = 'profile' (reuses existing routing).
// targetPeerOrigins = undefined → broadcast to all active peers.
// NO appendMutationLog — presence must not be replayed from history.
queueOutboxEvent(
user.id,
user.id,
'presence_update',
JSON.stringify(event),
undefined,
'profile',
);
}