Cache Revalidation


Translations

./lib/get-i18n.js

const data = await drupal.getTranslations({
withCache: true,
cacheKey: "i18n config_translation_info_plugins locale",
})

When the Next.js app initially retrieves translations from Drupal backend_url/_translations, it stores them in the Redis cache using the key specified as i18n config_translation_info_plugins locale with no cache expiration time.

By following the Internationalization section, you have the flexibility to add an unlimited number of keywords that require translation.

What actually occurs when we translate a new keyword (in Drupal side), considering that Next.js app stores translations without an expiration time?

Next.js provides a convenient solution for clearing the Redis cache by offering an API endpoint at `/api/cache/clear`. This endpoint can be utilized to trigger the removal of cached data stored in Redis, allowing for a fresh start if necessary.

This API endpoint, /api/cache/clear, is precisely what Drupal utilizes to invalidate the translation cache whenever new keywords are added through the Back-Office.

./profiles/contrib/vactory_starter_kit/modules/vactory_decoupled/vactory_decoupled.module

/**
* Implements hook_form_alter().
*/
function vactory_decoupled_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ($form_id === 'locale_translate_edit_form') {
$form['#submit'][] = 'clear_next_cache_translation';
}
...
}
/**
* Clear frontend cache.
*/
function clear_next_cache_translation(&$form, FormStateInterface $form_state) {
$query = [
'invalidate' => 'translation',
];
clear_next_cache($query);
}

./lib/get-menus.js

const fetchMenu = async ({ name, locale, auth }) => {
const uid = auth?.auth?.user?.id || 0
const cacheKey = `config:system.menu.${name} user:${uid} language:${locale}`
let options = {
withCache: true,
cacheKey: cacheKey,
}
if (auth?.auth?.user?.id > 0) {
options["withAuth"] = () => `Bearer ${auth?.auth.accessToken}`
options["headers"] = {
"X-Auth-Provider": auth?.auth.provider,
}
}
const data = await drupal.getMenu(name, locale, options)
return data
}

When the Next.js app initially retrieves menus from Drupal backend_url/_menus?menu_name=main, it stores them in the Redis cache using the key specified as config:system.menu.${name} user:${uid} language:${locale} with no cache expiration time.

  • name: Menu name
  • locale: Current language
  • uid: User id

But, what occurs when we add, update or delete items from/to one of our menus?

Similar to the translation process, Drupal also use the endpoint /api/cache/clear to invalidate the front cache.

./profiles/contrib/vactory_starter_kit/modules/vactory_decoupled/vactory_decoupled.module

/**
* Implements hook_entity_update().
*/
function vactory_decoupled_entity_update(EntityInterface $entity) {
static $loaded;
if ($entity instanceof Node && !$entity->isNew()) {
$query = get_node_info($entity);
clear_next_cache($query);
}
if ($entity instanceof MenuItemExtrasMenuLinkContent && !isset($loaded)) {
$loaded = 1;
clear_next_cache([
'menu' => $entity->bundle(),
'invalidate' => 'menu',
]);
}
else {
if ($entity instanceof Menu) {
clear_next_cache([
'menu' => $entity->id(),
'invalidate' => 'menu',
]);
}
}
}
/**
* Implements hook_entity_delete().
*/
function vactory_decoupled_entity_delete(EntityInterface $entity) {
if ($entity instanceof MenuItemExtrasMenuLinkContent) {
$query = [
'menu' => $entity->bundle(),
'invalidate' => 'menu',
];
clear_next_cache($query);
}
}

Router

./pages/index.jsx

const cacheKey = `router:${joinedSlug} language:${locale} ${uid}`
const cached = await redis.get(cacheKey)
if (cached) {
router = JSON.parse(cached)
} else {
routerResponse = await drupal.getRoute(joinedSlug, locale, routerOptions)
router = await routerResponse.json()
await redis.set(
cacheKey,
JSON.stringify(router),
"EX",
process.env.REDIS_ROUTER_EXPIRE
)
}

