Sources, Refinement, and Writes FULL
A query's from is a URL or a function. That is the entire source model. Refinement happens in computeds, and writes follow one small pattern.
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 by construction. The engine guarantees that the last call wins, aborts superseded requests, and keeps the previous rows on screen flagged isStale until the new result lands. There is no flicker window where a slow early response overwrites a fast later one.
Function Sources
A function from returns rows, or a promise of rows, from anywhere. IndexedDB, a WebSocket message buffer, a sync engine, a computation. The framework calls it, applies the result, and never asks what is behind it:
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.
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:
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>
[...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 kickoff in init() works when the parent never changes after load. The subscription form is the one to reach for 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.
Writes
Queries read. Writes go through your normal actions, and the query is told to catch up:
wildflower.component('product-form', {
state: { draft: {} },
async addProduct() {
await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.draft)
});
wildflower.getQuery('products').invalidate();
}
});
Mutate, then invalidate. The refreshed result comes back through the same pipeline as every other update, with row identity keeping the DOM changes minimal.
Optimistic Updates with patch()
Sometimes the round trip is too slow for the interaction. The user adds an item and expects to see it now, with the server catching up behind the gesture. patch() is the sanctioned way to write into a query's rows ahead of the sync:
wildflower.component('order-list', {
state: { draft: '' },
async addItem() {
const q = wildflower.getQuery('orders');
const item = { id: 'tmp-' + Date.now(), name: this.draft, pending: true };
q.patch([item]); // on screen immediately
await fetch('/api/orders', { method: 'POST', body: JSON.stringify(item) });
q.invalidate(); // the server's answer reconciles
}
});
Patched data applies through the same pipeline as fetched data. Rows that share a key with existing rows update in place, rows with new keys append, and a row whose declared deleted field is truthy removes its target, which makes optimistic deletes one call. On a record query, patching replaces the record's fields. The store marks itself isStale until the next successful sync confirms or corrects what you wrote, and lastSync is untouched because nothing has actually synced.
pending: true and the row template binds a class to it. When the confirming sync returns the server's canonical rows, the temporary id and the pending flag disappear together, and row identity keeps everything else on screen untouched.
If the server rejects the write, call invalidate() anyway. The next sync restores the server's truth, which is the entire rollback story. Mutation queues, rollback journals, and offline outboxes are application or extension territory.
rows, the sync flags) from application code draws a dev warning (WF-950), because the next sync will overwrite whatever you wrote. patch() is the sanctioned form of that write. It never warns, it survives merges by key, and staleness tracking stays accurate.When Plain fetch() Is the Better Tool
A query earns its declaration 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(). Use the tool that matches how long the data needs to stay alive.