swrly 0.4.0
swrly: ^0.4.0 copied to clipboard
Server-state cache for Flutter — dedupe, cache by query key, stale-while-revalidate. A TanStack Query for Flutter.
0.4.0 #
The v0.4 line, stable. Four additive ergonomic knobs and the fixes from the
review of the dev prerelease — no breaking changes from 0.3.1:
Query.initialData/initialDataUpdatedAt— seed a cache entry with a real value at first observation, distinct fromplaceholderDatawhich never persists. Function-form (T Function()?) sidesteps the "wasnullomitted or genuinely null?" ambiguity. Pair withinitialDataUpdatedAtwhen hydrating from a source that was fresh some time ago, so the freshness clock stays honest.QueryFamilyvariant takesT Function(A arg)?so each member can synthesize its own seed. PerBACKLOG_TRIAGE.md B1.Query.refetchInterval— per-query polling. Ticks only while the entry has ≥1 subscriber, pauses when the last subscriber leaves and re-arms on re-subscribe. Each tick behaves likerefetch()— bypassesstaleTime, dedupes against an in-flight fetch. PerBACKLOG_TRIAGE.md A1.MutationBuilder.retry/retryDelay— matches the retry knobQueryalready has. Off by default (writes aren't idempotent in general). AnonMutaterollback runs only after all retries exhaust, so the optimistic UI survives transient failures a retry recovers from. PerBACKLOG_TRIAGE.md A4.QueryBuilder.notifyOn+QueryProp— cheap opt-in rebuild filter. Pass{QueryProp.data, QueryProp.error}and the widget stops rebuilding onisFetchingflicker orupdatedAtbumps it doesn't render. Default (null) matches previous behaviour. PerBACKLOG_TRIAGE.md B3.
Fixed since 0.4.0-dev.1, from an automated review of the release branch —
each pinned by a regression test that fails against the pre-fix code:
useSwrlyQuerynow re-subscribes when theQueryClientchanges. The effect keyed only on the query key, so swapping the client while keeping the key — a scoped DI client changing — left the hook subscribed to the old client: the new one never gotfetch(), and invalidation and cleanup stayed wired to a client the widget no longer read from.- Polling claims are refcounted per claimant. Two
QueryBuilders sharing arefetchIntervalkey meant flipping either toenabled: falsetore the timer down for the whole entry, silently stopping the other's polling until an unrelated rebuild re-primed it.useSwrlyQueryparticipates in the same refcount, so a builder letting go can't cancel a mounted hook's polling, and taking a claim arms the interval to the claimed rate — a claim that only counted left polling dead when the entry had no live timer. Claims track each claimant's rate, so a consumer that briefly slowed a shared key no longer strands the others at that rate once it unmounts. onSettledis honoured whenonSuccessoronErrorthrows. The callbacks ran as bare statements, so a throwingonSuccessskippedonSettled— dropping the cache invalidation apps habitually put there after a write the server had already committed. The callback's exception still surfaces to the caller.
swrly_hooks bumps in lockstep to 0.4.0 and carries the hook-side fix above.
0.3.1 #
Ecosystem release — no changes to lib/, the public API is byte-identical
with 0.3.0. This release exists to broadcast the surrounding scaffolding
that landed since:
swrly_hookscompanion package (pub.dev) —useSwrlyQuery/useSwrlyMutationforHookWidget-based screens. Split into its own package so hook non-users don't pay forflutter_hooksas a transitive dep; version-locked to swrly's ownmajor.minor(matching pattern toflutter_riverpod/hooks_riverpod).example/lib/patterns/— 5 runnable side-by-side demos of swrly combined withsetState/ Provider / Riverpod / Bloc / hooks, all driving the same posts screen so the differences are visible at a glance. See the README's "Using with your state management" section..claude/skills/— 9 Claude Code skills (swrly-init,swrly-refactor-{futurebuilder,stateful,provider,riverpod,bloc,hooks,spaghetti},swrly-audit) that scaffold and refactor swrly usage in downstream Flutter projects. Each skill was end-to-end verified against a real Flutter project (fresh scaffold, plus flutter_weather and the rrousselGit/riverpod pub example for the more complex cases).AGENTS.mdat repo root — a living rulebook AI coding assistants (Claude Code, Cursor, Aider, Copilot, Windsurf, ...) can follow when writing any server-state code in a swrly project, not just refactors. Point the assistant at the raw GitHub URL and it defaults to swrly conventions for new features and fetches deeper.claude/skills/*procedures on demand.doc/CONVENTIONS.md— the single-source rulebook every skill cites. Split out of the design plan so both people and AI can consult one file for the "swrly-shaped code" contract.
All existing 70 tests unchanged and passing (widget tests in
swrly_hooks unchanged too). swrly_hooks bumps in lockstep to
0.3.1-dev.1.
0.3.0 #
Stable release of the 0.3.0 line (the 0.3.0-dev.* notes below are the full
history). Headline additions since 0.2.x:
Query<T>/QueryFamily<T, A>— query definition objects. Declare a query's key, fetch function and options once, then consume that same definition imperatively (postsQuery.fetch()/refetch()), declaratively (QueryBuilder.of(postsQuery, builder: ...)) or for cache control (data/state/stream/setData/invalidate/remove). Retyping the(key, fn)pair at every call site made a mistyped key a silent cache miss rather than a compile error; a definition is the single spelling of the key. The cache still lives inQueryClient— aQueryis a stateless value object, and two definitions with the same key address the same entry. See SPEC §10.QueryFamilyfor parameterised queries — keys are always[...prefix, ...argKey(arg)], soinvalidateAll()/removeAll()are correct by construction.Query.invalidate()/Query.remove()are exact, not prefix-scoped;invalidateQueries(prefix)/removeQueries(prefix)keep prefix semantics.QueryClient.removeQueriesWhere(test)— predicate form ofremoveQueries, mirroringinvalidateQueriesWhere.- Docs that can't rot — the README carries no version literals, and
docs_freshness_test.dart/readme_snippets_test.dartfail the suite on a version literal, a CHANGELOG that has drifted frompubspec.yaml, an undocumented public type, or a code sample that no longer compiles.
70 tests; lib/src stays at 100% line coverage (330/330). Backward
compatible with 0.2.x — purely additive.
0.3.0-dev.2 #
Documentation hygiene — no API changes.
- The README no longer carries version numbers. Every release meant editing
a status line, two "New in X" lists, an Install pin, an inline
(0.2.0)marker and a "Shipped in" recap — and it rotted twice (0.2.0 shipped while the README still called it a prerelease and pinned^0.1.0). Nowpubspec.yamlis the version,CHANGELOG.mdis the history, the pub.dev badge renders the current number, and the README only describes what the library does. The two "New in X" lists are replaced by one version-free What you get; Install isflutter pub add swrly. - New:
test/docs_freshness_test.dart. Three guards that fail the suite rather than relying on remembering: the README must contain no version literal,CHANGELOG.md's newest entry must matchpubspec.yaml, and every public type must have an entry indoc/API.md. Together withreadme_snippets_test.dart(which compile-checks the samples), the docs now break the build when they drift instead of going stale quietly. - The public-type guard immediately found one gap:
QueryKeyHashhad no entry indoc/API.md. Documented, along withqueryKeyStartsWith— both are useful inside aninvalidateQueriesWhere/removeQueriesWherepredicate.
0.3.0-dev.1 #
First 0.3.0 feature, on the dev prerelease track for real-world verification.
- New:
Query<T>/QueryFamily<T, A>— query definition objects. Declare a query's key, fetch function and options once, then consume the same definition three ways: imperatively (postsQuery.fetch()/refetch()), declaratively (QueryBuilder.of(postsQuery, builder: ...)), or for cache control (data/state/stream/setData/invalidate/remove/copyWith). Previously the(key, fn)pair had to be retyped at every call site, where a mistyped key is a silent cache miss rather than a compile error. AQueryis a stateless value object — the cache still lives inQueryClient, and two definitions with the same key address the same entry. See SPEC §10. QueryFamilycovers parameterised queries. Keys are always[...prefix, ...argKey(arg)](default[arg]), soinvalidateAll()/removeAll()are correct by construction; supplyargKeywhen the argument is a record or custom object so the key is built from primitives instead oftoString().Query.invalidate()/Query.remove()are exact, not prefix-scoped — a definition names one entry, so invalidating['posts']leaves['posts', 'page', 2]alone.invalidateQueries(prefix)/removeQueries(prefix)keep their prefix semantics.- New:
QueryBuilder.of(query, builder: ...)— build a widget straight from a definition. Because a definition'sfnis a stable field rather than an inline closure, thequeryFnre-captured for invalidation refetches is identical across builds. - New:
QueryClient.removeQueriesWhere(test)— predicate form ofremoveQueries, mirroringinvalidateQueriesWhere. - Docs: README "Define a query once" section, SPEC §10, API reference for
both types; the SPEC's out-of-scope list no longer claims 0.2.0 features are
missing, and ROADMAP's duplicate
v0.2section is folded intov0.4. - Tests: 15 new tests for the definition objects, plus a new
test/readme_snippets_test.dartthat compile-checks every Dart snippet in the README anddoc/API.mdagainst the real API, so a doc example can't drift from the code without the suite going red. 67 tests total;lib/srcstays at 100% line coverage (330/330).
Backward compatible with 0.2.x — purely additive.
0.2.1 #
-
Fix:
observe()/stateOf()no longer disarm garbage collection. Both go through the internal entry lookup, which cancels a pending GC timer so an entry can't be disposed out from under a caller — but neither re-armed it, so observing (or synchronously reading) a key with no subscribers left the entry resident forever, andstateOfon an unknown key leaked the idle entry it created. Both now re-arm GC, matchingfetchQuery/setQueryData. Entries with a live subscriber (a mountedQueryBuilder) are unaffected. Found while documenting the non-widget path below. -
README: "Using swrly without widgets".
QueryClientis a complete API on its own; the README now documents the imperative path (fetchQuery) and the observe path (observe/stateOf) alongsideQueryBuilder, with a table of the three ways to read a query and patterns for prefetch-before-navigate, synchronous peeks and non-widget subscriptions. -
Documented that
observe()does not register a subscriber, so it does not hold an entry againstcacheTimeGC (README +doc/API.md). -
README status/install corrected for the released
0.2.0(they still described0.2.0as a prerelease and pinned^0.1.0). -
Example: don't notify instrumentation
ValueNotifiers during build. -
Tests: 50 (3 new, covering the GC re-arm);
lib/srcstays at 100% line coverage.
0.2.0 #
Stable release of the 0.2.0 line (the 0.2.0-dev.* notes below are the full
history). Headline additions since 0.1.x:
- Retry + backoff —
retry/retryDelayonQueryBuilder/fetchQuery, exponential default (1s→30s). - Optimistic updates with automatic rollback —
MutationBuilder.onMutateapplies an optimistic write and returns a rollback closure run on error. keepPreviousData/placeholderData— no loading flash on key changes (search / pagination); newQueryState.isPlaceholderData.- Example: a new Stress test screen — an in-app performance harness with
a live FPS / build / raster / jank readout, a cache-ops micro-benchmark, and
hundreds of live
QueryBuilders under continuous invalidation. - Docs: the README now has a measured Performance section.
Backward compatible with 0.1.x.
0.2.0-dev.4 #
- Example: rebuilt as a verification harness for all 0.2.0 features —
pagination with
keepPreviousData(dimmed previous page while loading), a "Fail next" retry demo, and a "make next create fail" toggle that shows the optimistic insert being rolled back automatically. - Tests: 100% line coverage on
lib/src(addedrefetch(),refetchOnResume, custom-keytoStringfallback, idle getters, singleton). - Docs: added
doc/VERIFICATION.md(status, remaining TODOs, how to verify the prerelease). README updated for the 0.2.0-dev feature set.
0.2.0-dev.3 #
- New:
keepPreviousData/placeholderData.QueryBuilder.keepPreviousDatakeeps rendering the previous key's data (flaggedstate.isPlaceholderData) while a new key loads, instead of flashing to a spinner — ideal for search / pagination.QueryBuilder.placeholderDatashows a static stand-in until the first real value arrives. Neither is cached or affects freshness. NewQueryState.isPlaceholderDataflag. See SPEC §9.
0.2.0-dev.2 #
- New: optimistic updates with automatic rollback.
MutationBuilder.onMutateruns beforemutationFn, applies your optimistic write, and returns a rollback closure; swrly runs it automatically on error (beforeonError) and keeps the optimistic value on success. Backward compatible — existingonSuccess/onError/onSettledsignatures are unchanged. See SPEC §9.
0.2.0-dev.1 #
First 0.2.0 feature, on the dev prerelease track for real-world verification.
- New: retry + backoff. A
queryFnthat throws is retried up toretrytimes with aretryDelay(attempt)backoff. Configure per query (QueryBuilder.retry/retryDelay,fetchQuery(retry:, retryDelay:)) or globally (QueryClient(defaultRetry:, defaultRetryDelay:)). Default is0retries (unchanged 0.1.x behaviour);defaultRetryDelayFnprovides an exponential 1s→30s backoff. While retrying the query staysisFetchingand only surfaces anerrorafter retries are exhausted; retries respect the generation guard (a supersede/dispose stops them). See SPEC §8.1.
0.1.1-dev.1 #
Prerelease of the 0.1.1 correctness patch, published for verification before the
stable 0.1.1 release. Everything below plus the review fix noted here.
- Fix (review):
QueryBuilderonly re-captures the refetcher (primeRefetcher) on a same-key rebuild whenenabledis true. Previously a disabled query that rebuilt could get a refetcher installed, so a laterinvalidateQuerieswould fetch a query the caller setenabled: false— violating SPEC §9. Added a regression test plus tests foronError/onSettledon unmount,hasDataretention across errors, andcopyWithfield clearing (31 tests total).
0.1.1 #
Correctness patch — behaviour fixes found in review, all backward compatible.
- Fix:
QueryState.hasDatais now based on a stored "has a successful value" flag instead ofdata != null. A query that legitimately resolves tonullnow reportsisSuccess && hasDataat the widget layer, matching the cache's own freshness contract (SPEC §2/§5.1). - Fix:
QueryState.copyWithcan now cleardata/error/stackTrace(via a sentinel). Entering a fetch and a successful refetch clear a stale error/stackTrace, so an old failure no longer leaks into a later loading/success state. - Fix:
MutationBuildernow runsonSuccess/onError/onSettledregardless ofmounted— a widget that disposes mid-flight no longer silently skips a cache invalidation done inonSuccess. OnlysetStateis guarded. - Fix:
QueryBuilderre-captures the currentqueryFn/staleTimeon a same-key rebuild (newQueryClient.primeRefetcher), so a laterinvalidateQueriesrefetch uses the current closure rather than a stale one. It still does not refetch on a plainqueryFnidentity change. - New:
QueryClient.invalidateQueriesWhere((key) => bool)— predicate form ofinvalidateQueriesfor sets a prefix can't express. - Docs: documented the last-writer-wins semantics for an entry's captured
queryFnwhen subscribers share a key (SPEC §6). Added 6 tests.
0.1.0 #
- Example: rebuilt around a real dio client hitting a public API, with a
live request counter + event log so the cache is observable — cache hits
show as "0 requests", keyed detail queries (
['post', id]) demonstrate per-key caching, and mutations show optimisticsetQueryData. Runs on web. - Docs: README overhauled — "why server state", how-it-works flow, and
comparison tables (vs
FutureBuilder, vs Riverpod/Bloc, vs a dio cache interceptor).docs/renamed todoc/(pub convention); added pubspec topics. - Bundles all the
0.0.2cache-correctness fixes below.
0.0.2 #
- Fix:
invalidateQueries(refetch: true)now actually refetches active subscribers (the lastqueryFnis captured per entry) instead of only marking data stale. - Fix: stale responses can no longer overwrite fresher data — each fetch carries a generation token and only the newest may write back.
- Fix: a successful
nullvalue is treated as fresh; nullable-data queries no longer refetch on every call. - Fix: freshness uses a monotonic clock, immune to wall-clock jumps.
- Fix: garbage collection can no longer dispose an entry that is being reused imperatively; idle imperatively-created entries are now collected too.
- Fix:
QueryBuilderkicks off the fetch whenenabledflips false → true. - Docs: added
docs/SPEC.md; expanded test coverage (dedup, race, GC, nullable, invalidate-refetch, widget lifecycle).
0.0.1 #
- Initial scaffold:
QueryClient,QueryBuilder,MutationBuilder, prefix invalidation, refetch-on-resume.