Skip to content
Sponsor

Reading and filling

Choose between the simple value API and the metadata-rich entry API.

Astilba Cache provides two read APIs. Use getOrSet() when you only need a value. Use getOrSetEntry() when the caller needs cache metadata or the factory may intentionally skip storage.

If terms such as factory, L1, L2, origin, or entry are new, see Core concepts.

const product = await cache.getOrSet({
key: `product:${productId}`,
tags: [compound("product", productId)],
factory: async ({ signal }) => loadProduct(productId, signal),
})

The factory receives a FactoryCtx without skip(). A skipped result therefore cannot be mistaken for a successful T.

const entry = await cache.getOrSetEntry<string>({
key: "optional-banner",
factory: async (ctx) => {
if (!shouldRenderBanner()) return ctx.skip()
return loadBanner()
},
})
if (!entry.skipped && entry.value !== undefined) {
render(entry.value)
}

getOrSetEntry() returns these fields:

Field Meaning in the current kernel
value The stored, filled, or stale value; undefined for a miss, skip, or negative entry.
tier l1, l2, origin, or miss. l1.5 exists in the type but is not emitted by the current implementation.
stale The returned value was not fresh at this read’s consistency level.
servedOnError A classified transient failure reused a stale candidate after serve-time revalidation.
durable On an origin result, whether the fill reached shared L2 or a newer durable entry won arbitration. Existing read hits currently report true even for an L1-only entry; treat hit durability metadata as provisional.
skipped The entry-form factory called ctx.skip(); nothing was stored.
age Always 0 for now; elapsed-time accounting is unfinished.

On a newly filled origin result, durable: false does not mean the factory failed. It can mean the value was confined to L1 by scope, no L1 existed to retain that private value, or a classified transient L2 write failure was suppressed so the caller could still use the result. The current hit path does not preserve that distinction and reports true; do not use hit-level durable as proof of shared persistence yet.

  1. Read configured tiers. Cache tries L1 before L2. It checks the stored codec identity before decoding, then validates the reconstructed entry.
  2. Join compatible work. Concurrent compatible calls share one in-isolate foreground factory execution.
  3. Run the factory. The factory receives an AbortSignal, optional request context, and typed failure helpers. The current kernel creates a fresh signal but does not yet abort it on a cache deadline.
  4. Fence the result. A hard invalidation observed during the fill can reject write-back so the result is not published against obsolete invalidation knowledge.
  5. Write by scope. Shared scopes may reach L2; principal-derived values are L1-only. A successful fill hydrates L1 when one is configured.

When a plain-value fill is fenced and no value can be served, getOrSet() throws FencedError. getOrSetEntry() reports a non-durable miss instead. The documented snapshot does not rerun the factory inside the same read; the caller may read again.

Singleflight joins calls only when their canonical key and structural settings agree. Tags, TTL, grace, negative-cache TTL, resolved scope, codec identity, consistency, and API form all participate. Tag order does not matter because tags are sorted and deduplicated first.

With dev: true, an incompatible same-key call fails loudly. Otherwise it runs separately and emits singleflight_option_mismatch telemetry.

Stored values carry a codec identity. Cache checks that identity before decoding, so an unexpected codec becomes a miss instead of a mistyped value. Intentional migrations can allow selected older identities through defaults.acceptCodecs.

The built-in codec is a plain JSON round trip. Use JSON-representable values only: it does not revive dates, classes, functions, or bigint values. Supply a custom Codec with a new identity when you need another wire format; if you accept an older identity, the current decoder must understand those older bytes.

A soft-stale eventual read currently awaits a best-effort refresh, then still returns the stale value for that call. The planned background adoption and retry lifecycle is not implemented, so this path does not yet provide background stale-while-revalidate latency.

FactoryCtx.dependsOn(), setTags(), and setTtl() are currently no-ops. ctx.graced is not populated and reuseGraced() throws NotImplementedError. Use call-level tags and the documented stale-on-error path instead.