The query runtime
A model says what a query means. A runtime connects that meaning to a place that stores it.
import { createQueryRuntime } from "@queryweave/core";
const runtime = createQueryRuntime({ model: products, adapter, navigation: "push", // the default for every transition on this runtime});Mutable environment — reads, navigates, and notifies
Mutable environment — reads, navigates, and notifies
- QueryModel (meaning)
- QueryRuntime (transitions)
- QueryAdapter (synchronization)
- Environment (environment)
QueryModel → QueryRuntime. QueryRuntime → QueryAdapter. QueryAdapter → Environment.
Reading
Section titled “Reading”const snapshot = runtime.read();
snapshot.status; // "valid" | "invalid"snapshot.values; // complete typed state, always presentsnapshot.issues; // everything decoding reportedsnapshot.result; // the underlying DecodeResultread decodes the adapter’s current query every time it is called. There is no cache, so a
snapshot can never be stale relative to the environment.
When decoding fails, values is the model’s defaults merged with whatever did decode. That is why
values is safe to render unconditionally, and why status — not a boolean on the snapshot —
carries the distinction.
Changing state
Section titled “Changing state”Five named operations, all asynchronous, all returning what they wrote:
await runtime.update({ page: 2 }); // merge a patchawait runtime.replace(nextValues); // swap the whole stateawait runtime.remove(["search"]); // omit keys from the outputawait runtime.reset(); // back to declared defaultsawait runtime.transaction((draft) => { // several changes, one write draft.search = "vue"; draft.page = 1;});Each returns a QueryTransitionResult:
const { navigation, output, snapshot } = await runtime.update({ page: 2 });
navigation; // "push" | "replace" — what was actually usedoutput; // the canonical entries written, including unmanaged keyssnapshot; // the state after the writeTransitions covers each operation and when to reach for it.
One transition, one notification
Section titled “One transition, one notification”Every transition follows the same path: start from the current typed state, apply the change once, encode once, navigate once, notify once.
The notification does not come from the transition. It arrives through the adapter’s subscription,
because the environment is the source of truth — a browser back button and a call to update must
produce the same kind of event, or subscribers would have two code paths to reconcile.
const seen: number[] = [];const unsubscribe = runtime.subscribe((snapshot) => { seen.push(snapshot.values.page); render(snapshot.values);});
await runtime.update({ page: 2 });seen; // [2] — one transition, one notificationThe runtime subscribes to the adapter lazily, on the first subscribe call, so a runtime that is
only ever read never attaches a listener.
Unmanaged keys survive
Section titled “Unmanaged keys survive”model.encode emits only the keys the model declares. The runtime is what preserves the rest:
// current URL: ?page=2&utm_source=newsletterawait runtime.update({ page: 3 });// written: ?page=3&utm_source=newsletterManaged keys are written first, in the model’s definition order, and unmanaged entries follow in their original order. This is what makes QueryWeave adoptable for part of a URL rather than all of it.
Disposal
Section titled “Disposal”runtime.dispose();Disposal clears the subscribers, detaches the adapter subscription, and makes every later operation throw. It is idempotent. In a framework binding this is wired to the component scope for you; in plain TypeScript, call it when the runtime’s owner goes away.
What the runtime does not do
Section titled “What the runtime does not do”Transitions are applied immediately and independently. There is no scheduling, throttling, coalescing, or cancellation, and two overlapping transitions are not reconciled — the second reads the state the first produced.
This absence is deliberate and recorded in ADR 0004. For a fast-changing input such as a search field, debounce at the call site:
let timer: ReturnType<typeof setTimeout> | undefined;
input.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => { void runtime.update({ search: input.value }, { navigation: "replace" }); }, 200);});How it is tested
Section titled “How it is tested”The runtime Vitest project drives a runtime against the memory adapter from
@queryweave/testing, asserting the single-notification rule, unmanaged-key preservation, and
disposal; tests/runtime/failures.test.ts covers behavior when decoding fails.