refine the nunjuck cache

This commit is contained in:
Kent Wang
2025-08-22 16:34:10 +08:00
parent 2d9c506cf7
commit e397382fa2
3 changed files with 72 additions and 41 deletions

View File

@@ -56,10 +56,8 @@ async function _highlightNunjucksTags(
showVariableSourceAndValue: boolean,
editorId: string,
) {
const renderCacheKey = Math.random() + '';
const renderString = (text: any) => render(text, renderCacheKey);
const renderContextWithCacheKey = () => renderContext(renderCacheKey);
const renderString = (text: any) => render(text);
const renderContextWithCacheKey = () => renderContext();
const activeMarks: CodeMirror.TextMarker[] = [];
const doc: CodeMirror.Doc = this.getDoc();

View File

@@ -66,7 +66,7 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
const codeMirror = useRef<CodeMirror.EditorFromTextArea | null>(null);
const { settings } = useRootLoaderData()!;
const { isOwner, isEnterprisePlan } = usePlanData();
const { handleRender, handleGetRenderContext } = useNunjucks();
const { handleRender, handleGetRenderContext } = useNunjucks({ enableCache: true });
const getKeyMap = useCallback(() => {
if (!readOnly && settings.enableKeyMapForInlineTextEditors && settings.editorKeyMap) {

View File

@@ -1,26 +1,37 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { getRenderContext, getRenderContextAncestors, render } from '~/common/render';
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import { useRequestLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { useRequestGroupLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId';
import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '~/templating';
import type { HandleRender, RenderContextOptions } from '~/templating/types';
import type { BaseRenderContext, HandleRender, RenderContextOptions } from '~/templating/types';
import { getKeys } from '~/templating/utils';
let getRenderContextPromiseCache: any = {};
export interface UseNunjucksOptions {
renderContext: Pick<Partial<RenderContextOptions>, 'purpose' | 'extraInfo'>;
interface CacheOptions {
//If true, will cache the render context
enableCache?: boolean;
//Cache duration in seconds, default is 5 seconds
cacheDuration?: number;
}
export const initializeNunjucksRenderPromiseCache = () => {
getRenderContextPromiseCache = {};
};
initializeNunjucksRenderPromiseCache();
export type UseNunjucksOptions = {
renderContext?: Pick<Partial<RenderContextOptions>, 'purpose' | 'extraInfo'>;
} & CacheOptions;
const defaultCacheDurationInSeconds = 5;
/**
* Access to functions useful for Nunjucks rendering
*
* Customized hook simplifies template rendering in React components by:
* - Retrieving render context based on current request/folder/workspace
* - Providing optional caching to optimize performance and avoid race conditions
* - Managing cache lifecycle
*
* @param options - Configuration options for rendering and caching
* @param options.renderContext.purpose - Purpose of the render operation (e.g., 'send', 'preview')
* @param options.renderContext.extraInfo - Additional information to include in render context
* @param options.enableCache - Whether to cache the render context. Mainly used for editors with many nunjucks to render
* @param options.cacheDuration - How long to keep the cached context in seconds (default: 5)
*
*/
export const useNunjucks = (options?: UseNunjucksOptions) => {
// for all types of requests
@@ -28,6 +39,9 @@ export const useNunjucks = (options?: UseNunjucksOptions) => {
// for request group (folder)
const { activeRequestGroup } = useRequestGroupLoaderData() || {};
const workspaceData = useWorkspaceLoaderData();
const { enableCache = false, cacheDuration = defaultCacheDurationInSeconds } = options || {};
const renderContextPromiseCache = useRef<Promise<BaseRenderContext>>();
const renderContextPromiseCacheTimer = useRef<ReturnType<typeof setTimeout>>();
const fetchRenderContext = useCallback(async () => {
const ancestors = await getRenderContextAncestors(
@@ -41,49 +55,68 @@ export const useNunjucks = (options?: UseNunjucksOptions) => {
});
}, [
requestData?.activeRequest,
activeRequestGroup,
workspaceData?.activeWorkspace,
workspaceData?.activeEnvironment._id,
options?.renderContext,
activeRequestGroup,
]);
const handleGetRenderContext = useCallback(
async (contextCacheKey?: string) => {
const context =
contextCacheKey && getRenderContextPromiseCache[contextCacheKey]
? await getRenderContextPromiseCache[contextCacheKey]
: await fetchRenderContext();
const keys = getKeys(context, NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME);
return { context, keys };
},
[fetchRenderContext],
);
const handleGetRenderContext = useCallback(async () => {
const context =
enableCache && renderContextPromiseCache.current
? await renderContextPromiseCache.current
: await fetchRenderContext();
const keys = getKeys(context, NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME);
return { context, keys };
}, [enableCache, fetchRenderContext]);
/**
* Heavily optimized render function
*
* @param text - template to render
* @param contextCacheKey - if rendering multiple times in parallel, set this
* @returns {Promise}
* @private
*/
const handleRender: HandleRender = useCallback(
async <T>(obj: T, contextCacheKey: string | null = null) => {
if (!contextCacheKey || !getRenderContextPromiseCache[contextCacheKey]) {
// NOTE: We're caching promises here to avoid race conditions
// @ts-expect-error -- TSCONVERSION contextCacheKey being null used as object index
getRenderContextPromiseCache[contextCacheKey] = fetchRenderContext();
async <T>(obj: T) => {
let getOrCreateRenderContext: ReturnType<typeof fetchRenderContext>;
if (enableCache) {
if (!renderContextPromiseCache.current) {
// create a new context promise to optimize performance and avoid race conditions
console.log(`[templating] Create Nunjucks render context cache`);
renderContextPromiseCache.current = fetchRenderContext();
// Implement time-based cache invalidation strategy for performance optimization:
// 1. Cache the render context to avoid expensive re-computation for multiple nunjucks
// 2. Set timeout to clear cache after specified duration to ensure context freshness
// 3. This pattern is mainly used for editors that render many nunjucks simultaneously
// (e.g., code-editor, one-line-editor)
// 4. With this approach, all templates rendered during the cache window share the same
// context promise, avoiding redundant context creation and race conditions
renderContextPromiseCacheTimer.current = setTimeout(() => {
console.log(`[templating] Remove Nunjucks render context cache`);
renderContextPromiseCache.current = undefined;
}, cacheDuration * 1000);
}
getOrCreateRenderContext = renderContextPromiseCache.current;
} else {
getOrCreateRenderContext = fetchRenderContext();
}
// Set timeout to delete the key eventually
// @ts-expect-error -- TSCONVERSION contextCacheKey being null used as object index
setTimeout(() => delete getRenderContextPromiseCache[contextCacheKey], 5000);
// @ts-expect-error -- TSCONVERSION contextCacheKey being null used as object index
const context = await getRenderContextPromiseCache[contextCacheKey];
const context = await getOrCreateRenderContext;
return render({ obj, context });
},
[fetchRenderContext],
[enableCache, fetchRenderContext, cacheDuration],
);
useEffect(() => {
const timer = renderContextPromiseCacheTimer.current;
return () => {
console.log(`[templating] Clear Nunjucks render context cache on unmount`);
// clear timeout when unmount
timer && clearTimeout(timer);
renderContextPromiseCache.current = undefined;
};
}, []);
return {
handleRender,
handleGetRenderContext,