TILLCACHE · PUB/SUB

Fan-out, over one stream.

GA

Publish 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.

01Publish

publish(channel, message)

ts
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.

02Subscribe

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.

ts
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.

ts
// 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 })
Pattern history
A pattern subscription catches up after a reconnect like any other. Messages published while no pattern subscription was open in the last five minutes aren’t replayed to one that opens later.
Always pass a signal
A subscription is a live connection. Wire an AbortController to your component or request lifecycle and abort() it on teardown — otherwise streams leak.
03Pattern

Tearing down in React

The signal maps cleanly onto an effect cleanup — subscribe on mount, abort on unmount:

tsx
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])
}
04Choosing

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.