# 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.