Caché y releases

ETag y solicitudes condicionales, las políticas de caché de desarrollo y de publicación, y por qué una URL con versión exacta se puede almacenar como inmutable.

  • Disponibilidad: Experimental
  • Evidencia: Leído del código fuente
  • Referencia

ETag y solicitudes condicionales

Toda respuesta correcta lleva un ETag fuerte, el digest SHA-256 del cuerpo, escrito "sha256-…". Envíalo de vuelta en If-None-Match para revalidar:

Estado Significado
200 El recurso, con ETag y Cache-Control
304 La etiqueta de If-None-Match está vigente. Se repiten ETag y Cache-Control, y no hay cuerpo.
JavaScriptconditional.mjs
// Revalidate a module with its ETag. A current tag answers 304 with no body.
const request = process.env.MODULE_REQUEST ?? '/m/@example/[email protected]/modules/text?target=browser&format=esm';
const origin = process.env.CDN_ORIGIN ?? process.env.DEV_ORIGIN;
const url = new URL(request, origin);

const first = await fetch(url);
const etag = first.headers.get('etag');
await first.arrayBuffer();
console.log(`first request: ${first.status}, ETag ${etag}`);

const second = await fetch(url, { headers: { 'If-None-Match': etag } });
console.log(`with If-None-Match: ${second.status}`);
console.log(`  ETag repeated: ${second.headers.get('etag') === etag}`);
console.log(`  Cache-Control: ${second.headers.get('cache-control')}`);

if (first.status !== 200 || second.status !== 304) process.exitCode = 1;

La etiqueta identifica bytes, no una receta de compilación. Dos servicios devuelven la misma etiqueta solo cuando devuelven los mismos bytes, y un artefacto de desarrollo y uno de producción de un mismo módulo son bytes distintos.

Dos políticas de caché

Las identidades, las opciones, los cuerpos y los errores son comunes a todos los servicios. El Cache-Control de una respuesta correcta es aquello en lo que los servicios difieren a propósito. El contrato nombra dos políticas:

Política Cache-Control La usa
development no-store La salida que sigue a sus fuentes. Ni un cliente ni un intermediario almacenan nada, y el ETag revalida cada solicitud.
published Exactamente uno de public y private, con max-age=<seconds>, y opcionalmente immutable. Nunca no-store ni no-cache. La salida de una identidad publicada cuyos bytes se retienen

La especificación indica max-age=31536000 (un año) e immutable como los valores de la política de publicación:

Text
Cache-Control: public, max-age=31536000, immutable
  • public es para la entrega abierta. private es para una respuesta que depende del acceso de quien la pide; consulta Acceso privado.
  • immutable solo es válido mientras el servicio nunca cambie los bytes de la URL. Si un release puede cambiar los bytes de una versión exacta es una decisión del servicio publicado, así que una respuesta conforme puede omitir immutable.
  • Una respuesta de error nunca es inmutable. En particular, OUTPUT_NOT_AVAILABLE no debe almacenarse como permanente: la salida se puede preparar más adelante.

Por qué las versiones exactas pueden ser inmutables

Una ruta nombra una versión exacta de un paquete, y la consulta nombra una salida de ella. Los releases son instantáneas inmutables: la promoción y la reversión mueven un entorno de un release a otro, nunca reescriben los artefactos de un release. Por eso una URL con versión exacta conserva sus bytes, que es lo que hace seguro un almacenamiento en caché prolongado.

Lo que cambia entre releases es la resolución: a qué versiones se asignan tus especificadores. Publicar un release nuevo cambia el mapa, no los módulos que están detrás de las URL existentes.

El prefijo del release

En el host de una aplicación publicada, cada recurso de un release se sirve bajo un prefijo propio del release, donde el número es el number secuencial del release:

