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.
Choose the return shape
Section titled “Choose the return shape”Return a value
Section titled “Return a value”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.
Return an entry
Section titled “Return an entry”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.
Follow the fill lifecycle
Section titled “Follow the fill lifecycle”- Read configured tiers. Cache tries L1 before L2. It checks the stored codec identity before decoding, then validates the reconstructed entry.
- Join compatible work. Concurrent compatible calls share one in-isolate foreground factory execution.
- 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. - Fence the result. A hard invalidation observed during the fill can reject write-back so the result is not published against obsolete invalidation knowledge.
- 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.
Compatible concurrent calls
Section titled “Compatible concurrent calls”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.
Codec changes become misses
Section titled “Codec changes become misses”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.
Current boundaries
Section titled “Current boundaries”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.
Related
Section titled “Related”- Preview walkthrough shows both value reads against a development-only Store.
- Core concepts explains the storage tiers and read vocabulary.
- Consistency and resilience explains when stale values may be reused.
- API status lists provisional metadata and unimplemented helpers.