KrishnaCosmic commited on
Commit
2cc8683
·
1 Parent(s): 8a2fb1b

apply changes

Browse files
src/app/api/sync/run/route.ts CHANGED
@@ -8,6 +8,9 @@
8
  import { NextRequest, NextResponse } from "next/server";
9
  import { getCurrentUser } from "@/lib/auth";
10
  import { runFullSync, runMaintainerSync, runContributorSync, reconcileOpenTriageIssue1, syncContributorPRsDirect, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
 
 
 
11
 
12
  export async function POST(request: NextRequest) {
13
  try {
@@ -20,37 +23,107 @@ export async function POST(request: NextRequest) {
20
  return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
21
  }
22
 
23
- // Determine sync type based on user role
24
- const userRole = user.role?.toUpperCase();
25
- let stats;
26
- let directSync = null;
27
-
28
- if (userRole === "MAINTAINER") {
29
- // Maintainers sync all open issues/PRs
30
- stats = await runMaintainerSync(user.id, user.githubAccessToken);
31
- } else {
32
- // Contributors sync only their authored PRs
33
- // First: Run search-based sync (may have indexing delay)
34
- stats = await runContributorSync(user.id, user.username, user.githubAccessToken);
35
-
36
- // Second: Direct fetch from repos where user has existing PRs (bypasses search delay)
37
- directSync = await syncContributorPRsDirect(user.id, user.username, user.githubAccessToken);
38
  }
39
 
40
- // Always reconcile the critical openTriage#1 issue to ensure immediate state sync
41
- const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken);
42
-
43
- return NextResponse.json({
44
- success: true,
45
- message: "Sync completed",
46
- role: userRole,
47
- stats,
48
- directSync, // Include direct sync results for contributors
49
- reconcile: {
50
- openTriageIssue1: reconcileResult,
51
- },
52
- nextSyncIntervalMs: SYNC_INTERVAL_MS,
53
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  } catch (error) {
55
  console.error("POST /api/sync/run error:", error);
56
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
 
8
  import { NextRequest, NextResponse } from "next/server";
9
  import { getCurrentUser } from "@/lib/auth";
10
  import { runFullSync, runMaintainerSync, runContributorSync, reconcileOpenTriageIssue1, syncContributorPRsDirect, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
11
+ import { db } from "@/db";
12
+ import { users } from "@/db/schema";
13
+ import { eq } from "drizzle-orm";
14
 
15
  export async function POST(request: NextRequest) {
16
  try {
 
23
  return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
24
  }
25
 
26
+ // Get current user record to check sync status
27
+ const userRecord = await db.select()
28
+ .from(users)
29
+ .where(eq(users.id, user.id))
30
+ .limit(1);
31
+
32
+ if (!userRecord || userRecord.length === 0) {
33
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
 
 
 
 
 
 
 
34
  }
35
 
36
+ const currentSyncStatus = userRecord[0].syncStatus;
37
+
38
+ // Circuit breaker: Check if sync already in progress
39
+ if (currentSyncStatus === 'SYNCING') {
40
+ console.log(`[SyncRun] User ${user.id} sync already in progress`);
41
+ return NextResponse.json({
42
+ error: "Sync already in progress",
43
+ status: "SYNCING",
44
+ message: "Please wait for the current sync to complete"
45
+ }, { status: 429 });
46
+ }
47
+
48
+ if (currentSyncStatus === 'PENDING') {
49
+ console.log(`[SyncRun] User ${user.id} sync is pending`);
50
+ return NextResponse.json({
51
+ error: "Sync is queued",
52
+ status: "PENDING",
53
+ message: "Sync request is queued and will start shortly"
54
+ }, { status: 202 });
55
+ }
56
+
57
+ // Set status to SYNCING before starting
58
+ await db.update(users)
59
+ .set({
60
+ syncStatus: 'SYNCING',
61
+ syncError: null
62
+ })
63
+ .where(eq(users.id, user.id));
64
+
65
+ console.log(`[SyncRun] Starting sync for user ${user.id}`);
66
+
67
+ try {
68
+ // Determine sync type based on user role
69
+ const userRole = user.role?.toUpperCase();
70
+ let stats;
71
+ let directSync = null;
72
+
73
+ if (userRole === "MAINTAINER") {
74
+ // Maintainers sync all open issues/PRs
75
+ stats = await runMaintainerSync(user.id, user.githubAccessToken);
76
+ } else {
77
+ // Contributors sync only their authored PRs
78
+ // First: Run search-based sync (may have indexing delay)
79
+ stats = await runContributorSync(user.id, user.username, user.githubAccessToken);
80
+
81
+ // Second: Direct fetch from repos where user has existing PRs (bypasses search delay)
82
+ directSync = await syncContributorPRsDirect(user.id, user.username, user.githubAccessToken);
83
+ }
84
+
85
+ // Always reconcile the critical openTriage#1 issue to ensure immediate state sync
86
+ const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken);
87
+
88
+ // Mark sync as COMPLETED
89
+ await db.update(users)
90
+ .set({
91
+ syncStatus: 'COMPLETED',
92
+ lastSyncAt: new Date().toISOString(),
93
+ syncError: null
94
+ })
95
+ .where(eq(users.id, user.id));
96
+
97
+ console.log(`[SyncRun] Sync completed for user ${user.id}`);
98
+
99
+ return NextResponse.json({
100
+ success: true,
101
+ message: "Sync completed",
102
+ role: userRole,
103
+ stats,
104
+ directSync, // Include direct sync results for contributors
105
+ reconcile: {
106
+ openTriageIssue1: reconcileResult,
107
+ },
108
+ nextSyncIntervalMs: SYNC_INTERVAL_MS,
109
+ });
110
+
111
+ } catch (syncError: any) {
112
+ // Mark sync as FAILED with error message
113
+ await db.update(users)
114
+ .set({
115
+ syncStatus: 'FAILED',
116
+ syncError: syncError.message || 'Unknown error'
117
+ })
118
+ .where(eq(users.id, user.id));
119
+
120
+ console.error(`[SyncRun] Sync failed for user ${user.id}:`, syncError);
121
+
122
+ return NextResponse.json({
123
+ error: "Sync failed",
124
+ message: syncError.message || "Internal server error"
125
+ }, { status: 500 });
126
+ }
127
  } catch (error) {
128
  console.error("POST /api/sync/run error:", error);
129
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
src/app/api/sync/status/route.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Sync Status API Route
3
+ *
4
+ * GET /api/sync/status - Check current sync status for the authenticated user
5
+ * Used by frontend to poll sync progress without triggering new syncs
6
+ */
7
+
8
+ import { NextRequest, NextResponse } from "next/server";
9
+ import { getCurrentUser } from "@/lib/auth";
10
+ import { db } from "@/db";
11
+ import { users } from "@/db/schema";
12
+ import { eq } from "drizzle-orm";
13
+
14
+ export async function GET(request: NextRequest) {
15
+ try {
16
+ const user = await getCurrentUser(request);
17
+ if (!user) {
18
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
19
+ }
20
+
21
+ // Get user's sync status
22
+ const userRecord = await db.select({
23
+ syncStatus: users.syncStatus,
24
+ lastSyncAt: users.lastSyncAt,
25
+ syncError: users.syncError,
26
+ })
27
+ .from(users)
28
+ .where(eq(users.id, user.id))
29
+ .limit(1);
30
+
31
+ if (!userRecord || userRecord.length === 0) {
32
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
33
+ }
34
+
35
+ const { syncStatus, lastSyncAt, syncError } = userRecord[0];
36
+
37
+ return NextResponse.json({
38
+ status: syncStatus || 'IDLE',
39
+ lastSyncAt: lastSyncAt || null,
40
+ error: syncError || null,
41
+ isIdl: syncStatus === 'IDLE' || syncStatus === 'COMPLETED',
42
+ isSyncing: syncStatus === 'SYNCING',
43
+ isPending: syncStatus === 'PENDING',
44
+ hasFailed: syncStatus === 'FAILED',
45
+ });
46
+
47
+ } catch (error) {
48
+ console.error("GET /api/sync/status error:", error);
49
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
50
+ }
51
+ }
src/db/migrations/add_sync_status.sql ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Migration: Add sync status tracking to users table
2
+ -- Date: 2026-02-12
3
+ -- Purpose: Prevent concurrent sync requests and track sync history
4
+
5
+ -- Add sync status columns to users table
6
+ ALTER TABLE users ADD COLUMN sync_status TEXT DEFAULT 'IDLE';
7
+ ALTER TABLE users ADD COLUMN last_sync_at TEXT;
8
+ ALTER TABLE users ADD COLUMN sync_error TEXT;
9
+
10
+ -- Create index for faster sync status queries
11
+ CREATE INDEX IF NOT EXISTS idx_users_sync_status ON users(sync_status);
12
+
13
+ -- Comments
14
+ -- sync_status: IDLE | PENDING | SYNCING | COMPLETED | FAILED
15
+ -- last_sync_at: ISO 8601 timestamp of last successful sync
16
+ -- sync_error: Error message if last sync failed
src/db/schema.ts CHANGED
@@ -38,6 +38,9 @@ export const users = sqliteTable("users", {
38
  avatarUrl: text("avatar_url").notNull(),
39
  role: text("role"), // UserRole enum
40
  githubAccessToken: text("github_access_token"),
 
 
 
41
  createdAt: text("created_at").notNull(),
42
  updatedAt: text("updated_at").notNull(),
43
  });
 
38
  avatarUrl: text("avatar_url").notNull(),
39
  role: text("role"), // UserRole enum
40
  githubAccessToken: text("github_access_token"),
41
+ syncStatus: text("sync_status").default("IDLE"), // IDLE, PENDING, SYNCING, COMPLETED, FAILED
42
+ lastSyncAt: text("last_sync_at"), // Timestamp of last successful sync
43
+ syncError: text("sync_error"), // Error message if sync failed
44
  createdAt: text("created_at").notNull(),
45
  updatedAt: text("updated_at").notNull(),
46
  });