# Data Layer ## Purpose All manga/chapter/library data is fetched via the Suwayomi GraphQL API (`/api/graphql`); images come from a REST surface. Settings persist locally via SharedPreferences; Hive backs the GraphQL cache store. ## Key files | Path | Responsibility | |---|---| | `lib/src/global_providers/global_providers.dart` | Creates/provides the HTTP + WebSocket `GraphQLClient`s, `SharedPreferences`, `HiveStore`, rate-limit queue, top-level prefs | | `lib/src/graphql/schema.graphql` / `fragments.graphql` | Full schema; shared `PageInfoDto` fragment | | `lib/src/graphql/__generated__/*.graphql.dart` | Codegen output (inputs, enums, scalars, fragments) | | `lib/src/constants/endpoints.dart` | `Endpoints.baseApi(...)` — the single URL builder (HTTP/GraphQL/WebSocket, port toggle) | | `lib/src/constants/db_keys.dart` | `DBKeys` — every SharedPreferences key + default | | `lib/src/utils/mixin/shared_preferences_client_mixin.dart` | `SharedPreferenceClientMixin` / `SharedPreferenceEnumClientMixin` | | `lib/src/widgets/server_image.dart` | `ServerImage` — `CachedNetworkImage` with per-auth-type header/token injection | | `lib/src/utils/avif/` | AVIF detection and the Linux AVIF→PNG bridge (`libtsumiru_avif.so`) | | `lib/src/utils/extensions/cache_manager_extensions.dart` | Uses `serverImageRequest` for imperative `CacheManager.getSingleFile` | | `lib/src/features//data/_repository.dart` (+ `graphql/*.graphql`) | Per-feature repositories + operations | ## The two GraphQL clients **`graphQlClientProvider`** (HTTP queries/mutations): - `HttpLink` to `Endpoints.baseApi(..., isGraphQl: true)`, wrapped in `TimeoutHttpClient` (configurable timeout + retry). - Auth links prepended per `authTypeKeyProvider`: `basic` → `AuthLink`; `simpleLogin`/`uiLogin` → `SuwayomiAuthLink` (see [auth.md](auth.md)). - Cache: `GraphQLCache(store: hiveStoreProvider)`, default `FetchPolicy.noCache` — **every query hits the network**; Hive is effectively write-only. **`graphQlSubscriptionClientProvider`** (WebSocket subscriptions): - `WebSocketLink`, `subProtocol: graphqlTransportWs`. - **Auth is on the socket, not a Link**: `ui_login` → `initialPayload` async fn returning `{Authorization: }` (no "Bearer "); `simple_login`/`basic` → `handshakeHeaders`. **`graphQlClientNotifierProvider`** — `ValueNotifier` for the `GraphQLProvider` widget. ## Repository pattern ``` features//data/_repository.dart # class taking GraphQLClient + @riverpod factory features//data/graphql/query.graphql # operations features//data/graphql/__generated__/... # graphql_codegen output features//domain//graphql/fragment.graphql # fragments near their domain type ``` - Repository methods call codegen extension methods: `client.query$AllCategories()`, `client.mutate$CreateCategory(Options$Mutation$CreateCategory(variables: ...))` — not raw `client.query(QueryOptions(...))`. - The `@riverpod` factory is a one-liner: `CategoryRepository(ref.watch(graphQlClientProvider))`. - After changing `.graphql` files or `@riverpod`: `dart run build_runner build --delete-conflicting-outputs`. ## Riverpod conventions - `@riverpod` on a function → a provider (repositories); `@riverpod` on a class extending `_$X` → a notifier provider. - Riverpod 3 codegen providers **auto-dispose by default** (`$NotifierProvider` / `$Notifier` with `isAutoDispose: true`) — there is no separate `AutoDispose*` type prefix anymore. Add `keepAlive: true` to opt a provider out. - **Persisted prefs** follow the mixin pattern: class mixes `SharedPreference[Enum]ClientMixin`, `build()` calls `initialize(DBKeys.key)` (watches `sharedPreferencesProvider`, `ref.listenSelf` auto-persists), callers use `.update(value)`. - `sharedPreferencesProvider` and `hiveStoreProvider` are `throw UnimplementedError()` sentinels — overridden in `ProviderScope` at startup. ## Image / CacheManager auth - `basic` → `Authorization` header. - `simple_login` → `Cookie` header (watched reactively via `.select`). - `ui_login` sends the token in the fetch URL and uses `accountImageCacheKey` for storage. A verified account key combines the catalog ID and untokened URL; token rotation and restart preserve it. Missing or changing accounts use an ephemeral session key. Other auth modes retain their URL keys. - `serverImageRequest` supplies the same fetch URL, key, and headers to raw reads, page prefetch, and reader share/gallery actions. `getServerFile` delegates to it. Covers and crop rendering use the same account-key helper; cache directories and manager instances stay shared. - Web server images use `ImageRenderMethodForWeb.HttpGet` to decode downloaded bytes. The HTML-image path can turn cached covers and source icons black after navigation on Flutter 3.47 ([upstream issue](https://github.com/flutter/flutter/issues/191800)). ## AVIF images - Flutter's Linux engine has no AVIF decoder. `lib/src/utils/avif/` detects AVIF from the ftyp box (`avif`/`avis` major or compatible brands) and converts to lossless PNG through the bundled `libtsumiru_avif.so`; the C ABI is declared in `linux/tsumiru_avif.h`. - The cover and reader-page cache managers replace an AVIF cache file in place with an atomic rename before bytes reach `CachedNetworkImage` or the engine decoder. Cache keys, directories, headers and expiry are unchanged. A failed conversion leaves the original file untouched and is retried when the file's identity changes. - Local and offline AVIF files are converted in memory only; the user's originals are never rewritten. The reader normalizes a page once before cropping, so the cropped result and the uncropped decode fallback both get decodable bytes. Tall-page strips, spread actions and `tallPageIntrinsicSize` share the same detector and decoder. - Conversions started from the UI isolate run in a background isolate with at most two at a time. Code that already runs inside a background isolate (strips, spread) converts inline. Non-Linux platforms, non-AVIF bytes, a missing shim and malformed input pass through unchanged, so the image fails as an ordinary decode error. ## Gotchas - **WebSocket auth via a `Link` is silently ineffective** — must use `initialPayload`/`SocketClientConfig.headers`. (Cost a debug session historically.) - **`ui_login` WS sends a bare token, not `Bearer `** (server `onInit` doesn't strip the prefix). HTTP uses `Bearer`. - **Keep fetch URLs separate from account cache keys.** Never use a bare URL key for UI-login images or include credentials in a cache key. - **Subscription 401 mid-stream is not handled** — `SuwayomiAuthLink` only inspects the first event. Acceptable because Suwayomi subscriptions are short-lived. - **Token refresh uses a raw, un-authed client** to avoid infinite recursion on 401. - **Default `FetchPolicy.noCache`** — don't expect GraphQL cache reads.