Freshness and Live Data FULL
Every query answers one question in its declaration. When should this data refresh? The answer is a ladder of options, from a single fetch to a server push stream, all sharing the same state surface and the same safety rules.
The Refresh Ladder
| Rung | Declaration | Behavior |
|---|---|---|
| Once | 'once' (default) | Fetch when first observed. Refresh only when asked. |
| Poll | 30 | Refetch every N seconds while the query is on screen. |
| Conditional poll | 'etag:60' | Re-check every N seconds with a conditional request. The server answers 304 when nothing changed, and no data moves. |
| Focus | 'focus' | Refresh when the tab regains focus. |
| Reconnect | 'reconnect' | Refresh when the browser comes back online. |
| Server push | 'sse' | Hold an EventSource open and apply what the server sends. |
Rungs combine as an array. A typical dashboard query reads refresh: ['focus', 'etag:60']. Fresh when the user returns, cheap in the background.
Conditional Requests
When a response carries an ETag header, the query remembers it. Every conditional re-check sends If-None-Match, and a 304 answer costs a round trip with no body, no parsing, and no DOM work. Static file hosts and most API servers do this without any configuration on your part.
A Live Feed Without a Server
Liveness does not require SSE. A function source plus invalidate() turns any event-producing transport into a live query. Here the transport is a simulated in-page feed; in your app it could be a WebSocket or a sync engine:
The pattern:
wildflower.query('prices', {
from: () => feed.snapshot(), // any transport, any shape source
key: 'id'
});
// Each transport event invalidates; the engine re-runs the source
// and patches only the rows that changed.
feed.onTick(() => wildflower.getQuery('prices').invalidate());
Server-Sent Events
The 'sse' rung holds a browser-native EventSource open and treats the server as the source of truth for when data changes. Two kinds of message are understood:
- A message with a JSON body is the new result set. It applies directly, and any fetch already in flight is discarded in its favor.
- An empty message is an invalidation signal. The query re-checks its
fromURL conditionally.
wildflower.query('liveItems', {
from: '/api/items', // initial load and catch-up fetches
refresh: 'sse',
stream: '/api/items/stream' // dedicated event-stream endpoint
});
The stream option exists because routing a fetch and an event stream through one URL tends to fall apart at proxies and gateways. When your setup does serve both from one URL, omit stream and the query connects to from.
isStale and syncError, keeps every row on screen, and lets EventSource reconnect on its own. The reopen triggers one conditional fetch to catch up on anything missed.
When a Sync Fails: Automatic Retry
Networks drop, servers restart, and a query in the wild will eventually see a failed fetch. By default a failure lands immediately. The first load sets error, a background refresh sets syncError, and your markup decides what to show. Declaring retry puts an automatic ladder between the failure and that landing:
wildflower.query('orders', {
from: '/api/orders',
key: 'id',
refresh: ['focus', 'etag:60'],
retry: 3
});
The number is the whole surface. Three retries run at one second, two seconds, and four seconds, doubling each time with a cap at thirty seconds. There is no policy object to configure, because a retry policy you can program is a library you now maintain.
While the ladder runs, the query holds its state rather than flickering through it. Rows stay on screen, error and syncError stay unset, and the loading or stale flag simply persists until the ladder resolves. A success on any rung applies normally and resets the count. Exhaustion lands exactly the state the failure would have landed on attempt one, so markup written without retry in mind keeps working unchanged.
- A manual
refresh()orinvalidate()starts a fresh episode. Any pending retry is cancelled and the count resets. The user's click always wins over the schedule. - Going offline suspends the ladder. Attempts are not burned against a network that is known to be down. When the browser comes back online, the ladder resumes where it left off. This works with or without the
'reconnect'rung. - Superseded requests never retry. When a newer call replaces an older one in flight, the old request is aborted, and an abort is not a failure.
Lifecycle: Active While Observed
A query is active while an element bound to it is in the document. When the last one leaves, the query waits about five seconds, then stands down. Timers stop, listeners detach, streams close, and any request in flight is aborted. The data and the remembered ETag stay put, so if the query is observed again it shows its last results instantly and catches up with a single conditional fetch.
The grace window makes this invisible. Toggling a panel with data-show, switching tabs, and list churn all pass through it without tearing anything down. You do not manage any of this, and there is nothing to configure.