qora 1.0.0
qora: ^1.0.0 copied to clipboard
A powerful async state management library for Dart, inspired by TanStack Query
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased #
1.0.0 - 2026-06-01 #
Fixed #
Failure.hashCode— fixed a bug whereFailurestates incorrectly computed their hash using theErrortype instead of their own type.QoraClient.watchFetchStatus— fixed a potential memory leak where the localStreamControllerwas not properly closed on cancel.QoraStatedocs — corrected outdated docstrings referencingErrorinstead ofFailure.
0.9.0 - 2026-06-01 #
Added #
QoraClient.hasActiveWatcher(Object key)— returnstruewhen the cache entry forkeyhas at least one activewatchQuerysubscriber. DevTools gateways use this to distinguish "refetch will fire a real network call" from "entry is stale but no fetcher is in scope".QoraTracker.needsSerialization—boolgetter on the tracker interface;QoraClientskips_serializeForTrackerentirely whenfalse, eliminating JSON serialization overhead forNoOpTrackerin production builds.NoOpTracker.needsSerialization— returnsfalse; zero-cost guard so production apps never pay the serialization cost.QoraClient.attachLifecycleManager(LifecycleManager)— wires a lifecycle manager post-construction;QoraScopenow calls this sorefetchOnWindowFocusactually triggers background revalidation on app resume.- Smart eviction timer — replaced
Timer.periodic(1 min)with a self-scheduling one-shotTimerthat fires exactly when the earliest inactive entry expires; reduces idle wakeups to zero for long cache-time configurations.
Changed #
MutationEvent→MutationUpdate(breaking) — eliminates the name collision with theMutationEventwire-protocol type inqora_devtools_shared. Affected public APIs:QoraClient.mutationEvents → Stream<MutationUpdate>,QoraClient.activeMutations → Map<String, MutationUpdate>.QoraTracker.onQueryFetched— addedString? dependsOnKeynamed parameter (forwarded fromQoraOptions.dependsOn); allows DevTools to draw real query→query dependency edges. Existing custom trackers must add the nullable parameter (ignored byNoOpTracker).
Fixed #
QueryCache.get<T>— replaced silentas CacheEntry<T>?cast with anis!check that throws a descriptiveStateErrornaming both the registered and requested types; catches key/type conflicts at the misuse site, not deep in a stream listener.QoraClient.removeQuery— was leaking aStreamControllerin_fetchStatusBus; now calls_fetchStatusBus.remove(sk)?.close()on eviction.QoraClient.watchQuerypolling timer — two concurrent subscriptions on the same key could cancel each other'srefetchIntervaltimers via sharedentry.refetchTimer; timer is now local to each generator invocation, cancelled in its ownfinallyblock.QoraClient._toJsonSafe—on NoSuchMethodErrorwas incorrect (Error, notException); restructured to a singlecatch (e)with anis NoSuchMethodErrorguard inside.QoraScopelifecycle wiring —initStatecalledlifecycleManager.start()but never passed the manager toQoraClient;refetchOnWindowFocuswas dead code; fixed by callingclient.attachLifecycleManager().PersistQoraClient.registerSerializer<T>— addedassert(name != null)guard with a descriptive message for obfuscated builds whereT.toString()produces mangled identifiers._evictExpiredEntries— replaced unconditional.toList()with a lazyList?only allocated when entries need removal.
0.8.0 - 2026-03-12 #
Added #
CacheEntry.setError(Object error)— transitions an entry toFailure<T>using the entry's own reifiedT; eliminates theFailure<dynamic>cast error that occurred whendebugSetQueryErrorused an untyped cache lookup.CacheEntry.markStale()— sets an internal_forcedStaleflag without pushing any state update to observers;isStale()now returnstruewhenever_forcedStaleis set, regardless ofstaleTime; the flag is cleared byupdateState().QoraClient.markStale(Object key)— silently flags a cache entry stale without transitioning toLoadingor triggering an immediate refetch; active observers see no change; the nextfetchQuery/watchQuerymount will seeisStale() == trueand trigger an SWR background revalidation.QoraTracker.onQueryRemoved(String key)— hook called whenremoveQueryevicts a cache entry;NoOpTrackerships an empty override.QoraTracker.onQueryMarkedStale(String key)— hook called whenmarkStalesilently flags an entry; differs fromonQueryInvalidatedin that no state transition or timeline fetch entry is implied;NoOpTrackerships an empty override.
Fixed #
QoraClient.debugSetQueryError()— previously called_cache.get<dynamic>()and pushedFailure<dynamic>into aStreamController<QoraState<T>>, causing aTypeErrorat runtime; now uses_cache.peek()and delegates toCacheEntry.setError()so theFailureis instantiated with the correct reifiedT.QoraClient.removeQuery()— did not notify the tracker; now calls_tracker.onQueryRemoved(sk)so DevTools overlays remove the corresponding row immediately.
0.7.0 - 2026-03-03 #
Added #
CancelToken— cooperative cancellation forfetchQuery,watchQuery, andprefetch; state restored to pre-fetch snapshot on cancellationQoraCancelException— thrown to the caller when a fetch is cancelled viaCancelTokenQoraTracker.onQueryCancelled(String key)— hook called when a fetch is cancelled;NoOpTrackerships an empty overrideQueryFiltertypedef —bool Function(String key, QoraState<dynamic> state, QoraOptions? lastOptions)— richer invalidation predicateQoraClient.invalidateQueries({required QueryFilter filter})— bulk invalidation usingQueryFilterCacheEntry.lastOptions— records the options from the last successful fetch; used byinvalidateQueriesQoraOptions.dependsOn— declares a query dependency;watchQueryfires reactively when the dependency resolves,fetchQuerythrowsStateErrorif unresolved,prefetchsilently skipsQoraClient.queueHydration(key, data, {updatedAt})— enqueue a pre-deserialized value for lazy typed injection; shared hydration mechanism forPersistQoraClientandSsrHydratorQoraClient.removeHydrationEntry(key)—@protected; removes a pending hydration entryQoraClient.clearHydrationQueue()—@protected; clears all pending hydration entriesSsrHydrator— Flutter Web SSR hydrator; readswindow.__QORA_STATE__, validates strictly, and callsqueueHydration(); XSS-safe viadartify()and per-deserializer try/catch; no-op stub on non-web platformsQoraTracker.onQueryFetching(String key)— hook called when a query transitions toLoading; pairs withonQueryFetchedfor fetch-duration tracking;NoOpTrackerships an empty overrideInfiniteData<TData, TPageParam>— immutable container for paginated pages;append(),prepend(),dropFirst(),dropLast(),flatten()InfiniteQueryState<TData, TPageParam>— sealed state machine:InfiniteInitial,InfiniteLoading,InfiniteSuccess,InfiniteFailureInfiniteQueryOptions<TData, TPageParam>— pagination config:initialPageParam,getNextPageParam,getPreviousPageParam,maxPagesInfiniteQueryObserver<TData, TPageParam>— pagination engine:fetch(),fetchNextPage(),fetchPreviousPage(),refetch()InfiniteQueryFunction<TData, TPageParam>typedef —Future<TData> Function(TPageParam pageParam)QoraClient.watchInfiniteState,getInfiniteQueryState,getInfiniteQueryData,setInfiniteQueryData,updateInfiniteQueryState,invalidateInfiniteQuery
Changed #
PersistQoraClienthydration delegated toQoraClient— hydration infrastructure lifted to the base class;PersistQoraClient.hydrate()now callsqueueHydration(); the six typed overrides removed
0.6.0 - 2026-03-02 #
Added #
NetworkMode— per-query enum (online/always/offlineFirst)FetchStatus— second-axis enum (fetching/paused/idle); observable viaQoraClient.watchFetchStatus(key)ReconnectStrategy— thundering-herd prevention on reconnect:maxConcurrent+jitter; named constructorsinstant()andconservative()OfflineMutationQueue— FIFO queue for offline writes; replays on reconnect;stopOnFirstErrorflag;OfflineReplayResultsurfacePendingMutation— type-erased queued-write containerQoraOfflineException— thrown byfetchQuerywhen offline with no cached dataQoraClient.attachConnectivityManager()— late-attach aConnectivityManager; called automatically byQoraScopeQoraClient.isOnline/networkStatus— real-time connectivity gettersQoraClient.watchFetchStatus(key)— stream ofFetchStatustransitionsQoraClient.offlineMutationQueue— sharedOfflineMutationQueueinstanceMutationSuccess.isOptimistic—truewhen the mutation was queued offline with anoptimisticResponseMutationOptions.offlineQueue— opt a mutation into theOfflineMutationQueueMutationOptions.optimisticResponse— syntheticTDatafor immediate UI feedbackQoraClientConfig.reconnectStrategy— global reconnect strategy; defaults to 5 concurrent / 100 ms jitterQoraOptions.networkMode— per-queryNetworkMode; defaults toNetworkMode.online
0.5.0 - 2026-03-01 #
Added #
PersistQoraClient—QoraClientsubclass that persists query results to aStorageAdapterand restores them on startupStorageAdapter— abstract key/value interface; ships withInMemoryStorageAdapterQoraSerializer<T>—toJson/fromJsonpair for a typePersistQoraClient.registerSerializer<T>— register a serializer; accepts optionalnamefor obfuscation safetyPersistQoraClient.hydrate()— reads storage, validates TTL, queues valid entries for lazy hydrationPersistQoraClient.persistQuery<T>— force-persist the current cached value with an optional TTL overridePersistQoraClient.evictFromStorage/clearStorage— storage-only evictionQoraClient.hydrateQuery<T>— inject a typedSuccess<T>with a customupdatedAtinto anInitialentryQoraClient.onFetchSuccess<T>—@protectedhook called after every successful fetch; used byPersistQoraClientto auto-persist
Fixed #
QoraStateSerialization.toJsonwrote'type': 'error'forFailurewhilefromJsonmatched on'failure';Failurestates were never restored correctly
0.4.0 - 2026-02-28 #
Added #
QoraTracker— abstract observability interface with lifecycle hooks for queries, mutations, and cache eventsNoOpTracker— defaultconstimplementation with zero overheadQoraClient(tracker:)— optional tracker injection; defaults toNoOpTracker
0.3.0 - 2026-02-25 #
Added #
MutationController<TData, TVariables, TContext>— manages the full mutation lifecycle:Idle → Pending → Success | FailureMutationState<TData, TVariables>— sealed class:MutationIdle,MutationPending,MutationSuccess,MutationFailure; each carries typedvariablesMutationOptions<TData, TVariables, TContext>— lifecycle callbacks:onMutate,onSuccess,onError,onSettled;retryCount/retryDelayMutatorFunction<TData, TVariables>typedefMutationTracker— abstract interface implemented byQoraClient; decouplesMutationControllerfrom the clientMutationEvent— type-erased event on every mutation state transition;mutatorId,status,data,error,variables,metadata,timestampQoraClientimplementsMutationTracker—mutationEventsstream,activeMutationssnapshot;debugInfo()now includesactive_mutationsMutationController.metadata—Map<String, Object?>?forwarded to everyMutationEventMutationController.id— uniquemutation_NidentifierMutationStateExtensions—fold<R>()andstatusgetterMutationStatusenum —idle | pending | success | errorMutationStateStreamExtensions—whereSuccess(),whereError(),dataOrNull()
Changed #
MutationFunctionrenamed toMutatorFunction
Fixed #
MutationController.streamrace condition — events emitted synchronously before the first microtask were lost; fixed with aStreamControllerwhoseonListenruns synchronously
0.2.0 - 2026-02-22 #
Added #
watchState<T>(key)— observe-only stream; no fetch triggeredprefetch<T>()— pre-warm the cache before navigation; no-op if already freshrestoreQueryData<T>(key, snapshot)— roll back an optimistic updateremoveQuery(key)— evict a single query and cancel any in-flight requestclear()— evict all queries and cancel all in-flight requestscachedKeys— all currently cached normalised query keysdebugInfo()— cache and pending-request count snapshot
Changed #
QoraState<T>rewritten as a sealed class —Initial | Loading | Success | Failure;LoadingandFailurecarrypreviousData- Polymorphic key system — all APIs now accept
Object(plainList<dynamic>orQoraKey); deep structural equality KeyCacheMap— custom map with deep recursive equality and order-independent map-key comparisoninvalidate(key)replacesinvalidateQuery(key)invalidateWhere(predicate)replacesinvalidateQueries(predicate)- Package structure reorganised —
cache/,config/,client/,key/,state/,utils/
Fixed #
- Normalised key lists wrapped in
List.unmodifiable()to prevent accidental mutation