Skip to content
Runtime
Framework

Vanilla TypeScript

Framework

The runtime on its own, with no framework in the dependency graph.

Package
@queryweave/core
Runtime
Any ECMAScript runtime
Depends on
Framework required
No
InstallQuick examplePackage: @queryweave/core

This is not a fallback. It is the baseline every other integration is built on: a model, an adapter, a runtime, and a render function you write yourself.

Terminal window
pnpm add @queryweave/core @queryweave/browser
filters.ts
import { createBrowserAdapter } from "@queryweave/browser";
import { createQueryRuntime, defineQueryModel, param } from "@queryweave/core";
const products = defineQueryModel({
search: param.text().optional(),
page: param.integer({ min: 1 }).default(1),
status: param.choice(["all", "active", "archived"]).default("all"),
});
const runtime = createQueryRuntime({
model: products,
adapter: createBrowserAdapter(),
});
const searchInput = document.querySelector<HTMLInputElement>("#search");
const results = document.querySelector<HTMLElement>("#results");
function render(values: ReturnType<typeof runtime.read>["values"]): void {
if (searchInput !== null && searchInput.value !== (values.search ?? "")) {
searchInput.value = values.search ?? "";
}
if (results !== null) {
results.textContent = `Page ${String(values.page)} · ${values.status}`;
}
}
runtime.subscribe((snapshot) => {
render(snapshot.values);
});
render(runtime.read().values);
searchInput?.addEventListener("input", () => {
void runtime.update({ search: searchInput.value || undefined }, { navigation: "replace" });
});

Three details are worth naming:

  • Render on subscribe and once immediately. Subscribing does not deliver the current state; it delivers changes. Read once to paint the first frame.
  • Guard the input assignment. Writing the same value back into a focused field moves the caret. Compare first.
  • || undefined clears the key. An empty string would produce ?search= and an empty issue; undefined omits it. See defaults and absence.

Nothing extra is needed. The adapter listens for popstate, and your subscriber receives the same snapshot it would after a programmatic transition.

No framework is involved here either — the simulator on this page is plain TypeScript over the same runtime.

History1 / 1

Products

6 matching

  • Edge runtime handbookactive$59
  • Node.js request toolkitactive$39
  • Nuxt deployment guidearchived$19
  • history.pushState
  • history.replaceState
  • popstate

The adapter writes through the History API and re-reads on popstate.

Type a query, press Enter.

Typed state

{
  "page": 1,
  "sort": "created_at",
  "status": "all"
}

Canonical URL

Valid
/products

    The runtime applies every transition immediately. For a search field, that is one history entry per keystroke unless you either use navigation: "replace" or debounce:

    let timer: ReturnType<typeof setTimeout> | undefined;
    searchInput?.addEventListener("input", () => {
    clearTimeout(timer);
    timer = setTimeout(() => {
    void runtime.update({ search: searchInput.value || undefined });
    }, 200);
    });

    Scheduling inside the runtime is planned and deliberately absent today, so this stays your decision.

    const unsubscribe = runtime.subscribe(handler);
    // later
    unsubscribe();
    runtime.dispose();

    In a page-lifetime application you can skip this. In anything that mounts and unmounts, do not.

    The same model reads the request:

    import { readRequestQuery } from "@queryweave/server";
    const result = readRequestQuery(request, products);
    const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
    // Render the same search, page, and filters the client will read from this URL.

    The client then creates its runtime and immediately reads the same URL, so the first client render matches what the server produced — no state transfer required, because the URL is the transfer.