A third party ATProto appview

display bug fix

+83 -81
+83 -81
server/storage.ts
··· 713 713 private statsCache: { data: unknown; timestamp: number } | null = null; 714 714 private readonly STATS_CACHE_TTL = 60000; 715 715 private statsQueryInProgress = false; 716 + private statsQueryPromise: Promise<any> | null = null; 716 717 private backgroundRefreshInterval: NodeJS.Timeout | null = null; 717 718 718 719 constructor(dbConnection?: DbConnection) { ··· 3873 3874 console.log('[STORAGE] Clearing stats cache'); 3874 3875 this.statsCache = null; 3875 3876 this.statsQueryInProgress = false; 3877 + this.statsQueryPromise = null; 3876 3878 } 3877 3879 3878 3880 private async refreshStatsInBackground() { ··· 3946 3948 console.log('[STORAGE] No Redis counts, falling back to PostgreSQL COUNT queries'); 3947 3949 3948 3950 // No Redis counts - fallback to PostgreSQL query (only on first load) 3949 - if (this.statsQueryInProgress) { 3950 - // Another request is already fetching, return zeros 3951 - return { 3952 - totalUsers: 0, 3953 - totalPosts: 0, 3954 - totalLikes: 0, 3955 - totalReposts: 0, 3956 - totalFollows: 0, 3957 - totalBlocks: 0, 3958 - }; 3951 + if (this.statsQueryInProgress && this.statsQueryPromise) { 3952 + // Another request is already fetching, wait for it instead of returning zeros 3953 + console.log('[STORAGE] Stats query already in progress, waiting for result...'); 3954 + return this.statsQueryPromise; 3959 3955 } 3960 3956 3961 3957 this.statsQueryInProgress = true; 3962 3958 3963 - try { 3964 - // Use accurate COUNT(*) queries instead of pg_stat_user_tables estimates 3965 - // Run all counts in a single query for better performance 3966 - const statsPromise = this.db.execute<{ 3967 - users: string; 3968 - posts: string; 3969 - likes: string; 3970 - reposts: string; 3971 - follows: string; 3972 - blocks: string; 3973 - }>(sql` 3974 - SELECT 3975 - (SELECT COUNT(*)::text FROM users) as users, 3976 - (SELECT COUNT(*)::text FROM posts) as posts, 3977 - (SELECT COUNT(*)::text FROM likes) as likes, 3978 - (SELECT COUNT(*)::text FROM reposts) as reposts, 3979 - (SELECT COUNT(*)::text FROM follows) as follows, 3980 - (SELECT COUNT(*)::text FROM blocks) as blocks 3981 - `); 3959 + // Create a promise for this query so concurrent requests can wait for it 3960 + this.statsQueryPromise = (async () => { 3961 + try { 3962 + // Use accurate COUNT(*) queries instead of pg_stat_user_tables estimates 3963 + // Run all counts in a single query for better performance 3964 + const statsPromise = this.db.execute<{ 3965 + users: string; 3966 + posts: string; 3967 + likes: string; 3968 + reposts: string; 3969 + follows: string; 3970 + blocks: string; 3971 + }>(sql` 3972 + SELECT 3973 + (SELECT COUNT(*)::text FROM users) as users, 3974 + (SELECT COUNT(*)::text FROM posts) as posts, 3975 + (SELECT COUNT(*)::text FROM likes) as likes, 3976 + (SELECT COUNT(*)::text FROM reposts) as reposts, 3977 + (SELECT COUNT(*)::text FROM follows) as follows, 3978 + (SELECT COUNT(*)::text FROM blocks) as blocks 3979 + `); 3980 + 3981 + const timeoutPromise = new Promise<never>((_, reject) => { 3982 + setTimeout(() => reject(new Error('Stats query timeout')), 10000); 3983 + }); 3982 3984 3983 - const timeoutPromise = new Promise<never>((_, reject) => { 3984 - setTimeout(() => reject(new Error('Stats query timeout')), 10000); 3985 - }); 3985 + const result = await Promise.race([statsPromise, timeoutPromise]); 3986 3986 3987 - const result = await Promise.race([statsPromise, timeoutPromise]); 3987 + const row = result.rows[0] as { 3988 + users?: string; 3989 + posts?: string; 3990 + likes?: string; 3991 + reposts?: string; 3992 + follows?: string; 3993 + blocks?: string; 3994 + }; 3988 3995 3989 - const row = result.rows[0] as { 3990 - users?: string; 3991 - posts?: string; 3992 - likes?: string; 3993 - reposts?: string; 3994 - follows?: string; 3995 - blocks?: string; 3996 - }; 3996 + const data = { 3997 + totalUsers: parseInt(row.users || '0'), 3998 + totalPosts: parseInt(row.posts || '0'), 3999 + totalLikes: parseInt(row.likes || '0'), 4000 + totalReposts: parseInt(row.reposts || '0'), 4001 + totalFollows: parseInt(row.follows || '0'), 4002 + totalBlocks: parseInt(row.blocks || '0'), 4003 + }; 3997 4004 3998 - const data = { 3999 - totalUsers: parseInt(row.users || '0'), 4000 - totalPosts: parseInt(row.posts || '0'), 4001 - totalLikes: parseInt(row.likes || '0'), 4002 - totalReposts: parseInt(row.reposts || '0'), 4003 - totalFollows: parseInt(row.follows || '0'), 4004 - totalBlocks: parseInt(row.blocks || '0'), 4005 - }; 4005 + console.log('[STORAGE] PostgreSQL COUNT query result:', data); 4006 4006 4007 - console.log('[STORAGE] PostgreSQL COUNT query result:', data); 4007 + // Cache the result 4008 + this.statsCache = { data, timestamp: Date.now() }; 4008 4009 4009 - // Cache the result 4010 - this.statsCache = { data, timestamp: Date.now() }; 4011 - this.statsQueryInProgress = false; 4010 + // Update Redis counters with accurate counts 4011 + const { redisQueue: redisQueueUpdate } = await import('./services/redis-queue'); 4012 + await Promise.all([ 4013 + redisQueueUpdate.setRecordCount('users', data.totalUsers), 4014 + redisQueueUpdate.setRecordCount('posts', data.totalPosts), 4015 + redisQueueUpdate.setRecordCount('likes', data.totalLikes), 4016 + redisQueueUpdate.setRecordCount('reposts', data.totalReposts), 4017 + redisQueueUpdate.setRecordCount('follows', data.totalFollows), 4018 + redisQueueUpdate.setRecordCount('blocks', data.totalBlocks), 4019 + ]).catch((err) => { 4020 + console.error('[STORAGE] Failed to update Redis counters:', err); 4021 + }); 4012 4022 4013 - // Update Redis counters with accurate counts 4014 - const { redisQueue: redisQueueUpdate } = await import('./services/redis-queue'); 4015 - await Promise.all([ 4016 - redisQueueUpdate.setRecordCount('users', data.totalUsers), 4017 - redisQueueUpdate.setRecordCount('posts', data.totalPosts), 4018 - redisQueueUpdate.setRecordCount('likes', data.totalLikes), 4019 - redisQueueUpdate.setRecordCount('reposts', data.totalReposts), 4020 - redisQueueUpdate.setRecordCount('follows', data.totalFollows), 4021 - redisQueueUpdate.setRecordCount('blocks', data.totalBlocks), 4022 - ]).catch((err) => { 4023 - console.error('[STORAGE] Failed to update Redis counters:', err); 4024 - }); 4023 + return data; 4024 + } catch (error) { 4025 + console.error('[STORAGE] Error getting stats:', error); 4025 4026 4026 - return data; 4027 - } catch (error) { 4028 - this.statsQueryInProgress = false; 4029 - console.error('[STORAGE] Error getting stats:', error); 4027 + // If query times out or fails, return cached data if available, otherwise zeros 4028 + if (this.statsCache) { 4029 + return this.statsCache.data; 4030 + } 4030 4031 4031 - // If query times out or fails, return cached data if available, otherwise zeros 4032 - if (this.statsCache) { 4033 - return this.statsCache.data; 4032 + return { 4033 + totalUsers: 0, 4034 + totalPosts: 0, 4035 + totalLikes: 0, 4036 + totalReposts: 0, 4037 + totalFollows: 0, 4038 + totalBlocks: 0, 4039 + }; 4040 + } finally { 4041 + this.statsQueryInProgress = false; 4042 + this.statsQueryPromise = null; 4034 4043 } 4044 + })(); 4035 4045 4036 - return { 4037 - totalUsers: 0, 4038 - totalPosts: 0, 4039 - totalLikes: 0, 4040 - totalReposts: 0, 4041 - totalFollows: 0, 4042 - totalBlocks: 0, 4043 - }; 4044 - } 4046 + return this.statsQueryPromise; 4045 4047 } 4046 4048 4047 4049 // Post aggregations operations