Ctrl K
ComunidadGuías…

docs/architecture/data-layer.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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<T>` / `SharedPreferenceEnumClientMixin<T>` |
| `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/<f>/data/<name>_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: <bare token>}` (no "Bearer "); `simple_login`/`basic` → `handshakeHeaders`.

**`graphQlClientNotifierProvider`** — `ValueNotifier<GraphQLClient>` for the `GraphQLProvider` widget.

## Repository pattern

```
features/<f>/data/<name>_repository.dart      # class taking GraphQLClient + @riverpod factory
features/<f>/data/graphql/query.graphql       # operations
features/<f>/data/graphql/__generated__/...    # graphql_codegen output
features/<f>/domain/<type>/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 <token>`** (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.