Skip to content

Commit 868ce29

Browse files
committed
feat: unstable_shouldDisable
1 parent 50a541c commit 868ce29

File tree

3 files changed

+200
-7
lines changed

3 files changed

+200
-7
lines changed

.changeset/swift-cows-brush.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modern-js/runtime-utils': patch
3+
---
4+
5+
feat: support unstable_shouldDisable
6+
feat: 支持 unstable_shouldDisable

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

+36-7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ interface CacheOptions {
3939

4040
interface CacheConfig {
4141
maxSize: number;
42+
unstable_shouldDisable?: ({
43+
request,
44+
}: {
45+
request: Request;
46+
}) => boolean | Promise<boolean>;
4247
}
4348

4449
interface CacheItem<T> {
@@ -180,6 +185,17 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
180185
const storage = getAsyncLocalStorage();
181186
const request = storage?.useContext()?.request;
182187
if (request) {
188+
let shouldDisableCaching = false;
189+
if (cacheConfig.unstable_shouldDisable) {
190+
shouldDisableCaching = await Promise.resolve(
191+
cacheConfig.unstable_shouldDisable({ request }),
192+
);
193+
}
194+
195+
if (shouldDisableCaching) {
196+
return fn(...args);
197+
}
198+
183199
let requestCache = requestCacheMap.get(request);
184200
if (!requestCache) {
185201
requestCache = new Map();
@@ -225,8 +241,19 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
225241
const storeKey =
226242
customKey && typeof cacheKey === 'symbol' ? 'symbol-key' : genKey;
227243

244+
let shouldDisableCaching = false;
245+
if (isServer && cacheConfig.unstable_shouldDisable) {
246+
const storage = getAsyncLocalStorage();
247+
const request = storage?.useContext()?.request;
248+
if (request) {
249+
shouldDisableCaching = await Promise.resolve(
250+
cacheConfig.unstable_shouldDisable({ request }),
251+
);
252+
}
253+
}
254+
228255
const cached = cacheStore.get(storeKey);
229-
if (cached) {
256+
if (cached && !shouldDisableCaching) {
230257
const age = now - cached.timestamp;
231258

232259
if (age < maxAge) {
@@ -282,13 +309,15 @@ export function cache<T extends (...args: any[]) => Promise<any>>(
282309

283310
const data = await fn(...args);
284311

285-
cacheStore.set(storeKey, {
286-
data,
287-
timestamp: now,
288-
isRevalidating: false,
289-
});
312+
if (!shouldDisableCaching) {
313+
cacheStore.set(storeKey, {
314+
data,
315+
timestamp: now,
316+
isRevalidating: false,
317+
});
290318

291-
store.set(cacheKey, cacheStore);
319+
store.set(cacheKey, cacheStore);
320+
}
292321

293322
if (onCache) {
294323
onCache({

packages/toolkit/runtime-utils/tests/universal/cache-server.test.ts

+158
Original file line numberDiff line numberDiff line change
@@ -689,4 +689,162 @@ describe('cache function', () => {
689689
expect(onCacheMock).not.toHaveBeenCalled();
690690
});
691691
});
692+
693+
describe('unstable_shouldDisable', () => {
694+
beforeEach(() => {
695+
jest.useFakeTimers();
696+
clearStore();
697+
});
698+
699+
afterEach(() => {
700+
jest.useRealTimers();
701+
configureCache({ maxSize: CacheSize.GB });
702+
});
703+
704+
it('should bypass cache when unstable_shouldDisable returns true', async () => {
705+
const mockFn = jest.fn().mockResolvedValue('test data');
706+
const cachedFn = cache(mockFn, { maxAge: CacheTime.MINUTE });
707+
708+
configureCache({
709+
maxSize: CacheSize.GB,
710+
unstable_shouldDisable: () => true,
711+
});
712+
713+
const handler = withRequestCache(async (req: Request) => {
714+
const result1 = await cachedFn('param1');
715+
const result2 = await cachedFn('param1');
716+
return { result1, result2 };
717+
});
718+
719+
await handler(new MockRequest() as unknown as Request);
720+
721+
expect(mockFn).toHaveBeenCalledTimes(2);
722+
});
723+
724+
it('should use cache when unstable_shouldDisable returns false', async () => {
725+
const mockFn = jest.fn().mockResolvedValue('test data');
726+
const cachedFn = cache(mockFn, { maxAge: CacheTime.MINUTE });
727+
728+
configureCache({
729+
maxSize: CacheSize.GB,
730+
unstable_shouldDisable: () => false,
731+
});
732+
733+
const handler = withRequestCache(async (req: Request) => {
734+
const result1 = await cachedFn('param1');
735+
const result2 = await cachedFn('param1');
736+
return { result1, result2 };
737+
});
738+
739+
await handler(new MockRequest() as unknown as Request);
740+
741+
expect(mockFn).toHaveBeenCalledTimes(1);
742+
});
743+
744+
it('should support dynamic decision based on request', async () => {
745+
const mockFn = jest.fn().mockResolvedValue('test data');
746+
const cachedFn = cache(mockFn, { tag: 'testTag' });
747+
748+
configureCache({
749+
maxSize: CacheSize.GB,
750+
unstable_shouldDisable: ({ request }) => {
751+
return request.url.includes('no-cache');
752+
},
753+
});
754+
755+
const handlerWithCache = withRequestCache(async (req: Request) => {
756+
const result1 = await cachedFn('param1');
757+
const result2 = await cachedFn('param1');
758+
return { result1, result2 };
759+
});
760+
761+
const handlerWithoutCache = withRequestCache(async (req: Request) => {
762+
const result1 = await cachedFn('param1');
763+
const result2 = await cachedFn('param1');
764+
return { result1, result2 };
765+
});
766+
767+
await handlerWithCache(
768+
new MockRequest('http://example.com') as unknown as Request,
769+
);
770+
771+
await handlerWithoutCache(
772+
new MockRequest('http://example.com/no-cache') as unknown as Request,
773+
);
774+
775+
expect(mockFn).toHaveBeenCalledTimes(3);
776+
});
777+
778+
it('should affect no-options cache as well', async () => {
779+
const mockFn = jest.fn().mockResolvedValue('test data');
780+
const cachedFn = cache(mockFn);
781+
configureCache({
782+
maxSize: CacheSize.GB,
783+
unstable_shouldDisable: () => true,
784+
});
785+
786+
const handler = withRequestCache(async (req: Request) => {
787+
const result1 = await cachedFn('param1');
788+
const result2 = await cachedFn('param1');
789+
return { result1, result2 };
790+
});
791+
792+
await handler(new MockRequest() as unknown as Request);
793+
794+
expect(mockFn).toHaveBeenCalledTimes(2);
795+
});
796+
797+
it('should support async decision function', async () => {
798+
const mockFn = jest.fn().mockResolvedValue('test data');
799+
const cachedFn = cache(mockFn, { maxAge: CacheTime.MINUTE });
800+
801+
configureCache({
802+
maxSize: CacheSize.GB,
803+
unstable_shouldDisable: async () => {
804+
return Promise.resolve(true);
805+
},
806+
});
807+
808+
const handler = withRequestCache(async (req: Request) => {
809+
const result1 = await cachedFn('param1');
810+
const result2 = await cachedFn('param1');
811+
return { result1, result2 };
812+
});
813+
814+
await handler(new MockRequest() as unknown as Request);
815+
816+
expect(mockFn).toHaveBeenCalledTimes(2);
817+
});
818+
819+
it('should still trigger onCache callback even when cache is disabled', async () => {
820+
const mockFn = jest.fn().mockResolvedValue('test data');
821+
const onCacheMock = jest.fn();
822+
823+
const cachedFn = cache(mockFn, {
824+
maxAge: CacheTime.MINUTE,
825+
onCache: onCacheMock,
826+
});
827+
828+
configureCache({
829+
maxSize: CacheSize.GB,
830+
unstable_shouldDisable: () => true,
831+
});
832+
833+
const handler = withRequestCache(async (req: Request) => {
834+
const result1 = await cachedFn('param1');
835+
const result2 = await cachedFn('param1');
836+
return { result1, result2 };
837+
});
838+
839+
await handler(new MockRequest() as unknown as Request);
840+
841+
expect(mockFn).toHaveBeenCalledTimes(2);
842+
expect(onCacheMock).toHaveBeenCalledTimes(2);
843+
expect(onCacheMock).toHaveBeenCalledWith(
844+
expect.objectContaining({
845+
status: 'miss',
846+
}),
847+
);
848+
});
849+
});
692850
});

0 commit comments

Comments
 (0)