Sources and Refinement FULL

A query's from is a URL or a function. That is the entire source model. Refinement happens in computeds, so there is no filter syntax or query language to learn.

URL Sources

A string from is fetched with the freshness ladder attached. The params option appends query parameters, and refresh({ params }) re-runs the request with new ones:

wildflower.query('orders', {
    from: '/api/orders',
    key: 'id',
    params: { status: 'open' }
});

// Later, from an action:
wildflower.component('order-search', {
    state: { status: 'open' },
    searchOrders() {
        wildflower.getQuery('orders').refresh({ params: { status: this.status } });
    }
});

Rapid re-queries are safe. The engine applies the last call, aborts superseded requests, and keeps the previous rows on screen flagged isStale until the new result arrives. There is no flicker window where a slow early response overwrites a fast later one.

params can also be a function, which is the form to use when the values change. It is resolved once per request, and every request resolves it, including the ones the engine starts on its own: a refetch after a write settles, a poll tick, a focus refresh. That is what keeps a paged query pointed at the page the reader is on.

wildflower.query('orders', {
    from: '/api/orders',
    key: 'id',
    params: () => ({
        status: wildflower.getStore('route').status,
        page: wildflower.getStore('route').page
    })
});

Values passed to refresh({ params }) merge over the resolved ones key by key and apply to that one request. The next request the engine starts on its own resolves the declaration again and reasserts its own values; development builds warn (WF-979) at the refetch that drops an override. If a value should stick, put it where the function reads it. When the new params name a different list rather than a different page, pass clear: true as well, so the previous list's rows do not stay up while the new one loads; see A different list, not a different page.

URL Templates v1.5+

A URL can carry :token path segments, filled from params. A param the path consumes leaves the query string, so it appears in exactly one place:

wildflower.query('comments', {
    from: '/api/articles/:slug/comments',
    key: 'id',
    params: () => ({ slug: wildflower.getStore('route').slug })
});

// requests /api/articles/how-to-train/comments

The same param can appear in a path on one URL and in a query string on another within a single query, depending on which URL names it. That is expected rather than a bug. A token is consumed by the URL that has it, and any param no URL consumed serializes normally.

Token values are percent-encoded, so one token is always exactly one path segment. A null value counts as absent, so a path never receives the string "null". When params is a static object, tokens are checked at registration, which catches a typo at load rather than at first fetch. What a token can be covers the grammar and why existing URLs are safe.

Waiting for a value

A query is usually declared before its route parameter exists. The example above is bound to an element that may well be on screen before the router has resolved route.slug, and the query's own activation fetch fires as soon as it is observed.

A read that cannot resolve its path counts as not ready rather than failed. The framework sends nothing, records nothing as an error, and leaves rows already on screen exactly as they are. A query that has never had data reports isLoading, since there is no usable data yet and nothing has failed. Bind a spinner to it and the spinner keeps showing rather than flickering through an empty state. When the value arrives, refresh the query and it loads normally.

// The router sets the value, then asks for the data.
wildflower.getStore('route').slug = 'how-to-train';
wildflower.getQuery('comments').refresh();

Development builds warn about the wait once per token, so a genuine typo still shows up in the console. A misspelled token never resolves, so without the warning the query would simply never fetch. A write with an unresolved token does the opposite and rejects; Not ready is not failed explains why the two differ.

Headers and Credentials v1.5+

An API that needs an Authorization header is the usual reason a URL source is not enough. headers takes a plain object, or a function for anything that changes:

wildflower.query('feed', {
    from: '/api/articles',
    key: 'slug',
    headers: () => ({ Authorization: 'Token ' + wildflower.getStore('auth').token })
});

The function runs once per request attempt, retries included, so a token refreshed between a failure and the retry is the one that actually goes out.

This only helps if the query retries. Most 4xx responses are treated as final and are not sent again. The exceptions are 401, 408, and 429. A 401 is retried because the credential can change between attempts. But retry defaults to 0, so a query that does not set it makes one attempt and stops.

wildflower.query('feed', {
    from: '/api/articles',
    key: 'slug',
    retry: 2,   // without this, a 401 is never retried
    headers: () => ({ Authorization: 'Token ' + wildflower.getStore('auth').token })
});

Getting a new token is your job. The query calls your function again on each attempt and uses whatever it returns. It does not know how the token is obtained or when it expires.

Most applications talk to one API, so repeating the declaration on every query is redundant. Set it once, per origin:

wildflower.config({
    headers: {
        self: () => ({ Authorization: 'Token ' + wildflower.getStore('auth').token })
    }
});

'self' is the page's own origin, which is what a relative URL matches. Name an absolute origin instead, 'https://api.example.com', for an API hosted elsewhere. A query's own headers overrides the default key by key and inherits the rest.

The keys are origins because from and to can name any host, and a credential with no origin attached would be sent to the first third-party host someone adds a query for. Credentials and redirects covers the limits of that guarantee, including the cross-origin redirect the platform cannot let any framework intercept.

The 'sse' rung's event stream is the exception. The browser's EventSource cannot carry headers, so declared headers apply to reads and writes and never to the stream, and development builds warn with WF-984 when a query with headers opens one. Authenticate a stream with a token in its URL query string, or with cookies on a same-origin stream.

