feat(core): support invalidate & revalidate on dynamic loader

This commit is contained in:
Fuma Nama
2026-05-29 16:47:48 +08:00
parent 0f79c36f92
commit 184022fd4d
9 changed files with 45 additions and 26 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@fumadocs/local-md': patch
'fumadocs-core': patch
---
Support `invalidate` & `revalidate` on dynamic loader
+1 -6
View File
@@ -15,12 +15,7 @@ import { Loader2, MessageCircleIcon, RefreshCw, SearchIcon, Send, X } from 'luci
import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import {
DefaultChatTransport,
type Tool,
type UIMessage,
type UIToolInvocation,
} from 'ai';
import { DefaultChatTransport, type Tool, type UIMessage, type UIToolInvocation } from 'ai';
import { Markdown } from '../markdown';
import { Presence } from '@radix-ui/react-presence';
@@ -91,7 +91,7 @@ const source = dynamicLoader(
export async function getSource() {
const { isEnabled } = await draftMode();
if (isEnabled) source.revalidate();
if (isEnabled) source.invalidate();
return source.get();
}
```
@@ -242,14 +242,18 @@ const page = docs.getPage(['...']);
Dynamic sources are cached by the loader.
If your source changes over time, call `revalidate()` to clear the cache:
If your source changes over time, call `invalidate()` to clear the cache, or `revalidate()` to update the cache:
```ts
source.revalidate();
source.invalidate();
await source.revalidate();
```
For [multi-source](/docs/headless/source-api/source#multiple-sources) setups, you can revalidate a specific source:
For [multi-source](/docs/headless/source-api/source#multiple-sources) setups, you can invalidate/revalidate a specific source:
```ts
source.revalidate('docs');
source.invalidate('docs');
await source.revalidate('docs');
```
@@ -110,7 +110,7 @@ export function createMySource(): DynamicSource<{
configure(loader) {
// you can revalidate the loader from source, e.g.
onMyEvent(() => {
loader.revalidate();
void loader.revalidate();
});
},
};
+1 -1
View File
@@ -17,8 +17,8 @@
"@ai-sdk/react": "^3.0.193",
"@floating-ui/react-dom": "^2.1.8",
"@fumadocs/story": "workspace:*",
"@mixedbread/sdk": "^0.71.0",
"@llmgateway/ai-sdk-provider": "^3.8.0",
"@mixedbread/sdk": "^0.71.0",
"@openrouter/ai-sdk-provider": "^2.9.0",
"@orama/core": "^1.2.19",
"@orama/orama": "^3.1.18",
+25 -11
View File
@@ -13,7 +13,10 @@ export interface DynamicLoaderConfig extends LoaderConfig {
export interface DynamicLoader<Config extends DynamicLoaderConfig = DynamicLoaderConfig> {
get: () => Promise<LoaderOutput<Config>>;
revalidate: (source?: Config['source']) => void;
/** update & re-compute dynamic sources */
revalidate: (source?: Config['source']) => Promise<void>;
/** remove computed cache of dynamic sources */
invalidate: (source?: Config['source']) => void;
get $inferPage(): Config['page'];
get $inferMeta(): Config['meta'];
@@ -46,22 +49,25 @@ export function dynamicLoader<I extends Input, I18n extends I18nConfig | undefin
}
}
async function resolveSources(): Promise<ResolvedSource> {
if (isStaticSource(input)) return input;
if (isDynamicSource(input)) return resolveDynamicSource(input);
async function resolveSources(skipCache = false): Promise<ResolvedSource> {
if (isStaticSource(input) || isDynamicSource(input)) {
return resolveSource(input, skipCache);
}
const entries = await Promise.all(
Object.entries(input).map(async ([k, v]): Promise<[string, StaticSource]> => {
if (isStaticSource(v)) return [k, v];
return [k, await resolveDynamicSource(v)];
}),
Object.entries(input).map(async ([k, v]) => [k, await resolveSource(v, skipCache)]),
);
return Object.fromEntries(entries);
}
function resolveDynamicSource(v: DynamicSource): Awaitable<StaticSource> {
let resolved = sourceCache.get(v);
function resolveSource(
v: StaticSource | DynamicSource,
skipCache = false,
): Awaitable<StaticSource> {
if (isStaticSource(v)) return v;
let resolved = skipCache ? undefined : sourceCache.get(v);
if (resolved) return resolved;
const files = v.files();
@@ -89,7 +95,15 @@ export function dynamicLoader<I extends Input, I18n extends I18nConfig | undefin
}),
$inferPage: undefined as never,
$inferMeta: undefined as never,
revalidate(name) {
async revalidate(name) {
// rewrite cache, wait until next `get()` to compute `loader()`
if (name === undefined) {
await resolveSources(true);
} else if (!isStaticSource(input) && !isDynamicSource(input)) {
await resolveSource(input[name], true);
}
},
invalidate(name) {
if (name === undefined) {
sourceCache.clear();
} else if (!isStaticSource(input) && !isDynamicSource(input)) {
+1 -1
View File
@@ -169,7 +169,7 @@ export function localMd<
const absolutePath = path.resolve(file);
cachedStaticSource = null;
storage.invalidateCache(absolutePath);
for (const v of registeredLoaders) v.revalidate();
for (const v of registeredLoaders) v.invalidate();
},
async devServer(url = getDevServerUrlFromEnv()) {
if (!url) {
+1 -1
View File
@@ -84,7 +84,7 @@
"xml-js": "^1.6.11"
},
"peerDependencies": {
"@scalar/api-client-react": "2.0.1",
"@scalar/api-client-react": "^2.0.20",
"@types/react": "*",
"fumadocs-core": "^16.9.0",
"fumadocs-ui": "^16.9.0",