Fan-out, over one stream.
GAPublish a message to a channel and every current subscriber gets it. Delivery runs over Server-Sent Events, so a subscriber is a long-lived HTTP stream — no extra protocol, no socket server to run.
publish(channel, message)
import { cache } from './cache'
// Fan a message out to everyone subscribed to the channel:
await cache.publish('presence', { user: 'ada', status: 'online' })
// → { delivered: 1 } once stored for subscribers (not a subscriber count)publish() reaches whoever is subscribed. Each channel keeps its last 1,000 messages for 24 hours after its latest publish, which is what lets a reconnecting subscriber catch up; a subscriber that wasn’t connected at all doesn’t get history. A message is any JSON value up to 256 KB. For work that must not be missed, use a queue.
subscribe(channel, onMessage, opts)
subscribe() opens an SSE stream and calls your onMessage with a { channel, message, publishedAt } envelope for each message. It runs until you abort the signal: the server rotates streams about once a minute and the client reconnects on its own, resuming after the last message it saw, so a rotation or a brief network drop loses nothing. It rejects only on a permanent error such as a revoked token. Channel names are letters, digits and _ . : -, up to 128 characters. A namespace holds up to 100 open subscriptions at once.
import { cache } from './cache'
const ac = new AbortController()
cache.subscribe('presence', (msg) => {
// msg.message is what was published; msg.publishedAt is an ISO timestamp
console.log('presence update', msg.message, msg.publishedAt)
}, { signal: ac.signal })
// Later, stop receiving and close the stream:
ac.abort()Subscribe to a pattern to hear several channels on one stream: * matches any run of characters and ? exactly one. Use msg.channel to see where each message came from. Patterns are for subscribing only; you always publish to a named channel.
// Every channel that starts with "orders."
cache.subscribe('orders.*', (msg) => {
console.log(msg.channel, msg.message) // e.g. 'orders.eu', { id: 'o_1' }
}, { signal: ac.signal })AbortController to your component or request lifecycle and abort() it on teardown — otherwise streams leak.Tearing down in React
The signal maps cleanly onto an effect cleanup — subscribe on mount, abort on unmount:
import { useEffect } from 'react'
import type { PubSubMessage } from '@tillstack/cache-node'
import { cache } from './cache'
function usePresence(onUpdate: (m: PubSubMessage) => void) {
useEffect(() => {
const ac = new AbortController()
cache.subscribe('presence', onUpdate, { signal: ac.signal })
return () => ac.abort() // tear down on unmount
}, [onUpdate])
}Pub/sub vs. a queue
- Pub/sub — one-to-many, ephemeral, delivered to live subscribers. Great for presence, live dashboards, cache-invalidation pings, and UI updates.
- Queues — one message, one worker, durable and retried until acked. Great for jobs that must run, where “nobody was subscribed” isn’t an acceptable outcome.
Back to the TillCache overview, or read up on Backends & vendors to see where a namespace’s channels are anchored.