Skip to content

Commit c44c385

Browse files
committed
Migrate to Vitest, refactor cache-handler API names
1 parent 1421a1c commit c44c385

34 files changed

+990
-523
lines changed

.vscode/settings.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
"typescript.surveys.enabled": false,
88
"typescript.tsdk": "node_modules/typescript/lib",
99
"explorer.fileNesting.enabled": true,
10-
"explorer.fileNesting.patterns": {
11-
"*.ts": "$(capture).test.ts, $(capture).test.tsx",
12-
"*.tsx": "$(capture).test.ts, $(capture).test.tsx"
13-
},
1410
"cSpell.words": ["nextjs", "prerendered", "codestyle"],
1511
"editor.defaultFormatter": "biomejs.biome",
1612
"[jsonc]": {

apps/cache-testing/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"type": "module",
77
"scripts": {
88
"build": "next build && tsx create-instances.ts",
9-
"check-types": "tsc --noEmit",
109
"cluster:start": "pm2 start cluster.config.js --env production",
1110
"cluster:stop": "pm2 kill",
1211
"e2e": "playwright test --config=./playwright.config.ts",

apps/cache-testing/run-app-instances.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { scheduler } from 'node:timers/promises';
44
import Fastify from 'fastify';
5-
// biome-ignore lint/style/noNamespaceImport: pm2 works only with Namespace import
5+
// biome-ignore lint/style/noNamespaceImport: pm2 works only with namespace import
66
import * as pm2Default from 'pm2';
77

88
const { default: pm2 } = pm2Default as unknown as {

docs/cache-handler-docs/src/app/usage/creating-a-custom-handler/page.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Create a file called `cache-handler.mjs` next to your `next.config.js` with the
1818

1919
```js filename="cache-handler.mjs" copy
2020
import { CacheHandler } from '@neshca/cache-handler';
21-
import { isImplicitTag } from '@neshca/cache-handler/helpers';
21+
import { isTagImplicit } from '@neshca/cache-handler/helpers';
2222
import { createClient, commandOptions } from 'redis';
2323

2424
CacheHandler.onCreation(async () => {
@@ -179,7 +179,7 @@ CacheHandler.onCreation(async () => {
179179

180180
// Check if the tag is implicit.
181181
// Implicit tags are not stored in the cached values.
182-
if (isImplicitTag(tag)) {
182+
if (isTagImplicit(tag)) {
183183
// Mark the tag as revalidated at the current time.
184184
await client.hSet(
185185
commandOptions({ signal: AbortSignal.timeout(timeoutMs) }),

internal/typescript-config/test.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"display": "Test",
4+
"compilerOptions": {
5+
"target": "es2022",
6+
"lib": ["es2023"],
7+
8+
"module": "NodeNext",
9+
"moduleDetection": "force",
10+
"esModuleInterop": true,
11+
12+
"strict": true,
13+
"noUncheckedIndexedAccess": true,
14+
"noImplicitOverride": true,
15+
"skipLibCheck": true,
16+
17+
"noEmit": true
18+
}
19+
}

packages/cache-handler/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@
2424
"type": "module",
2525
"exports": {
2626
".": "./dist/cache-handler.js",
27+
"./helpers/*": "./dist/helpers/*.js",
2728
"./handlers/*": "./dist/handlers/*.js",
28-
"./helpers": "./dist/helpers/helpers.js",
2929
"./instrumentation/*": "./dist/instrumentation/*.js"
3030
},
3131
"scripts": {
3232
"build": "tsc",
3333
"check-types": "tsc --noEmit",
3434
"dev": "tsc --watch",
35-
"test": "tsx --test src/**/*.test.ts",
36-
"test:watch": "tsx --watch --test src/**/*.test.ts"
35+
"test": "vitest run",
36+
"test:watch": "vitest"
3737
},
3838
"dependencies": {
3939
"cluster-key-slot": "^1.1.2",
@@ -44,7 +44,8 @@
4444
"@repo/typescript-config": "workspace:*",
4545
"@types/node": "22.13.9",
4646
"tsx": "4.19.3",
47-
"typescript": "5.8.2"
47+
"typescript": "5.8.2",
48+
"vitest": "3.0.7"
4849
},
4950
"peerDependencies": {
5051
"next": ">= 15 < 16",

packages/cache-handler/src/cache-handler.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import type {
1515
} from './next-common-types.js';
1616
import { CachedRouteKind } from './next-common-types.js';
1717

18-
import { createValidatedAgeEstimationFunction } from './helpers/create-validated-age-estimation-function.js';
19-
import { getTagsFromHeaders } from './helpers/get-tags-from-headers.js';
18+
import { composeAgeEstimationFn } from './utils/compose-age-estimation-fn.js';
19+
import { getTagsFromHeaders } from './utils/get-tags-from-headers.js';
2020

2121
export type { CacheHandlerValue };
2222

@@ -623,8 +623,7 @@ export class CacheHandler implements NextCacheHandler {
623623
CacheHandler.#defaultStaleAge = Math.floor(defaultStaleAge);
624624
}
625625

626-
CacheHandler.#estimateExpireAge =
627-
createValidatedAgeEstimationFunction(estimateExpireAge);
626+
CacheHandler.#estimateExpireAge = composeAgeEstimationFn(estimateExpireAge);
628627

629628
CacheHandler.#serverDistDir = serverDistDir;
630629

packages/cache-handler/src/handlers/experimental-redis-cluster.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import calculate from 'cluster-key-slot';
2-
import type { createCluster } from 'redis';
31
import type { CacheHandlerValue, Handler } from '../cache-handler.js';
42
import type { CreateRedisStringsHandlerOptions } from '../common-types.js';
3+
import calculate from 'cluster-key-slot';
4+
import type { createCluster } from 'redis';
55

66
import { REVALIDATED_TAGS_KEY } from '../constants.js';
7-
import { getTimeoutRedisCommandOptions } from '../helpers/get-timeout-redis-command-options.js';
8-
import { isImplicitTag } from '../helpers/is-implicit-tag.js';
7+
import { createRedisTimeoutConfig } from '../helpers/create-redis-timeout-config.js';
8+
import { isTagImplicit } from '../helpers/is-tag-implicit.js';
99

1010
type CreateRedisClusterHandlerOptions<T = ReturnType<typeof createCluster>> =
1111
CreateRedisStringsHandlerOptions & {
@@ -83,7 +83,7 @@ export default function createHandler({
8383
name: 'experimental-redis-cluster',
8484
async get(key, { implicitTags }) {
8585
const result = await cluster.get(
86-
getTimeoutRedisCommandOptions(timeoutMs),
86+
createRedisTimeoutConfig(timeoutMs),
8787
keyPrefix + key,
8888
);
8989

@@ -104,7 +104,7 @@ export default function createHandler({
104104
}
105105

106106
const revalidationTimes = await cluster.hmGet(
107-
getTimeoutRedisCommandOptions(timeoutMs),
107+
createRedisTimeoutConfig(timeoutMs),
108108
revalidatedTagsKey,
109109
Array.from(combinedTags),
110110
);
@@ -115,7 +115,7 @@ export default function createHandler({
115115
Number.parseInt(timeString, 10) > cacheValue.lastModified
116116
) {
117117
await cluster.unlink(
118-
getTimeoutRedisCommandOptions(timeoutMs),
118+
createRedisTimeoutConfig(timeoutMs),
119119
keyPrefix + key,
120120
);
121121

@@ -126,7 +126,7 @@ export default function createHandler({
126126
return cacheValue;
127127
},
128128
async set(key, cacheHandlerValue) {
129-
const options = getTimeoutRedisCommandOptions(timeoutMs);
129+
const options = createRedisTimeoutConfig(timeoutMs);
130130

131131
let setOperation: Promise<string | null>;
132132

@@ -184,9 +184,9 @@ export default function createHandler({
184184
async revalidateTag(tag) {
185185
// If the tag is an implicit tag, we need to mark it as revalidated.
186186
// The revalidation process is done by the CacheHandler class on the next get operation.
187-
if (isImplicitTag(tag)) {
187+
if (isTagImplicit(tag)) {
188188
await cluster.hSet(
189-
getTimeoutRedisCommandOptions(timeoutMs),
189+
createRedisTimeoutConfig(timeoutMs),
190190
revalidatedTagsKey,
191191
tag,
192192
Date.now(),
@@ -201,7 +201,7 @@ export default function createHandler({
201201

202202
do {
203203
const remoteTagsPortion = await cluster.hScan(
204-
getTimeoutRedisCommandOptions(timeoutMs),
204+
createRedisTimeoutConfig(timeoutMs),
205205
keyPrefix + sharedTagsKey,
206206
cursor,
207207
hScanOptions,
@@ -242,7 +242,7 @@ export default function createHandler({
242242
}
243243

244244
const unlinkPromisesForSlot = client.unlink(
245-
getTimeoutRedisCommandOptions(timeoutMs),
245+
createRedisTimeoutConfig(timeoutMs),
246246
keys,
247247
);
248248

@@ -252,15 +252,15 @@ export default function createHandler({
252252
}
253253

254254
const updateTagsOperation = cluster.hDel(
255-
{ isolated: true, ...getTimeoutRedisCommandOptions(timeoutMs) },
255+
{ isolated: true, ...createRedisTimeoutConfig(timeoutMs) },
256256
keyPrefix + sharedTagsKey,
257257
tagsToDelete,
258258
);
259259

260260
await Promise.allSettled([...unlinkPromises, updateTagsOperation]);
261261
},
262262
async delete(key) {
263-
await cluster.unlink(getTimeoutRedisCommandOptions(timeoutMs), key);
263+
await cluster.unlink(createRedisTimeoutConfig(timeoutMs), key);
264264
},
265265
};
266266
}

packages/cache-handler/src/handlers/local-lru.ts

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,108 @@
1-
import type { LruCacheOptions } from '../lru-cache-next-adapter/cache-types/next-cache-handler-value.js';
2-
import createCacheStore from '../lru-cache-next-adapter/cache-types/next-cache-handler-value.js';
3-
41
import type { Handler } from '../cache-handler.js';
52
import { NEXT_CACHE_IMPLICIT_TAG_ID } from '../next-common-types.js';
3+
import {
4+
type CacheHandlerValue,
5+
CachedRouteKind,
6+
} from '../next-common-types.js';
7+
import { LRUCache } from 'lru-cache';
8+
9+
function calculateObjectSize({ value }: CacheHandlerValue): number {
10+
// Return default size if value is falsy
11+
if (!value) {
12+
return 25;
13+
}
14+
15+
switch (value.kind) {
16+
case CachedRouteKind.REDIRECT: {
17+
// Calculate size based on the length of the stringified props
18+
return JSON.stringify(value.props).length;
19+
}
20+
case CachedRouteKind.IMAGE: {
21+
// Throw a specific error for image kind
22+
throw new Error(
23+
'Image kind should not be used for incremental-cache calculations.',
24+
);
25+
}
26+
case CachedRouteKind.FETCH: {
27+
// Calculate size based on the length of the stringified data
28+
return JSON.stringify(value.data || '').length;
29+
}
30+
case CachedRouteKind.APP_ROUTE: {
31+
// Size based on the length of the body
32+
return value.body.length;
33+
}
34+
case CachedRouteKind.PAGES: {
35+
return value.html.length + JSON.stringify(value.pageData).length;
36+
}
37+
case CachedRouteKind.APP_PAGE: {
38+
return value.html.length + (value.rscData?.length || 0);
39+
}
40+
default: {
41+
return 0;
42+
}
43+
}
44+
}
45+
46+
export function createCacheStore(
47+
options?: LruCacheOptions,
48+
): LRUCache<string, CacheHandlerValue> {
49+
return createConfiguredCache(calculateObjectSize, options);
50+
}
651

752
/**
8-
* @deprecated Use {@link LruCacheOptions} instead.
53+
* Configuration options for the LRU cache.
54+
*
55+
* @since 1.0.0
956
*/
10-
export type LruCacheHandlerOptions = LruCacheOptions;
57+
export type LruCacheOptions = {
58+
/**
59+
* Optional. Maximum number of items the cache can hold.
60+
*
61+
* @default 1000
62+
*
63+
* @since 1.0.0
64+
*/
65+
maxItemsNumber?: number;
66+
/**
67+
* Optional. Maximum size in bytes for each item in the cache.
68+
*
69+
* @default 104857600 // 100 Mb
70+
*
71+
* @since 1.0.0
72+
*/
73+
maxItemSizeBytes?: number;
74+
};
75+
76+
const MAX_ITEMS_NUMBER = 1000;
77+
const MAX_ITEM_SIZE_BYTES = 100 * 1024 * 1024;
78+
79+
const DEFAULT_OPTIONS: LruCacheOptions = {
80+
maxItemsNumber: MAX_ITEMS_NUMBER,
81+
maxItemSizeBytes: MAX_ITEM_SIZE_BYTES,
82+
};
1183

12-
export type { LruCacheOptions };
84+
/**
85+
* Creates a configured LRUCache.
86+
*
87+
* @param calculateSizeCallback - A callback function to calculate the size of cache items.
88+
*
89+
* @param options - Optional configuration options for the cache.
90+
*
91+
* @returns A new instance of LRUCache.
92+
*/
93+
export function createConfiguredCache<CacheValueType extends object | string>(
94+
calculateSizeCallback: (value: CacheValueType) => number,
95+
{
96+
maxItemsNumber = MAX_ITEMS_NUMBER,
97+
maxItemSizeBytes = MAX_ITEM_SIZE_BYTES,
98+
} = DEFAULT_OPTIONS,
99+
): LRUCache<string, CacheValueType> {
100+
return new LRUCache<string, CacheValueType>({
101+
max: maxItemsNumber,
102+
maxSize: maxItemSizeBytes,
103+
sizeCalculation: calculateSizeCallback,
104+
});
105+
}
13106

14107
/**
15108
* Creates an LRU (Least Recently Used) cache Handler.

0 commit comments

Comments
 (0)