174 lines
4.4 KiB
TypeScript
174 lines
4.4 KiB
TypeScript
import { config } from '@/config';
|
|
import logger from '@/utils/logger';
|
|
import md5 from '@/utils/md5';
|
|
|
|
import type CacheModule from './base';
|
|
|
|
type CacheHitResponse = {
|
|
hit: true;
|
|
value: string;
|
|
};
|
|
|
|
const status = { available: false };
|
|
|
|
let baseUrl: string | undefined;
|
|
let apiToken: string | undefined;
|
|
|
|
const toRemoteKey = (key: string) => `rsshub:http-cache:${md5(key)}`;
|
|
|
|
const cacheUrl = (key: string, refresh = false) => {
|
|
const url = `${baseUrl}/v1/cache/${encodeURIComponent(toRemoteKey(key))}`;
|
|
return refresh ? `${url}?refresh=1` : url;
|
|
};
|
|
|
|
const requestSignal = () => (typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(config.requestTimeout) : undefined);
|
|
|
|
const request = async (url: string, init: Omit<RequestInit, 'headers'> = {}, headers: Record<string, string> = {}) => {
|
|
if (!status.available || !apiToken) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return await fetch(url, {
|
|
...init,
|
|
headers: {
|
|
authorization: `Bearer ${apiToken}`,
|
|
...headers,
|
|
},
|
|
signal: requestSignal(),
|
|
});
|
|
} catch (error) {
|
|
logger.error('HTTP cache request failed:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const readResponseText = async (response: Response) => {
|
|
try {
|
|
return await response.text();
|
|
} catch {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const readCacheHitResponse = async (response: Response) => {
|
|
try {
|
|
return (await response.json()) as CacheHitResponse;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const logUnexpectedResponse = async (operation: string, response: Response) => {
|
|
const message = await readResponseText(response);
|
|
logger.error(`HTTP cache ${operation} failed with status ${response.status}${message ? `: ${message}` : ''}`);
|
|
|
|
if (response.status === 401) {
|
|
status.available = false;
|
|
}
|
|
};
|
|
|
|
export default {
|
|
init: () => {
|
|
baseUrl = config.httpCache.url?.replace(/\/+$/, '');
|
|
apiToken = config.httpCache.token;
|
|
|
|
if (!baseUrl || !apiToken) {
|
|
status.available = false;
|
|
logger.error('HTTP cache requires CACHE_HTTP_URL and CACHE_HTTP_TOKEN.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
new URL(baseUrl);
|
|
} catch {
|
|
status.available = false;
|
|
logger.error('HTTP cache URL is invalid.');
|
|
return;
|
|
}
|
|
|
|
status.available = true;
|
|
logger.info('HTTP cache configured.');
|
|
},
|
|
get: async (key: string, refresh = true) => {
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
|
|
const response = await request(cacheUrl(key, refresh), { method: 'GET' });
|
|
if (!response) {
|
|
return null;
|
|
}
|
|
|
|
if (response.status === 404) {
|
|
return '';
|
|
}
|
|
|
|
if (!response.ok) {
|
|
await logUnexpectedResponse('get', response);
|
|
return null;
|
|
}
|
|
|
|
const data = await readCacheHitResponse(response);
|
|
if (data?.hit === true && typeof data.value === 'string') {
|
|
return data.value;
|
|
}
|
|
|
|
logger.error('HTTP cache get returned an invalid response.');
|
|
return null;
|
|
},
|
|
has: async (key: string) => {
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
|
|
const response = await request(cacheUrl(key), { method: 'HEAD' });
|
|
if (!response) {
|
|
return false;
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return true;
|
|
}
|
|
|
|
if (response.status === 404) {
|
|
return false;
|
|
}
|
|
|
|
await logUnexpectedResponse('has', response);
|
|
return false;
|
|
},
|
|
set: async (key: string, value?: string | Record<string, any>, maxAge = config.cache.contentExpire) => {
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
if (!value || value === 'undefined') {
|
|
value = '';
|
|
}
|
|
if (typeof value === 'object') {
|
|
value = JSON.stringify(value);
|
|
}
|
|
|
|
const response = await request(
|
|
cacheUrl(key),
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
ttl: maxAge,
|
|
value,
|
|
}),
|
|
},
|
|
{
|
|
'content-type': 'application/json',
|
|
}
|
|
);
|
|
|
|
if (response && response.status !== 204) {
|
|
await logUnexpectedResponse('set', response);
|
|
}
|
|
},
|
|
clients: {},
|
|
status,
|
|
} as CacheModule;
|