Cache Revalidation


Cache revalidation ensures that your cached content stays synchronized with changes made in the Drupal backend. Vactory provides automatic cache invalidation for various content types and allows for custom revalidation rules.

Translation Cache Revalidation

Translation Caching Process

packages/core/src/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.

Automatic Translation Cache Invalidation

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

packages/core/src/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.

Cache Key Components:

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

Automatic Menu Cache Invalidation

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

Similar to the translation process, Drupal also uses 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 Cache Revalidation

Router Caching Process

./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.

Cache Key Components:

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

Node Cache Revalidation

Node Caching Process

./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.

Cache Key Components:

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

Automatic Node Cache Invalidation

What occurs when we add, update or delete the node?

Similar to the translation and menus process, Drupal also uses 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 Use Cases

Front Cache Revalidation Module

In scenarios where you 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 Steps

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

  2. Click Configure entity

  3. Configure the plugin

Revalidation Configuration

  1. 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 Custom Invalidation Rules

Edit the cache clear API endpoint to add custom invalidation logic:

./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" })
}

Summary

The Vactory cache revalidation system provides:

  • Automatic Invalidation: Content changes in Drupal automatically trigger cache clearing
  • Granular Control: Different cache keys for different content types and user contexts
  • Custom Rules: Advanced revalidation module for complex invalidation scenarios
  • Performance Optimization: Strategic cache expiration times for different content types

This ensures your users always see the most current content while maintaining optimal performance through intelligent caching strategies.