Redis pub/sub
Un Dynamic Snapshot sirve todo el contenido desde un snapshot en memoria del servidor y lo refresca en caliente cuando publicas, sin rebuild ni una llamada al API por lectura. El patrón tiene dos piezas: de dónde se lee el snapshot (snapshotLoader) y quién dispara el refresco (refreshSnapshot()).
En esta versión el JSON vive en Redis. Un publisher exporta el snapshot con exportSnapshot(), hace SET + PUBLISH, y todas las instancias suscritas se refrescan a la vez con un único mensaje.
Cómo funciona
- Origen: Redis. El
snapshotLoaderlee la key del snapshot. - Disparo con pub/sub: en lugar de un
POSTpor instancia, un únicoPUBLISHnotifica a todas las réplicas suscritas y cada una llama arefreshSnapshot().
Cuándo elegirla
- Pensada para escalado horizontal: refresca N instancias con un solo mensaje, sin webhooks por instancia ni N llamadas al API.
- Requiere un Redis accesible por todas las réplicas.
Paso a paso
Parte del proyecto de la versión por endpoint. Cambiamos el snapshotLoader por Redis, añadimos la suscripción y un publisher.
-
Levanta Redis e instala dependencias:
Ventana de terminal docker run -d --name redis -p 6379:6379 redis:7npm i redis.env REDIS_URL=redis://localhost:6379 -
Crea la conexión a Redis. Registramos un handler de
errorpara que una caída de conexión no tumbe el proceso:src/lib/redis.ts import { createClient as createRedis } from 'redis';export const redis = createRedis({ url: process.env.REDIS_URL });// node-redis lanza en eventos 'error' no manejados; los logueamos para que// una conexión caída no tumbe el proceso.redis.on('error', err => console.error('Redis error', err));export const redisReady = redis.connect(); -
Cambia el
snapshotLoaderpara leer de Redis y suscríbete al canal. Al primer uso, cada instancia escucha y se refresca sola cuando el publisher avisa. Si el snapshot aún no existe en Redis, no cacheamos el fallo, para que el siguiente request lo reintente:src/lib/content-island.ts import { createClient } from '@content-island/api-client';import { redis, redisReady } from './redis';const accessToken = process.env.CONTENT_ISLAND_TOKEN!;const SNAPSHOT_KEY = 'content-island:snapshot';const CHANNEL = 'content-island:updated';// Express corre como un único proceso de larga vida: un singleton a nivel de// módulo basta para compartir un cliente (y un snapshot) entre peticiones.export const contentIslandClient = createClient({accessToken,mode: 'snapshot',// El loader lee el snapshot de Redis.snapshotLoader: async () => {await redisReady;return (await redis.get(SNAPSHOT_KEY)) ?? '';},});// Al primer uso, esta instancia se suscribe al canal y se refresca sola cada// vez que el publisher avisa. También hace la carga inicial del snapshot.let primed: Promise<unknown> | null = null;export function ensureSnapshot() {if (!primed) {primed = (async () => {await redisReady;// Una conexión de Redis no puede suscribirse y ejecutar comandos a la// vez, así que el suscriptor usa su propia conexión duplicada.const sub = redis.duplicate();sub.on('error', err => console.error('Redis subscriber error', err));await sub.connect();await sub.subscribe(CHANNEL, () => {contentIslandClient.refreshSnapshot().catch(console.error);});await contentIslandClient.refreshSnapshot(); // carga inicial})().catch(err => {// No cacheamos el fallo: si el snapshot aún no existe en Redis, el// siguiente request lo reintenta en vez de reusar una promesa rechazada.primed = null;throw err;});}return primed;} -
Crea el publisher: exporta, hace
SETyPUBLISH. Fuera del servidor carga el.envcondotenv:scripts/publish-redis.mts import 'dotenv/config';import { exportSnapshot } from '@content-island/api-client';import { createClient as createRedis } from 'redis';const redis = createRedis({ url: process.env.REDIS_URL });await redis.connect();const snapshot = await exportSnapshot({ accessToken: process.env.CONTENT_ISLAND_TOKEN! });await redis.set('content-island:snapshot', JSON.stringify(snapshot));await redis.publish('content-island:updated', '1');await redis.quit();console.log('✅ snapshot publicado en Redis');Añade un script en el
package.jsonpara lanzarlo:package.json "scripts": {"publish:redis": "tsx scripts/publish-redis.mts"} -
Pruébalo con dos instancias (que es la gracia). Haz una primera carga en Redis y arranca dos APIs en puertos distintos:
Ventana de terminal npm run publish:redis # primera carga en RedisPORT=3000 npm run dev # en una terminalPORT=3001 npm run dev # en otraPublica contenido nuevo y lanza
npm run publish:redisuna sola vez. ConsultaGET /en las dos instancias: ambas actualizan suexportedAta la vez, con un únicoPUBLISH.
Ejemplo
Código completo (incluye el docker-compose.yml): content-island/examples-dynamic-snapshot → express/03-redis-pub-sub.