@inlay/cache
Advanced tools
+146
| # @inlay/cache | ||
| 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: | ||
| ```json | ||
| { | ||
| "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/cache | ||
| ``` | ||
| ## Usage | ||
| ```ts | ||
| 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: | ||
| ```json | ||
| { | ||
| "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: | ||
| ```ts | ||
| 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 | TagLink` | ||
| - **`TagRecord`** — `{ $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 |
+1
-1
| { | ||
| "name": "@inlay/cache", | ||
| "version": "0.0.1", | ||
| "version": "0.0.2", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "author": "Dan Abramov <dan.abramov@gmail.com>", |
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
7959
196.98%4
33.33%0
-100%147
Infinity%