diff --git a/src/index.ts b/src/index.ts index eef39a1..a709215 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,49 @@ -import { env, randomUUIDv7 } from 'bun'; import homepage from '@routes/index.html'; +import auth from '@server/auth'; +import ptApi from '@server/pt-api'; +import favicon from '@static/favicon.png'; import robotsTxt from '@static/robots.txt'; import sitemapTxt from '@static/sitemap.txt'; -import favicon from '@static/favicon.png'; -import ptApi from '@server/pt-api'; -import auth from '@server/auth'; +import { env } from 'bun'; const development = env.NODE_ENV !== 'production'; const faviconInit = { headers: new Headers({ 'Content-Type': 'image/png' }) }; +let ptEntriesETag = ''; +let entriesETag = ''; let entriesCache = ''; let entriesCacheTime = 0; -const entriesCacheTimeLimit = 1000 * 15; +const entriesCacheMaxAge = 3; +const entriesCacheTimeLimit = 1000 * entriesCacheMaxAge; + +function getEtag(data: string) { + const sha = Bun.SHA256.hash(data); + return Buffer.from(sha.buffer).toString('base64url'); +} + +async function checkEntriesCache() { + if (entriesCacheTime + entriesCacheTimeLimit >= Date.now()) + return; + + try { + const [etag, data] = await ptApi.query(entriesETag); + if (etag === ptEntriesETag || !Array.isArray(data)) return; // No new data + if (etag !== null) ptEntriesETag = etag; -async function getEntriesCached() { - if (entriesCacheTime + entriesCacheTimeLimit < Date.now()) { - const data = await ptApi.query(); entriesCache = JSON.stringify(data); + entriesETag = getEtag(entriesCache); entriesCacheTime = Date.now(); + } catch (err) { + console.error('Failed to fetch entries:', err); } - return entriesCache; +} + +function etagResponse(etag: string, cacheControl?: string) { + const headers = new Headers(); + headers.set('ETag', etag); + if (cacheControl) headers.set('Cache-Control', cacheControl); + return new Response('', { status: 304, headers }); } Bun.serve({ @@ -41,14 +64,20 @@ Bun.serve({ return auth.verifyFailResponse(); } - return new Response(await getEntriesCached(), { + await checkEntriesCache(); + if (req.headers.get('if-none-match') === entriesETag) { + return etagResponse(entriesETag, `max-age=${entriesCacheMaxAge}`); + } + + return new Response(entriesCache, { headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Cache-Control': `max-age=${entriesCacheMaxAge}`, + 'ETag': entriesETag, } }); }, }, - '/auth/logout': { async POST() { return auth.logoutResponse(); diff --git a/src/server/pt-api.ts b/src/server/pt-api.ts index 122e7fb..e3c57c4 100644 --- a/src/server/pt-api.ts +++ b/src/server/pt-api.ts @@ -20,14 +20,28 @@ const bodyHeaders = new Headers({ Authorization: `Bearer ${env.BEARER_TOKEN}`, }); -async function query(): Promise> { +async function query(etag?: string): Promise<[etag: string | null, data: Array | null]> { try { + const headers = new Headers({ + Authorization: `Bearer ${env.BEARER_TOKEN}`, + }); + + if (etag) { + headers.set('if-none-match', etag); + } + const response = await fetch(entriesUrl, { method: 'GET', - signal: AbortSignal.timeout(50000), - headers: noBodyHeaders, + signal: AbortSignal.timeout(5000), + headers, }); + const respEtag = response.headers.get('etag'); + + if (response.status === 304) { + return [respEtag, null]; + } + if (!response.ok) { throw new Error('Failed to fetch entries'); } @@ -37,7 +51,7 @@ async function query(): Promise> { throw new Error('Invalid entries data'); } - return data; + return [respEtag, data]; } catch (err) { console.error(err); throw new Error('Failed to fetch entries');