All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
0.11.0 - 2026-07-31
- Mutation lifecycle hooks accept a single event vector:
{:on-success [::saved]}. The multi-event form{:on-success [[::saved] [::toast]]}still works.
0.10.1 - 2026-05-04
- Include clj-kondo hooks in the build jar to be imported in target projects
0.10.0 - 2026-05-04
::rfq/mark-inactiveis now a noop if no active query with the caller params exists. Previously would not check this
- Added
clj-kondohooks forreg-query,reg-mutation, andinit!— enables go-to-definition, find-references, and unused-keyword warnings for registered query/mutation keys in clojure-lsp. To activate, run once in your project:This copies the hooks intoclj-kondo --copy-configs --dependencies --lint src
.clj-kondo/imports/automatically. No manual config required.
0.9.0 - 2026-04-26
set-query-datanow marks the entry stale and preserves:fetching?⚠️ Behavior change —::rfq/set-query-dataandrfq-db/set-query-datanow write:stale? true(was:stale? false). Cache writes from your code are by definition unverified — the nextensure-query(or any active subscriber) will background-refetch to confirm. If a request is already in flight whenset-query-datais called,:fetching?is preserved astrue(was unconditionally cleared) so subscribers don't see a momentary "not fetching" lie. This makes the placeholder-data pattern work out of the box (see issue #1): seed:todo/getfrom:todo/liston route enter, dispatchensure-query, and the user sees instant data while the real fetch runs in the background. Optimistic-update flows are unaffected in practice — the in-flight:fetching?guard dedupes requests, and the mutation's:invalidatestriggers the same refetch you'd want anyway.
- New
docs/placeholder-data.md— documents seeding the cache from existing client data on route enter, the behavior contract ofset-query-data, and how the pattern relates to prefetching and optimistic updates.
0.8.0 - 2026-04-26
rfq/parse-result-event— utility that turns one of the four query result event vectors (::rfq/query-success,::rfq/query-failure,::rfq/infinite-page-success,::rfq/infinite-page-failure) into a map{:event-id :k :params (:data | :error) [:mode]}. Returnsnilfor any other event vector. Centralizes the rfq event-shape so users can implement future proof (non breaking change) interceptors of the lifecycle events. This is a band aid on the fact that these events should've been dispatched with map based args from the start (Oops).db/get-query— now has multi-arity support.(get-query db qid)called directly with the query-id &(get-query db k params)where qid is built inside
- Renamed
docs/mutation-hooks.md→docs/lifecycle-hooks.mdand added a "Observing Query Lifecycle" section documenting thereg-global-interceptorpattern for query telemetry, analytics, and route-scoped side effects. Mutation hook content is unchanged. References inREADME.md,docs/api-reference.md, and the skill docs updated. - Updated ai skill with the latest changes and patterns
0.7.0 - 2026-04-11
::rfq/infinite-query-data— passive derived subscription returning just the:datafield of an infinite query ({:pages [...] :has-next? bool :has-prev? bool ...}). Components subscribed here only re-render when pages actually change, not when:fetching-next?,:fetching-prev?, or:stale?toggle. Useful in stale-while-revalidate patterns.
has-prev?is now always present in infinite query data — previouslyhas-prev?was only assoc'd into the data map when:get-previous-cursorwas configured, making(:has-prev? data)return nil (not false) for forward-only queries. It is now always included asfalsewhen no previous page exists.prev-cursoris still only included when:get-previous-cursoris configured.
0.6.0 - 2026-04-11
- Derived subscriptions no longer trigger fetches
⚠️ Breaking change —::rfq/query-data,::rfq/query-status,::rfq/query-fetching?, and::rfq/query-errorwere incorrectly depending on the effectful::rfq/querysubscription, causing a fetch to be triggered whenever any of them were subscribed to. They now derive from the passive::rfq/query-state, matching their documented "no fetch" contract. If you were relying on these subscriptions to trigger fetches, replace them with::rfq/queryor dispatch::rfq/ensure-queryexplicitly.
0.5.0 - 2026-04-07
- Polling via
mark-active/mark-inactiveevents —mark-activenow accepts an opts map with:polling-interval-ms(falls back to query config) and:sub-id(defaults to:default).mark-inactivestops the subscriber's poll. This makes event-based lifecycle a first-class citizen for polling — no effectful subscription required. get-query!/get-mutation!in registry — throwing variants ofget-query/get-mutationthat raiseex-infowhen the key is not registered, replacing the repeated(or (get-query k) (throw ...))pattern.
- Polling no longer fires duplicate requests when a fetch is in-flight — polling now dispatches
::rfq/poll-refetchinstead of::rfq/refetch-query. By default, if a query is already fetching when a poll tick fires, the tick is skipped — preventing stale-response races where a late T0 response overwrites a fresher T1 response. Set:polling-mode :forceon the query config to restore the old behavior. Manualrefetch-querycalls remain unconditional.
- Polling logic moved from subscriptions to events — the effectful
::rfq/querysubscription no longer manages polling directly. It passes opts through tomark-active/mark-inactive, which are now the single authority for polling lifecycle. - Event handlers use
registry/get-query!— all event handlers now use the throwingget-query!/get-mutation!variants, removing duplicated boilerplate.
0.4.0 - 2026-04-03
re-frame.query.dbnamespace — new namespace of puredb → dbfunctions for reading and transforming the query cache inline inside your own event handlers, without dispatching extra events. Import as[re-frame.query.db :as rfq-db].get-query [db k params]— full cache entryget-query-data [db k params]— just:dataset-query-data [db k params data]— write to cache (:success, fresh)remove-query [db qid]— evict one inactive querygarbage-collect [db]/[db now]— bulk eviction of expired inactive queries
- Event handlers delegate to
re-frame.query.db— theset-query-data,remove-query, andgarbage-collectevent handlers now call the pure db functions directly, eliminating duplicated logic.
0.3.1 - 2026-04-03
enable-debug-logging!accepts options map — pass{:clj->js? false}to skipclj->jsconversion and log raw ClojureScript values instead. Useful when Chrome custom formatters (e.g. cljs-devtools) are enabled. Default behaviour (clj->js? true) is unchanged.
0.3.0 - 2026-04-02
invalidate-tagsrefetched all active queries — previously, any tag invalidation triggered a refetch of every active query in the cache, regardless of whether its tags matched. Now only queries whose tags match the invalidation set are refetched, as intended. This eliminates N×M unnecessary network requests in apps with multiple active queries.
ensure-queryrejects infinite queries — dispatching::rfq/ensure-queryon a query registered with:infiniteconfig now throws with a clear error directing users to::rfq/ensure-infinite-query. Previously this would silently treat the query as a regular single-result query, producing incorrect cache state.- Bidirectional infinite queries (
fetch-previous-page) — infinite queries now support backward pagination via:get-previous-cursorin the:infiniteconfig. Callrfq/fetch-previous-pageor dispatch::rfq/fetch-previous-pageto prepend pages. When:max-pagesis set, prepending trims from the end (opposite offetch-next-pagewhich trims from the start), enabling a true sliding window in both directions. Queries without:get-previous-cursorare unchanged — no new keys appear in their data.
- Early validation in event handlers — all event handlers (
ensure-query,refetch-query,execute-mutation,ensure-infinite-query,refetch-infinite-query) now validate config at the top of theletbinding using(or (get-query k) (throw ...)). Previously, some handlers destructured the config before validating it, allowing silentnilpropagation. dispatch-hooksalways returns[]— the internaldispatch-hookshelper now returns an empty vector instead ofnilwhen no hooks are configured. This simplifies all mutation handler callsites by removingfnil/seqguard ceremony.idle-statederived fromdefault-query— the subscription idle states insubs.cljcare now derived fromutil/default-queryviaselect-keys, eliminating a duplicated source of truth that could silently diverge.- Test suite split into domain-specific namespaces — the monolithic
query_test.cljc(2600 lines) is now 6 focused files (events_test,mutations_test,invalidation_test,gc_test,polling_test,infinite_test) plus a sharedtest_helpersnamespace.
0.2.0 - 2026-03-22
- Passive subscriptions — pure reads from
app-dbwith no side effects::rfq/query-state— same shape as::rfq/query, no fetching/lifecycle::rfq/infinite-query-state— same shape as::rfq/infinite-query, no fetching/lifecycle- Use with manual lifecycle management via
::rfq/ensure-query,::rfq/mark-active,::rfq/mark-inactivein navigation hooks
- Extracted
resolve-queryandresolve-infinite-querypure functions insubs.cljc— shared by both effectful and passive subscriptions, eliminating duplicated logic
0.1.0 - 2026-03-22
Initial public release.
- Core query system —
reg-query,ensure-query,refetch-query,query-success,query-failure - Mutation system —
reg-mutation,execute-mutation,mutation-success,mutation-failure,reset-mutation ::rfq/querysubscription —reg-sub-rawwith automatic fetch, mark-active/inactive, polling, and GC lifecycle- Tag-based cache invalidation —
invalidate-tagsmarks matching queries stale and refetches active ones - Per-query garbage collection — timer-based eviction of inactive queries after
cache-time-ms - Polling — per-query and per-subscriber intervals, lowest non-zero wins, auto-start/stop
- Conditional fetching —
:skip?option on subscriptions for dependent queries - Prefetching —
rfq/prefetchto pre-populate cache before subscribing - Infinite queries — cursor-based pagination with
:infiniteconfig,::rfq/infinite-querysubscription,fetch-next-page, sequential re-fetch on invalidation,:max-pagessliding window transform-response/transform-error— per-query and per-mutation response transformationset-query-data— directly set cached query data for optimistic updatesreset-api-state!— clear all query/mutation state and cancel all timers- Mutation lifecycle hooks —
:on-start,:on-success,:on-failureonexecute-mutationopts map init!— declarative one-shot registry initialization (queries, mutations, default-effect-fn)- Per-query effect override —
:effect-fnon individual queries/mutations for WebSocket, GraphQL, etc. - Abort controller support —
:abort-keyand:abort-requestfor cancelling in-flight requests - Derived subscriptions:
query-data,query-status,query-fetching?,query-error,mutation,mutation-status - Debug logging via
rfq/enable-debug-logging! - Reagent and UIx example apps with 8 tabs each
- 80 unit tests (371 assertions), 60 Playwright e2e tests
- GitHub Actions CI with parallel unit + e2e jobs