SemiLayerDocs

Feeds — Caching

Feed pages are cacheable: same (lens, feedName, context, cursor, pageSize) tuple returns the same response within a short window (until the underlying data evolves). The client does the caching — you BYO the storage backend.

@semilayer/client exposes a FeedCache interface and five built-in adapters. The React hook threads cache hits through automatically.

The interface

interface FeedCache {
  get(key: string): Promise<string | null>
  set(key: string, value: string, ttlSeconds: number): Promise<void>
  delete(key: string): Promise<void>
  clear?(): Promise<void>
}

Keys are deterministic hashes — the client builds them. Values are JSON strings of FeedPage. TTL is suggested, not enforced by the client.

Built-in adapters

import {
  inMemoryFeedCache,
  localStorageFeedCache,
  sessionStorageFeedCache,
  indexedDbFeedCache,
  nodeFsFeedCache,
} from '@semilayer/client/cache'

const client = new BeamClient({
  baseUrl, apiKey,
  feedCache: inMemoryFeedCache({ max: 200 }),
})
AdapterScopeWhen to reach for it
inMemoryFeedCache({ max })Per-tab, per-sessionDefault. Fast, simple, no persistence.
localStorageFeedCache()Per-origin, persistentLong-scroll feeds you want to keep between visits.
sessionStorageFeedCache()Per-tab, persistent until closePer-tab history without leaking across tabs.
indexedDbFeedCache()Per-origin, persistent, largeHeavy feeds with many pages (>1MB of cached data).
nodeFsFeedCache({ dir })Per-process, persistentServer-side rendering / Beam running in Node.

Per-call override

You can pass a cache instance per call — useful when a single feed wants different persistence than the client's default:

await beam.articles.feed.foryou(
  { context, pageSize: 10 },
  { cache: localStorageFeedCache() },   // override
)

// Or disable caching for this call
await beam.articles.feed.foryou(
  { context, pageSize: 10 },
  { cache: null },
)

React hook

useFeed accepts a cache option that the hook uses for all its page fetches:

import { useFeed } from '@semilayer/react'
import { localStorageFeedCache } from '@semilayer/client/cache'
import { beam } from '@/lib/beam'

export function ForYou() {
  const { items, fetchMore } = useFeed(beam.articles.feed.foryou, {
    context: { topics: ['security'] },
    cache:   localStorageFeedCache(),
    onCacheMetric: (metric) => console.log(metric),
    // { kind: 'hit' | 'miss' | 'set', key: string, sizeBytes?: number }
  })
  /* ... */
}

The onCacheMetric callback fires once per cache interaction — useful for wiring real-time hit-rate telemetry without touching the adapter.

Custom adapters

Implement the four-method interface against any store you like:

import type { FeedCache } from '@semilayer/client'
import Redis from 'ioredis'

export function redisFeedCache(redis: Redis): FeedCache {
  return {
    async get(key)              { return redis.get(key) },
    async set(key, value, ttl)  { await redis.set(key, value, 'EX', ttl) },
    async delete(key)           { await redis.del(key) },
    async clear()               { /* scan + unlink */ },
  }
}

Drop into new BeamClient({ feedCache: redisFeedCache(redis) }). Works for server-side feed pipelines, SSR with a shared Redis, edge deployments — any store with get/set/del.

TTLs and invalidation

The client sets TTL based on the feed's evolve.minNotifyInterval — a feed with ticks every 30s uses a 30-second TTL. For feeds without pollOnIngest, TTL defaults to 5 minutes.

You can clear manually when your side of the world changes:

// User updates their profile → cached feeds may be stale
await client.feedCache?.clear()

useFeed's refetch() forces a network call regardless of cache state.

What doesn't get cached

  • Subscribe streams — live ticks go through the WebSocket, never through the cache.
  • Explain responses — single-record, sk-only, not worth caching.
  • Non-200 responses — errors are never cached.

Server-side context cache

Separate from the client cache is a server-side context cache that stores embedded text results (e.g. the vector for liked_titles: [...]). That's transparent to clients and tunable with four env vars on the service: FEED_CONTEXT_CACHE_MAX, _TTL_MS, _PER_ORG_MIN, _PER_ORG_MAX_PCT. Admins can watch hit rates at /admin/feed-cache/stats in the Console.

That cache is the reason liked_titles doesn't re-embed on every request — same text, same pod, one API call regardless of caller volume.