Skip to content

Commit 3a66335

Browse files
authored
feat: support getKey for cache funtion (#7102)
1 parent ba7f7ce commit 3a66335

File tree

9 files changed

+286
-15
lines changed

9 files changed

+286
-15
lines changed

.changeset/dirty-turkeys-crash.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modern-js/runtime-utils': patch
3+
---
4+
5+
feat: support getKey for cache funtion
6+
feat: 为 cahce 函数支持 getKey

packages/document/main-doc/docs/en/guides/basic-features/data/data-cache.mdx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ X.65.5 and above versions are required
1212

1313
## Basic Usage
1414

15+
:::note
16+
17+
If you use the `cache` function in BFF, you should import the cache funtion from `@modern-js/server-runtime/cache`
18+
19+
`import { cache } from '@modern-js/server-runtime/cache'`
20+
21+
:::
22+
23+
1524
```ts
1625
import { cache } from '@modern-js/runtime/cache';
1726
import { fetchUserData } from './api';
@@ -34,6 +43,7 @@ const loader = async () => {
3443
- `tag`: Tag to identify the cache, which can be used to invalidate the cache
3544
- `maxAge`: Cache validity period (milliseconds)
3645
- `revalidate`: Time window for revalidating the cache (milliseconds), similar to HTTP Cache-Control's stale-while-revalidate functionality
46+
- `getKey`: Simplified cache key generation function based on function parameters
3747
- `customKey`: Custom cache key function
3848

3949
The type of the `options` parameter is as follows:
@@ -43,6 +53,7 @@ interface CacheOptions {
4353
tag?: string | string[];
4454
maxAge?: number;
4555
revalidate?: number;
56+
getKey?: <Args extends any[]>(...args: Args) => string;
4657
customKey?: <Args extends any[]>(options: {
4758
params: Args;
4859
fn: (...args: Args) => any;
@@ -159,6 +170,72 @@ revalidateTag('dashboard-stats'); // Invalidates the cache for both getDashboard
159170
```
160171

161172

173+
#### `getKey` Parameter
174+
175+
The `getKey` parameter simplifies cache key generation, especially useful when you only need to rely on part of the function parameters to differentiate caches. It's a function that receives the same parameters as the original function and returns a string or number as the cache key:
176+
177+
```ts
178+
import { cache, CacheTime } from '@modern-js/runtime/cache';
179+
import { fetchUserData } from './api';
180+
181+
const getUser = cache(
182+
async (userId, options) => {
183+
// Here options might contain many configurations, but we only want to cache based on userId
184+
return await fetchUserData(userId, options);
185+
},
186+
{
187+
maxAge: CacheTime.MINUTE * 5,
188+
// Only use the first parameter (userId) as the cache key
189+
getKey: (userId, options) => userId,
190+
}
191+
);
192+
193+
// The following two calls will share the cache because getKey only uses userId
194+
await getUser(123, { language: 'en' });
195+
await getUser(123, { language: 'fr' }); // Cache hit, won't request again
196+
197+
// Different userId will use different cache
198+
await getUser(456, { language: 'en' }); // Won't hit cache, will request again
199+
```
200+
201+
You can also use Modern.js's `generateKey` function together with getKey to generate the cache key:
202+
203+
:::info
204+
205+
The `generateKey` function in Modern.js ensures that a consistent and unique key is generated even if object property orders change, guaranteeing stable caching.
206+
207+
:::
208+
209+
```ts
210+
import { cache, CacheTime, generateKey } from '@modern-js/runtime/cache';
211+
import { fetchUserData } from './api';
212+
213+
const getUser = cache(
214+
async (userId, options) => {
215+
return await fetchUserData(userId, options);
216+
},
217+
{
218+
maxAge: CacheTime.MINUTE * 5,
219+
getKey: (userId, options) => generateKey(userId),
220+
}
221+
);
222+
```
223+
224+
Additionally, `getKey` can also return a numeric type as a cache key:
225+
226+
```ts
227+
const getUserById = cache(
228+
fetchUserDataById,
229+
{
230+
maxAge: CacheTime.MINUTE * 5,
231+
// Directly use the numeric ID as the cache key
232+
getKey: (id) => id,
233+
}
234+
);
235+
236+
await getUserById(42); // Uses 42 as the cache key
237+
```
238+
162239
#### `customKey` parameter
163240

164241
The `customKey` parameter is used to customize the cache key. It is a function that receives an object with the following properties and returns a string or Symbol type as the cache key:
@@ -167,6 +244,18 @@ The `customKey` parameter is used to customize the cache key. It is a function t
167244
- `fn`: Reference to the original function being cached
168245
- `generatedKey`: Cache key automatically generated by the framework based on input parameters
169246

247+
:::info
248+
249+
Generally, the cache will be invalidated in the following scenarios:
250+
1. The referenced cached function changes
251+
2. The function's input parameters change
252+
3. The maxAge condition is no longer satisfied
253+
4. The `revalidateTag` method has been called
254+
255+
`customKey` can be used in scenarios where function references are different but shared caching is desired. If it's just for customizing the cache key, it is recommended to use `getKey`.
256+
257+
:::
258+
170259
This is very useful in some scenarios, such as when the function reference changes , but you want to still return the cached data.
171260

172261
```ts
@@ -271,7 +360,7 @@ The `onCache` callback receives an object with the following properties:
271360
- `params`: The parameters passed to the cached function
272361
- `result`: The result data (either from cache or newly computed)
273362

274-
This callback is only invoked when the `options` parameter is provided. When using the cache function without options (for SSR request-scoped caching), the `onCache` callback is not called.
363+
This callback is only invoked when the `options` parameter is provided. When using the cache function without options, the `onCache` callback is not called.
275364

276365
The `onCache` callback is useful for:
277366
- Monitoring cache performance

packages/document/main-doc/docs/zh/guides/basic-features/data/data-cache.mdx

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ sidebar_position: 4
1212

1313
## 基本用法
1414

15+
:::note
16+
17+
如果在 BFF 中使用 `cache` 函数,应该从 `@modern-js/server-runtime/cache` 导入相关函数
18+
19+
`import { cache } from '@modern-js/server-runtime/cache'`
20+
21+
:::
22+
1523
```ts
1624
import { cache } from '@modern-js/runtime/cache';
1725
import { fetchUserData } from './api';
@@ -33,6 +41,7 @@ const loader = async () => {
3341
- `tag`: 用于标识缓存的标签,可以基于这个标签使缓存失效
3442
- `maxAge`: 缓存的有效期 (毫秒)
3543
- `revalidate`: 重新验证缓存的时间窗口(毫秒),与 HTTP Cache-Control 的 stale-while-revalidate 功能一致
44+
- `getKey`: 简化的缓存键生成函数,根据函数参数生成缓存键
3645
- `customKey`: 自定义缓存键生成函数,用于在函数引用变化时保持缓存
3746

3847
`options` 参数的类型如下:
@@ -42,6 +51,7 @@ interface CacheOptions {
4251
tag?: string | string[];
4352
maxAge?: number;
4453
revalidate?: number;
54+
getKey?: <Args extends any[]>(...args: Args) => string;
4555
customKey?: <Args extends any[]>(options: {
4656
params: Args;
4757
fn: (...args: Args) => any;
@@ -152,6 +162,58 @@ const getComplexStatistics = cache(
152162
revalidateTag('dashboard-stats'); // 会使 getDashboardStats 函数和 getComplexStatistics 函数的缓存都失效
153163
```
154164

165+
#### `getKey` 参数
166+
167+
`getKey` 参数用于自定义缓存键的生成方式,例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数,接收与原始函数相同的参数,返回一个字符串作为缓存键:
168+
169+
```ts
170+
import { cache, CacheTime } from '@modern-js/runtime/cache';
171+
import { fetchUserData } from './api';
172+
173+
const getUser = cache(
174+
async (userId, options) => {
175+
// 这里 options 可能包含很多配置,但我们只想根据 userId 缓存
176+
return await fetchUserData(userId, options);
177+
},
178+
{
179+
maxAge: CacheTime.MINUTE * 5,
180+
// 只使用第一个参数(userId)作为缓存键
181+
getKey: (userId, options) => userId,
182+
}
183+
);
184+
185+
// 下面两次调用会共享缓存,因为 getKey 只使用了 userId
186+
await getUser(123, { language: 'zh' });
187+
await getUser(123, { language: 'en' }); // 命中缓存,不会重新请求
188+
189+
// 不同的 userId 会使用不同的缓存
190+
await getUser(456, { language: 'zh' }); // 不会命中缓存,会重新请求
191+
```
192+
193+
你也可以使用 Modern.js 提供的 `generateKey` 函数配合 getKey 生成缓存的键:
194+
195+
:::info
196+
197+
Modern.js 中的 `generateKey` 函数确保即使对象属性顺序发生变化,也能生成一致的唯一键值,保证稳定的缓存
198+
199+
:::
200+
201+
```ts
202+
import { cache, CacheTime, generateKey } from '@modern-js/runtime/cache';
203+
import { fetchUserData } from './api';
204+
205+
const getUser = cache(
206+
async (userId, options) => {
207+
return await fetchUserData(userId, options);
208+
},
209+
{
210+
maxAge: CacheTime.MINUTE * 5,
211+
getKey: (userId, options) => generateKey(userId),
212+
}
213+
);
214+
```
215+
216+
155217
#### `customKey` 参数
156218

157219
`customKey` 参数用于定制缓存的键,它是一个函数,接收一个包含以下属性的对象,返回值必须是字符串或 Symbol 类型,将作为缓存的键:
@@ -160,6 +222,18 @@ revalidateTag('dashboard-stats'); // 会使 getDashboardStats 函数和 getCompl
160222
- `fn`:原始被缓存的函数引用
161223
- `generatedKey`:框架基于入参自动生成的原始缓存键
162224

225+
:::info
226+
227+
一般在以下场景,缓存会失效:
228+
1. 缓存的函数引用发生变化
229+
2. 函数的入参发生变化
230+
3. 不满足 maxAge
231+
4. 调用了 `revalidateTag`
232+
233+
`customKey` 可以用在函数引用不同,但希望共享缓存的场景,如果只是自定义缓存键,推荐使用 `getKey`
234+
235+
:::
236+
163237
这在某些场景下非常有用,比如当函数引用发生变化时,但你希望仍然返回缓存的数据。
164238

165239
```ts
@@ -268,7 +342,7 @@ await getUser(2); // 缓存未命中
268342
- `params`: 传递给缓存函数的参数
269343
- `result`: 结果数据(来自缓存或新计算的)
270344

271-
这个回调只在提供 `options` 参数时被调用。当使用不带选项的缓存函数(用于 SSR 请求范围内的缓存)时,不会调用 `onCache` 回调。
345+
这个回调只在提供 `options` 参数时被调用。当使用无 options 的缓存函数时,不会调用 `onCache` 回调。
272346

273347
`onCache` 回调对以下场景非常有用:
274348
- 监控缓存性能

packages/server/server-runtime/package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,29 @@
2424
".": {
2525
"node": {
2626
"jsnext:source": "./src/index.ts",
27-
"types": "./dist/types/index.d.ts",
2827
"import": "./dist/esm-node/index.js",
2928
"require": "./dist/cjs/index.js"
3029
},
30+
"types": "./dist/types/index.d.ts",
3131
"default": "./dist/cjs/index.js"
32+
},
33+
"./cache": {
34+
"node": {
35+
"jsnext:source": "./src/cache.ts",
36+
"import": "./dist/esm-node/cache.js",
37+
"require": "./dist/cjs/cache.js"
38+
},
39+
"types": "./dist/types/cache.d.ts",
40+
"default": "./dist/cjs/cache.js"
3241
}
3342
},
3443
"typesVersions": {
3544
"*": {
3645
".": [
3746
"./dist/types/index.d.ts"
47+
],
48+
"cache": [
49+
"./dist/types/cache.d.ts"
3850
]
3951
}
4052
},
@@ -46,7 +58,8 @@
4658
},
4759
"dependencies": {
4860
"@swc/helpers": "0.5.13",
49-
"@modern-js/server-core": "workspace:*"
61+
"@modern-js/server-core": "workspace:*",
62+
"@modern-js/runtime-utils": "workspace:*"
5063
},
5164
"devDependencies": {
5265
"@scripts/build": "workspace:*",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from '@modern-js/runtime-utils/cache';
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
// This file is an `async_storage` proxy for browser bundle.
22
import type { Storage } from './async_storage.server';
33

4+
const isBrowser =
5+
typeof window !== 'undefined' && typeof window.document !== 'undefined';
6+
47
export const getAsyncLocalStorage = (): Storage | null => {
5-
console.error('You should not get async storage in browser');
6-
return null;
8+
if (isBrowser) {
9+
console.error('You should not get async storage in browser');
10+
return null;
11+
} else {
12+
try {
13+
const serverStorage = require('./async_storage.server');
14+
return serverStorage.getAsyncLocalStorage();
15+
} catch (err) {
16+
console.error('Failed to load server async storage', err);
17+
return null;
18+
}
19+
}
720
};

packages/toolkit/runtime-utils/src/universal/cache.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface CacheOptions {
2929
tag?: string | string[];
3030
maxAge?: number;
3131
revalidate?: number;
32+
getKey?: <Args extends any[]>(...args: Args) => string;
3233
customKey?: <Args extends any[]>(options: {
3334
params: Args;
3435
fn: (...args: Args) => any;
@@ -38,7 +39,7 @@ interface CacheOptions {
3839
}
3940

4041
interface CacheConfig {
41-
maxSize: number;
42+
maxSize?: number;
4243
unstable_shouldDisable?: ({
4344
request,
4445
}: {
@@ -86,7 +87,7 @@ function getLRUCache() {
8687
Function | string | symbol,
8788
Map<string, CacheItem<any>>
8889
>({
89-
maxSize: cacheConfig.maxSize,
90+
maxSize: cacheConfig.maxSize ?? CacheSize.GB,
9091
sizeCalculation: (value: Map<string, CacheItem<any>>): number => {
9192
if (!value.size) {
9293
return 1;
@@ -171,6 +172,7 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
171172
revalidate = 0,
172173
customKey,
173174
onCache,
175+
getKey,
174176
} = options || {};
175177
const store = getLRUCache();
176178

@@ -187,9 +189,9 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
187189
if (request) {
188190
let shouldDisableCaching = false;
189191
if (cacheConfig.unstable_shouldDisable) {
190-
shouldDisableCaching = await Promise.resolve(
191-
cacheConfig.unstable_shouldDisable({ request }),
192-
);
192+
shouldDisableCaching = await cacheConfig.unstable_shouldDisable({
193+
request,
194+
});
193195
}
194196

195197
if (shouldDisableCaching) {
@@ -225,7 +227,7 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
225227
}
226228
}
227229
} else if (typeof options !== 'undefined') {
228-
const genKey = generateKey(args);
230+
const genKey = getKey ? getKey(...args) : generateKey(args);
229231
const now = Date.now();
230232

231233
const cacheKey = getCacheKey(args, genKey);
@@ -246,9 +248,9 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
246248
const storage = getAsyncLocalStorage();
247249
const request = storage?.useContext()?.request;
248250
if (request) {
249-
shouldDisableCaching = await Promise.resolve(
250-
cacheConfig.unstable_shouldDisable({ request }),
251-
);
251+
shouldDisableCaching = await cacheConfig.unstable_shouldDisable({
252+
request,
253+
});
252254
}
253255
}
254256

0 commit comments

Comments
 (0)