Browser
Adapter
History API synchronization with push, replace, and popstate.
- Package
- @queryweave/browser
- Runtime
- Browser
- Depends on
- @queryweave/core
- Framework required
- No
What it owns
Section titled “What it owns”- Reading
location.search. - Writing through
history.pushStateandhistory.replaceState, preserving the path, the hash, and the existing history state object. - Listening for
popstateand reporting the change.
What it does not own
Section titled “What it does not own”Decoding, validation, defaults, and canonical form — all of that is the model. The adapter moves strings.
Install
Section titled “Install”pnpm add @queryweave/core @queryweave/browserExample
Section titled “Example”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),});
const adapter = createBrowserAdapter();const runtime = createQueryRuntime({ model: products, adapter });
runtime.subscribe((snapshot) => { render(snapshot.values);});
await runtime.update({ search: "vue" }); // ?search=vueawait runtime.update({ page: 2 }, { navigation: "replace" });// ?search=vue&page=2 — the current entry was replacedHistory1 / 1
- 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/productsNothing happens at import time
Section titled “Nothing happens at import time”The adapter resolves its target when you create it, not when the module is evaluated, and it
attaches the popstate listener only when something subscribes. Importing
@queryweave/browser in a server bundle is therefore harmless; calling createBrowserAdapter()
there is what fails, and it fails with a clear message rather than a ReferenceError.
// Server rendering: create the adapter on the client only.if (typeof window !== "undefined") { adapter = createBrowserAdapter();}An explicit target
Section titled “An explicit target”const iframe = document.querySelector<HTMLIFrameElement>("#product-preview");const target = iframe?.contentWindow;if (target === null || target === undefined) throw new Error("Product preview is missing.");
// This runtime reads and writes the preview frame, not the parent page.const adapter = createBrowserAdapter({ target });const runtime = createQueryRuntime({ model: products, adapter });
await runtime.update({ search: "books", page: 2 });// iframe URL: /preview?search=books&page=2Useful for tests, for multi-frame applications, and for any case where the ambient window is not the one you mean.
Cleanup
Section titled “Cleanup”adapter.dispose();Disposal removes the popstate listener, clears subscribers, and makes later navigation throw. In
a single-page application with one long-lived runtime you rarely need it; in a component that
creates its own adapter, tie it to teardown.
Edge cases
Section titled “Edge cases”- Path and hash are preserved. The adapter rebuilds the URL from
location.pathname, the new query, andlocation.hash. - An empty query drops the
?. Writing an empty output produces/products, not/products?. history.stateis carried across. The adapter passes the current state object back intopushStateandreplaceState, so router state stored there is not destroyed.- After
dispose,pushandreplacethrow. Reading does not.
How it is tested
Section titled “How it is tested”The browser Vitest project runs in real Chromium through Vitest Browser Mode, and the Playwright
suite drives the browser playground end to end — including back and forward.