npm stats
  • Search
  • About
  • Repo
  • Sponsor
  • more
    • Search
    • About
    • Repo
    • Sponsor

Made by Antonio Ramirez

@inlay/cache

0.0.2

@gaearon

npmSnykSocket
Downloads:339
$ npm install @inlay/cache
DailyWeeklyMonthlyYearly

@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/cache

Usage

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

FunctionDescription
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