Ctrl K
ComunidadGuías…

lib/middleware/cache.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { MiddlewareHandler } from 'hono';
import xxhash from 'xxhash-wasm';

import { config } from '@/config';
import RequestInProgressError from '@/errors/types/request-in-progress';
import type { Data } from '@/types';
import cacheModule from '@/utils/cache/index';
import { hasItemFilters } from '@/utils/route-limit';

const bypassList = new Set(['/', '/robots.txt', '/logo.png', '/favicon.ico']);

const { h64ToString } = await xxhash();
// only give cache string, as the `!` condition tricky
// XXH64 is used to shrink key size
// plz, write these tips in comments!
const middleware: MiddlewareHandler = async (ctx, next) => {
    if (!cacheModule.status.available || bypassList.has(ctx.req.path)) {
        await next();
        return;
    }

    const requestPath = ctx.req.path;
    const format = `:${ctx.req.query('format') || config.format}`;
    const limit = ctx.req.query('limit') ? `:${ctx.req.query('limit')}` : '';
    // Routes may select entries before fetching details according to this option.
    const sortedQuery = ctx.req.query('sorted');
    const sorted = sortedQuery === undefined ? '' : sortedQuery === 'false' ? ':sorted=false' : ':sorted=true';
    const filtered = hasItemFilters(ctx) ? ':filtered' : '';
    const requestKey = h64ToString(requestPath + format + limit + sorted + filtered);
    const key = 'rsshub:koa-redis-cache:' + requestKey;
    const controlKey = 'rsshub:path-requested:' + requestKey;

    let value = await cacheModule.globalCache.get(key);

    // Only atomic backends can coordinate fetchers. HTTP/KV may return stale
    // control keys after a completed request, while their feed cache stays useful.
    let isRequesting = false;
    let ownsClaim = false;
    if (!value && cacheModule.globalCache.supportsAtomicClaims) {
        ownsClaim = await cacheModule.globalCache.claim(controlKey, config.cache.requestTimeout);
        isRequesting = !ownsClaim;
    }

    if (isRequesting) {
        let retryTimes = process.env.NODE_ENV === 'test' ? 1 : 10;
        let bypass = false;
        while (retryTimes > 0) {
            // eslint-disable-next-line no-await-in-loop
            await new Promise((resolve) => setTimeout(resolve, process.env.NODE_ENV === 'test' ? 3000 : 6000));
            // eslint-disable-next-line no-await-in-loop
            if ((await cacheModule.globalCache.get(controlKey)) !== '1') {
                bypass = true;
                break;
            }
            retryTimes--;
        }
        if (!bypass) {
            throw new RequestInProgressError('This path is currently fetching, please come back later!');
        }
        value = await cacheModule.globalCache.get(key);
    }

    if (value) {
        ctx.status(200);
        ctx.header('RSSHub-Cache-Status', 'HIT');
        ctx.set('data', JSON.parse(value));
        await next();
        return;
    }

    if (isRequesting) {
        // waited out a stale claim without finding a cache entry, take over the fetch
        ownsClaim = await cacheModule.globalCache.claim(controlKey, config.cache.requestTimeout);
        if (!ownsClaim) {
            throw new RequestInProgressError('This path is currently fetching, please come back later!');
        }
    }

    // let routers control cache
    ctx.set('cacheKey', key);
    if (ownsClaim) {
        ctx.set('cacheControlKey', controlKey);
    }

    try {
        await next();

        const data: Data = ctx.get('data');
        if (ctx.res.headers.get('Cache-Control') !== 'no-cache' && data) {
            data.lastBuildDate = new Date().toUTCString();
            ctx.set('data', data);
            const body = JSON.stringify(data);
            await cacheModule.globalCache.set(key, body, config.cache.routeExpire);
        }
    } finally {
        // Release after writing the feed, including failures after the route ran.
        if (ownsClaim) {
            await cacheModule.globalCache.set(controlKey, '0', config.cache.requestTimeout);
        }
    }
};

export default middleware;