Caching
mountly keeps two cooperating caches. Both live in memory and survive re-mounts within a page session.
Module cache
Section titled “Module cache”Stores resolved widget modules, keyed by moduleId.
- One key per
moduleIdregardless ofloadModuleimplementation. - In-flight deduplication. If 12 hovers fire before the first
import()resolves, you get one fetch and 12 callers awaiting one promise. - An aborted load clears the cache entry so the next attempt starts fresh.
You rarely touch it directly. Use the exported moduleCache to seed entries or invalidate from outside the runtime:
import { moduleCache } from "mountly";
// e.g. force a re-fetch on next preloadmoduleCache.delete("signup-card");Data cache
Section titled “Data cache”Stores loadData responses, keyed by a stable serialisation of the feature context. The default builder excludes the non-serialisable fields (element, event) and uses sorted-key JSON of everything else, prefixed with the moduleId.
default cache key: `${moduleId}:${stableJSON(context - {element, event})}`If you pass context: { locale: "en", segment: "pro" } the cache key becomes:
signup-card:{"locale":"en","segment":"pro","triggerType":"hover"}Custom cache keys
Section titled “Custom cache keys”Override getCacheKey for coarser caching or contexts with non-serialisable values:
createOnDemandFeature({ moduleId: "signup-card", loadModule: …, loadData: …, // Cache by locale only — ignore everything else. getCacheKey: (ctx) => `signup-card:${ctx.locale}`,});In-flight deduplication
Section titled “In-flight deduplication”Same rule as the module cache: two concurrent activations with the same key share a single fetch. The runtime’s DedupCache is exported as dataCache if you need to interact directly:
import { dataCache } from "mountly";
// Manually invalidate when the user updates their plandataCache.delete(`signup-card:${currentLocale}`);What the caches don’t do
Section titled “What the caches don’t do”- They don’t persist across page reloads. (You can layer a Service Worker on top of
moduleCachefor that.) - They don’t honour HTTP cache headers. That’s the browser’s job, downstream of the loader.
- They don’t ship invalidation primitives beyond
delete(). Use timeouts or app-level events.
The DedupCache primitive
Section titled “The DedupCache primitive”Both caches are instances of the same class:
import { DedupCache } from "mountly";
const cache = new DedupCache<MyValue>();
const value = await cache.resolve("key", async () => fetchSomething(), { signal });Concurrent resolve calls with the same key share a single promise. Pass an AbortSignal to cancel.
A typical timeline
Section titled “A typical timeline”T+0 user hovers #cta → preload(ctx)T+0 moduleCache.resolve("signup-card", loadModule)T+0 dataCache.resolve("signup-card:{...}", loadData) — runs in parallelT+128 module resolves → cachedT+340 data resolves → cachedT+1500 user clicks → activate(ctx)T+1500 cache hits both. activate completes synchronouslyT+1500 mount → renderOpen the same widget elsewhere on the page and both caches hit, so mount happens within a frame.
Inspecting the cache
Section titled “Inspecting the cache”The devtools panel shows live module and data cache contents alongside feature states. Use it to spot an over-aggressive prefetch or a missed cache-key collision.