# htmx-query
> Compact guidance for AI coding agents working with this repository.
## Purpose
htmx-query is a small htmx 2 and 4 extension that adds opt-in stale-while-revalidate (SWR) HTML caching, retry with exponential backoff, in-flight GET deduplication, and optimistic template insertion. It is not a JSON data store or SPA state manager.
## Runtime and distribution
- Requires `htmx.org >=2.0.0 <3 || >=4.0.0-beta6 <5`. htmx 4 remains pinned
to the verified prerelease floor until 4.0.0 is stable.
- `dist/htmx-query.min.js`: self-registering IIFE for script tags/CDNs.
- `dist/htmx-query.js`: ESM. Import `register` and call `register(htmx)`.
- `dist/htmx-query.cjs`: CommonJS.
- `src/index.d.ts`: TypeScript declarations for the public registration and query APIs.
- Source is ES2018 JavaScript in `src/`; build with `npm run build`.
## Installation
```html
...
```
htmx 4 extensions are global. htmx 2 requires `hx-ext="query"` on the body or
the intended inherited subtree.
```js
import htmx from 'htmx.org';
import { register } from 'htmx-query';
register(htmx);
```
## Public API
### Attributes
| Attribute | Meaning |
|---|---|
| `hx-swr="TTL"` | GET-only cache. Fresh entries render and cancel the request; stale entries render then revalidate. Also opts into dedupe. |
| `hx-swr-key="key"` | Override cache identity; default is `verb:finalRequestPath`. |
| `hx-swr-vary="Header, ..."` | Add explicitly approved request-header values to the cache key. `Cookie` and `Authorization` disable caching and dedupe. |
| `hx-swr-prefetch="hover focus visible"` | Token list. Explicitly opt into one same-origin best-effort GET that fills the SWR cache without swapping the source element: `hover` for pointer users, `focus` for keyboard users, `visible` on viewport entry (IntersectionObserver; inert where unsupported). |
| `hx-retry="N"` | Retry eligible failures up to N times; the effective maximum is 10. |
| `hx-retry-delay="ms"` | Retry base delay, default 1000 ms; exponential backoff is capped at 10x base and 30 seconds, with equal jitter. Numeric and HTTP-date Retry-After values override it before the same cap. |
| `hx-retry-unsafe` | Allow retrying non-GET verbs. |
| `hx-optimistic="#template"` | Append cloned template nodes to the target before request completion. |
### JavaScript helpers and events
```js
htmx.query.invalidate('/todos'); // backwards-compatible substring match
htmx.query.invalidate('/todos', { mode: 'path' }); // exact resource path plus / and ? boundaries
htmx.query.clear();
htmx.query.peek(); // Map copy for debugging
htmx.query.stats(); // { namespace, cache: { entries, bytes, hits, ... }, dedupe: { inflight, waiters } }
htmx.query.debug(); // read-only stats plus cache keys
htmx.query.resetMetrics(); // reset hit/miss/stale-error counters, keep entries
htmx.query.setNamespace('account-id'); // scopes new cache keys and clears previous account data
htmx.query.configure({ cacheEvents: ['evict', 'skip'] }); // true, false, or an action filter
htmx.query.configure({ cache: { maxEntries, maxCacheBytes, maxEntryBytes, maxVariants } }); // resize cache bounds; invalid fields ignored, shrinking evicts immediately, returns effective limits
htmx.query.configure({ persist: true }); // opt-in sessionStorage mirror for this tab only; hydrates on enable, writes back on pagehide/hidden; namespace-scoped and dropped on namespace change or clear()
htmx.query.configure({ crossTab: true }); // opt-in BroadcastChannel propagation of invalidation only (never cached HTML) to same-origin tabs sharing the namespace
htmx.query.put(key, html, { ttl }); // seed an entry under an hx-swr-key value or 'get:'; namespace-scoped; ttl seconds acts as origin max-age (effective TTL = min(hx-swr, ttl)); returns boolean
```
Successful responses may send `HX-Cache-Invalidate` as a JSON object or array,
for example `{"path":"/todos","mode":"path"}`. Only non-empty string paths
are honored; malformed headers and failed responses are ignored.
- `hq:invalidated`: bubbles from `body` after invalidation; detail is `{ prefix, mode, count }`.
- `hq:retryExhausted`: bubbles from the requesting element after retries are exhausted.
- `hq:cache`: bubbles from `body` with `{ action, key?, bytes?, entries?, reason? }` for `hit`, `miss`, `store`, `evict`, `skip`, and `clear`.
- `hq:staleError`: bubbles from `body` with `{ key, status }` when a stale rendered fragment's background revalidation fails.
- `hq:prefetch`: bubbles from `body` with `{ action: 'success' | 'error' | 'skip', path?, reason? }` for an explicitly opted-in prefetch.
The documented attributes, `register()` return value, query methods, stats, and
`hq:*` detail shapes are the public API. Update `src/index.d.ts`, README, and
the consumer type test together; review CHANGELOG.md before upgrading a
pre-1.0 minor version.
## Binding behavior and invariants
- Feature modules consume the normalized event produced by
`src/htmx-adapter.js`; do not reach directly into htmx 2 `requestConfig`/XHR
or htmx 4 `detail.ctx`/Fetch shapes.
- The before-request order is cache serve, dedupe, then optimistic insertion.
The adapter applies each major's cancellation semantics.
- Cache only successful, non-empty GET responses from elements with `hx-swr`. Never cache error responses, `hx-swap-oob` payloads, or responses with `Cache-Control: no-store` or `private` (including parameterized directives). Skip responses whose `Vary` header includes a request header other than `HX-Request`, because that dimension is absent from the cache key.
- `hx-swr-vary` may opt in non-sensitive request headers (for example `Accept-Language`) as key dimensions; it must list every non-HX-Request header named by the response `Vary`. `Cookie` and `Authorization` are never key dimensions.
- `no-cache` validates before reuse, `max-age` caps `hx-swr`, and stale `must-revalidate` entries are not rendered before validation. `Age` and apparent `Date` age backdate cache entries; `Expires` provides an origin-relative TTL when `max-age` is absent. Browsers hide Set-Cookie response headers; personalized endpoints must explicitly send `private` or `no-store`.
- A stale cached response with an `ETag` sends `If-None-Match`. On a `304`, retain the cached HTML and refresh its age; suppress htmx's otherwise-empty 304 swap. When no stale HTML was rendered before the validation (a `no-cache` entry, or a refused render beyond the stale-while-revalidate window), the `304` swaps the validated cached entry into the target instead.
- `stale-while-revalidate=N` bounds stale rendering to N seconds past ORIGIN freshness (the longer of `hx-swr` and `max-age`/`Expires`); beyond it the revalidation proceeds without a stale swap. `stale-if-error=N` lets rendered stale HTML stand through a failed revalidation without an `hq:staleError` event. Absent directives leave stale serving unlimited.
- A non-numeric `hx-swr` value degrades to `0` (always stale, always revalidate); it never disables cache serving.
- Cached swaps preserve the requester's resolved `hx-swap` and `hx-select`
behavior. htmx 2 uses its implicit inheritance controls; htmx 4 uses explicit
`:inherited` attributes. Selector HTML is memoized once per cache entry and
selector, with at most 16 selector variants per entry; variants beyond the
byte budget are not retained.
- For predictable first-hit latency, prefer caching a small server fragment directly; reserve `hx-select` for a small extraction from a larger response.
- Dedupe only applies to cache-enabled GETs. A winner failure must release its in-flight key even when its requester has been removed from the document.
- Retry state is scoped to requesting element + verb + final URL. A scheduled retry bypasses cache and dedupe. Preserve the original target captured before an error response can apply `HX-Retarget`.
- Optimistic rollback removes only the nodes inserted by the template; it must not restore `innerHTML` wholesale because unrelated DOM changes can occur while the request is pending.
- The cache holds at most 100 entries and 1 MiB of raw plus selected HTML; no entry, including variants, exceeds 256 KiB. Invalidation intentionally scans this bounded map. Do not add a trie/index unless the bound or profiling changes materially.
- Server-rendered HTML is the trust boundary. Do not add client-side sanitization in the extension; applications must sanitize untrusted server output before it reaches htmx. Use `setNamespace(accountId)` on account changes; it clears the previous namespace automatically.
## Source layout
- `src/index.js`: htmx extension router and public registration.
- `src/htmx-adapter.js`: dependency-free htmx 2 XHR / htmx 4 Fetch lifecycle normalization.
- `src/cache.js`: bounded cache, invalidation, selected HTML variants.
- `src/swr.js`: serve/store SWR behavior.
- `src/dedupe.js`: in-flight request registry and waiter settlement.
- `src/retry.js`: retry contexts, backoff, and target preservation.
- `src/optimistic.js`: targeted optimistic node insertion/removal.
- `src/utils.js`: htmx/request helpers and cached swaps.
- `src/prefetch.js`: opt-in hover/focus cache warm-up and its lifecycle events.
- `src/invalidate.js`: HX-Cache-Invalidate response-header handling.
- `src/scope.js`: cache-key namespace scoping.
- `test/`: Vitest + jsdom + real htmx integration tests; `test/browser/` is Playwright coverage against the demo in Chromium, Firefox, and WebKit.
- `docs/index.html`: static landing page; `examples/demo.html`: interactive demo.
- `docs/production.md`: production integration, CSP, SRI, namespace, invalidation, and accessible-prefetch recipe.
## Safe change workflow
1. Retrieve project knowledge with Noli before substantive changes.
2. Update `.noli/concepts.yaml` before a new design or behavior; run `noli generate --config noli.yaml --apply --format json` and validate it.
3. Add or update a regression test for each behavior change.
4. Run `npm run prepublishOnly` (build, full tests, size limit).
5. Run the configured lint/syntax checks if present.
## Commands
```bash
npm run lint
npm run typecheck
npm run test
npm run test:browser
npm run build
npm run size
npm run bench # bounded-cache baseline, not a CI threshold
npm run bench:check # conservative major-regression guardrail
npm run prepublishOnly
npm run demo # interactive demo at http://127.0.0.1:8484
```
## Documentation
- `README.md`: package reference.
- `docs/index.html`: landing page, installation, and examples.
- `docs/migrating-to-htmx-4.md`: staged htmx 2-to-4 application migration,
verification, and rollback guide.
- `llms.txt`: this AI-agent guide.
- `llm.txt`: compatibility pointer to this guide.
- `CHANGELOG.md` and `LICENSE`: release history and MIT license.
- `RELEASING.md`: owner-only Trusted Publishing and release checklist.