Text
/_r/<release number>/index.html                     the HTML shell of a frontend target
/_r/<release number>/bootstrap.js                   the script that imports the entry
/_r/<release number>/loader.js                      SystemJS, in the system loader mode only
/_r/<release number>/resolution.json[?target=…&format=…]   the resolution of each target
/_r/<release number>/importmap.json[?target=…&format=…]    the same, as an import map relative to itself
/_r/<release number>/m/@example/[email protected]/modules/core/router?target=browser&format=esm

/_r/<release number> es un origen base en el sentido del contrato de entrega: un servicio enrutado incluye su prefijo de ruta en su origen. Debajo de él, la ruta y la consulta son exactamente las compartidas, así que sigue valiendo la misma URL relativa de módulo y nada cambia en la gramática. Las referencias relativas que contienen las salidas, como ../assets/<path> desde una hoja de estilos, se quedan dentro del release. Esta base es lo que recibe un proceso de Node o un programa de Deno; consulta Estrategias por entorno.

Todo documento que genera un release (el shell, el import map, la resolución, el bootstrap) direcciona un paquete por su fuente, tal como la definen las fuentes: npm sin prefijo, otro registro como /m/<registry id>/…, un repositorio Git como /m/git/… y un archivo comprimido como /m/digest/…. Un servidor de desarrollo escribe las mismas direcciones para los paquetes de npm y de registros que sirve, así que un consumidor solo cambia el origen base entre ambos. Dentro de un release, una versión de paquete viene de una sola fuente, así que la base de un release también responde la ruta sin prefijo de un paquete de otro registro, con los mismos bytes y el mismo validador, como un alias de compatibilidad que ningún documento generado nombra; no construyas direcciones sobre él. El origen de entrega compartido responde solo la forma calificada para un paquete de otro registro, porque allí una ruta sin prefijo significa npm.

https://<application host>/_r/12/m/@example/app@1.0.0/modules/core/router?target=browser&format=esm

https://<application host>/_r/12
Origen base: lo único que cambias
/m/
Espacio de nombres
@example/app
Paquete
@1.0.0
Versión exacta
/modules/
Familia de recursos
core/router
Subruta del módulo público
?target=browser&format=esm
Opciones

El alias actual se lee una vez por navegación

La única dirección mutable es la propia navegación: / y cualquier ruta de navegación responden el shell HTML del release que el entorno vincula ahora. Esa vinculación se lee una sola vez, para la navegación. A partir de ahí, el shell nombra cada recurso posterior bajo su propio /_r/<release number>/: hojas de estilos, el import map, el bootstrap, el cargador y, a través de ellos, cada módulo y cada recurso.

Esas solicitudes no vuelven a consultar la vinculación. Por eso una promoción entre la solicitud del HTML y un import diferido posterior no puede mezclar dos releases, y una pestaña que se abrió antes de una promoción sigue cargando el release con el que empezó.

Respuesta Cache-Control
Un recurso público bajo /m/… o /_r/<release number>/… public, max-age=31536000, immutable
Un recurso restringido de una aplicación privada private, max-age=60, con Vary: Cookie; nunca public, nunca immutable
El HTML de navegación de una aplicación pública public, max-age=0, must-revalidate, con un ETag
El HTML de navegación de una aplicación privada private, max-age=0, must-revalidate, con Vary: Cookie

Una ruta bajo /m, /_r o /_beyond nunca es una navegación: un recurso que falta allí responde el 404 JSON del contrato, nunca HTML. Cualquier otra ruta sin extensión de archivo recibe el shell del release actual, que es lo que hace funcionar los enlaces profundos.

Comprobar una política desde el código

JavaScript
import { Policy } from '@beyond-js/artifact-api';

Policy.development.header(); // 'no-store'
Policy.published.header(); // 'public, max-age=31536000, immutable'
Policy.published.header({ visibility: 'private', age: 600 }); // 'private, max-age=600, immutable'

Policy.published.accepts('private, max-age=600'); // true
Policy.published.violation('no-store'); // why the value is not published caching