/** * Sync Trigger API Route * * POST /api/sync/run - Manually trigger a full GitHub sync * Supports role-based sync with ETag caching for efficient API usage */ import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/auth"; import { runMaintainerSync, runContributorSync, syncContributorPRsDirect, reconcileOpenTriageIssue1 } from "@/lib/sync/github-sync"; const SYNC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes // Layer 2 Defense: In-memory set to prevent race conditions // This provides instant protection before database queries const activeSyncs = new Set(); export async function POST(request: NextRequest) { try { const user = await getCurrentUser(request); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (!user.githubAccessToken) { return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 }); } // Circuit Breaker: In-memory check (prevents concurrent syncs per user) if (activeSyncs.has(user.id)) { console.log(`[SyncRun] User ${user.id} sync blocked by in-memory guard`); return NextResponse.json({ error: "Sync already in progress", status: "SYNCING", message: "Please wait for the current sync to complete" }, { status: 429 }); } // Add to in-memory set BEFORE starting sync activeSyncs.add(user.id); console.log(`[SyncRun] Added user ${user.id} to activeSyncs. Size: ${activeSyncs.size}`); try { console.log(`[SyncRun] Starting sync for user ${user.id}`); // Determine sync type based on user role const userRole = user.role?.toUpperCase(); let stats; let directSync = null; if (userRole === "MAINTAINER") { // Maintainers sync all open issues/PRs stats = await runMaintainerSync(user.id, user.githubAccessToken); } else { // Contributors sync only their authored PRs // First: Run search-based sync (may have indexing delay) stats = await runContributorSync(user.id, user.username, user.githubAccessToken); // Second: Direct fetch from repos where user has existing PRs (bypasses search delay) directSync = await syncContributorPRsDirect(user.id, user.username, user.githubAccessToken); } // Always reconcile the critical openTriage#1 issue to ensure immediate state sync const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken); console.log(`[SyncRun] Sync completed for user ${user.id}`); return NextResponse.json({ success: true, message: "Sync completed", role: userRole, stats, directSync, // Include direct sync results for contributors reconcile: { openTriageIssue1: reconcileResult, }, nextSyncIntervalMs: SYNC_INTERVAL_MS, }); } catch (syncError: any) { console.error(`[SyncRun] Error syncing for user ${user.id}:`, syncError); return NextResponse.json({ error: "Sync failed", message: syncError.message || "Internal server error" }, { status: 500 }); } finally { // CRITICAL: Always remove from in-memory set, even on error activeSyncs.delete(user.id); console.log(`[SyncRun] Removed user ${user.id} from activeSyncs. Size: ${activeSyncs.size}`); } } catch (error) { console.error("POST /api/sync/run error:", error); return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } } export async function GET(request: NextRequest) { // Return sync configuration info return NextResponse.json({ syncIntervalMs: SYNC_INTERVAL_MS, syncIntervalMinutes: SYNC_INTERVAL_MS / 60000, description: "GitHub sync runs every 5 minutes. POST to trigger manually.", features: [ "ETag caching - skips sync if no changes (304 Not Modified)", "State reconciliation - marks issues as closed if not in open list", "Role-based filtering - Contributors see only their authored PRs", ], }); }