Skip to content

feat: switch to cachet to speed up team page #1512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions pages/api/team-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import teamMembers from '../../public/team.json'

// Cache duration in seconds (12 hours)
const CACHE_DURATION = 12 * 60 * 60

// In-memory cache for user data
// Note: This will be reset whenever the serverless function cold starts
let userDataCache: Record<string, any> = {}
let cacheTimestamp = 0

async function fetchUserData(slackId: string) {
try {
const response = await fetch(`https://cachet.dunkirk.sh/users/${slackId}`, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache'
}
})

return await response.json()
} catch (error) {
console.error(`Error fetching data for ${slackId}:`, error)
return { message: 'Error fetching user data' }
}
}

async function refreshCache() {
// Skip refresh if cache is still fresh
const now = Date.now()
if (now - cacheTimestamp < CACHE_DURATION * 1000 && Object.keys(userDataCache).length > 0) {
return userDataCache
}

console.log('Refreshing team data cache...')
const newCache: Record<string, any> = {}

// Process members in batches to avoid rate limiting
const batchSize = 5
const slackIds = teamMembers
.filter(member => member.slackId)
.map(member => member.slackId)

for (let i = 0; i < slackIds.length; i += batchSize) {
const batch = slackIds.slice(i, i + batchSize)
const promises = batch.map(async slackId => {
const data = await fetchUserData(slackId)
if (!data.message) {
newCache[slackId] = {
pronouns: data.pronouns,
displayName: data.displayName,
user: data.user
}
}
})

await Promise.all(promises)

// Small delay between batches
if (i + batchSize < slackIds.length) {
await new Promise(resolve => setTimeout(resolve, 500))
}
}

userDataCache = newCache
cacheTimestamp = now
return newCache
}

// Export for internal use by other API routes
export async function getUserCache(forceRefresh = false) {
if (forceRefresh || Object.keys(userDataCache).length === 0 || Date.now() - cacheTimestamp > CACHE_DURATION * 1000) {
await refreshCache()
}

return userDataCache
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// Force refresh if requested
const forceRefresh = req.query.refresh === 'true'

// Get the cache (refreshing if needed)
await getUserCache(forceRefresh)

// Set cache control headers
res.setHeader('Cache-Control', `s-maxage=${CACHE_DURATION}, stale-while-revalidate`)
res.status(200).json({
cache: userDataCache,
timestamp: cacheTimestamp,
expires: new Date(cacheTimestamp + CACHE_DURATION * 1000).toISOString()
})
}
65 changes: 41 additions & 24 deletions pages/api/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,55 @@ interface TeamMember {
overrideAvatar: string
email: string
website: string
pronouns: string
pronouns?: string
avatar: string
}

// Cache duration in seconds (5 minutes)
const CACHE_DURATION = 5 * 60

export async function fetchTeam() {
const current: TeamMember[] = []
const acknowledged: TeamMember[] = []

for (const member of teamMembers as TeamMember[]) {
if (process.env.SLACK_API_TOKEN) {
const formData = new FormData()
formData.append('token', process.env.SLACK_API_TOKEN)
formData.append('user', member.slackId)

const slackData = await fetch(
`https://hackclub.slack.com/api/users.profile.get?user=${member.slackId}`,
{
method: 'POST',
headers: {
'content-type': 'multipart/form-data',
cookie: process.env.SLACK_API_COOKIE || ''
},
body: formData
try {
// Get user cache directly from the team-cache module
// This is more reliable in serverless environments than making HTTP requests
const { getUserCache } = await import('./team-cache')
const userDataCache = await getUserCache()

for (const member of teamMembers as TeamMember[]) {
// Always use cachet.dunkirk.sh for avatar images
if (member.slackId) {
member.avatar = `https://cachet.dunkirk.sh/users/${member.slackId}/r`

// Apply cached pronouns if available
if (userDataCache[member.slackId]?.pronouns) {
member.pronouns = userDataCache[member.slackId].pronouns
}
).then(r => r.json())
}

if (slackData.ok) {
member.pronouns = slackData.profile.pronouns
if (member.acknowledged) {
acknowledged.push(member)
} else {
console.warn('Not found:', member.slackId)
current.push(member)
}
}
} catch (error) {
console.error('Error fetching cached team data:', error)

// Fallback if cache fails
for (const member of teamMembers as TeamMember[]) {
// Always use cachet.dunkirk.sh for avatar images
if (member.slackId) {
member.avatar = `https://cachet.dunkirk.sh/users/${member.slackId}/r`
}

if (member.acknowledged) {
acknowledged.push(member)
} else {
current.push(member)
if (member.acknowledged) {
acknowledged.push(member)
} else {
current.push(member)
}
}
}

Expand All @@ -59,5 +71,10 @@ export default async function handler(
_req: NextApiRequest,
res: NextApiResponse
) {
// Set cache headers
res.setHeader(
'Cache-Control',
`s-maxage=${CACHE_DURATION}, stale-while-revalidate`
)
res.status(200).json(await fetchTeam())
}