feat: 304 not modified

This commit is contained in:
dsrkafuu 2022-03-13 18:11:08 +08:00
parent 375d82c6f8
commit 702cd041a0
10 changed files with 156 additions and 48 deletions

View File

@ -12,6 +12,7 @@
"publish": "wrangler publish"
},
"dependencies": {
"base64-arraybuffer": "~1.0.2",
"itty-router": "~2.5.2"
},
"devDependencies": {

View File

@ -2,6 +2,7 @@ lockfileVersion: 5.3
specifiers:
'@cloudflare/wrangler': ~1.19.8
base64-arraybuffer: ~1.0.2
clean-webpack-plugin: ~4.0.0
eslint: ~8.10.0
itty-router: ~2.5.2
@ -10,6 +11,7 @@ specifiers:
webpack-cli: ~4.9.2
dependencies:
base64-arraybuffer: 1.0.2
itty-router: 2.5.2
devDependencies:
@ -332,6 +334,11 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/base64-arraybuffer/1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
dev: false
/brace-expansion/1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:

View File

@ -20,7 +20,8 @@ router.all('*', async () => {
});
addEventListener('fetch', (event) => {
const req = event.request;
event.respondWith(
router.handle(event.request, event).catch((e) => genErrorResponse(e))
router.handle(req, event).catch((e) => genErrorResponse(req, e))
);
});

View File

@ -1,3 +1,73 @@
import { getETag } from './utils';
/**
* @param {Request} req
* @param {BodyInit|null|undefined} body
* @param {ResponseInit|undefined} init
*/
export async function genResponse(req, body, init) {
// 304
const buffer = await new Response(body, init).arrayBuffer();
const etag = await getETag(buffer);
if (!etag) {
return res;
}
if (req.headers.get('If-None-Match') === etag) {
return new Response(null, {
status: 304,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': init.headers['Cache-Control'] || 'no-cache',
ETag: etag,
},
});
}
// 200
const res = new Response(body, init);
res.headers.set('Access-Control-Allow-Origin', '*');
if (!init.headers['Cache-Control']) {
res.headers.set('Cache-Control', 'no-cache');
}
res.headers.set('ETag', etag);
return res;
}
/**
* @param {Request} req
* @param {FetchEvent} event
* @param {string} proxy
*/
export async function genProxyResponse(req, event, proxy) {
// check cache
const cacheKey = new Request(new URL(req.url).toString(), req);
const cache = caches.default;
let usingCache = true;
let resCache = await cache.match(cacheKey);
if (!resCache) {
usingCache = false;
resCache = await fetch(proxy);
event.waitUntil(cache.put(cacheKey, resCache.clone()));
}
// 304
const etag = resCache.headers.get('ETag');
if (etag && req.headers.get('If-None-Match') === etag) {
return new Response(null, {
status: 304,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': resCache.headers.get('Cache-Control'),
ETag: etag,
'X-Proxy-Cache': 'HIT',
},
});
}
// normal response
const res = new Response(resCache.body, resCache);
res.headers.set('Access-Control-Allow-Origin', '*');
res.headers.set('X-Proxy-Cache', usingCache ? 'HIT' : 'MISS');
return res;
}
/**
* custom api error
*/
@ -14,10 +84,12 @@ export class ResError extends Error {
}
/**
* @param {Request} req
* @param {any} e
*/
export function genErrorResponse(e) {
const headers = {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain',
'Cache-Control': 'no-cache',
};
@ -28,15 +100,3 @@ export function genErrorResponse(e) {
return new Response('Internal Server Error', { status: 500, headers });
}
}
/**
* @param {string} html
*/
export function genHTMLResponse(html) {
return new Response(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'max-age=0, private, must-revalidate',
},
});
}

View File

@ -1,30 +1,28 @@
/* global KV */
import { genResponse } from '../response';
import { validateID } from '../utils';
/**
* @param {Request} req
*/
export async function get(req) {
const id = validateID(req.params.id);
// get times from KV
const count = Number.parseInt(await KV.get(id)) || 0;
return new Response(JSON.stringify({ count }), {
return await genResponse(req, JSON.stringify({ count }), {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json; charset=utf-8',
},
});
}
/**
* @param {Request} req
*/
export async function del(req) {
const id = validateID(req.params.id);
await KV.delete(id);
return new Response(null, {
return await genResponse(req, null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache',
},
});
}

View File

@ -1,18 +1,13 @@
import { genProxyResponse } from '../response';
/**
* @param {Request} req
* @param {FetchEvent} event
*/
export async function get(req, event) {
const cacheKey = new Request(new URL(req.url).toString(), req);
const cache = caches.default;
let usingCache = true;
let resCache = await cache.match(cacheKey);
if (!resCache) {
usingCache = false;
resCache = await fetch(
'https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon.ico'
);
event.waitUntil(cache.put(cacheKey, resCache.clone()));
}
const res = new Response(resCache.body, resCache);
res.headers.set('X-Custom-Cache', usingCache ? 'HIT' : 'MISS');
return new Response(res.body, res);
return await genProxyResponse(
req,
event,
'https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon.ico'
);
}

View File

@ -1,7 +1,14 @@
/* global KV */
import themes from '../../themes';
import { genResponse } from '../response';
import { validateID, minify } from '../utils';
/**
* @param {number} count
* @param {string} theme
* @param {number|string} length
* @returns
*/
function genImage(count, theme, length) {
let nums;
if (length === 'auto') {
@ -29,6 +36,10 @@ function genImage(count, theme, length) {
return minify(svg);
}
/**
* @param {Request} req
* @param {FetchEvent} event
*/
export async function get(req, event) {
const id = validateID(req.params.id);
let { theme, length } = req.query;
@ -48,10 +59,10 @@ export async function get(req, event) {
// set time asynchronously (no await)
event.waitUntil(KV.put(id, count.toString()));
return new Response(image, {
return await genResponse(req, image, {
status: 200,
headers: {
'Content-Type': 'image/svg+xml; charset=utf-8',
'Cache-Control': 'no-cache',
},
});
}

View File

@ -1,5 +1,10 @@
export async function get() {
return new Response(null, {
import { genResponse } from '../response';
/**
* @param {Request} req
*/
export async function get(req) {
return await genResponse(req, null, {
status: 301,
headers: {
Location: 'https://github.com/dsrkafuu/moe-counter#readme',

View File

@ -1,15 +1,42 @@
import { encode } from 'base64-arraybuffer';
import { ResError } from './response';
/**
* @param {string} str
*/
export function minify(str) {
if (typeof str !== 'string') {
return str;
}
return str.replace(/ *\n */g, '').trim();
}
/**
* @param {string} id
* @returns
*/
export function validateID(id) {
if (!/^[a-z][0-9a-z:!@#$%^&*_-]{0,255}$/i.test(id)) {
throw new ResError(400, 'Invalid Counter ID');
}
return id;
}
/**
* https://github.com/jshttp/etag/blob/master/index.js#L39
* @param {ArrayBuffer} buffer
*/
export async function getETag(buffer) {
// fast empty etag
if (buffer.byteLength === 0) {
return 'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
}
try {
// body hash
const digest = await crypto.subtle.digest('SHA-1', buffer);
const base64 = encode(digest);
const hash = base64.substring(0, 27); // remove trailing `=`s
// body length
const length = buffer.byteLength.toString(16);
return `W/"${length}-${hash}"`;
} catch {
return null;
}
}

View File

@ -24,5 +24,8 @@ module.exports = {
optimization: {
minimize: false,
},
performance: {
hints: false,
},
plugins: [new CleanWebpackPlugin()],
};