If the API answers from the page's own origin, there is nothing to configure. Requests carry the session cookie already, on reads and writes alike, because browsers send cookies on same-origin requests by default. A Rails, Laravel, or Django application serving its API from the same host needs no headers declaration at all.

A shared cookie domain is not a shared origin. An origin is the scheme, host, and port matched exactly, so app.example.com and api.example.com are different origins. Cookies use different rules. One set with Domain=example.com is valid for both hosts, and it ignores the port. So a page on app.example.com calling api.example.com makes a cross-origin request, and the browser sends no cookie by default, even though the cookie is valid for that host. An app and an API on sibling subdomains is the common case here, and it needs the explicit form below.

To send a cookie across origins, call fetch yourself with credentials: 'include'. The server must also allow it, with Access-Control-Allow-Credentials: true and an Access-Control-Allow-Origin naming your origin. A wildcard is rejected when credentials are sent:

wildflower.query('feed', {
    key: 'slug',
    from: () => fetch('https://api.example.com/articles', { credentials: 'include' })
        .then(r => r.json()),
    to: (item) => fetch('https://api.example.com/articles/' + item.slug, {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item)
    })
});

The query has no credentials option, and there is no plan to add one. headers is keyed by origin so you can read which hosts get a credential straight off the config. The browser decides on its own when to send a cookie, and an origin key has no say in it, so a credentials option would send credentials to hosts the config never mentions. Calling fetch yourself puts that decision in the code, where you can see it.

Cookies also make clearPersisted() more important. A server can end a session without telling the page, and rows saved by persist would otherwise stay on disk after the user signs out.

Shaping the Response v1.5+

Plenty of APIs wrap their rows in an envelope. select names the array inside it:

wildflower.query('feed', {
    from: '/api/articles',
    key: 'slug',
    // { articles: [...], articlesCount: 47 }
    select: d => d.articles
});

Without it, a response that is not an array becomes a single row whose fields are that object's, which renders one row with the fields articles and articlesCount and no error at all. Development builds warn with WF-980, along with the neighbouring case where a response or a select resolves nothing and empties the list. Record-shaped queries are exempt, since a single object is their answer.

select is also where you read anything else the envelope carries. It runs on every read, so a total sent alongside the rows can be captured there:

select: d => {
    wildflower.getStore('route').feedTotal = d.articlesCount;
    return d.articles;
}

Function Sources

A function from returns rows, or a promise of rows, from anywhere, including IndexedDB, a WebSocket message buffer, a sync engine, or a computation. The framework calls it and applies the result, and what happens inside is entirely yours:

wildflower.query('drafts', {
    from: () => db.drafts.orderBy('modified').toArray(),  // IndexedDB
    key: 'id'
});

// Liveness is the app's call:
db.on('changes', () => wildflower.getQuery('drafts').invalidate());

A function source pairs with the timer rungs if you want scheduled re-runs, or with invalidate() if your transport already knows when things change.

A function from is called with no arguments and builds its own request, so params and select have nothing to act on for reads, and development builds warn at registration with WF-978. params is still honored for the :tokens in write URLs, which is what lets a query keep a function source while declaring its writes (the mixed shape).

Refinement Is Computeds

There is no filter syntax and no query language. A computed reads the query, refines it in plain JavaScript, and data-list renders the computed. Reads inside computeds track automatically, so the chain re-runs when rows arrive:

Live Example Search and category filter over query rows Open Full Example ↗
The whole pattern:
wildflower.component('catalog', {
    state: { filter: '', category: '' },
    computed: {
        visible() {
            const q = wildflower.getQuery('catalogItems'); // auto-tracks
            const f = this.filter.toLowerCase();
            const c = this.category;
            return q.rows.filter(p =>
                (!c || p.category === c) &&
                p.name.toLowerCase().includes(f));
        }
    }
});
<input data-model="filter">
<tbody data-list="visible" data-key="id"> … </tbody>
Copy before you sort. Computeds must not mutate the rows they read. Write [...q.rows].sort(…) rather than q.rows.sort(…). Development builds warn when a computed mutates state during its own evaluation.

Dependent Queries

When one query's parameters come from another's result, chain them explicitly. Queries are store-backed entities, so the reactive way to chain is the same way components react to any store: subscribe to the parent and refresh the dependent whenever its rows change. This keeps the chain alive through every later refresh of the parent, including focus refreshes and account switches.

wildflower.query('currentUser', { from: '/api/me', refresh: 'focus' });

wildflower.query('assignments', {
    from: '/api/assignments',
    key: 'id'
});

// In the component that owns the relationship:
wildflower.component('workbench', {
    state: {},
    subscribe: { currentUser: ['rows'] },
    onStoreUpdate(store, path) {
        if (store === 'currentUser') {
            const user = this.stores.currentUser.rows[0];
            if (user) {
                wildflower.getQuery('assignments').refresh({ params: { userId: user.id } });
            }
        }
    }
});

A one-shot call in init() works when the parent never changes after load. Use the subscription form whenever the parent is itself live. Do not chain from inside a computed; computeds are reads, and development builds warn when one mutates state during its own evaluation.

When Plain fetch() Is the Better Tool

Declare a query when you want standing freshness, shared state surfaces, or the loading and error machinery. A one-shot load that a component uses once and never refreshes is still a fine job for fetch() in init().

Full Shared Favorites

Two clients reading one server. Field-level claims let a star and a rename on the same row settle independently, and an intent journal makes undo just another write.