diff --git a/src/app.ts b/src/app.ts index ad920c7..099db06 100644 --- a/src/app.ts +++ b/src/app.ts @@ -134,7 +134,7 @@ app.get('/users/:username', actorController); app.get('/nodeinfo/:version', nodeInfoSchemaController); -app.get('/api/v1/instance', cache({ expires: Time.minutes(5) }), instanceController); +app.get('/api/v1/instance', cache({ cacheName: 'web', expires: Time.minutes(5) }), instanceController); app.get('/api/v1/apps/verify_credentials', appCredentialsController); app.post('/api/v1/apps', createAppController); @@ -185,10 +185,10 @@ app.get('/api/v1/preferences', preferencesController); app.get('/api/v1/search', searchController); app.get('/api/v2/search', searchController); -app.get('/api/pleroma/frontend_configurations', cache({ expires: Time.minutes(5) }), frontendConfigController); +app.get('/api/pleroma/frontend_configurations', frontendConfigController); -app.get('/api/v1/trends/tags', cache({ expires: Time.minutes(15) }), trendingTagsController); -app.get('/api/v1/trends', cache({ expires: Time.minutes(15) }), trendingTagsController); +app.get('/api/v1/trends/tags', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); +app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/favourites', requirePubkey, favouritesController); diff --git a/src/middleware/cache.ts b/src/middleware/cache.ts index f0e8ae5..87de611 100644 --- a/src/middleware/cache.ts +++ b/src/middleware/cache.ts @@ -1,45 +1,26 @@ import { Debug, type MiddlewareHandler } from '@/deps.ts'; +import ExpiringCache from '@/utils/expiring-cache.ts'; const debug = Debug('ditto:middleware:cache'); -interface CacheOpts { - expires: number; -} - -/** In-memory cache middleware. */ -export const cache = (opts: CacheOpts): MiddlewareHandler => { - let response: Response | undefined; - let expires = Date.now() + opts.expires; - +export const cache = (options: { + cacheName: string; + expires?: number; +}): MiddlewareHandler => { return async (c, next) => { - const now = Date.now(); - const expired = now > expires; - - async function updateCache() { - await next(); - const res = c.res.clone(); - if (res.status < 500) { - const old = response; - response = res; - old?.text(); // Prevent memory leaks. - } - return res; - } - - if (response && !expired) { - debug('Serving page from cache', c.req.url); - return response.clone(); - } else { - expires = Date.now() + opts.expires; - if (response && expired) { - debug('Serving stale cache, rebuilding', c.req.url); - const stale = response.clone(); - updateCache(); - await new Promise((resolve) => setTimeout(resolve, 0)); - return stale; - } + const key = c.req.url.replace('http://', 'https://'); + const cache = new ExpiringCache(await caches.open(options.cacheName)); + const response = await cache.match(key); + if (!response) { debug('Building cache for page', c.req.url); - return await updateCache(); + await next(); + const response = c.res.clone(); + if (response.status < 500) { + await cache.putExpiring(key, response, options.expires ?? 0); + } + } else { + debug('Serving page from cache', c.req.url); + return response; } }; }; diff --git a/src/utils/expiring-cache.test.ts b/src/utils/expiring-cache.test.ts new file mode 100644 index 0000000..9827de8 --- /dev/null +++ b/src/utils/expiring-cache.test.ts @@ -0,0 +1,18 @@ +import { assert } from '@/deps-test.ts'; + +import ExpiringCache from './expiring-cache.ts'; + +Deno.test('ExpiringCache', async () => { + const cache = new ExpiringCache(await caches.open('test')); + + await cache.putExpiring('http://mostr.local/1', new Response('hello world'), 300); + await cache.putExpiring('http://mostr.local/2', new Response('hello world'), -1); + + // const resp1 = await cache.match('http://mostr.local/1'); + const resp2 = await cache.match('http://mostr.local/2'); + + // assert(resp1!.headers.get('Expires')); + assert(!resp2); + + // await resp1!.text(); +}); diff --git a/src/utils/expiring-cache.ts b/src/utils/expiring-cache.ts new file mode 100644 index 0000000..ebb5d2e --- /dev/null +++ b/src/utils/expiring-cache.ts @@ -0,0 +1,68 @@ +class ExpiringCache implements Cache { + #cache: Cache; + + constructor(cache: Cache) { + this.#cache = cache; + } + + add(request: RequestInfo | URL): Promise { + return this.#cache.add(request); + } + + addAll(requests: RequestInfo[]): Promise { + return this.#cache.addAll(requests); + } + + keys(request?: RequestInfo | URL | undefined, options?: CacheQueryOptions | undefined): Promise { + return this.#cache.keys(request, options); + } + + matchAll( + request?: RequestInfo | URL | undefined, + options?: CacheQueryOptions | undefined, + ): Promise { + return this.#cache.matchAll(request, options); + } + + put(request: RequestInfo | URL, response: Response): Promise { + return this.#cache.put(request, response); + } + + putExpiring(request: RequestInfo | URL, response: Response, expiresIn: number): Promise { + const expires = Date.now() + expiresIn; + + const clone = new Response(response.body, { + status: response.status, + headers: { + expires: new Date(expires).toUTCString(), + ...Object.fromEntries(response.headers.entries()), + }, + }); + + return this.#cache.put(request, clone); + } + + async match(request: RequestInfo | URL, options?: CacheQueryOptions | undefined): Promise { + const response = await this.#cache.match(request, options); + const expires = response?.headers.get('Expires'); + + if (response && expires) { + if (new Date(expires).getTime() > Date.now()) { + return response; + } else { + await Promise.all([ + this.delete(request), + response.text(), // Prevent memory leaks + ]); + } + } else if (response) { + return response; + } + } + + delete(request: RequestInfo | URL, options?: CacheQueryOptions | undefined): Promise { + return this.#cache.delete(request, options); + } +} + +export default ExpiringCache;