Skip to content

Commit

Permalink
Refactor lru-cache Handler to overcome ttl limitations (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
better-salmon authored Jan 11, 2024
1 parent 58b4d58 commit 9dcb393
Show file tree
Hide file tree
Showing 9 changed files with 54 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-moose-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@neshca/cache-handler': patch
---

Refactored `lru-cache` Handler to overcome ttl via setTimeout limitations. Added `timeoutMs` option to `server` Handler.
9 changes: 2 additions & 7 deletions apps/cache-testing/cache-handler-server.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
const { scheduler } = require('node:timers/promises');
const { IncrementalCache } = require('@neshca/cache-handler');
const createServerCache = require('@neshca/cache-handler/server').default;
const createLruCache = require('@neshca/cache-handler/local-lru').default;

const baseUrl = process.env.REMOTE_CACHE_SERVER_BASE_URL ?? 'http://localhost:8080';

async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

IncrementalCache.onCreation(async () => {
await wait();
await scheduler.wait(1000);

const httpCache = createServerCache({
baseUrl,
Expand Down
9 changes: 2 additions & 7 deletions apps/cache-testing/run-app-instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
/* eslint-disable import/no-named-as-default-member -- pm2 does't have named exports */

import process from 'node:process';
import { scheduler } from 'node:timers/promises';
import Fastify from 'fastify';
import pm2 from 'pm2';

function wait(delay: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}

const args = process.argv.slice(2).reduce<Record<string, string>>((acc, arg) => {
const [key, value] = arg.split('=');

Expand Down Expand Up @@ -74,7 +69,7 @@ app.get('/restart/:port', async (request, reply) => {
}

// workaround for unstable tests
void wait(1000).then(async () => {
void scheduler.wait(1000).then(async () => {
await reply.code(200).send({ restarted: name });
});
});
Expand Down
9 changes: 2 additions & 7 deletions apps/cache-testing/tests/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { scheduler } from 'node:timers/promises';
import { test, expect } from '@playwright/test';

function wait(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

const paths = [
'/app/with-params/dynamic-true/200',
// '/app/with-params/dynamic-false/200', // this fails with native next.js cache
Expand Down Expand Up @@ -146,7 +141,7 @@ test.describe('Time-based revalidation', () => {
await expect(page.getByTestId('cache-state')).toContainText('stale', { timeout: 7500 });

// Temporary workaround: Addressing intermittent test failures observed in GitHub Actions.
await wait(1000);
await scheduler.wait(1000);

await page.reload();

Expand Down
2 changes: 1 addition & 1 deletion docs/cache-handler-docs/theme.config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default {
key: '0.6.0-release',
text: (
<a href={`${process.env.NEXT_PUBLIC_BASE_URL}`}>
🎉 Version 0.6.3 is out, offering stale-while-revalidate strategy emulation!
🎉 Version 0.6.4 is out, offering stale-while-revalidate strategy emulation!
</a>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ function calculateObjectSize({ value }: CacheHandlerValue): number {

export type { LruCacheOptions };

export function createCache(options?: LruCacheOptions): LRUCache<string, CacheHandlerValue> {
export function createCache(
options?: LruCacheOptions,
): LRUCache<string, CacheHandlerValue & { maxAgeSeconds?: number }> {
return createConfiguredCache(calculateObjectSize, options);
}
2 changes: 1 addition & 1 deletion packages/cache-handler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Flexible API to replace the default Next.js cache, accommodating custom cache solutions for multi-instance self-hosted deployments**

🎉 Version 0.6.3 is out, offering stale-while-revalidate strategy emulation!
🎉 Version 0.6.4 is out, offering stale-while-revalidate strategy emulation!

Check out the [changelog](https://github.com/caching-tools/next-shared-cache/blob/canary/packages/cache-handler/CHANGELOG.md)

Expand Down
37 changes: 29 additions & 8 deletions packages/cache-handler/src/handlers/local-lru.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable import/no-default-export -- use default here */
import type { LruCacheOptions } from '@neshca/next-lru-cache/next-cache-handler-value';
import { createCache } from '@neshca/next-lru-cache/next-cache-handler-value';
import type { Cache } from '../cache-handler';
import type { Cache, CacheHandlerValue } from '../cache-handler';
import { calculateEvictionDelay } from '../helpers/calculate-eviction-delay';
import type { UseTtlOptions } from '../common-types';
import { MAX_INT32 } from '../constants';

export type LruCacheHandlerOptions = LruCacheOptions & UseTtlOptions;

Expand All @@ -31,20 +30,42 @@ export type LruCacheHandlerOptions = LruCacheOptions & UseTtlOptions;
*
* @remarks
* - Use this Handler as a fallback for any remote store Handler instead of the filesystem when you use only the App router.
* - The max TTL value is 2147483.647 seconds (24.8 days) due to a `setTimeout` limitation.
*/
export default function createLruCache({ useTtl = false, ...lruOptions }: LruCacheHandlerOptions = {}): Cache {
const lruCache = createCache(lruOptions);

return {
name: 'local-lru',
get(key) {
return Promise.resolve(lruCache.get(key));
},
set(key, value, maxAgeSeconds = 0) {
const evictionDelay = calculateEvictionDelay(maxAgeSeconds * 1000, useTtl);
const cacheValue = lruCache.get(key);

if (!cacheValue) {
return Promise.resolve(null);
}

const { lastModified, maxAgeSeconds } = cacheValue;

if (!useTtl || !maxAgeSeconds) {
return Promise.resolve(cacheValue as CacheHandlerValue);
}

const ageSeconds = lastModified ? Math.floor((Date.now() - lastModified) / 1000) : 0;

lruCache.set(key, value, evictionDelay ? { ttl: Math.min(evictionDelay, MAX_INT32) } : undefined);
const evictionAge = calculateEvictionDelay(maxAgeSeconds, useTtl);

if (!evictionAge || evictionAge > ageSeconds) {
return Promise.resolve(cacheValue as CacheHandlerValue);
}

lruCache.delete(key);

return Promise.resolve(null);
},
set(key, value, maxAgeSeconds) {
lruCache.set(key, {
...value,
maxAgeSeconds,
});

return Promise.resolve();
},
Expand Down
10 changes: 9 additions & 1 deletion packages/cache-handler/src/handlers/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export type ServerCacheHandlerOptions = {
* The base URL of the cache store server.
*/
baseUrl: URL | string;
/**
* Timeout in milliseconds for remote cache store operations.
*/
timeoutMs?: number;
};

/**
Expand All @@ -33,7 +37,7 @@ export type ServerCacheHandlerOptions = {
* - the `getRevalidatedTags` method retrieves revalidated tags from the server.
* - the `revalidateTag` method updates the revalidation time for a specific tag in the server cache.
*/
export default function createCache({ baseUrl }: ServerCacheHandlerOptions): Cache {
export default function createCache({ baseUrl, timeoutMs }: ServerCacheHandlerOptions): Cache {
return {
name: 'server',
async get(key) {
Expand All @@ -42,6 +46,7 @@ export default function createCache({ baseUrl }: ServerCacheHandlerOptions): Cac
url.searchParams.set('key', key);

const response = await fetch(url, {
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
// @ts-expect-error -- act as an internal fetch call
next: {
internal: true,
Expand Down Expand Up @@ -69,6 +74,7 @@ export default function createCache({ baseUrl }: ServerCacheHandlerOptions): Cac
headers: {
'Content-Type': 'application/json',
},
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
// @ts-expect-error -- act as an internal fetch call
next: {
internal: true,
Expand All @@ -84,6 +90,7 @@ export default function createCache({ baseUrl }: ServerCacheHandlerOptions): Cac
const url = new URL('/getRevalidatedTags', baseUrl);

const response = await fetch(url, {
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
// @ts-expect-error -- act as an internal fetch call
next: {
internal: true,
Expand All @@ -107,6 +114,7 @@ export default function createCache({ baseUrl }: ServerCacheHandlerOptions): Cac
headers: {
'Content-Type': 'application/json',
},
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
// @ts-expect-error -- act as an internal fetch call
next: {
internal: true,
Expand Down

0 comments on commit 9dcb393

Please sign in to comment.