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.
rows, the sync flags) from application code draws a dev warning (WF-950), because the next sync will overwrite whatever you wrote. The write still lands: writing rows optimistically and then calling invalidate() to reconcile is a legitimate pattern, and the warning fires once per store so it never nags a deliberate choice.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.