Architecture
QueryWeave separates query semantics from runtime integration. Everything else on this page follows from that sentence.
The layers
Section titled “The layers”Definition, transformation, runtime transitions, and environment, top to bottom.
- Definition layer (param, QueryCodec, QueryModel)
- Transformation (decode, encode, normalize)
- Runtime transitions (QueryRuntime)
- Environment (QueryAdapter / QuerySource)
Definition layer → Transformation. Transformation → Runtime transitions. Runtime transitions → Environment.
Written as a dependency direction:
Definition layer param, QueryCodec, QueryModel ↓Transformation decode, encode, normalize ↓Runtime transitions QueryRuntime ↓Environment QueryAdapter / QuerySourceA layer may depend on the one above it. Nothing depends downwards: the model cannot see the runtime, and the runtime cannot see which environment it is talking to beyond the four-method contract.
Two environment shapes
Section titled “Two environment shapes”Mutable adapter chain on the left; read-only source chain on the right.
- QueryModel — Mutable environment (meaning)
- QueryRuntime (transitions)
- QueryAdapter (reads, navigates, notifies)
- Browser, router, memory (mutable environment)
- Request — Request-scoped source (incoming)
- QuerySource (read-only)
- QueryModel.decode() (typed result)
QueryModel → QueryRuntime. QueryRuntime → QueryAdapter. QueryAdapter → Browser, router, memory. Request → QuerySource. QuerySource → QueryModel.decode().
A request-scoped environment implements QuerySource — one method. A mutable environment implements
QueryAdapter, which extends it with push, replace, and subscribe.
This distinction is in the type system rather than in documentation, so server code physically
cannot call push. A capability that does not exist is absent, not present and throwing.
Package dependency direction
Section titled “Package dependency direction”@queryweave/core ↑ ├── @queryweave/browser ├── @queryweave/server │ ↑ │ └── @queryweave/node ├── @queryweave/standard-schema ├── @queryweave/testing ├── @queryweave/vue │ ↑ │ └── @queryweave/nuxt └── @queryweave/vue-router ↑ └── @queryweave/nuxtTwo consequences worth naming:
@queryweave/vue-routerdepends on core alone, not on@queryweave/vue. The adapter is useful without the binding, so coupling them would force an unnecessary dependency on anyone who wants router synchronization with their own binding.@queryweave/nodedepends on@queryweave/serverrather than duplicating it. Node’s contribution is resolving an absolute URL; decoding stays in one place.
Constraints the compiler enforces
Section titled “Constraints the compiler enforces”@queryweave/core compiles with lib: ["ES2023"] and types: []. There is no DOM library and no
Node library, so:
window,document,history,location, andprocessdo not exist as names,URLSearchParamsdoes not exist either — it is accepted structurally asIterable<QueryEntry>,- and the urlencoded codec is hand-written, because there is no
URLSearchParamsto delegate to.
That last point comes with an obligation: formatQueryString must stay byte-identical to
URLSearchParams.prototype.toString, and a test compares them.
isolatedDeclarations is on for every publishable package except @queryweave/nuxt, whose module
type cannot be expressed under it. Exported functions and constants therefore carry explicit type
annotations.
Constraints a script enforces
Section titled “Constraints a script enforces”Compiler settings cannot express “this package must never import Vue”. Three repository checks do, each reading something the previous one cannot see:
| Check | Reads | Catches |
|---|---|---|
pnpm boundary:check |
sources and manifests | Dependency direction, forbidden imports, forbidden globals, workspace protocols, publishability |
pnpm artifacts:check |
built output and archives | Externalization, phantom dependencies, CommonJS emits, export-map targets, type resolution |
pnpm consumers:check |
installed packed archives | Anything only a real consumer sees |
The globals check matches whole words in source text, comments included. That is blunt on purpose:
a comment mentioning window in @queryweave/core is a signal that someone is thinking about the
wrong layer.
The identity check
Section titled “The identity check”tests/api-identity.test.ts fails the build if useQueryState, useQueryStates, parseAs*,
createParser, createLoader, createSerializer, or withDefault appears in any package or
application source, or if react appears in any manifest.
This is not hostility toward other libraries. It is a guard against a specific failure mode: a model-first API accreting hook-shaped conveniences one pull request at a time until it is a worse copy of something else.
Validation strategy
Section titled “Validation strategy”Core codecs decode and encode without choosing a validation vendor.
@queryweave/standard-schema is the only interoperability boundary, and it depends on the
specification’s types alone. No published package may take a validator as a runtime dependency, and
the boundary checker enforces that by name for Zod, Valibot, ArkType, Yup, Joi, Superstruct, and
io-ts.
Testing strategy
Section titled “Testing strategy”Thirteen Vitest projects, one per boundary: core, runtime, testing, browser, server,
node, standard-schema, vue, vue-router, nuxt, docs, types, and repository. The
browser project runs in Chromium, Firefox, and WebKit through Vitest Browser Mode. The types
project asserts both what the inferred types accept and what they reject.
Beyond the unit suite: a Playwright suite drives the browser playground in the same three engines,
and eight consumer fixtures install packed archives into clean projects outside the workspace —
including a Nuxt fixture that builds and server-renders real pages/ routes, two of them
concurrently with different queries to prove they do not share state, and a Vue Router 4 fixture
beside the Vue Router 5 one. The support matrix lists what each fixture pins.
Where each area is tested
Section titled “Where each area is tested”| Area | Suites and fixtures |
|---|---|
| Query models | The core project covers definition, inference, and composition. tests/types asserts both what the inferred types accept and what they reject, so a widened type fails the build. |
| Parameters | tests/core/semantics.test.ts covers presence, recovery, and the issue codes each family emits. tests/core/parameters.test.ts covers grammar, defaults, empty lists, transforms, and exceptions. tests/core/round-trip.test.ts asserts that decoding an encoded value returns the original for every family. |
| Decoding and encoding | tests/core/query-input.test.ts covers parsing and formatting against URLSearchParams. tests/core/model.test.ts covers results, ordering, and omission. tests/core/round-trip.test.ts asserts decode-encode stability. |
| Issues | tests/core/semantics.test.ts asserts the code emitted for each failure mode. tests/runtime/failures.test.ts asserts that a runtime snapshot stays usable while reporting them. |
| Runtime | The runtime project drives a runtime against the memory adapter and asserts the single-notification rule, unmanaged-key preservation, and disposal. tests/runtime/failures.test.ts covers behavior when decoding fails. tests/runtime/serialization.test.ts covers overlapping transitions, outcomes, listener errors, and pending decodes. |
| Transitions | tests/runtime/runtime.test.ts asserts one write and one notification per operation, patch merging, removal, reset, and transaction batching, against the memory adapter. tests/runtime/serialization.test.ts asserts ordering under an asynchronous adapter, every outcome, and the memory adapter’s guard option, which simulates a refusing environment. tests/runtime/scheduling.test.ts asserts throttled writes, the composition of held transitions, cancellation, and disposal. |
| Browser | The browser project runs in Chromium, Firefox, and WebKit through Vitest Browser Mode. The Playwright suite drives the browser playground end to end in the same three engines, including back navigation. |
| Server | tests/server/server.test.ts covers reading, encoding, and link building, including unmanaged-key preservation, malformed URL strings, and the relative-base fallback. The node fixture installs the packed archive together with @queryweave/node and runs the same helpers over a real request. |
| Node.js | tests/node/node.test.ts covers URL resolution, header precedence, the forwarded-header opt-in, HTTP/2 pseudo-headers, TLS sockets, mounted routers, hostile paths and headers, and decoding. The node fixture runs the packed archive against a real node:http server. |
| Testing | tests/testing/memory-adapter.test.ts covers the stack, truncation, notification, and disposal. This package has the highest coverage threshold in the repository, because everything else is tested through it. |
| Standard Schema | tests/standard-schema/compatibility.test.ts runs one contract across Zod, Valibot, and ArkType, asserting normalized issues, inverses, throwing transforms, and inferred output types, and never a vendor’s own error shape. The standard-schema fixture repeats a Valibot slice of it against the packed archive. |
| Vue | The vue project runs bindings inside effect scopes and server renders against the memory adapter, asserting reactivity, field bindings, issue propagation, pending decodes, and scope cleanup. The vue fixture type-checks the published archive against a Vue application. |
| Vue Router | tests/vue-router/adapter.test.ts drives a real router through push, replace, refused and redirected navigation, a subscriber that unmounts before another subscribes, prototype-named keys, and repeated keys. The vue-router and vue-router-4 fixtures run the packed archive on both supported majors. |
| Nuxt | tests/nuxt/module.test.ts and tests/nuxt/registration.test.ts cover module setup, plugin registration, and the server-side refusal. The nuxt fixture also renders a second route with its own model. It runs no browser, so hydration and client-side navigation are covered by the Vue Router and browser suites. |
Deferred decisions
Section titled “Deferred decisions”These are open, and their absence is deliberate:
- Tuple, object, nested, and JSON families, whose URL spellings ADR 0011 already fixes.
- Per-parameter configurability of default omission and recovery policy.
- Model-level refinements that change the model’s output type.
- Server response contribution, such as canonical-URL redirects.
- Freezing the public contracts for 1.0.
See the roadmap for sequencing.
The decision records
Section titled “The decision records”Each major API decision has an ADR in the repository:
| ADR | Decides |
|---|---|
| 0001 | Model-first public API, and the identities rejected |
| 0002 | Query input, duplicate keys, canonical encoding |
| 0003 | Parameter semantics and the issue taxonomy |
| 0004 | Runtime transitions and adapter contracts |
| 0005 | Validation interoperability |
| 0006 | Vue binding shape and Nuxt request scoping |
| 0007 | API consistency corrections and packaging uniformity |
| 0008 | Tree-shakable parameter constructors |
| 0009 | Transition outcomes, serialized transitions, pending decodes, refinement inverses |
| 0010 | Throttled writes, held transitions, cancellation |
| 0011 | Date and instant families; URL spellings for tuples, objects, and JSON |