mk_graphql 1.2.3
mk_graphql: ^1.2.3 copied to clipboard
A clean, modern, simplified GraphQL client for Dart & Flutter with normalized caching, composable links, and stream-based operation execution.
1.2.3 #
- Fix: token refresh could fail with "refresh token already revoked" because the same session was refreshed more than once with servers that rotate refresh tokens. Fixed all the ways that happened:
- A 401 that arrives after a refresh has already completed (the request was sent with the pre-refresh access token) now retries with the refreshed token instead of starting another refresh and rotating the refresh token again.
- Refreshes are coordinated across every
GraphQLClientthat uses arefreshCallback(they all shareTokenStorage.i), so two clients hitting a 401 at once, or one client's late 401 after the other refreshed, no longer each refresh with the same refresh token.AuthLinkaccepts an optionalRefreshCoordinatorto share refresh state between links that refresh the same session. - A refresh that finishes after the session was revoked (e.g. after
refreshTimeoutfired) no longer writes its tokens back into storage and resurrects the session. - Persisting a refreshed
TokenPairis retried up to 3 times; if storage still rejects it, the new pair is kept in memory and used for requests and the next refresh (the stored refresh token is already revoked server-side), and is flushed to storage on the next refresh. It is dropped on revoke or if the stored token changes (new login/logout).
1.2.2 #
- Breaking: the built-in
TokenStorageandTokenPairare removed in favour of thetoken_storagepackage, whichmk_graphqlnow depends on and re-exports (import 'package:mk_graphql/mk_graphql.dart'still gives you both).mk_graphqlno longer depends onflutter_secure_storage,shared_preferencesorequatabledirectly.- Call
await TokenStorage.init()once at startup (replacesclearOnInstall(), which also handled the fresh-install wipe). TokenPairis no longerconst(itscreatedAt/updatedAtdefault toDateTime.now()when omitted). On a refresh the client still keeps the storedcreatedAtand stampsupdatedAtitself.- API renames:
getToken()→token,deleteToken()→clearToken().setTokenListenernow takes avoid Function(TokenPair?)(was the access token string) and holds a single listener (a second call replaces the first instead of adding one). - Storage layout changed (a single namespaced JSON entry instead of four keys), so existing sessions are not migrated: users are logged out once after upgrading.
- New from the package:
TokenPair.isExpired,TokenStorage.hasToken/isTokenExpired, and generic secure key-value storage (put/get/putObject/getObject/remove/watchKey). - Minimum SDK raised to Dart 3.9 / Flutter 3.35 to match
token_storage.
- Call
1.2.1 #
- New: mutations and subscriptions now force
FetchPolicy.noCacheunconditionally — enforced centrally inGraphQLClient.request(), regardless of the client'sdefaultFetchPolicyor a per-requestfetchPolicyoverride (a hand-built request, or a future generator default, can no longer make a mutation/subscription read or write the normalized cache under its own operation key).GraphQLRequest.optimisticResponseis unaffected — it still writes to the optimistic layer other queries read from, which is how a mutation drives an optimistic UI update elsewhere in the cache. - New:
GraphQLClient(refreshTimeout: ...)bounds how long a single token-refresh call may take before it's treated as a failed refresh (same outcome as it throwing — the session is revoked).null(the default) waits indefinitely, unchanged from before. - New:
GraphQLClient(maxConcurrentBackgroundRefetches: ...)caps how many ofnotifyReconnected/notifyAppResumed/invalidateQueries's background refetches run at once.null(the default) runs them all concurrently, unchanged from before. - New:
GraphQLPaginator.loadAll({interPageDelay})— waitsinterPageDelaybetween pages (after the first) instead of requesting every page back-to-back as fast as each resolves.null(the default) is unchanged. - Fix:
WebSocketLinknow resendssubscribefor every subscription that survives a socket drop, once the reconnect succeeds. Previously the socket reconnected but no subscription message was ever resent, so every subscriber silently stopped receiving data after any network blip, with no error and no indication anything was wrong. - Perf:
InMemoryCache.readQuerymemoizes its denormalized+parsed result per query key (invalidated by a version counter bumped on any write/evict/clear/optimistic change), so repeated reads of an unchanged key skip the full denormalize+parse walk — measured ~10x faster for the common case of severalwatchQuerysubscribers attaching to the same key back-to-back.writeQuerynow collects touched__typenames in the same pass that normalizes the response instead of denormalizing it again afterwards purely to re-walk it — measured ~23% faster on a 500-item response.HiveCachenow diffs against its last-persisted snapshot and writes only changed/removed records instead of rewriting the entire box on every batch — measured ~28% faster per persist cycle. Seebenchmark/cache_benchmark_test.dart(flutter test benchmark/cache_benchmark_test.dart) for the numbers on your own machine. - Perf: minor allocation/computation cuts:
computeRequestKeyis now memoized perGraphQLRequestinstance (was recomputed on everyreadQuery/writtenAt/writeQuery/dedup lookup);InMemoryCache._getRecordskips an allocation entirely when no optimistic response is in flight;DioLinkno longer walks the whole variables tree building path strings just to conclude a request has no file upload.
1.2.1-dev.6 #
- New:
llms.txtandllms-full.txt(per the llmstxt.org convention), published alongside the package so an AI coding agent integratingmk_graphqlinto a consuming app has a self-contained reference —llms-full.txtinlines setup, auth wiring, caching, pagination, and common-mistakes guidance without requiring the rest of the monorepo.
1.2.1-dev.5 #
- Fix:
GraphQLClient(defaultHeaders: ...)can now override the transport's built-inAcceptandContent-Typeheaders — previously they were silently ignored, because the headers only reached the request as theDio's base options, which the link's own per-request headers beat.defaultHeadersare now passed toDioLink(and toAuthLink's internal refresh client) directly. A defaultContent-Typestill never breaks a multipart upload:DioLinknow strips it case-insensitively before Dio sets the boundary header. - Fix:
GraphQLClientno longer modifies a caller-suppliedDio. It used to copydefaultHeadersintodio.options.headers, which overwrote the Dio's own same-named headers and leaked the GraphQL defaults into the caller's other (e.g. REST) requests. Behaviour change for anyone who relied on that side effect — set those headers on your ownDioinstead. - New:
AuthLinkacceptsdefaultHeaders(applied to its internal refresh client's requests).
1.2.1-dev.4 #
- New: silent token refresh.
TokenPairgained optionalcreatedAt(when the session's tokens were first issued — preserved across refreshes) andupdatedAt(last write).TokenStoragepersists both (new__token_created_at__/__token_updated_at__keys; sessions stored before this change load withnulltimestamps and getcreatedAtbackfilled on their next refresh) and gainedupdateToken(pair), which writes a refreshed pair without notifying token listeners.GraphQLClientnow uses it whenrefreshCallbacksucceeds, soTokenStorage.i.setTokenListener(...)— typically what drives app routing — is no longer triggered by a refresh; the next request just reads the new tokens from storage.saveToken(login) anddeleteToken(logout/revoke) still notify, the latter withnull. - Breaking:
TokenStorage.setTokenListenerno longer fires after a token refresh. If you relied on it to observe refreshed tokens (e.g. to reconnect a websocket), readTokenStorage.i.getToken()instead —updatedAttells you when they last changed.
1.2.1-dev.3 #
- New:
notifyAppPaused(). Pairs with the existingnotifyAppResumed()— call it from your app-lifecycle observer when the app backgrounds. While paused, every opportunistic background refetch (staleness-on-attach,refetchIntervalpolling,invalidateQueries,notifyReconnected) is skipped instead of hitting the network for a screen nobody's watching;refetchIntervaltimers keep ticking on schedule (free), only the network call each tick would have made is skipped.notifyAppResumed()clears the pause and catches up everything currently watched in one shot. A request made directly (future/stream/refetch) is never affected. See Caching: Pausing background refetches. - New:
RetryOptions.jitter(0.0–1.0, default0.0) randomizes each computed retry delay by up to that fraction in either direction, so many client instances that failed at the same moment don't all retry in lockstep and re-cause the spike that failed them. - New:
WebSocketLinkreconnect delay now backs off exponentially (reconnectBackoffMultiplier, default1.5) fromreconnectInterval, capped atmaxReconnectInterval(default 30s), withreconnectJitter(default0.2) — previously it reconnected at a fixedreconnectInterval(default 3s) forever, which could hammer a dead server/absent network indefinitely. The attempt counter resets on every successfulconnection_ack. - New: exported
cappedBackoffDelay({attempt, initialDelay, maxDelay, multiplier}), the exponential-backoff-with-cap helperWebSocketLinkuses internally, for anyone scheduling their own bounded retry/reconnect logic. - Breaking:
FetchPolicy.cacheFirstnow background-revalidates on every cache hit by default.GraphQLClient.defaultStaleTime(how long a cached entry is considered fresh) changed from "always fresh once cached" toDuration.zero, matching TanStack Query's own default. AcacheFirstcache hit still returns the cached value instantly with no UI-visible delay, but now also immediately kicks off a background network refetch, the same emission shapecacheAndNetworkhas always had. Apps that relied oncacheFirstnever re-hitting the network for a given query need to set an explicitstaleTime(per-requestGraphQLRequest.staleTime, or client-widedefaultStaleTime) to keep the old behavior — see Caching: Stale-while-revalidate. - New: Request deduplication. Concurrent identical queries/subscriptions (same operation, document, and variables) now share a single in-flight network call automatically. Mutations and any request carrying an explicit
CancelTokennever dedup. No configuration needed. - New: Stale-while-revalidate caching + GC. Added
GraphQLRequest.staleTime/cacheTimeandGraphQLClient.defaultStaleTime/defaultCacheTime.cacheTime(unset by default — never GC) evicts a query's cache entrycacheTimeafter its last activewatchQuerysubscriber cancels, unless a new subscriber attaches first. - New:
client.invalidateQueries({operationNames, types, refetchActive})evicts cached queries matching an operation name and/or a__typenametouched by their cached result, background-refetching any currently activewatchQuerymatches by default. - New: Retry with backoff. Added
RetryOptions(maxAttempts,initialDelay,backoffMultiplier,retryIf, andRetryOptions.none),GraphQLClient.defaultRetry, andGraphQLRequest.retry. Queries/subscriptions retry automatically whendefaultRetryis set; mutations never auto-retry unless they set their ownretry. - New: Refetch triggers. Added
client.notifyReconnected()/client.notifyAppResumed()(background-refetch every currently activewatchQuery) andGraphQLRequest.refetchInterval(periodic background refetch while a request has an active watcher). - New:
HiveCache, ahive_ce-backedCacheimplementation that persists the normalized cache to disk across app restarts (write-through over an in-memory delegate; optimistic writes are never persisted). Requires the app to callHive.initFlutter()/Hive.init(path)beforeHiveCache.open(boxName: ...). See Caching: Persistent cacheHiveCache. - New:
GraphQLClient.create({...}), an async counterpart to the existing synchronous factory whosecacheparameter accepts aFutureOr<Cache>?, socache: HiveCache.open(...)can be passed directly without a separateawait. - Fix:
InMemoryCache.writeQueryno longer chokes on a query result whose nested values are un-serialized model objects (e.g. a generated*Modelholding another generated model). Nested non-JSON values are now coerced throughtoJson()and recursed into before normalization, so entities nested inside a wrapper model are normalized and their__typenames are collected forinvalidateQueries(types: ...)— previously such a result could be stored un-normalized and be missed by type-based invalidation. - Dependency:
equatableconstraint raised from^2.1.0to^3.0.0(onlyTokenPairuses it, and it needed no code changes). Apps that pinequatable2.x elsewhere must move to 3.x to use this release.
1.2.1-dev.2 #
- Breaking: Reworked token storage.
TokenStorageis now a concrete singleton (TokenStorage.i, backed byflutter_secure_storage+shared_preferences) instead of an injectable abstract interface —SecureTokenStorageand thetokenStorage:constructor parameter are removed;GraphQLClientalways talks toTokenStorage.i.AuthTokenis replaced byTokenPair(nowEquatable). RemovedGraphQLClient.setAuthToken(),.clearAuthToken(), and.onAuthTokenChange— there's no longer a change-notification stream; drive app-wide auth state offAuthLink.registerRevokeTokenListener(...)andTokenStorage.i.setTokenListener(...)instead..isAuthenticatedremains and now readsTokenStorage.i.accessToken. - Breaking: Renamed the
tokenHeaderparameter onGraphQLClient/AuthLinktoaccessTokenHeader. Added a new optionalrefreshTokenHeadercallback — when provided, its headers are attached to every request sent through the internal refresh client (the one passed intoonTokenRefresh/refreshCallback) instead ofaccessTokenHeader, for refresh endpoints that expect the refresh token itself as a header. - Breaking: Renamed
AccessTokenCallbacktoTokenHeaderCallbackand changed its signature fromFunction()toFunction(String token)—accessTokenHeader/refreshTokenHeader/SseLink.tokenHeadernow receive the current token (read fromTokenStorage.iby the caller) as an argument instead of having to readTokenStorage.ithemselves, and are only invoked when a token is actually stored (sotokenis never null in practice, and the callback's return type is now non-nullableMap<String, String>too).refreshTokenHeaderstill has no default — the refresh token normally reaches your refresh endpoint via therefreshCallbackparameter instead (for a body/variable); passrefreshTokenHeaderonly if it should also/instead go out as a header. - Breaking:
HttpLinkandBatchLinknow run ondio.Dioinstead ofpackage:http— theirclient:constructor parameter is nowdio.Dio?instead ofhttp.Client?, and thehttppackage dependency is dropped entirely.HttpLink's multipart upload detection now matchesDioLink's: passUpload/dio.MultipartFilevalues (nothttp.MultipartFile, which the publicUploadtypedef never pointed to) as GraphQL variables.HttpLinkConnectionException/HttpLinkCanceledExceptionare unchanged and still thrown for the equivalent failures.HttpLinkkeeps its manual best-effort cancellation race (rather than delegating toDio's owncancelToken:) since that still depends on adapter cooperation it doesn't want to require. - Added
isUnauthorizedtoGraphQLClient/AuthLink— an optionalAuthFailure Function(UnauthorizedContext context)that classifies a possibly-unauthorized response/exception, so a custom API's own convention (a non-standard error code, message, or status field) can drive the client's behavior instead of the built-in default.UnauthorizedContextnormalizeserrors/statusCode/errorCodethe same way whether the failure came from a streamedResponseor a thrownServerException, so detection logic doesn't need to juggle two nullable inputs.AuthFailurehas three outcomes:none(pass through),refresh(attempt a refresh and retry once), andrevoke(skip the refresh attempt entirely and revoke immediately — for failures a refresh wouldn't fix, like a valid token that simply lacks permission). The built-in default mapsextensions.code/HTTP status ofUNAUTHORIZED/UNAUTHENTICATED/401 torefreshandFORBIDDEN/403 torevoke(previously grouped with 401 and always attempted a refresh first). - Internal: Consolidated GraphQL error parsing/mapping into
graphql_error_mapper.dart(extractErrorCode,mapGraphQLError(s)) — previously theextensions['code']extraction and thegql_exec.GraphQLError→ this package'sGraphQLErrormapping were each hand-rolled in three-plus places (GraphQLExceptionExtension,GraphQLClient._executeNetworkRequest,UnauthorizedContext.errorCode). - Fix: A revoked/failed-refresh request no longer surfaces to
future()/stream()as a genericAppServerException('No response received from GraphQL server.').AuthLinknow always surfaces the real cause: the originalFORBIDDEN/401 response or exception when there's nothing more specific, or aRevokeTokenExceptiondescribing why the refresh attempt itself failed (wrapping whateveronTokenRefresh/refreshCallbackthrew) when that's more specific. - Fix:
HttpLinkandBatchLinknow parse responses with the sameResponseParserDioLink/SseLink/WebSocketLinkalready use, instead of a hand-rolled parse that kept onlyerrors[].messageand silently droppedlocations/path/extensions— meaningextensions.code(whichAuthLink's unauthorized detection andGraphQLExceptionExtensionboth key off) was lost for any error routed through either link. - Split
auth_link.dartintoauth_types.dart(typedefs,AuthFailure,UnauthorizedContext),refresh_header_link.dart(the internal linkrefreshTokenHeaderuses), andauth_link.dart(justAuthLinkitself) — the public API (package:mk_graphql/mk_graphql.dart) is unchanged. - Fix:
SseLinkno longer has its own duplicated, non-deduped 401-refresh logic. It now surfaces an unauthorized response as aServerException(via the sameclassifyDioExceptionclassificationDioLinkuses) so an outerAuthLinkcatches and retries it through its existing single-flight refresh lock — the same path every other link goes through — instead of racing its own independent retry againstAuthLink's. - HttpLink: Added cancellation support via the existing
DioLinkCancelTokenContextEntrycontext entry (the sameCancelTokenused everywhere else in the pipeline) — cancelling stopsHttpLinkfrom waiting on the in-flight request and throwsHttpLinkCanceledException. Notepackage:httphas no request-abort primitive, so the underlying request itself keeps running in the background; it's just no longer waited on. Also addedHttpLinkConnectionExceptionfor connection-level failures (http.ClientException, e.g. DNS/socket errors) and reclassified malformed response bodies asResponseFormatException— previously both fell into a single genericServerExceptionbucket. Added test coverage (previously untested). - Fix:
AuthLink's internal token-refresh client now shares the exact same configuredDioinstance as the rest of the pipeline (samedefaultHeaders, same interceptors), instead of silently falling back to a second, bareDio()whenever the app didn't pass its owndio:toGraphQLClient(...). Previously, refresh requests would go out missing any headers configured viadefaultHeaders. - Fix: Fixed a race in
WebSocketLinkwhere two subscriptions started back-to-back, before the first connection finished handshaking, could each open their ownWebSocketChannel— the second silently replacing (and leaking) the first. Connection attempts are now single-flight, mirroringAuthLink's existing refresh dedup. - Resource cleanup:
GraphQLClient.dispose(),AuthLink,SseLink,HttpLink, andBatchLinknow close theDio/http.Clientthey created for themselves (never one the caller passed in — that stays the caller's to close).BatchLink.dispose()also cancels its pending batch timer and fails any queued-but-undispatched requests instead of leaving their callers hanging forever. - Performance:
TypenameLinkand the query-document printing done by every terminating link (DioLink,SseLink,WebSocketLink,HttpLink,BatchLink) and byLoggerLinknow cache their result perDocumentNodeinstance. Generated requests hold onestatic finalDocumentNodeper operation (and hand-written ones resolve through the shared parse cache), so the same instance recurs on every call of a given operation — previously each call re-walked and rebuilt the whole AST (TypenameLink) and re-serialized it back to a query string (printNode, on every single network request) from scratch. - Internal: De-duplicated the twice-written
dio.ResponseJSON-body coercion inDioLinkinto one helper.
1.2.0 #
- Breaking:
OnRefreshCallback(used byonTokenRefreshonGraphQLClient,AuthLink, andSseLink) now receives a plainGraphQLClient(no auth link attached) instead of a rawDioLink, so refresh logic can use the typed client API (refreshClient.future(...),.stream(...)) instead of hand-rolling a raw HTTP request. - Fix:
AuthLinknow also catches a thrownServerException(e.g.DioLinkServerException, raised for a real non-2xx HTTP status) and treats a 401/403 status — or a parsedUNAUTHORIZED/UNAUTHENTICATED/FORBIDDENGraphQL error code — as unauthorized. Previously the refresh-and-retry flow only fired for HTTP 200 responses carrying a GraphQL error payload, silently skipping real HTTP-level 401/403s. - Managed token storage:
GraphQLClientcan now own its auth session end-to-end via the newrefreshCallbackparameter andTokenStorage(backed byflutter_secure_storagethrough the newSecureTokenStorage, whoseonChangeis driven byFlutterSecureStorage.registerListener; injectable for tests viatokenStorage). SupplyrefreshCallback: (refreshClient, refreshToken) async => AuthToken(...)and the client reads/writes theAuthorizationheader and persists refreshed tokens itself — no more app-side token storage plumbing. NewGraphQLClient.setAuthToken()(call after login),.clearAuthToken()(call on logout),.isAuthenticated, and.onAuthTokenChange(aStream<AuthToken?>that emits on every login/refresh andnullon logout or a revoked refresh — listen to it to drive app-wide auth state, e.g. redirect to login). - Breaking: Removed
onTokenRefreshfromGraphQLClient(still available on the lower-levelAuthLink/SseLink) —refreshCallbackis now the sole source of truth for refresh at the client level.RefreshCallback'srefreshTokenparameter is now non-nullable: the client skips calling it and revokes immediately when there's no stored refresh token, instead of asking every callback to null-check it. - However many requests fail at once,
AuthLinkmakes at most one refresh call and queues the rest behind it, retrying all of them once it resolves — already true via its single-flight refresh lock, now covered by an explicit concurrent-401 test through therefreshCallbackpath. - Pre-parsed request documents: Added
GraphQLRequest.documentNode(aDocumentNode), whichGraphQLClientnow sends instead of parsingdocumentper request. Defaults to the existing shared string-keyed parse cache (GraphQLClient.getParsedDocument) for hand-written requests — no behavior change there — butmk_graphql_generatornow overrides it with astatic finalfield computed once per generated request class, skipping even that cache lookup on every subsequent request of that type. - Fix:
DioLinkno longer lets a malformed GraphQL response body (errors/datapresent but wrongly shaped — a non-Maperror entry, a missingmessage, etc.) escape as a raw, unclassifiedTypeError.ResponseParser.parseResponsedoes unchecked casts internally;DioLinknow wraps that call so a bad body always surfaces asDioLinkParserException→ParseException(for a 2xx response) or degradesDioLinkServerException.parsedResponsetonullwhile still preserving the real status code (for a non-2xx response), instead of a bug-shaped exception with no stack trace or classification. - Added
DioLinkConnectionException(DioExceptionType.connectionError— no connectivity, DNS failure, socket error) andDioLinkCertificateException(DioExceptionType.badCertificate— TLS/SSL validation failure) as dedicatedDioLinkexception types, matching the existing timeout/cancel/server distinction instead of bucketing both into the genericDioLinkUnkownException.GraphQLExceptionExtension.fromLinkExceptionmaps them toNetworkExceptionand the newCertificateExceptionrespectively (previouslyconnectionErrorwas reclassified after the fact by unwrappingDioLinkUnkownException.originalExceptionin two separate places — now decided once, at the source).
1.1.0 #
- Dynamic Pagination Variables: Added
applyVariables()andupdateVariables()toGraphQLPaginatorto dynamically update active variables (e.g. search queries, genre/status filters, sorting) on page 1 without recreating the paginator or wiping other active filter criteria. - Refetch & Pagination Reset Lifecycle:
refetch()resets_baseRequest = initialRequest, restoring page 1 and clearing active filters/search to defaults, whileloadNext()persists and accumulates items across pages. - Fine-Grained Page Tracking: Added optional
getPageandhasNextPagecallbacks toGraphQLPaginator.page(...)to allow authoritative backend pagination metadata (such asPageInfo.hasNextPage) to govern whether subsequent pages exist. ErrorPolicySupport: AddedErrorPolicy.none,ErrorPolicy.ignore, andErrorPolicy.allacross requests, client executions, copy constructors, and client defaults.- Performance & Cache Optimization: Added deterministic canonical JSON variable cache key sorting and Document AST caching.
- Documentation: Updated comprehensive guides for client requests, caching, links, and Flutter BLoC
emit.forEachpagination patterns.
1.0.1 #
- Pagination: added
GraphQLPaginatorwithoffset/page/cursorfactories, a reactivePaginationState(items,isLoading,isLoadingMore,hasMore,error), andGraphQLClient/GraphQLRequestextension methods (paginate,paginateOffset,paginatePage,paginateCursor) for building infinite lists without hand-rolled pagination state. - Added an optional
requestIdoverride onGraphQLRequest, enabling cross-instance optimistic-response coalescing (used internally byCompositeGraphQLRequest). - Repository moved into the unified
mk_graphqlmonorepo alongsidemk_graphql_generator; no API changes from the move itself.
1.0.0 #
- Initial release of
mk_graphql. - Composable Link Architecture:
AuthLinkwith automatic 401 interception, thread-safe asynchronous token refresh queuing, and token revocation hooks.DioLinkpowered by Dio with full cancellation support (CancelToken), automatic GraphQL Multipart Request spec uploads (Uploadscalar /MultipartFile), and HTTP interceptors.SseLinkfor Server-Sent Events (SSE) subscriptions with dynamic token header injection and 401 retry support.WebSocketLinkfor GraphQL over WebSockets (graphql-wsand legacy protocol).HttpLinkfor lightweight HTTP operations.BatchLinkfor batching multiple GraphQL requests into a single HTTP payload.TypenameLinkfor automatic__typenameinjection into AST selection sets.LoggerLinkfor structured console logging with customizable options.
- Client & Caching:
GraphQLClientsupporting both Future (future()) and Stream (stream()) operations.- Pluggable cache interface with built-in normalized
InMemoryCache. - Configurable fetch policies (
cacheFirst,cacheOnly,networkOnly,cacheAndNetwork,noCache). - Optimistic response support.
- Fully typed responses and exceptions (
GraphQLResponse,GraphQLException,UnAuthorizedException,RequestCancelledException).