utilize etags

This commit is contained in:
Niki Wix Skaarup 2025-04-13 19:00:35 +02:00
parent 5afe411dd4
commit e314f6f3e9
Signed by: nikiskaarup
GPG key ID: FC2F1B116F6E788C
2 changed files with 59 additions and 16 deletions

View file

@ -1,26 +1,49 @@
import { env, randomUUIDv7 } from 'bun';
import homepage from '@routes/index.html'; 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 robotsTxt from '@static/robots.txt';
import sitemapTxt from '@static/sitemap.txt'; import sitemapTxt from '@static/sitemap.txt';
import favicon from '@static/favicon.png'; import { env } from 'bun';
import ptApi from '@server/pt-api';
import auth from '@server/auth';
const development = env.NODE_ENV !== 'production'; const development = env.NODE_ENV !== 'production';
const faviconInit = { headers: new Headers({ 'Content-Type': 'image/png' }) }; const faviconInit = { headers: new Headers({ 'Content-Type': 'image/png' }) };
let ptEntriesETag = '';
let entriesETag = '';
let entriesCache = ''; let entriesCache = '';
let entriesCacheTime = 0; 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); entriesCache = JSON.stringify(data);
entriesETag = getEtag(entriesCache);
entriesCacheTime = Date.now(); 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({ Bun.serve({
@ -41,14 +64,20 @@ Bun.serve({
return auth.verifyFailResponse(); 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: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'Cache-Control': `max-age=${entriesCacheMaxAge}`,
'ETag': entriesETag,
} }
}); });
}, },
}, },
'/auth/logout': { '/auth/logout': {
async POST() { async POST() {
return auth.logoutResponse(); return auth.logoutResponse();

View file

@ -20,14 +20,28 @@ const bodyHeaders = new Headers({
Authorization: `Bearer ${env.BEARER_TOKEN}`, Authorization: `Bearer ${env.BEARER_TOKEN}`,
}); });
async function query(): Promise<Array<SelectEntry>> { async function query(etag?: string): Promise<[etag: string | null, data: Array<SelectEntry> | null]> {
try { try {
const headers = new Headers({
Authorization: `Bearer ${env.BEARER_TOKEN}`,
});
if (etag) {
headers.set('if-none-match', etag);
}
const response = await fetch(entriesUrl, { const response = await fetch(entriesUrl, {
method: 'GET', method: 'GET',
signal: AbortSignal.timeout(50000), signal: AbortSignal.timeout(5000),
headers: noBodyHeaders, headers,
}); });
const respEtag = response.headers.get('etag');
if (response.status === 304) {
return [respEtag, null];
}
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch entries'); throw new Error('Failed to fetch entries');
} }
@ -37,7 +51,7 @@ async function query(): Promise<Array<SelectEntry>> {
throw new Error('Invalid entries data'); throw new Error('Invalid entries data');
} }
return data; return [respEtag, data];
} catch (err) { } catch (err) {
console.error(err); console.error(err);
throw new Error('Failed to fetch entries'); throw new Error('Failed to fetch entries');