anilink-api-wrapper 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,522 +1,70 @@
1
- # AniLink
1
+ <p align="center">
2
+ <img src="docs-src/public/logo.png" alt="AniLink" width="256" />
3
+ </p>
4
+
5
+ <h1 align="center">AniLink</h1>
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/anilink-api-wrapper.svg)](https://www.npmjs.com/package/anilink-api-wrapper)
4
8
  [![npm downloads](https://img.shields.io/npm/dm/anilink-api-wrapper.svg)](https://www.npmjs.com/package/anilink-api-wrapper)
5
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE)
6
10
  [![CI](https://github.com/RLAlpha49/AniLink/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/RLAlpha49/AniLink/actions/workflows/ci.yml)
7
11
  [![CodeQL](https://github.com/RLAlpha49/AniLink/actions/workflows/codeql.yml/badge.svg?branch=master)](https://github.com/RLAlpha49/AniLink/actions/workflows/codeql.yml)
8
- [![Documentation](https://img.shields.io/website?url=https%3A%2F%2Frlalpha49.github.io%2FAniLink%2F&label=docs)](https://rlalpha49.github.io/AniLink/)
9
-
10
- A typed TypeScript wrapper for the [AniList GraphQL API](https://docs.anilist.co/). AniLink turns raw AniList GraphQL into a set of named methods. You can query a user, save a list entry, or toggle a favourite. You do not need to write query strings or hand-roll HTTP.
11
-
12
- > 🧪 Try the [AniLink API Explorer](https://rlalpha49.github.io/AniLink/explorer/) — build and test AniLink calls live against AniList.
13
-
14
- ## Why It Exists
15
-
16
- AniList exposes a GraphQL API that is flexible but verbose. Every request needs a query document, a variables object, and careful handling of the response shape. AniLink removes that ceremony. You call a method, pass a plain object, and get back typed data. AniLink also validates your variables before they leave your app. A wrong field type fails fast with a clear message instead of a confusing API error.
17
-
18
- ## What You Can Do With It
19
-
20
- AniLink uses one instance with a single `anilist` surface:
21
-
22
- - `anilist.query` fetches data. This includes users, media, characters, staff, studios, reviews, activities, threads, notifications, and more.
23
-
24
- - `anilist.query.page` returns paginated versions of the same resources.
25
-
26
- - `anilist.paginate`, `anilist.paginatePages`, and `anilist.paginateChunks` walk every page or chunk for you, so you do not hand-roll `hasNextPage` loops.
12
+ [![Documentation](https://img.shields.io/website?url=https%3A%2F%2Fanilink.alpha49.com%2F&label=docs)](https://anilink.alpha49.com/)
27
13
 
28
- - `anilist.mutation` changes data. You can update your profile, save and delete list entries, and post activities and replies. You can also toggle likes and favourites and manage reviews, threads, and AniChart settings.
29
-
30
- - `anilist.custom` sends any raw query or mutation when you need something the named methods do not cover.
31
-
32
- ```typescript
33
- import { AniLink } from "anilink-api-wrapper";
34
-
35
- // Remember that you can create multiple instances with different auth tokens
36
- // or one instance without a token for public queries.
37
- const aniLink = new AniLink();
38
- const aniLinkAuth = new AniLink("your-auth-token");
39
-
40
- // Fetch a user
41
- const user = await aniLink.anilist.query.user({ id: 542244 });
42
-
43
- // Save an anime to your list
44
- await aniLinkAuth.anilist.mutation.saveMediaListEntry({
45
- mediaId: 1,
46
- status: "COMPLETED",
47
- score: 9,
48
- });
49
-
50
- // Send a raw query
51
- const viewer = await aniLinkAuth.anilist.custom("query { Viewer { id } }");
52
- ```
14
+ A typed TypeScript wrapper for the [AniList GraphQL API](https://docs.anilist.co/) and the [MyAnimeList REST API](https://myanimelist.net/apiconfig/references/api/v2). One class, two isolated provider surfaces, normalized errors, retries, and a generated operation reference.
53
15
 
54
- ## Key Features
55
-
56
- - **Typed end to end.** Every method has a typed variables interface and a typed response. Your editor catches mistakes before runtime.
57
-
58
- - **No GraphQL strings for common tasks.** The named query and mutation methods cover the everyday AniList operations.
59
-
60
- - **Variable validation.** AniLink checks required fields and types before sending. It throws a descriptive error like `Invalid id: 542244. Expected type: number` when something is wrong.
61
-
62
- - **Optional auth.** Construct with a token for authenticated calls, or without one for public queries. You can create multiple instances with different tokens.
63
-
64
- - **Custom escape hatch.** `anilist.custom` accepts any query or mutation string with an optional variables object.
65
-
66
- - **Pagination built in.** Page queries accept `page` and `perPage`, and the `paginate` / `paginatePages` / `paginateChunks` helpers walk every page or chunk with a max-page guard.
67
-
68
- - **Clear error handling.** API errors, including rate limits, surface as thrown errors you can catch and retry.
69
-
70
- ## Who It Is For
71
-
72
- AniLink is for developers building tools around AniList. This includes trackers, recommendation engines, Discord bots, dashboards, or anything that reads or writes AniList data. If you would rather call `query.media({ id: 1 })` than maintain GraphQL documents, this is for you.
73
-
74
- ## Getting Started
75
-
76
- ### Install
16
+ ## Quickstart
77
17
 
78
18
  ```bash
79
19
  npm install anilink-api-wrapper
80
20
  ```
81
21
 
82
- ### Initialize
83
-
84
22
  ```typescript
85
23
  import { AniLink } from "anilink-api-wrapper";
86
24
 
87
- // With a token (required for authenticated queries and mutations)
88
- const aniLinkAuth = new AniLink("your-auth-token");
89
-
90
- // Without a token (public queries only)
25
+ // AniList (GraphQL) public queries need no token
91
26
  const aniLink = new AniLink();
92
- ```
93
-
94
- Get a token by registering an application on the [AniList developer settings](https://anilist.co/settings/developer).
27
+ const anime = await aniLink.anilist.query.media({ id: 21, type: "ANIME" });
95
28
 
96
- #### Client options
97
-
98
- The second constructor argument accepts optional transport settings. Options are **per instance**: creating a second `AniLink` never changes how an existing one behaves.
99
-
100
- | Option | Type | Default | Description |
101
- | --------------------- | ------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
- | `timeout` | `number` | `30000` | Milliseconds before a request is aborted. `0` disables the timeout. Timeout errors carry the effective duration as `timeoutMs`. |
103
- | `signal` | `AbortSignal` | — | Cancel in-flight requests (for example when a user navigates away). |
104
- | `retry` | `boolean \| Partial<RetryPolicy>` | default policy | Automatic retries for transient failures are **on by default** (`maxRetries: 3` over HTTP `429`/`5xx`, network, and timeout errors). Pass `retry: false` to opt out. See [Retry with backoff](#retry-with-backoff). |
105
- | `paceWithRateLimit` | `boolean` | `false` | Opt into proactive pacing: when the `x-ratelimit-remaining` header drops below `rateLimitFloor`, the next request waits until the window resets instead of discovering the limit via a `429`. |
106
- | `rateLimitFloor` | `number` | `1` | Remaining-quota threshold below which `paceWithRateLimit` delays the next request. |
107
- | `circuitBreaker` | `{ threshold: number; cooldownMs: number }` | — | Opt into a per-client circuit breaker: after `threshold` consecutive failures, requests fail fast with `CIRCUIT_OPEN_ERROR` until `cooldownMs` elapses. |
108
- | `onError` | `(error, context) => void` | — | Invoked when an attempt fails and once more when retries are exhausted. |
109
- | `onRetry` | `(error, context) => void` | — | Invoked before each retry wait with the scheduled delay. |
110
- | `onRequestStart` | `(context) => void` | — | Invoked just before each attempt is sent. |
111
- | `onResponse` | `(context) => void` | — | Invoked after each attempt completes, with the elapsed `durationMs`. |
112
- | `exposeRawAxiosError` | `boolean` | `false` | Attach the original Axios error to thrown errors for local debugging. |
113
-
114
- ```typescript
115
- const aniLink = new AniLink("your-auth-token", {
116
- timeout: 10_000,
117
- retry: { maxRetries: 2 },
118
- onResponse: ({ url, durationMs }) => console.log(url, `${durationMs}ms`),
119
- });
29
+ // MyAnimeList (REST) — isolated credential slot
30
+ const client = new AniLink({ mal: { accessToken: "mal-token" } });
31
+ const malAnime = await client.mal.anime.get(21, { fields: ["id", "title", "main_picture"] });
120
32
  ```
121
33
 
122
- The hook context carries `url`, `method`, the 1-based `attempt`, the stable error `code`, the HTTP `status` for API failures, and `nextDelayMs` when the failure will be retried.
34
+ ## What you can do
123
35
 
124
- ### OAuth
36
+ | Provider | Namespace | Capabilities |
37
+ | --------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | **AniList** | `aniLink.anilist` | Queries, page queries, mutations, pagination helpers, `custom()`, data helpers |
39
+ | **MyAnimeList** | `aniLink.mal` | `anime.get`, `manga.get`, and `user.me` REST reads with field selection, plus `anime`/`manga` `updateMyListStatus` and `deleteFromList` list-status writes |
125
40
 
126
- AniLink ships helpers for the AniList OAuth2 authorization-code flow, so you can obtain and refresh the token you pass to the `AniLink` constructor instead of hand-rolling the HTTP calls.
127
-
128
- First, register an application on the [AniList developer settings](https://anilist.co/settings/developer) to get a client ID and client secret, and pick a redirect URI.
129
-
130
- Send the user to the authorization URL to approve your application:
131
-
132
- ```typescript
133
- import { buildAuthorizationUrl } from "anilink-api-wrapper";
134
-
135
- const state = crypto.randomUUID(); // a fresh random value per login attempt
136
- const authorizeUrl = buildAuthorizationUrl("your-client-id", "https://example.com/callback", state);
137
- // Redirect the user to `authorizeUrl`. After approval, AniList sends them back
138
- // to your redirect URI with `?code=` and `state=` query parameters.
139
- ```
140
-
141
- The third `state` parameter is **optional** but strongly recommended as additional CSRF protection. Always pass a random `state`, bind it to the user's session, and validate that the `state` on the redirect matches before exchanging the code. This cross-site request forgery (CSRF) check stops attackers from completing authorization flows your users never started.
142
-
143
- Exchange the authorization code from the redirect for an access token:
144
-
145
- ```typescript
146
- import { getAccessToken, AniLink } from "anilink-api-wrapper";
147
-
148
- const { access_token, refresh_token } = await getAccessToken(
149
- "your-client-id",
150
- "your-client-secret",
151
- code, // the `code` query parameter from the redirect
152
- "https://example.com/callback"
153
- );
154
-
155
- const aniLink = new AniLink(access_token);
156
- ```
157
-
158
- When the access token expires, exchange the stored refresh token for a new one. Note that the refresh response may not include a new `refresh_token`, in which case you keep using the one you stored:
159
-
160
- ```typescript
161
- import { refreshAccessToken } from "anilink-api-wrapper";
162
-
163
- const { access_token, refresh_token: rotated } = await refreshAccessToken(
164
- "your-client-id",
165
- "your-client-secret",
166
- refresh_token
167
- );
168
-
169
- const nextRefreshToken = rotated ?? refresh_token;
170
- const aniLink = new AniLink(access_token);
171
- ```
172
-
173
- AniList reports the token lifetime as `expires_in` seconds. Use `getTokenExpiry` to refresh proactively before the token expires instead of waiting for a `401`:
174
-
175
- ```typescript
176
- import { getTokenExpiry, refreshAccessToken } from "anilink-api-wrapper";
177
-
178
- if (Date.now() >= getTokenExpiry(tokenResponse).getTime() - 60_000) {
179
- // Refresh at least a minute before expiry.
180
- tokenResponse = await refreshAccessToken(
181
- "your-client-id",
182
- "your-client-secret",
183
- nextRefreshToken
184
- );
185
- }
186
- ```
187
-
188
- ### Query
189
-
190
- ```typescript
191
- const user = await aniLink.anilist.query.user({ id: 542244, asHtml: true });
192
- const media = await aniLink.anilist.query.media({ id: 1, type: "ANIME" });
193
- const viewer = await aniLink.anilist.query.viewer({ asHtml: true });
194
- ```
195
-
196
- ### Paginate
197
-
198
- Page queries accept `page` and `perPage` and return a single page with `pageInfo`. Fetch one page when you know the range:
199
-
200
- ```typescript
201
- const page = await aniLink.anilist.query.page.medias({
202
- page: 1,
203
- perPage: 10,
204
- type: "ANIME",
205
- sort: ["POPULARITY_DESC"],
206
- });
207
- ```
208
-
209
- To walk every page, use the helpers on `aniLink.anilist`. They track `page`/`perPage` and `hasNextPage` for you and stop at a `maxPages` guard, so a runaway loop cannot fetch forever.
210
-
211
- `paginate` collects every item across all pages into one array:
212
-
213
- ```typescript
214
- const result = await aniLink.anilist.paginate(
215
- (page, perPage) => aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" }),
216
- "media",
217
- { perPage: 50, maxPages: 10 }
218
- );
219
- console.log(result.items.length, result.pageCount, result.truncated);
220
- ```
221
-
222
- `paginatePages` yields each raw page response in turn. Use it for streaming or early exit when you do not need every item in memory:
223
-
224
- ```typescript
225
- for await (const page of aniLink.anilist.paginatePages((page, perPage) =>
226
- aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" })
227
- )) {
228
- console.log(page.pageInfo.currentPage, page.media.length);
229
- if (page.media.length > 0 && page.media[0].id === 1) break;
230
- }
231
- ```
232
-
233
- `paginateChunks` walks `MediaListCollection` chunks, which AniList returns with `hasNextChunk` instead of `pageInfo`:
234
-
235
- ```typescript
236
- const result = await aniLink.anilist.paginateChunks(
237
- (chunk, perChunk) =>
238
- aniLink.anilist.query.mediaListCollection({
239
- userId: 542244,
240
- type: "ANIME",
241
- chunk,
242
- perChunk,
243
- }),
244
- "lists",
245
- { perChunk: 500, maxChunks: 20 }
246
- );
247
- console.log(result.items.length, result.chunkCount, result.truncated);
248
- ```
249
-
250
- Each helper returns `truncated: true` when it stopped at the guard before the source ran out of pages or chunks.
251
-
252
- By default pages and chunks are fetched strictly one at a time. Pass `concurrency` to keep several requests in flight while collecting results — useful for large traversals where per-page latency dominates. Results are always returned in order regardless of which request finishes first, scheduling stops as soon as the source reports no more data, and the `maxPages` / `maxChunks` guards still apply:
253
-
254
- ```typescript
255
- const result = await aniLink.anilist.paginate(
256
- (page, perPage) => aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" }),
257
- "media",
258
- { perPage: 50, maxPages: 10, concurrency: 4 }
259
- );
260
- console.log(result.items.length, result.pageCount, result.truncated);
261
- ```
262
-
263
- Keep `concurrency` modest (2–8): AniList rate-limits aggressive clients, and values above 8 are clamped down to 8.
264
-
265
- ### Mutate
266
-
267
- ```typescript
268
- await aniLink.anilist.mutation.saveMediaListEntry({
269
- mediaId: 1,
270
- status: "COMPLETED",
271
- score: 9,
272
- });
273
-
274
- await aniLink.anilist.mutation.toggleFavourite({ animeId: 1 });
275
- ```
276
-
277
- ### Handle errors
278
-
279
- AniLink throws typed errors with stable `code` values. HTTP failures are
280
- represented by `AniLinkApiError` and expose the HTTP `status`; network,
281
- timeout, and cancellation failures use `AniLinkNetworkError`. Calling an
282
- authenticated operation without a token throws `AniLinkAuthError`. Successful
283
- AniList response data is returned normally as the typed result of each
284
- query or mutation. For failed API requests, `AniLinkApiError.data` contains
285
- the upstream AniList response body. AniLink does not expose the raw Axios
286
- response object, request headers, bearer token, or request internals.
287
-
288
- ```typescript
289
- import {
290
- AniLinkApiError,
291
- AniLinkAuthError,
292
- AniLinkGraphQLError,
293
- AniLinkNetworkError,
294
- } from "anilink-api-wrapper";
295
-
296
- try {
297
- const user = await aniLink.anilist.query.user({ id: 542244 });
298
- console.log(user);
299
- } catch (error: unknown) {
300
- if (error instanceof AniLinkApiError) {
301
- console.error(error.code, error.status, error.data);
302
-
303
- if (error.status === 429) {
304
- // Rate-limit accounting from the response headers, when present.
305
- console.error("Quota reset at:", error.rateLimit?.reset);
306
- }
307
- } else if (error instanceof AniLinkGraphQLError) {
308
- // The request returned HTTP 200 but carried GraphQL errors.
309
- console.error(error.graphqlErrors.map((e) => e.message));
310
- console.error(error.data); // any partial data returned alongside the errors
311
- } else if (error instanceof AniLinkAuthError) {
312
- console.error(error.code, error.message);
313
- } else if (error instanceof AniLinkNetworkError) {
314
- console.error(error.code, error.message);
315
- } else {
316
- throw error;
317
- }
318
- }
319
- ```
320
-
321
- The available transport codes are `API_ERROR`, `GRAPHQL_ERROR`, `NETWORK_ERROR`,
322
- `TIMEOUT_ERROR`, `ABORTED_ERROR`, `CIRCUIT_OPEN_ERROR`, `AUTH_ERROR`,
323
- `VALIDATION_ERROR`, and `UNKNOWN_ERROR`.
324
-
325
- GraphQL-level failures can arrive inside an HTTP 200 response. AniLink throws
326
- these as `AniLinkGraphQLError` (a subclass of `AniLinkApiError` with `status`
327
- `200`) instead of returning half-valid data. The upstream `errors` array is
328
- available on `error.graphqlErrors`, and any partial `data` on `error.data`.
329
-
330
- When AniList includes rate-limit headers (`x-ratelimit-limit`,
331
- `x-ratelimit-remaining`, `x-ratelimit-reset`), every `AniLinkApiError` exposes
332
- them as a read-only `rateLimit` object so schedulers and UIs can self-throttle.
333
-
334
- For local debugging, you can opt into the original Axios error:
335
-
336
- ```typescript
337
- const debugClient = new AniLink("your-auth-token", {
338
- exposeRawAxiosError: true,
339
- });
340
-
341
- try {
342
- await debugClient.anilist.query.user({ id: 542244 });
343
- } catch (error: unknown) {
344
- if (error instanceof AniLinkApiError) {
345
- console.error(error.rawAxiosError);
346
- }
347
- }
348
- ```
349
-
350
- `exposeRawAxiosError` defaults to `false`. Use it for local debugging;
351
- the raw Axios error can contain request configuration and bearer-token
352
- headers.
353
-
354
- ### Retry with backoff
355
-
356
- AniLink retries transient failures for you by default: HTTP `429` and `5xx`
357
- responses plus network and timeout errors are retried with exponential
358
- backoff (`maxRetries: 3`). The `Retry-After` header is honored for `429`
359
- responses (capped at 60 seconds). Pass `retry: false` to opt out and send
360
- every request exactly once, or pass a partial policy to tune individual
361
- knobs on top of the defaults.
362
-
363
- ```typescript
364
- // Opt out of the default policy
365
- const aniLink = new AniLink("your-auth-token", { retry: false });
366
-
367
- // Or tune the default policy
368
- const aniLink = new AniLink("your-auth-token", {
369
- retry: {
370
- maxRetries: 3, // retries after the initial attempt
371
- baseDelayMs: 250, // first backoff delay
372
- maxDelayMs: 5_000, // backoff cap
373
- retryOnStatus: [429, 500, 502, 503, 504],
374
- retryOnNetworkError: true,
375
- jitter: true, // randomize each wait within [0, computed delay]
376
- },
377
- });
378
- ```
379
-
380
- Backoff delays use **full jitter** by default: each wait is a random value
381
- between `0` and the computed exponential cap. This spreads out retries from
382
- many concurrent clients instead of letting them re-fire in lockstep against
383
- the shared rate limit. Server-dictated `Retry-After` waits are never jittered.
384
- Pass `jitter: false` for deterministic delays.
385
-
386
- ### Rate-limit pacing and circuit breaking
387
-
388
- Two further resilience knobs are available per instance, both off by default:
389
-
390
- ```typescript
391
- import { AniLinkNetworkError } from "anilink-api-wrapper";
392
-
393
- const aniLink = new AniLink("your-auth-token", {
394
- // Slow down before AniList does: once a successful response reports fewer
395
- // remaining requests than `rateLimitFloor`, hold the next request until
396
- // the window resets instead of eating a 429.
397
- paceWithRateLimit: true,
398
- rateLimitFloor: 5,
399
- // Fail fast during sustained outages: after 5 consecutive failures,
400
- // requests throw `CIRCUIT_OPEN_ERROR` without touching the network for
401
- // 30 seconds, then one probe request is let through again.
402
- circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
403
- });
404
-
405
- try {
406
- await aniLink.anilist.query.user({ id: 542244 });
407
- } catch (error: unknown) {
408
- if (error instanceof AniLinkNetworkError && error.code === "CIRCUIT_OPEN_ERROR") {
409
- // Back off; the breaker will probe recovery automatically.
410
- }
411
- }
412
- ```
413
-
414
- ### Error hook
415
-
416
- The `onError` hook is invoked when a request ultimately fails after all
417
- retries are exhausted (or immediately when retries are disabled). Use it
418
- to implement your own fallback (for example a cache or an offline queue).
419
-
420
- ```typescript
421
- import { AniLink, AniLinkApiError } from "anilink-api-wrapper";
422
-
423
- const aniLink = new AniLink("your-auth-token", {
424
- onError: (error, context) => {
425
- console.error(`Request to ${context.url} failed on attempt ${context.attempt}`);
426
- if (error instanceof AniLinkApiError && error.status === 429) {
427
- // queue the request for later
428
- }
429
- },
430
- });
431
- ```
432
-
433
- ### Observability hooks
434
-
435
- Beyond `onError`, three optional hooks let you instrument the request
436
- lifecycle without wrapping any methods. All are per-instance options:
437
-
438
- ```typescript
439
- const aniLink = new AniLink("your-auth-token", {
440
- // Fires just before each attempt is sent.
441
- onRequestStart: ({ url, method, attempt }) => {
442
- console.log(`#${attempt} -> ${method} ${url}`);
443
- },
444
- // Fires after each attempt completes, success or failure.
445
- onResponse: ({ url, durationMs }) => {
446
- console.log(`${url} took ${durationMs}ms`);
447
- },
448
- // Fires before each retry wait, with the scheduled delay.
449
- onRetry: (error, { attempt, nextDelayMs, status }) => {
450
- console.warn(
451
- `attempt ${attempt} failed (${status ?? error.code}); retrying in ${nextDelayMs}ms`
452
- );
453
- },
454
- });
455
- ```
456
-
457
- - `onRequestStart` receives `{ url, method, attempt }`.
458
- - `onResponse` receives the same context plus `durationMs`, and fires for
459
- failed attempts too, so it can drive latency histograms.
460
- - `onRetry` receives the normalized error plus `{ url, method, attempt, code,
461
- status?, nextDelayMs }`. When you do not set `onRetry`, per-attempt
462
- notifications fall back to `onError` (which then fires before each retry
463
- wait and once more at terminal failure).
464
-
465
- These hooks are synchronous and fire in addition to the promise result; they
466
- never change request behavior. Hook exceptions are isolated: a throwing hook
467
- is reported as a console warning and never crashes a request or distorts
468
- retry and error classification.
469
-
470
- Common status codes from the AniList API:
471
-
472
- - `400` bad request
473
- - `401` unauthorized
474
- - `404` not found
475
- - `429` too many requests (rate limit)
476
- - `500` internal server error
41
+ Both surfaces share one transport layer (timeouts, retries, pacing, circuit breaker, hooks) while keeping credentials and transport settings isolated per provider slot.
477
42
 
478
43
  ## Documentation
479
44
 
480
- Full method and parameter reference: [AniLink documentation](https://rlalpha49.github.io/AniLink/).
481
-
482
- More usage examples: [ANILIST_API_EXAMPLES](https://github.com/RLAlpha49/AniLink/blob/master/Examples/ANILIST_API_EXAMPLES.md).
483
-
484
- Coverage versus the current AniList schema — which upstream operations AniLink wraps, which it deliberately does not, and any detected drift — is rendered into [`artifacts/anilist-api-compare/report.md`](artifacts/anilist-api-compare/report.md) by every CI run and kept as a retained workflow artifact.
485
-
486
- ### Upstream compatibility
487
-
488
- AniList's GraphQL API is unversioned: fields can appear, be deprecated, or disappear at any time. AniLink pins a schema snapshot and gates every pull request against it with `npm run anilist:api:compare -- --strict --ignore-unimplemented`, so drift is caught before merge rather than at your runtime. The comparison report classifies upstream changes as:
489
-
490
- - **Discrepancy** — an implemented operation's contract no longer matches the snapshot (missing field, changed type). Strict mode fails CI; these ship as a patch or minor fix-forward.
491
- - **Removed operation** — an operation AniLink wrapped no longer exists upstream. The wrapper method is dropped and the removal ships as a **minor** release with a documented migration snippet using `custom()`.
492
- - **Deprecated operation** — AniList marked a field or operation deprecated. AniLink keeps wrapping it, tracks the deprecation in the report, and removes it only when AniList does.
493
- - **Unimplemented operation** — an upstream operation AniLink has never wrapped. These are listed in the report; operations that can never be wrapped are recorded with a dated review note in `IGNORED_UNIMPLEMENTED_OPERATIONS` (`lib/api-compare/compare.ts`).
494
-
495
- Until a removal ships, `custom()` lets you call any upstream operation directly:
496
-
497
- ```typescript
498
- const result = await aniLink.anilist.custom(
499
- "query ($id: Int) { Media (id: $id) { id title { romaji } } }",
500
- { id: 1 }
501
- );
502
- ```
45
+ | Surface | Start here |
46
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
47
+ | **Guides** | [Introduction](https://anilink.alpha49.com/introduction) · [Getting started](https://anilink.alpha49.com/getting-started) · [Provider configuration](https://anilink.alpha49.com/provider-configuration) · [Per-request options](https://anilink.alpha49.com/per-request-options) · [Error handling](https://anilink.alpha49.com/error-handling) · [Retries & resilience](https://anilink.alpha49.com/retries-and-resilience) · [Cancellation & timeouts](https://anilink.alpha49.com/cancellation-and-timeouts) · [Observability](https://anilink.alpha49.com/observability) · [Recipes](https://anilink.alpha49.com/recipes) · [TypeScript patterns](https://anilink.alpha49.com/typescript-patterns) · [Troubleshooting](https://anilink.alpha49.com/troubleshooting) |
48
+ | **AniList guides** | [Authentication](https://anilink.alpha49.com/guides/anilist/authentication) · [Client configuration](https://anilink.alpha49.com/guides/anilist/configuration) · [Querying](https://anilink.alpha49.com/guides/anilist/querying) · [Page queries](https://anilink.alpha49.com/guides/anilist/page-queries) · [Pagination](https://anilink.alpha49.com/guides/anilist/pagination) · [Mutations](https://anilink.alpha49.com/guides/anilist/mutations) · [Custom queries](https://anilink.alpha49.com/guides/anilist/custom-queries) · [Helpers](https://anilink.alpha49.com/guides/anilist/helpers) |
49
+ | **MAL guides** | [Authentication](https://anilink.alpha49.com/guides/mal/authentication) · [Client configuration](https://anilink.alpha49.com/guides/mal/configuration) · [Operations](https://anilink.alpha49.com/guides/mal/operations) |
50
+ | **Operation reference** | [Overview](https://anilink.alpha49.com/operations/) · [AniList catalog](https://anilink.alpha49.com/operations/anilist) · [MAL catalog](https://anilink.alpha49.com/operations/mal) |
51
+ | **API reference (TypeDoc)** | [AniLink](https://anilink.alpha49.com/classes/AniLink.AniLink.html) — full generated reference at the [docs root](https://anilink.alpha49.com/) |
503
52
 
504
53
  ## Development
505
54
 
506
55
  ```bash
507
56
  npm install # install dependencies
508
- npm run typecheck # TypeScript type checking
509
- npm test # run unit tests
510
- npm run test:integration # run integration tests
511
- npm run lint # lint source and tests
512
- npm run build # build to dist/
513
- npm run docs:generate # generate API docs to docs/
57
+ npm run check # typecheck, lint, tests, format, JSDoc, api-compare, build
58
+ npm run docs:generate # TypeDoc + operation reference + guides site into docs/
59
+ npm run docs:dev # serve the guides site locally
514
60
  ```
515
61
 
62
+ See the [contributing guide](CONTRIBUTING.md) for workflow details.
63
+
516
64
  ## Resources
517
65
 
518
- - [Contributing guide](CONTRIBUTING.md)
519
66
  - [Changelog](CHANGELOG.md) and [GitHub Releases](https://github.com/RLAlpha49/AniLink/releases)
67
+ - [Contributing guide](CONTRIBUTING.md)
520
68
 
521
69
  ## License
522
70