11 KiB
cache
arts/cache es la capa de cache de datos del framework. No es un Map con TTL:
es un motor de coherencia para decidir si un dato se puede servir, si está fresco,
si debe revalidarse, bajo qué scope de seguridad vive y qué invalidaciones lo
afectan.
El patrón sigue el resto de artefactos:
import { createEngineCache } from '$svrs/cache';
import { createActiveCache } from '$cache';
createEngineCache()vive en$svrs/cachey es la API imperativa para server, servicios, repositorios, workers y tests.createActiveCache()vive en$cachey añade estado reactivo para Svelte.App.cacheexiste cuando la app declaracache: defineActiveCache(...)enservices.- El core puro vive en
$libs/cachecomocreateCacheRuntime().
Qué Resuelve
cache responde a preguntas que una cache simple no contesta:
- Si el dato existe, si está
fresh,stale,expiredo invalidado. - Si puede servirse stale mientras se refresca en background.
- Si puede servirse stale cuando el origen falla.
- Si pertenece a scope
public,tenant,actor,permissionocustom. - Si un cambio de tag o key prefix invalidó la entrada.
- Si hay otra petición idéntica en vuelo y debe deduplicarse.
- Por qué tomó una decisión, vía
explain().
Uso Mínimo
import { createEngineCache, memoryCacheAdapter, CACHE_POLICY_INTERACTIVE } from '$svrs/cache';
const Cache = createEngineCache({
adapter: memoryCacheAdapter(),
defaultPolicy: CACHE_POLICY_INTERACTIVE
});
const project = await Cache.query({
key: ['project', projectId],
scope: 'public',
tags: [{ type: 'project', id: projectId }],
fetcher: () => ProjectRepo.findById(projectId)
});
La segunda lectura con la misma key/scope/policy servirá el valor cacheado si sigue válido.
App.cache
createActiveApp() expone App.cache cuando se declara el servicio:
const App = createActiveApp({
services: {
cache: defineActiveCache()
}
});
const value = await App.cache.query({
key: ['settings'],
scope: 'public',
fetcher: loadSettings
});
For real apps, configure policies, adapter or scope resolver:
const App = createActiveApp({
services: {
cache: defineActiveCache({
scopeResolver: () => ({
tenantId: App.session?.current?.data?.tenantId,
actorId: App.session?.current?.user?.id,
permissionHash: App.perm?.currentSnapshot.version,
locale: App.langs.getLocale()
})
})
}
});
Si una página demo o test se prerenderiza en modo producción y usa ese adapter memory de forma intencional, configúralo de forma explícita:
const App = createActiveApp({
cache: {
defaultMemoryAdapter: {
suppressProductionWarning: true
}
}
});
No uses esa opción para esconder una cache de producción accidental. En una app real, pasa
cache.adapter con el backend que quieras usar o deja el warning activo hasta decidir la estrategia.
Si usas scope: 'actor' o scope: 'permission', el resolver debe aportar los valores
necesarios. Si faltan, el motor falla en vez de mezclar datos de usuarios.
Reacting to identity / session changes
ActiveCache is a passive runtime: it never subscribes to the bus on its
own. Cross-module reactions live in orca presets declared at the App
level. The standard preset clears the cache on session identity change
and on session revoke:
import { createActiveApp, applyStandardOrca } from '$active-app';
const App = createActiveApp({
services: {
cache: defineActiveCache({}),
session: defineActiveSession({ ... })
}
});
applyStandardOrca(App);
// → on SESSION_EVENT_IDENTITY_CHANGED: App.cache.clear()
// → on SESSION_EVENT_REVOKED: App.cache.clear()
Cherry-pick if the standard set is too aggressive:
import {
applyCacheClearOnIdentityChange,
applyCacheClearOnRevoke
} from '$active-app';
applyCacheClearOnIdentityChange(App);
// SESSION_EVENT_REVOKED is not handled — caches survive sign-out.
Cache itself listens to nothing. It exposes clear() / invalidate() and
trusts the orchestration layer to call them. This keeps cache invalidation
policy out of the cache art and on the App composition.
Keys
Las keys son arrays deterministas:
['posts', { page: 1, filters: { status: 'published' } }][('tenant', tenantId, 'projects')][
'profile'
];
El normalizador:
- Ordena keys de objetos.
- Omite campos
undefineden objetos. - Soporta
Date,Map,Set,URLSearchParamsyBigInt. - Rechaza funciones, símbolos, números no finitos y referencias circulares.
Scopes
Los scopes evitan fugas de datos:
scope: 'public' // compartible
scope: 'tenant' // requiere tenantId
scope: 'actor' // requiere actorId; tenantId opcional
scope: 'permission' // requiere actorId + permissionHash
scope: { mode: 'custom', values: { tenantId, reportId } }
Regla de calidad: datos privados nunca deberían cachearse como public.
Policies
Las políticas expresan intención:
import {
CACHE_POLICY_INTERACTIVE,
CACHE_POLICY_CATALOG,
CACHE_POLICY_PRIVATE_SESSION
} from '$svrs/cache';
Incluidas:
interactive: UI normal,stale-while-revalidate.catalog: datos estables, ventanas largas.privateSession: datos privados, persistencia desactivada por defecto.realtime: casi sin cache.immutable: datos versionados o inmutables.
Puedes definir las tuyas:
import { createEngineCache } from '$svrs/cache';
const Cache = createEngineCache({
policies: {
dashboard: {
freshFor: '15s',
staleFor: '2m',
staleIfErrorFor: '10m',
gcAfter: '30m',
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
});
Modos
CACHE_READ_MODE_CACHE_FIRST;
CACHE_READ_MODE_STALE_WHILE_REVALIDATE;
CACHE_READ_MODE_MUST_REVALIDATE;
CACHE_READ_MODE_BYPASS_CACHE;
CACHE_READ_MODE_NO_STORE;
cache-first: sirve caché mientras esté fresh.stale-while-revalidate: sirve stale dentro de ventana y refresca en background.must-revalidate: si no está fresh, bloquea y va al origen.bypass-cache: ignora lectura, llama al origen y escribe resultado.no-store: ignora lectura, llama al origen y no escribe.
Tags Y Epochs
Las entradas pueden declarar tags:
tags: [
{ type: 'project', id: projectId },
{ type: 'project', id: 'LIST' }
];
Invalidar un tag no escanea ni borra todas las keys. Incrementa un epoch:
await Cache.invalidate({
tag: { type: 'project', id: projectId },
scope: 'tenant'
});
Cada entry guarda los epochs con los que fue escrita. En la siguiente lectura, si el epoch actual no coincide, la entry queda invalidada.
También hay invalidación por key exacta y key prefix:
await Cache.invalidate({ key: ['project', projectId], scope: 'tenant' });
await Cache.invalidate({ keyPrefix: ['projects'], scope: 'tenant' });
ActiveCache
createActiveCache() expone la misma API que el engine y añade estado reactivo:
const Cache = createActiveCache();
const entry = Cache.entry({
key: ['dashboard'],
scope: 'public',
fetcher: loadDashboard
});
await entry.load();
entry.data;
entry.status;
entry.error;
entry.loading;
ActiveCacheEntry no hace magia en efectos. Cargas explícitamente con load() o
refresh(), y el estado cambia sin bucles reactivos.
entry.set(value) reutiliza por defecto la policy, tags, schemaVersion,
persist y scope definidos en la entry, y permite sobrescribirlos por llamada.
Mutate
La mutación v1 es conservadora:
await Cache.mutate({
commit: () => App.http.patch(`/projects/${projectId}`, { body: patch }),
invalidate: [{ tag: { type: 'project', id: 'LIST' }, scope: 'tenant' }],
update: [
{
key: ['project', projectId],
scope: 'tenant',
reducer: (project) => ({ ...project, ...patch })
}
]
});
Semántica:
- Ejecuta
commit(). - Si falla, no toca la cache.
- Si funciona, invalida tags/prefix/keys.
- Aplica updates exactos.
Optimistic journal queda fuera de v1.
Explain
explain() existe desde v1 porque cache sin introspección se vuelve opaca:
const info = await Cache.explain(['project', projectId], {
scope: 'tenant',
schemaVersion: 'Project:v1'
});
Devuelve adapter, key efectiva, estado, decisión, razón, schema y epochs.
Adapters
Memory
const adapter = memoryCacheAdapter({
maxEntries: 5000,
maxSizeBytes: 64 * 1024 * 1024
});
Soporta TTL, LRU aproximado, epochs e introspección.
Emite el warning constante CACHE_MEMORY_ADAPTER_PRODUCTION_WARNING si se crea
en runtime de producción. Es correcto para tests, demos, L1 por proceso y
desarrollo local; para producción multi-instancia usa un adapter intencional
o una composición tiered cuando esté disponible.
Storage
const adapter = storageCacheAdapter({
storage: App.storage.adapter,
namespace: 'cache'
});
Útil para persistencia cliente. Respeta persist: false, por lo que una policy
privada puede impedir escritura persistente.
El adapter mantiene un índice interno para que Cache.clear() pueda borrar tanto
entries como epochs persistidos; esto es importante en logout/wipe.
Redis, tiered cache, browser Cache API y adapters edge quedan para fases posteriores. El contrato está preparado para que Redis se integre sin dependencia dura.
Eventos Y Logger
El engine emite eventos:
Cache.on(CACHE_EVENT_ALL, (event) => {
console.log(event.type, event.reason);
});
arts/cache enruta esos eventos al EngineLogger inyectado:
- Hits/misses/sets/invalidation en
debug. - Errores de adapter, refresh y stale-if-error en
warn.
Las categorías y mensajes viven en consts.ts, no como strings dispersos.
Seguridad
Defaults y recomendaciones:
- No uses
publicpara datos conAuthorization, sesión o permisos. - Usa
tenant,actoropermissionpara datos privados. privateSessionno persiste por defecto.- Cambios de permisos deben invalidar scope
permissiono cambiarpermissionHash. - Logout o cambio de identidad debería llamar a
Cache.clear()/ invalidar scopes privados; useapplyStandardOrca(App)(or the individual presets) so this happens automatically when the session art emitsSESSION_EVENT_*. - El cliente cachea para UX, no para seguridad. Las decisiones autoritativas viven en servidor.
Página De Prueba
La demo interactiva está en:
La documentacion actual de runtime vive en /active.
Muestra App.cache, entry reactiva, invalidación por tag, scope actor/tenant,
eventos y explain().
Roadmap
v1 incluido:
createEngineCache()createActiveCache()memoryCacheAdapter()storageCacheAdapter()- key normalization
- scopes
- policies
- stale-while-revalidate
- stale-if-error
- singleflight
- tag/prefix epochs
- mutate conservador
- explain
- logger integration
App.cache- orca presets (
applyCacheClearOnIdentityChange,applyCacheClearOnRevoke)
Siguiente:
tieredCacheAdapter()- adapter Redis por interfaz, sin dependencia dura
- integración HTTP explícita
- optimistic journal
- browser Cache API para
Request/Response