Skip to content

Commit

Permalink
add semaphores for scoreboard route
Browse files Browse the repository at this point in the history
  • Loading branch information
henrygd committed Jun 29, 2024
1 parent 824e8e1 commit 7b4ba0b
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 33 deletions.
86 changes: 53 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Elysia, NotFoundError } from 'elysia'
import { parseHTML } from 'linkedom'
import ExpiryMap from 'expiry-map'
import { getSemaphore } from '@henrygd/semaphore'

const validPaths = ['stats', 'rankings', 'standings', 'history', 'scoreboard', 'schedule']

Expand Down Expand Up @@ -77,43 +78,62 @@ export const app = new Elysia()
})
// scoreboard route to fetch data from data.ncaa.com json endpoint
.get('/scoreboard/:sport/*', async ({ store, params, set }) => {
if (scoreboardCache.has(store.cacheKey)) {
return scoreboardCache.get(store.cacheKey)
}

// get division from url
const division = params['*'].split('/')[0]

// find date in url
const urlDateMatcher = /(\d{4}\/\d{2}\/\d{2})|(\d{4}\/(\d{2}|P))/
let urlDate = params['*'].match(urlDateMatcher)?.[0]

if (urlDate) {
// return 400 if date is more than a year in the future
// (had runaway bot requesting every day until I noticed it in 2195)
if (new Date(urlDate).getFullYear() > new Date().getFullYear() + 1) {
set.status = 400
throw new Error('Invalid date')
const semCacheKey = getSemaphore(store.cacheKey)
await semCacheKey.acquire()
try {
if (scoreboardCache.has(store.cacheKey)) {
set.headers['x-score-cache'] = 'hit'
return scoreboardCache.get(store.cacheKey)
}
} else {
// if date not in passed in url, fetch date from today.json
urlDate = await getTodayUrl(params.sport, division)
}

const url = `https://data.ncaa.com/casablanca/scoreboard/${params.sport}/${division}/${urlDate}/scoreboard.json`
// get division from url
const division = params['*'].split('/')[0]

// find date in url
const urlDateMatcher = /(\d{4}\/\d{2}\/\d{2})|(\d{4}\/(\d{2}|P))/
let urlDate = params['*'].match(urlDateMatcher)?.[0]

if (urlDate) {
// return 400 if date is more than a year in the future
// (had runaway bot requesting every day until I noticed it in 2195)
if (new Date(urlDate).getFullYear() > new Date().getFullYear() + 1) {
set.status = 400
throw new Error('Invalid date')
}
} else {
// if date not in passed in url, fetch date from today.json
urlDate = await getTodayUrl(params.sport, division)
}

// fetch data
log(`Fetching ${url}`)
const res = await fetch(url)
if (!res.ok) {
throw new NotFoundError(JSON.stringify({ message: 'Resource not found' }))
const url = `https://data.ncaa.com/casablanca/scoreboard/${params.sport}/${division}/${urlDate}/scoreboard.json`

const semUrl = getSemaphore(url)
await semUrl.acquire()
try {
// check cache
if (scoreboardCache.has(url)) {
set.headers['x-score-cache'] = 'hit'
return scoreboardCache.get(url)
}
// fetch data
log(`Fetching ${url}`)
const res = await fetch(url)
if (!res.ok) {
throw new NotFoundError(JSON.stringify({ message: 'Resource not found' }))
}
const data = await res.text()

// cache data
scoreboardCache.set(store.cacheKey, data)
scoreboardCache.set(url, data)

return data
} finally {
semUrl.release()
}
} finally {
semCacheKey.release()
}
const data = await res.text()

// cache data
scoreboardCache.set(store.cacheKey, data)

return data
})
// all other routes fetch data by scraping ncaa.com
.get('/*', async ({ query: { page }, path, store }) => {
Expand Down
25 changes: 25 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ describe('General', () => {
const finish = performance.now() - start
expect(finish).toBeLessThan(10)
})
it('semaphore queues simultaneous requests for same scoreboard resource', async () => {
const requests = []
// will fail when baseball season starts again bc date will be different
// should be replace with whatever sport has the longest until it starts again
const routes = ['/scoreboard/baseball/d1', '/scoreboard/baseball/d1/2024/06/24/all-conf']
for (let i = 0; i < 3; i++) {
for (const route of routes) {
requests.push(
app.handle(new Request(`http://localhost${route}`)).then((res) => res.headers)
)
}
}
const headers = await Promise.all(requests)
let nonCached = 0
let cached = 0
for (const header of headers) {
if (header.get('x-score-cache') === 'hit') {
cached++
} else {
nonCached++
}
}
expect(nonCached).toBe(1)
expect(cached).toBe(headers.length - 1)
})
})

describe('Header validation', () => {
Expand Down

0 comments on commit 7b4ba0b

Please sign in to comment.