Core Concepts

ActivityBus

Coming back online starts a delta catch-up for every channel, and it also forces Flux to decide, per channel, whether anything still has to be fetched over HTTP. ActivityBus tracks each catch-up from the moment it starts to the moment its result arrives, so that decision is made from a real answer and never from a guess about how long catch-up should take. Network and realtime health, which every tab shares, sit beside it in a separate tracker described below.

Why a fixed wait is not a safe way to avoid a race

The obvious fix is to make the freshness check wait a while after reconnect before deciding anything is stale. But any fixed wait is a guess about how long catch-up will take, and a wrong guess hurts in both directions. Too short, and the freshness check fires its own network refetch while the delta stream is still mid delivery. Too long, and a channel whose catch-up genuinely failed sits stale for far longer than necessary.

Flux has no grace timer between a reconnect and a refetch. The decision waits on real information: the result of the catch-up, and the current health of the network and the realtime connection. It proceeds the moment that information arrives, not a moment sooner or later than the data justifies.

1

A channel is either catching up or it is not

When the leader tab begins a reconnect, every channel that can be caught up over a delta is placed in catch-up mode before the socket even opens. The one exception is a channel that was filled from a real network fetch within the last 5 seconds. That channel is bootstrap-fresh: its data is seconds old, so it only subscribes and listens, and no delta is requested for it. Adapters with no delta support never put a channel in catch-up mode at all, because nothing would ever end it.
The flag does not expire on a timer. It is cleared by one thing only: the channel's own catch-up reporting a result. A backend that takes one second to answer and a backend that takes a full minute are therefore treated identically correctly. The channel stays in catch-up mode for as long as it actually is.
2

Completion is a result, not a duration

The engine tells the bus when it sends a catch-up request and again when the answer arrives. The answer is one of four outcomes:
  • Applied: rows newer than the channel's last confirmed time were delivered and written.
  • Confirmed current: the backend answered, and nothing changed. This is an answer, not an absence of one.
  • Too old: the gap is older than the backend can enumerate, so a catch-up could not be trusted. The channel needs a refetch.
  • Failed: no usable answer (timeout, request error, connection dropped). The channel is not marked current.
Every path settles, including the empty ones

A channel leaves catch-up mode when its catch-up RESULT arrives, whatever that result is: rows applied, confirmed current, too old, or failed. That includes the cases where nothing came back at all. An empty reply ends catch-up just as surely as a reply full of rows, and a failure ends it just as surely as a success. Nothing ever has to wait for silence to find out that a channel has nothing left to say.

Separately, a stream of live delta events still uses a quiet-period timer (8 seconds by default) to notice that a burst of events has ended. That timer is a secondary exit for the streaming case. It is never the only way out of catch-up mode, and an empty or failed catch-up never depends on it.
3

Waiting is event driven

The bus emits an event for every change: catch-up begin, catch-up settled, syncing, synced, cleared. Anything that needs to wait for a set of channels subscribes and resolves the instant all of them are settled. If they are already settled, it resolves immediately and nothing is polled.
A ceiling is a backstop, not a grace period

Anything that waits on catch-up carries a hard ceiling, but the ceiling is a backstop for a lost signal, not a grace period. If it is ever reached, the channels it was waiting on are treated as UNVERIFIED, never as up to date. A ceiling that quietly counted as "fine" would reintroduce the exact guess this page is about avoiding.

Revalidation does not poll the bus. It awaits the catch-up results directly, then re-evaluates from the persisted coverage record, and it re-runs whenever a health signal changes (see below).
4

What a result changes

Before a result is recorded as coverage, the engine waits for any rows that arrived with it to finish writing to disk. If a write fails, or the writes do not finish inside a short backstop, the result is downgraded to failed, so coverage never gets ahead of what is actually on disk. Applied and confirmed current advance the channel's confirmed time and mark it live. Too old and failed open a sticky gap that only a refetch can clear. The full model is in core_concepts/sync_anchors.
5

No network is not the same as no realtime

Two independent layers are tracked, because they call for opposite reactions. Every tab observes its own network state. The leader tab owns the realtime state of each channel and publishes it to the other tabs.

Network. offline means the browser reports no connection: nothing is fetched, the app serves from disk, and the moment live coverage ended is recorded. unreachable means the browser claims it is online but real requests keep failing (a captive portal, a dead link). It is detected from request outcomes, never from the browser's online flag alone, and it clears on the next success.

Realtime, per channel. suspended (network offline, sockets intentionally down), connecting, live, degraded (repeated connect failures, watchdog resets or flapping, with retries continuing in the background), and unavailable with a cause.

health.ts (excerpt)
1// Network and realtime health, from any tab.
2// Followers read the leader's published state; the leader reads its own.
3
4flux.realtime.health.getNetwork()
5// 'online' | 'offline' | 'unreachable'
6
7flux.realtime.getChannelHealth('pricing_table')
8// {
9// state: 'suspended' | 'connecting' | 'live' | 'degraded' | 'unavailable',
10// cause: null | 'noop' | 'no-binding' | 'limit' | 'auth',
11// since: 1718000000000,
12// retryAt: null | 1718000060000, // next planned slow retry, if any
13// }
14
15flux.realtime.isLeaderAlive()
16// true while a leader tab is beating
6

Why a channel may have no realtime at all

"Realtime is not connected" can mean several different things, and Flux keeps them apart because each calls for a different response:
  • No adapter (noop): known at startup and permanent. Every channel is TTL-only.
  • No realtime binding: on a backend where realtime subscribes to a table, a registration without table has nothing to subscribe to. That channel is TTL-only and everything else is unaffected.
  • Unstable network: after several consecutive connect failures, watchdog resets or quick drops, the channels read as degraded while retries continue. A connection that stays up long enough resets the count.
  • Quota or credentials: if the adapter reports why a connection failed, a limit or auth failure backs off on a slow, jittered schedule (starting around a minute and growing to fifteen) instead of retrying quickly and making a quota problem worse. Adapters that cannot classify failures report them as ordinary connection failures.
7

One tab does the revalidating, not every tab

The freshness check is owned by the realtime leader tab. It holds the socket, so it has the health truth, and anything it refetches reaches the other tabs through the tab mirror channel. Follower tabs forward their triggers to the leader instead of running their own check, so five tabs regaining focus produce one evaluation, not five fetches.
If no leader is alive at that moment, one follower runs the check under a single-runner lock and the rest return immediately. The leader publishes its health on the cross-tab channel on every change and on a regular beat, and mirrors the latest snapshot to local storage so a newly opened tab has an answer immediately. A leader that goes silent for too long reads as degraded.
The check re-runs on signals: focus and visibility, the network coming back, any health change, a new leader being elected, realtime connecting, a catch-up result, a bootstrap completing, a user session starting, or a channel being invalidated. Bursts of signals are coalesced into one run, and a refetch that just failed is not retried again within a few seconds.
None of this needs configuration in the common case. It is active as soon as a realtime adapter is in use. With no adapter, channels are TTL-only: nothing enters catch-up mode, the bus stays idle, and freshness is a plain TTL check with nothing to race against.

ActivityBus is what the delta catch-up described in core_concepts/sync_anchors is coordinated against. For the storage side of what "fresh" and "stale" mean, see core_concepts/5_layer_stack.

Powered by Flux