diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5aa3ca521..dc5d76d15 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,3 +1,5 @@ +import type { AbstractFetchStrategy, EndpointParams, FetchResponse } from './data'; + export type CustomPostType = { slug: string; endpoint: string; @@ -89,4 +91,55 @@ export type HeadlessConfig = { redirects?: boolean; devMode?: boolean; }; + cache?: { + /** + * TTL in milliseconds + */ + ttl?: + | number + | (( + fetcbStrategy: AbstractFetchStrategy, + ) => number); + + /** + * Whether it should cache this request + */ + enabled: + | boolean + | (( + fetcbStrategy: AbstractFetchStrategy, + ) => boolean); + + /** + * If set, this function will be executed before calling the cache.set method + * It's useful if you want to remove things from the data before caching. + * + * @param fetcbStrategy The fetch strategy instance + * + * @returns + */ + beforeSet?: ( + fetcbStrategy: AbstractFetchStrategy, + data: FetchResponse, + ) => Promise>; + + /** + * If set, this function will be executed after restoring data from cache (cache.get) and can be used + * to reconstruct things that were removed in beforeSet. + * + * @param fetcbStrategy The fetch strategy instnace + * @returns + */ + afterGet?: ( + fetcbStrategy: AbstractFetchStrategy, + data: FetchResponse, + ) => Promise>; + + /** + * The path to a custom cache handler. + * + * If set will override the strategy + */ + cacheHandler?: string; + }; }; diff --git a/packages/core/src/utils/config.ts b/packages/core/src/utils/config.ts index 39bdaab81..ef61e150e 100644 --- a/packages/core/src/utils/config.ts +++ b/packages/core/src/utils/config.ts @@ -31,6 +31,7 @@ export function getHeadstartWPConfig() { integrations, debug, preview, + cache, } = __10up__HEADLESS_CONFIG; const defaultTaxonomies: CustomTaxonomies = [ @@ -81,6 +82,7 @@ export function getHeadstartWPConfig() { integrations, debug, preview, + cache, sites: (sites || []).map((site) => { // if host is not defined but hostUrl is, infer host from hostUrl if (typeof site.host === 'undefined' && typeof site.hostUrl !== 'undefined') { @@ -111,7 +113,7 @@ export const getHeadlessConfig = getHeadstartWPConfig; * @returns */ export function getSite(site?: HeadlessConfig) { - const settings = getHeadlessConfig(); + const settings = getHeadstartWPConfig(); const headlessConfig: HeadlessConfig = { sourceUrl: site?.sourceUrl || settings.sourceUrl, hostUrl: site?.hostUrl, @@ -122,6 +124,7 @@ export function getSite(site?: HeadlessConfig) { useWordPressPlugin: site?.useWordPressPlugin || settings.useWordPressPlugin || false, integrations: site?.integrations || settings.integrations, preview: site?.preview || settings.preview, + cache: site?.cache || settings.cache, }; return headlessConfig; @@ -136,7 +139,7 @@ export function getSite(site?: HeadlessConfig) { * @returns */ export function getSiteByHost(hostOrUrl: string, locale?: string) { - const settings = getHeadlessConfig(); + const settings = getHeadstartWPConfig(); let normalizedHost = hostOrUrl; if (normalizedHost.startsWith('https://') || normalizedHost.startsWith('http://')) { @@ -174,7 +177,7 @@ export function getSiteByHost(hostOrUrl: string, locale?: string) { * @returns HeadlessConfig */ export function getSiteBySourceUrl(sourceUrl: string) { - const settings = getHeadlessConfig(); + const settings = getHeadstartWPConfig(); const site = settings.sites && settings.sites.find((site) => site.sourceUrl === sourceUrl); return getSite(site); @@ -186,7 +189,7 @@ export function getSiteBySourceUrl(sourceUrl: string) { * @param sourceUrl */ export function getCustomTaxonomies(sourceUrl?: string) { - const { customTaxonomies } = sourceUrl ? getSiteBySourceUrl(sourceUrl) : getHeadlessConfig(); + const { customTaxonomies } = sourceUrl ? getSiteBySourceUrl(sourceUrl) : getHeadstartWPConfig(); // at this point this is always an array return customTaxonomies as CustomTaxonomies; @@ -226,7 +229,7 @@ export function getCustomTaxonomy(slug: string, sourceUrl?: string) { * @param sourceUrl */ export function getCustomPostTypes(sourceUrl?: string) { - const { customPostTypes } = sourceUrl ? getSiteBySourceUrl(sourceUrl) : getHeadlessConfig(); + const { customPostTypes } = sourceUrl ? getSiteBySourceUrl(sourceUrl) : getHeadstartWPConfig(); return customPostTypes as CustomPostTypes; } diff --git a/packages/next/src/data/server/cache.ts b/packages/next/src/data/server/cache.ts index 69b2ebaca..0d5a731e9 100644 --- a/packages/next/src/data/server/cache.ts +++ b/packages/next/src/data/server/cache.ts @@ -1,6 +1,5 @@ import TTLCache from '@isaacs/ttlcache'; -const kTTLCache = Symbol.for('ttlcache'); -const g = globalThis as typeof globalThis & { [kTTLCache]?: TTLCache }; -g[kTTLCache] ??= new TTLCache({ max: 10000 }); -export const cache = g[kTTLCache]; +export const cache = new TTLCache({ max: 10000 }); + +export default cache; diff --git a/packages/next/src/data/server/fetchHookData.ts b/packages/next/src/data/server/fetchHookData.ts index f0aec9ab2..0421eb275 100644 --- a/packages/next/src/data/server/fetchHookData.ts +++ b/packages/next/src/data/server/fetchHookData.ts @@ -2,18 +2,20 @@ import { AbstractFetchStrategy, EndpointParams, FetchOptions, + FetchResponse, FilterDataOptions, + HeadlessConfig, LOGTYPE, PostParams, log, } from '@headstartwp/core'; import { GetServerSidePropsContext, GetStaticPropsContext } from 'next'; import { serializeKey } from '@headstartwp/core/react'; -import deepmerge from 'deepmerge'; +import { all as merge } from 'deepmerge'; import { PreviewData } from '../../handlers/types'; import { convertToPath } from '../convertToPath'; import { getSiteFromContext } from './getSiteFromContext'; -import { cache } from './cache'; +import cacheHandler from './cache'; /** * The supported options for {@link fetchHookData} @@ -37,22 +39,7 @@ export interface FetchHookDataOptions

{ /** * Controls server-side caching */ - cache?: { - /** - * TTL in milliseconds - */ - ttl?: number; - - /** - * Whether it should cache this request - */ - enabled: boolean; - - /** - * The cache strategy - */ - strategy?: 'lru'; - }; + cache?: HeadlessConfig['cache']; } function isPreviewRequest

(params: P, urlParams: P): params is P & PostParams { @@ -84,10 +71,29 @@ export function prepareFetchHookData | GetStaticPropsContext, options: FetchHookDataOptions = {}, ) { + const { sourceUrl, integrations, cache: globalCacheConfig } = getSiteFromContext(ctx); + + const cacheConfig = merge([ + { + enabled: false, + ttl: 5 * 60 * 100, + }, + options.cache ?? {}, + globalCacheConfig ?? {}, + ]); + + const isCacheEnabled = + typeof cacheConfig?.enabled === 'boolean' + ? cacheConfig.enabled + : cacheConfig?.enabled(fetchStrategy); + // should never cache when cache is not enabled, or when is previewing or when burstCache is set to true - const shouldCache = (options?.cache?.enabled ?? false) && !ctx.preview; + const shouldCache = isCacheEnabled && !ctx.preview; + const ttl = typeof cacheConfig?.ttl !== 'undefined' ? cacheConfig.ttl : 5 * 60 * 1000; + const cacheTTL = typeof ttl === 'number' ? ttl : ttl(fetchStrategy); + const cacheHandler = + typeof cacheConfig?.cacheHandler === 'undefined' ? './cache' : cacheConfig.cacheHandler; - const { sourceUrl, integrations } = getSiteFromContext(ctx); const params: Partial

= options?.params || {}; fetchStrategy.setBaseURL(sourceUrl); @@ -106,14 +112,20 @@ export function prepareFetchHookData; + const finalParams = merge([defaultParams, urlParams, params]) as Partial

; return { cacheKey: fetchStrategy.getCacheKey(finalParams), params: finalParams, urlParams, path: stringPath, - shouldCache, + cache: { + enabled: shouldCache, + ttl: cacheTTL, + cacheHandler, + beforeSet: cacheConfig?.beforeSet, + afterGet: cacheConfig?.afterGet, + }, }; } @@ -156,7 +168,7 @@ export async function fetchHookData | null = null; const cacheKey = serializeKey(key); - if (shouldCache) { - data = await cache.get(cacheKey); + if (cache.enabled) { + data = (await cacheHandler.get(cacheKey)) as FetchResponse; - if (debug?.devMode) { - if (data) { + if (data) { + if (debug?.devMode) { log(LOGTYPE.INFO, `[fetchHookData] cache hit for ${cacheKey}`); - } else { - log(LOGTYPE.INFO, `[fetchHookData] cache miss for ${cacheKey}`, ctx); } + + if (typeof cache.afterGet === 'function') { + data = await cache.afterGet(fetchStrategy, data); + } + } else if (debug?.devMode) { + log(LOGTYPE.INFO, `[fetchHookData] cache miss for ${cacheKey}`); } } @@ -215,13 +231,17 @@ export async function fetchHookData { + // cache app endpoints in-memory by default + return fetchStrategy.getEndpoint() === '/wp-json/headless-wp/v1/app'; + }, + }, }; diff --git a/projects/wp-nextjs/src/pages/index.js b/projects/wp-nextjs/src/pages/index.js index da8d4d1d7..40c4e1966 100644 --- a/projects/wp-nextjs/src/pages/index.js +++ b/projects/wp-nextjs/src/pages/index.js @@ -80,11 +80,7 @@ export async function getStaticProps(context) { let appSettings; let slug; try { - appSettings = await fetchHookData(useAppSettings.fetcher(), context, { - cache: { - enabled: true, - }, - }); + appSettings = await fetchHookData(useAppSettings.fetcher(), context); /** * The static front-page can be set in the WP admin. The default one will be 'front-page'