When a user requests a page, Next.js sends a request to Drupal to get the necessary route information for the requested page. Once the information is received, the Next.js app stores it in the Redis cache. The cache entry is identified using a key that follows the format router:${joinedSlug} language:${locale} ${uid}. Additionally, the cache entry is configured with an expiration time based on the value set in the environment variable REDIS_ROUTER_EXPIRE.

  • joinedSlug: Current route slug url
  • locale: Current language
  • uid: User id

Node

./pages/index.jsx

const cacheKey = `node:${router.entity.id} bundle:${router.entity.bundle} language:${locale} user:${uid} slug:${joinedSlugKey}`
const cached = await redis.get(cacheKey)
if (cached) {
node = JSON.parse(cached)
} else {
node = await drupal.getNode(router, nodeParams, locale, joinedSlug, nodeOptions)
if (!node.internal_extra.cache_exclude) {
await redis.set(
cacheKey,
JSON.stringify(node),
"EX",
process.env.REDIS_NODE_EXPIRE
)
}
}

After storing the route information, Next.js needs to send an additional request to Drupal in order to retrieve the necessary data related to the specific node being accessed. Once the information is received, the Next.js app stores it in the Redis cache. The cache entry is identified using a key that follows the format node:${router.entity.id} bundle:${router.entity.bundle} language:${locale} user:${uid} slug:${joinedSlugKey}. Additionally, the cache entry is configured with an expiration time based on the value set in the environment variable REDIS_NODE_EXPIRE.

  • router.entity.id: Node id
  • router.entity.bundle: Node bundle
  • locale: Current langague
  • uid: User id
  • joinedSlugKey: Current route slug url

But, what occurs when we add, update or delete the node?

Similar to the translation, Menus process, Drupal also use the endpoint /api/cache/clear to invalidate the front cache.

./profiles/contrib/vactory_starter_kit/modules/vactory_decoupled/vactory_decoupled.module

/**
* Implements hook_entity_update().
*/
function vactory_decoupled_entity_update(EntityInterface $entity) {
static $loaded;
if ($entity instanceof Node && !$entity->isNew()) {
$query = get_node_info($entity);
clear_next_cache($query);
}
}
/**
* Implements hook_entity_predelete().
*/
function vactory_decoupled_entity_predelete(EntityInterface $entity) {
if ($entity instanceof Node) {
$query = get_node_info($entity);
clear_next_cache($query);
}
}

Advanced uses cases

Front Cache Revalidation Module

In the scenario where we need to invalidate the cache of specific pages based on changes in another entity, what options are available to address this situation?

To handle such situations, the Vactory Starter Kit offers a custom module called vactory_decoupled_revalidator. This module provides a solution by allowing the creation of rules that enable on-demand revalidation. With the vactory_decoupled_revalidator module, you can define a set of rules that specify the conditions for cache invalidation, thereby providing a flexible mechanism to trigger revalidation when necessary.

Configuration

  1. Visit /admin/structure/revalidator-entity-type

  2. Click Configure entity

  3. Configure the plugin

And save changes.

You have the freedom to add as many plugins as you need for your specific requirements. To do so, you can refer to the README.md file of the module, which provides instructions and guidelines on how to add plugins effectively.

Configure new invalidation redis cache rule

Edit : ./pages/api/cache/clear.js

./pages/api/cache/clear.js

import { redis, isHttpMethod } from "@vactorynext/core/server"
export default async function handler(req, res) {
const secret = req.headers["x-cache-secret"] || ""
isHttpMethod(req, res, ["GET"])
if (process.env.CACHE_SECRET === undefined) {
res.status(500).json({ status: "CACHE_SECRET environment variable not specified!" })
return
}
if (process.env.CACHE_SECRET !== secret) {
res.status(500).json({ status: "secret key doesn't match" })
return
}
if (req?.query?.invalidate) {
console.log(
`[Cache]: received command to clear ${req.query.invalidate} cache`,
req.query
)
const pipeline = redis.pipeline()
if (req.query.invalidate === "node" && req?.query?.id) {
...
}
...
if (req.query.invalidate === "custom_key") {
// Your custom code here
}
} else {
console.log(`[Cache]: received command to clear all caches`)
await redis.flushall()
}
res.status(200).json({ status: "Cache cleared" })
}