@inlay/cache
v0.0.2
Published
Every Inlay XRPC component response may include a [`CachePolicy`](https://pdsls.dev/at://did:plc:mdg3w2kpadcyxy33pizokzf3/com.atproto.lexicon.schema/at.inlay.defs#schema:cachePolicy) — a lifetime and a set of invalidation tags:
Readme
@inlay/cache
Every Inlay XRPC component response may include a CachePolicy — a lifetime and a set of invalidation tags:
{
"life": "hours",
"tags": [
// Invalidate me when this record changes
{ "$type": "at.inlay.defs#tagRecord", "uri": "at://did:plc:abc/app.bsky.actor.profile/self" }
]
}This package lets you build that policy declaratively. Instead of constructing the object by hand, call cacheLife and cacheTagRecord anywhere during your handler — including inside async helper functions.
A server runtime can collect these calls and produce the cache policy object.
Install
npm install @inlay/cacheUsage
import { $ } from "@inlay/core";
import { cacheLife, cacheTagRecord, cacheTagLink } from "@inlay/cache";
async function fetchRecord(uri) {
cacheTagRecord(uri); // Invalidate me when this record changes
cacheLife("max");
const [, , repo, collection, rkey] = uri.split("/");
const params = new URLSearchParams({ repo, collection, rkey });
const res = await fetch(
`https://slingshot.microcosm.blue/xrpc/com.atproto.repo.getRecord?${params}`
);
return (await res.json()).value;
}
async function fetchProfileStats(did) {
cacheLife("hours");
cacheTagLink(`at://${did}`, "app.bsky.graph.follow"); // Invalidate me on backlinks
const params = new URLSearchParams({ actor: did });
const res = await fetch(
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?${params}`
);
const data = await res.json();
return { followersCount: data.followersCount };
}
async function ProfileCard({ uri }) {
const profile = await fetchRecord(uri);
const did = uri.split("/")[2];
const stats = await fetchProfileStats(did);
return $("org.atsui.Stack", { gap: "small" },
$("org.atsui.Avatar", { src: profile.avatar, did }),
$("org.atsui.Text", {}, profile.displayName),
$("org.atsui.Text", {}, `${stats.followersCount} followers`),
);
}A server runtime could then provide a way to run ProfileCard and collect its cache policy:
{
"node": {
"$": "$",
"type": "org.atsui.Stack",
"props": {
"gap": "small",
"children": [
{ "$": "$", "type": "org.atsui.Avatar", "props": { "src": "...", "did": "did:plc:ragtjsm2j2vknwkz3zp4oxrd" }, "key": "0" },
{ "$": "$", "type": "org.atsui.Text", "props": { "children": ["Paul Frazee"] }, "key": "1" },
{ "$": "$", "type": "org.atsui.Text", "props": { "children": ["308032 followers"] }, "key": "2" }
]
}
},
"cache": {
"life": "hours",
"tags": [
{ "$type": "at.inlay.defs#tagRecord", "uri": "at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.actor.profile/self" },
{ "$type": "at.inlay.defs#tagLink", "subject": "at://did:plc:ragtjsm2j2vknwkz3zp4oxrd", "from": "app.bsky.graph.follow" }
]
}
}Server
There is no prescribed server runtime. Cache functions write to a Dispatcher on Symbol.for("inlay.cache"). Your server provides the implementation. Minimal example:
import { AsyncLocalStorage } from "node:async_hooks";
import { serializeTree } from "@inlay/core";
import type { Dispatcher } from "@inlay/cache";
const LIFE_ORDER = ["seconds", "minutes", "hours", "max"];
const cacheStore = new AsyncLocalStorage();
// Install the dispatcher — cache functions will write here
globalThis[Symbol.for("inlay.cache")] = {
cacheLife(life) { cacheStore.getStore().lives.push(life); },
cacheTag(tag) { cacheStore.getStore().tags.push(tag); },
} satisfies Dispatcher;
// Run a handler and collect its cache policy
async function runHandler(handler, props) {
const state = { lives: [], tags: [] };
const node = await cacheStore.run(state, () => handler(props));
const life = state.lives.reduce((a, b) =>
LIFE_ORDER.indexOf(a) < LIFE_ORDER.indexOf(b) ? a : b
);
return {
node: serializeTree(node),
cache: { life, tags: state.tags },
};
}
const result = await runHandler(ProfileCard, {
uri: "at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.actor.profile/self",
});
console.log(JSON.stringify(result, null, 2));
// => { node: { ... }, cache: { life: "hours", tags: [...] } }Installation happens via a global so that coordination doesn't depend on package versioning or hoisting working correctly. Helpers like fetchRecord and fetchProfileStats can be moved into libraries.
API
| Function | Description |
|----------|-------------|
| cacheLife(life) | Set cache duration. Strictest (shortest) call wins. Values: "seconds", "minutes", "hours", "max" |
| cacheTagRecord(uri) | Invalidate when this AT Protocol record is created, updated, or deleted |
| cacheTagLink(subject, from?) | Invalidate when any record linking to subject changes. Optionally restrict to a specific collection |
Types
Life—"seconds" | "minutes" | "hours" | "max"CacheTag—TagRecord | TagLinkTagRecord—{ $type: "at.inlay.defs#tagRecord", uri: string }TagLink—{ $type: "at.inlay.defs#tagLink", subject: string, from?: string }Dispatcher— interface for the server runtime to implement
