Skip to content

Commit

Permalink
feat: introducing global cache config
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasio committed Apr 1, 2024
1 parent ac0bcc0 commit d94cd83
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 47 deletions.
53 changes: 53 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AbstractFetchStrategy, EndpointParams, FetchResponse } from './data';

export type CustomPostType = {
slug: string;
endpoint: string;
Expand Down Expand Up @@ -89,4 +91,55 @@ export type HeadlessConfig = {
redirects?: boolean;
devMode?: boolean;
};
cache?: {
/**
* TTL in milliseconds
*/
ttl?:
| number
| (<E, P extends EndpointParams, R>(
fetcbStrategy: AbstractFetchStrategy<E, P, R>,
) => number);

/**
* Whether it should cache this request
*/
enabled:
| boolean
| (<E, P extends EndpointParams, R>(
fetcbStrategy: AbstractFetchStrategy<E, P, R>,
) => 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?: <E, P extends EndpointParams, R>(
fetcbStrategy: AbstractFetchStrategy<E, P, R>,
data: FetchResponse<R>,
) => Promise<FetchResponse<R>>;

/**
* 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?: <E, P extends EndpointParams, R>(
fetcbStrategy: AbstractFetchStrategy<E, P, R>,
data: FetchResponse<R>,
) => Promise<FetchResponse<R>>;

/**
* The path to a custom cache handler.
*
* If set will override the strategy
*/
cacheHandler?: string;
};
};
13 changes: 8 additions & 5 deletions packages/core/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function getHeadstartWPConfig() {
integrations,
debug,
preview,
cache,
} = __10up__HEADLESS_CONFIG;

const defaultTaxonomies: CustomTaxonomies = [
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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://')) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
7 changes: 3 additions & 4 deletions packages/next/src/data/server/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import TTLCache from '@isaacs/ttlcache';

const kTTLCache = Symbol.for('ttlcache');
const g = globalThis as typeof globalThis & { [kTTLCache]?: TTLCache<string, any> };
g[kTTLCache] ??= new TTLCache({ max: 10000 });
export const cache = g[kTTLCache];
export const cache = new TTLCache({ max: 10000 });

export default cache;
86 changes: 53 additions & 33 deletions packages/next/src/data/server/fetchHookData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -37,22 +39,7 @@ export interface FetchHookDataOptions<P = unknown, T = unknown> {
/**
* 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<P>(params: P, urlParams: P): params is P & PostParams {
Expand Down Expand Up @@ -84,10 +71,29 @@ export function prepareFetchHookData<T = unknown, P extends EndpointParams = End
ctx: GetServerSidePropsContext<any, PreviewData> | GetStaticPropsContext<any, PreviewData>,
options: FetchHookDataOptions<P, T> = {},
) {
const { sourceUrl, integrations, cache: globalCacheConfig } = getSiteFromContext(ctx);

const cacheConfig = merge<HeadlessConfig['cache']>([
{
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<P> = options?.params || {};

fetchStrategy.setBaseURL(sourceUrl);
Expand All @@ -106,14 +112,20 @@ export function prepareFetchHookData<T = unknown, P extends EndpointParams = End
const defaultParams = fetchStrategy.getDefaultParams();
const urlParams = fetchStrategy.getParamsFromURL(stringPath, params);

const finalParams = deepmerge.all([defaultParams, urlParams, params]) as Partial<P>;
const finalParams = merge([defaultParams, urlParams, params]) as Partial<P>;

return {
cacheKey: fetchStrategy.getCacheKey(finalParams),
params: finalParams,
urlParams,
path: stringPath,
shouldCache,
cache: {
enabled: shouldCache,
ttl: cacheTTL,
cacheHandler,
beforeSet: cacheConfig?.beforeSet,
afterGet: cacheConfig?.afterGet,
},
};
}

Expand Down Expand Up @@ -156,7 +168,7 @@ export async function fetchHookData<T = unknown, P extends EndpointParams = Endp
params,
urlParams,
path,
shouldCache,
cache,
} = prepareFetchHookData(fetchStrategy, ctx, options);

const { debug, preview } = getSiteFromContext(ctx);
Expand Down Expand Up @@ -192,18 +204,22 @@ export async function fetchHookData<T = unknown, P extends EndpointParams = Endp
}
}

let data;
let data: FetchResponse<R> | null = null;
const cacheKey = serializeKey(key);

if (shouldCache) {
data = await cache.get(cacheKey);
if (cache.enabled) {
data = (await cacheHandler.get(cacheKey)) as FetchResponse<R>;

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}`);
}
}

Expand All @@ -215,13 +231,17 @@ export async function fetchHookData<T = unknown, P extends EndpointParams = Endp
...options.fetchStrategyOptions,
});

if (shouldCache) {
if (cache.enabled) {
if (debug?.devMode) {
log(LOGTYPE.INFO, `[fetchHookData] cache store for ${cacheKey}`);
}

await cache.set(cacheKey, data, {
ttl: 5 * 60,
if (typeof cache.beforeSet === 'function') {
data = await cache.beforeSet(fetchStrategy, data);
}

await cacheHandler.set(cacheKey, data, {
ttl: cache.ttl,
});
}
}
Expand Down
7 changes: 7 additions & 0 deletions projects/wp-nextjs/headstartwp.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,11 @@ module.exports = {
*/
usePostLinkForRedirect: true,
},

cache: {
enabled: (fetchStrategy) => {
// cache app endpoints in-memory by default
return fetchStrategy.getEndpoint() === '/wp-json/headless-wp/v1/app';
},
},
};
6 changes: 1 addition & 5 deletions projects/wp-nextjs/src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit d94cd83

Please sign in to comment.