From ae004b72e16887791ef47fd87f2b3e4ca791a778 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:30:54 +0100 Subject: [PATCH] feat(federation): trigger checkpoint sync on startup for peers that have never been synced --- packages/server/src/utils/federationWorker.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/packages/server/src/utils/federationWorker.ts b/packages/server/src/utils/federationWorker.ts index fae905b8..2edd1034 100644 --- a/packages/server/src/utils/federationWorker.ts +++ b/packages/server/src/utils/federationWorker.ts @@ -568,11 +568,103 @@ async function processHealthCheckTick(): Promise { // ─── Lifecycle ────────────────────────────────────────────────────────────── +/** + * Trigger checkpoint sync for peers that have never been synced (lastSyncedAt === 0). + * This catches historical messages that existed before the relay was enabled. + */ +async function runInitialSyncForNewPeers(): Promise { + if (!isFederationRelayEnabled()) return; + + const db = getDb(); + const unsyncedPeers = db + .select() + .from(schema.federationPeers) + .where(and( + eq(schema.federationPeers.status, 'active'), + eq(schema.federationPeers.lastSyncedAt, 0), + )) + .all(); + + if (unsyncedPeers.length === 0) return; + + const ourOrigin = config.domain ? `https://${config.domain}` : `http://localhost:${config.port}`; + + for (const peer of unsyncedPeers) { + try { + console.log(`[federation-worker] Running initial sync with ${peer.origin}...`); + let sinceTimestamp = 0; + let totalEvents = 0; + + // Paginate through all events from the peer + while (true) { + const body = JSON.stringify({ sinceTimestamp, limit: 100 }); + const headers = buildFederationHeaders(body, peer.hmacSecret, ourOrigin); + + const response = await fetch(`${peer.origin}/api/federation/sync`, { + method: 'POST', + headers, + body, + signal: AbortSignal.timeout(30_000), + }); + + if (!response.ok) { + console.error(`[federation-worker] Sync with ${peer.origin} failed: ${response.status}`); + break; + } + + const data = await response.json() as { events: FederationRelayEvent[]; hasMore: boolean; checkpoint: number }; + + if (data.events.length === 0) break; + + // Relay the events through our own relay endpoint logic + // For simplicity, POST them to ourselves + const relayBody = JSON.stringify({ + version: 1, + sourceInstance: peer.origin, + events: data.events, + }); + const relayHeaders = buildFederationHeaders(relayBody, peer.hmacSecret, peer.origin); + + await fetch(`${ourOrigin}/api/federation/relay`, { + method: 'POST', + headers: relayHeaders, + body: relayBody, + signal: AbortSignal.timeout(30_000), + }); + + totalEvents += data.events.length; + sinceTimestamp = data.checkpoint; + + if (!data.hasMore) break; + } + + // Update lastSyncedAt so this doesn't run again + db.update(schema.federationPeers) + .set({ lastSyncedAt: Date.now() }) + .where(eq(schema.federationPeers.id, peer.id)) + .run(); + + if (totalEvents > 0) { + console.log(`[federation-worker] Initial sync with ${peer.origin}: ${totalEvents} events synced`); + } else { + console.log(`[federation-worker] Initial sync with ${peer.origin}: no events to sync`); + } + } catch (err) { + console.error(`[federation-worker] Initial sync with ${peer.origin} failed:`, err); + // Don't update lastSyncedAt — will retry next startup + } + } +} + export function startFederationWorkers(): void { console.log('[federation-worker] Federation workers started'); scheduleOutboxTick(); scheduleFileQueueTick(); scheduleHealthCheckTick(); + // Run initial sync for newly peered instances (async, non-blocking) + runInitialSyncForNewPeers().catch((err) => { + console.error('[federation-worker] Initial sync error:', err); + }); } export function stopFederationWorkers(): void {