Boundaries and Guarantees FULL v1.5+
What a query guarantees at its edges, and where it will not guess. None of this is needed to use a query. It answers the question behind a surprising result, and it states the limits of each guarantee.
Not Ready Is Not Failed
A read that cannot resolve a :token in its path sends nothing, records nothing, and waits. A write in the same position rejects and identifies the token (WF-972).
A read fires on the framework's schedule and may simply be early, so treating an unresolved token as a failure would turn ordinary route timing into an error state. A write runs because your code called it, so a missing value there is a bug in the call rather than a matter of timing.
// route.slug is still null here.
wildflower.getQuery('comments').refresh(); // waits, isLoading stays true
wildflower.getQuery('comments').write({ id: 7, body: 'edited' });
// rejects: the :slug token has no value
Development builds warn about a waiting token once, so a route that has not resolved yet is distinguishable from a token misspelled badly enough that it never will.
What a Token Can Be
URLs that worked before templates existed still work. A token has to be an identifier and can never start with a digit, so the port in http://localhost:3000/api/tasks is not a token. Only the path is scanned, never the scheme, the authority, or the query string, so ?filter=type:book and https://user:pass@host/api are untouched. There is nothing to escape.
Token values are percent-encoded, so one token is always exactly one path segment. A value containing a slash cannot become two segments, and a value carrying .. cannot climb the path, so application state interpolated into a URL cannot retarget the request.
One case has no escape syntax: a literal :word in a path, as in Google-style custom methods. Percent-encode the colon.
from: '/api/articles/:slug/comments' // :slug is a token
from: 'http://localhost:3000/api/tasks' // :3000 is a port, not a token
from: '/api/items?filter=type:book' // query string is never scanned
from: '/v1/items%3AbatchGet' // a literal colon, percent-encoded
Credentials and Redirects
Framework-level header defaults are keyed by origin. from and to can name any host, so a default with no origin attached would be sent to the first third-party host someone adds a query for. Scoping also makes "which origins receive credentials" one readable declaration rather than a search.
The guarantee covers the request you make, and stops at a redirect. On a cross-origin hop the platform removes Authorization and forwards every other header, so an API key declared for the first origin arrives at the second. No framework code runs between the two requests, so the hop can be reported but not intercepted: development builds warn (WF-986) the first time a response with declared headers shows one. Point from at the final URL so no redirect happens.
Headers are not scrubbed on the hop. A scrub is a list of header names to remove, and such a list becomes incomplete as new credential headers appear; the platform's own list grew from Authorization to cookies to proxy credentials over years. The warning reports every cross-origin hop regardless of which headers are involved.
What a Function Source Cannot Honor
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.
There is one exception. params still fills :token segments in write URLs, so a query can keep a function source and declare params for its writes. That mixed shape fits a read that chooses between two endpoints at runtime, which a single URL cannot express, and it keeps the write side declarative where the read side cannot be.
The Refetch Race
A write that resolves with nothing is followed by a refetch, and that refetch asks your server for the rows immediately after the save. If the machine answering reads is a step behind the machine that took the write, which happens with read replicas and with caches in front of an API, the answer can still be the old data. The save worked, but the rows repaint from before it, and the change appears to vanish until a later refresh brings it back.
The query applies what the server says. It cannot tell a stale replica answer from a change another user just made, and discarding the second to protect against the first would lose real edits. Where your infrastructure reads from a replica, declare a confirmation. The save's own response then carries the saved record, applies directly, and no refetch follows it.
// Refetches after the save, and can read from a lagging replica:
to: '/api/items/:id',
body: item => item
// Applies the save's own response, so no read follows it:
to: {
update: {
url: '/api/items/:id',
method: 'PATCH',
body: item => item,
confirmation: d => d.item
}
}
The reverse race is handled for you. A refresh already in flight when a save confirms describes the world from before the save, so its answer is discarded rather than allowed to overwrite confirmed rows. The next refresh fetches normally.
Why Writes Never Retry
Reads retry on a ladder; writes revert. A failed read can safely re-run because a GET is idempotent. A POST is not: one that failed after the server had already committed the change looks, from the client's side, identical to one that failed before it arrived. Retrying blind risks creating the same order twice.
So the choice of whether to retry, and the write() call that does it, stay with you. The retry option is a read-side option and never applies to writes.
Why Two Queries Do Not Share a Row
Queries are independent stores, each holding its own copy of whatever rows it has. The same server entity appearing in two queries means two copies, each with its own claims, and a write through one leaves the other alone until that query refreshes on its own schedule.
The alternative is a shared entity, where a write in one place updates every view of that row. That requires a normalized cache, with identity rules, merge policies, and a dependency graph to keep the copies consistent. Independent copies mean a write cannot reach a view it was not aimed at. The cost is lag between sibling views, and invalidateQueries() closes it where two views must agree immediately.
await wildflower.getQuery('order-detail').write({ id: 42, status: 'shipped' });
await wildflower.invalidateQueries('orders', 'dashboard-counts');
The Store Is Engine-Owned
A query's store fields belong to the engine. Assigning to rows or to the sync flags from application code draws a development warning (WF-950), because the next sync overwrites whatever you wrote. patch() is the sanctioned form of that write: it never warns, it merges by key, and staleness tracking stays accurate.