diff --git a/CHANGELOG.md b/CHANGELOG.md index a94744803..be52c9af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ## Added +- 🚀(frontend) Adds api caching #961 - ✨(back) allow theme customnization using a configuration file #948 - ✨ Add a custom callout block to the editor #892 - 🚩(frontend) version MIT only #911 diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 57269b163..8dcb62530 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -58,7 +58,14 @@ class Base(Configuration): * DB_USER """ - DEBUG = False + DEBUG_RAW = values.Value( + default=False, + environ_name="DEBUG", + environ_prefix=None, + ) + DEBUG_NAMESPACES = DEBUG_RAW.split(",") if isinstance(DEBUG_RAW, str) else [] + DEBUG = bool(DEBUG_RAW) + USE_SWAGGER = False API_VERSION = "v1.0" @@ -761,6 +768,9 @@ def post_setup(cls): "OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. " ) + if cls.DEBUG and "no-cache" in cls.DEBUG_NAMESPACES: + cls.THEME_CUSTOMIZATION_CACHE_TIMEOUT = 0 + class Build(Base): """Settings used when the application is built. @@ -795,6 +805,17 @@ class Development(Base): CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"] DEBUG = True + # Enable debug namespaces as needed + # + # DEBUG_NAMESPACES = [ + # 'no-cache', + # 'features:language' + # ] + # + # They can be enabled in all environments + # via DEBUG environment variable + # DEBUG="features:language,no-cache,..." + SESSION_COOKIE_NAME = "impress_sessionid" USE_SWAGGER = True diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index d9ecfdece..b7713c200 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -31,7 +31,9 @@ "@openfun/cunningham-react": "3.0.0", "@react-pdf/renderer": "4.3.0", "@sentry/nextjs": "9.15.0", - "@tanstack/react-query": "5.75.4", + "@tanstack/query-sync-storage-persister": "5.76.0", + "@tanstack/react-query": "5.75.5", + "@tanstack/react-query-persist-client": "5.76.0", "canvg": "4.0.3", "clsx": "2.1.1", "cmdk": "1.1.1", diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx index 03ce5097d..20eb0d94a 100644 --- a/src/frontend/apps/impress/src/core/AppProvider.tsx +++ b/src/frontend/apps/impress/src/core/AppProvider.tsx @@ -1,7 +1,11 @@ import { CunninghamProvider } from '@openfun/cunningham-react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'; +import { QueryClient } from '@tanstack/react-query'; +import type { Persister } from '@tanstack/react-query-persist-client'; +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; +import debug from 'debug'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useCunninghamTheme } from '@/cunningham'; import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth'; @@ -13,12 +17,16 @@ import { ConfigProvider } from './config/'; * QueryClient: * - defaultOptions: * - staleTime: - * - global cache duration - we decided 3 minutes - * - It can be overridden to each query + * - global time until cache is considered stale and will be refetched in the background + * - instant if debug flag "no-cache" active - 3 minutes otherwise + * - gcTime: + * - global time until cache is purged from the persister and needs to be renewed + * - since its cached in localStorage, we can set it to a long time (48h) */ const defaultOptions = { queries: { - staleTime: 1000 * 60 * 3, + staleTime: debug.enabled('no-cache') ? 0 : 1000 * 60 * 3, // 3 minutes + gcTime: debug.enabled('no-cache') ? 0 : 1000 * 60 * 60 * 48, // 48 hours retry: 1, }, }; @@ -29,11 +37,21 @@ const queryClient = new QueryClient({ export function AppProvider({ children }: { children: React.ReactNode }) { const { theme } = useCunninghamTheme(); const { replace } = useRouter(); - const initializeResizeListener = useResponsiveStore( (state) => state.initializeResizeListener, ); + const persister = useMemo(() => { + // Create persister only when the browser is available + if (typeof window !== 'undefined') { + return createSyncStoragePersister({ + storage: window.localStorage, + }); + } + // Return undefined otherwise (PersistQueryClientProvider handles undefined persister gracefully) + return undefined; + }, []); + useEffect(() => { return initializeResizeListener(); }, [initializeResizeListener]); @@ -60,12 +78,15 @@ export function AppProvider({ children }: { children: React.ReactNode }) { }, [replace]); return ( - + {children} - + ); } diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index fdfe8c97e..f1e594b2b 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import debug from 'debug'; import { APIError, errorCauses, fetchAPI } from '@/api'; import { Theme } from '@/cunningham/'; @@ -19,45 +20,24 @@ interface ConfigResponse { SENTRY_DSN?: string; } -const LOCAL_STORAGE_KEY = 'docs_config'; - -function getCachedTranslation() { - try { - const jsonString = localStorage.getItem(LOCAL_STORAGE_KEY); - return jsonString ? (JSON.parse(jsonString) as ConfigResponse) : undefined; - } catch { - return undefined; - } -} - -function setCachedTranslation(translations: ConfigResponse) { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(translations)); -} - export const getConfig = async (): Promise => { const response = await fetchAPI(`config/`); if (!response.ok) { - throw new APIError('Failed to get the doc', await errorCauses(response)); + throw new APIError('Failed to get the config', await errorCauses(response)); } const config = response.json() as Promise; - setCachedTranslation(await config); return config; }; -export const KEY_CONFIG = 'config'; +export const QKEY_CONFIG = 'config'; export function useConfig() { - const cachedData = getCachedTranslation(); - const oneHour = 1000 * 60 * 60; - return useQuery({ - queryKey: [KEY_CONFIG], + queryKey: [QKEY_CONFIG], queryFn: () => getConfig(), - initialData: cachedData, - staleTime: oneHour, - initialDataUpdatedAt: Date.now() - oneHour, // Force initial data to be considered stale + staleTime: debug.enabled('no-cache') ? 0 : 1000 * 60 * 60, // 1 hour }); } diff --git a/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx b/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx index 026beec9f..eefb436b3 100644 --- a/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx +++ b/src/frontend/apps/impress/src/features/auth/api/useAuthQuery.tsx @@ -1,4 +1,5 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query'; +import debug from 'debug'; import { APIError, errorCauses, fetchAPI } from '@/api'; @@ -33,7 +34,7 @@ export function useAuthQuery( return useQuery({ queryKey: [KEY_AUTH], queryFn: getMe, - staleTime: 1000 * 60 * 15, // 15 minutes + staleTime: debug.enabled('no-cache') ? 0 : 1000 * 60 * 15, // 15 minutes ...queryConfig, }); } diff --git a/src/frontend/apps/impress/src/pages/_app.tsx b/src/frontend/apps/impress/src/pages/_app.tsx index 683341c28..37b98bf97 100644 --- a/src/frontend/apps/impress/src/pages/_app.tsx +++ b/src/frontend/apps/impress/src/pages/_app.tsx @@ -18,6 +18,28 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page); const { t } = useTranslation(); + if (process.env.NODE_ENV === 'development') { + /** + * Enable debug namespaces as needed + * + * They can be enabled: + * + * via DEBUG environment variable + * DEBUG="features:language,no-cache,..." + * + * via browser console + * window.debug = "features:language,no-cache,..."; + * + * via Local storage + * window.localStorage.debug = "features:language,no-cache,..."; + * + * via code (uses Local storage) + * import debug from 'debug'; + * debug.enable('no-cache,features:language,...'); + */ + //debug.enable('no-cache,features:language'); + } + return ( <> diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 2cb36297b..1b98bf3b0 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -4881,16 +4881,36 @@ dependencies: "@typescript-eslint/utils" "^8.18.1" -"@tanstack/query-core@5.75.4": - version "5.75.4" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.75.4.tgz#e05f2fe4145fb5354271ad19e63eec61f6ce3012" - integrity sha512-pcqOUgWG9oGlzkfRQQMMsEFmtQu0wq81A414CtELZGq+ztVwSTAaoB3AZRAXQJs88LmNMk2YpUKuQbrvzNDyRg== +"@tanstack/query-core@5.75.5": + version "5.75.5" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.75.5.tgz#198c27c21b5d15aad09167af8a021f214e194d4b" + integrity sha512-kPDOxtoMn2Ycycb76Givx2fi+2pzo98F9ifHL/NFiahEDpDwSVW6o12PRuQ0lQnBOunhRG5etatAhQij91M3MQ== + +"@tanstack/query-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.76.0.tgz#3b4d5d34ce307ba0cf7d1a3e90d7adcdc6c46be0" + integrity sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg== "@tanstack/query-devtools@5.74.7": version "5.74.7" resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz#c9b022b386ac86e6395228b5d6912e6444b3b971" integrity sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw== +"@tanstack/query-persist-client-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.76.0.tgz#a3bcdd687384dc6b5b61b402bef153ad54515321" + integrity sha512-xcTZjILf4q49Nsl6wcnhBYZ4O0gpnuNwV6vPIEWIrwTuSNWz2zd/g9bc8SxnXy7xCV8SM1H0IJn8KjLQIUb2ag== + dependencies: + "@tanstack/query-core" "5.76.0" + +"@tanstack/query-sync-storage-persister@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.76.0.tgz#29643062f1a424873afb22032ce70ee72436bb9b" + integrity sha512-N8d8voY61XkM+jfXTySduLrevD6wRM3pwQ1kG0syLiWWx/sX2+CpaTMSPr0GggjQuhmjhUPo83LaV+e449tizA== + dependencies: + "@tanstack/query-core" "5.76.0" + "@tanstack/query-persist-client-core" "5.76.0" + "@tanstack/react-query-devtools@5.75.4": version "5.75.4" resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.75.4.tgz#89614363d63c997ade81ade4a18e90b57512d4d8" @@ -4898,12 +4918,19 @@ dependencies: "@tanstack/query-devtools" "5.74.7" -"@tanstack/react-query@5.75.4": - version "5.75.4" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.75.4.tgz#721e1cdf7debb110671f558dc2b6276f637554a5" - integrity sha512-Vf65pzYRkf8fk9SP1ncIZjvaXszBhtsvpf+h45Y/9kOywOrVZfBGUpCdffdsVzbmBzmz6TCFes9bM0d3pRrIsA== +"@tanstack/react-query-persist-client@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.76.0.tgz#97718fec844708cb98a5902d4b1eeb72adea555b" + integrity sha512-QPKgkHX1yC1Ec21FTQHBTbQcHYI+6157DgsmxABp94H7/ZUJ3szZ7wdpdBPQyZ9VxBXlKRN+aNZkOPC90+r/uA== + dependencies: + "@tanstack/query-persist-client-core" "5.76.0" + +"@tanstack/react-query@5.75.5": + version "5.75.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.75.5.tgz#650a6b58c7bcd4c33062426e05c4467571148a4f" + integrity sha512-QrLCJe40BgBVlWdAdf2ZEVJ0cISOuEy/HKupId1aTKU6gPJZVhSvZpH+Si7csRflCJphzlQ77Yx6gUxGW9o0XQ== dependencies: - "@tanstack/query-core" "5.75.4" + "@tanstack/query-core" "5.75.5" "@tanstack/react-table@8.20.6": version "8.20.6"