diff --git a/packages/insomnia/src/ui/components/.client/codemirror/extensions/nunjucks-tags.ts b/packages/insomnia/src/ui/components/.client/codemirror/extensions/nunjucks-tags.ts index 45f64dc166..12ac52dbb3 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/extensions/nunjucks-tags.ts +++ b/packages/insomnia/src/ui/components/.client/codemirror/extensions/nunjucks-tags.ts @@ -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(); diff --git a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx index 366989b154..ef45c6a872 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx @@ -66,7 +66,7 @@ export const OneLineEditor = forwardRef const codeMirror = useRef(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) { diff --git a/packages/insomnia/src/ui/context/nunjucks/use-nunjucks.ts b/packages/insomnia/src/ui/context/nunjucks/use-nunjucks.ts index 0117b5bc7c..6bc89df654 100644 --- a/packages/insomnia/src/ui/context/nunjucks/use-nunjucks.ts +++ b/packages/insomnia/src/ui/context/nunjucks/use-nunjucks.ts @@ -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, '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, '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>(); + const renderContextPromiseCacheTimer = useRef>(); 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 (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 (obj: T) => { + let getOrCreateRenderContext: ReturnType; + 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,