anilink-api-wrapper 1.29.2 → 2.0.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.
Files changed (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +523 -241
  3. package/dist/AniLink.d.ts +11397 -0
  4. package/dist/AniLink.mjs +6664 -0
  5. package/dist/anilist.d.ts +1 -0
  6. package/dist/anilist.mjs +5 -0
  7. package/package.json +111 -46
  8. package/dist/AniLink.js +0 -248
  9. package/dist/apis/anilist/Custom.js +0 -42
  10. package/dist/apis/anilist/interfaces/Activity.js +0 -436
  11. package/dist/apis/anilist/interfaces/Basic.js +0 -33
  12. package/dist/apis/anilist/interfaces/CoverImage.js +0 -15
  13. package/dist/apis/anilist/interfaces/Distribution.js +0 -23
  14. package/dist/apis/anilist/interfaces/ExternalLink.js +0 -14
  15. package/dist/apis/anilist/interfaces/Favoured.js +0 -2
  16. package/dist/apis/anilist/interfaces/FuzzyDate.js +0 -12
  17. package/dist/apis/anilist/interfaces/Image.js +0 -13
  18. package/dist/apis/anilist/interfaces/ListScores.js +0 -2
  19. package/dist/apis/anilist/interfaces/Media.js +0 -13
  20. package/dist/apis/anilist/interfaces/Name.js +0 -15
  21. package/dist/apis/anilist/interfaces/NextAiringEpisode.js +0 -14
  22. package/dist/apis/anilist/interfaces/Notification.js +0 -25
  23. package/dist/apis/anilist/interfaces/Ranking.js +0 -19
  24. package/dist/apis/anilist/interfaces/SiteTrend.js +0 -33
  25. package/dist/apis/anilist/interfaces/Staff.js +0 -2
  26. package/dist/apis/anilist/interfaces/Stat.js +0 -2
  27. package/dist/apis/anilist/interfaces/Statistics.js +0 -2
  28. package/dist/apis/anilist/interfaces/StreamingEpisode.js +0 -15
  29. package/dist/apis/anilist/interfaces/Studio.js +0 -2
  30. package/dist/apis/anilist/interfaces/Tag.js +0 -18
  31. package/dist/apis/anilist/interfaces/Title.js +0 -15
  32. package/dist/apis/anilist/interfaces/Trailer.js +0 -14
  33. package/dist/apis/anilist/interfaces/UserStats.js +0 -23
  34. package/dist/apis/anilist/interfaces/responses/mutation/Favourites.js +0 -83
  35. package/dist/apis/anilist/interfaces/responses/query/AiringSchedule.js +0 -18
  36. package/dist/apis/anilist/interfaces/responses/query/AniChartUser.js +0 -2
  37. package/dist/apis/anilist/interfaces/responses/query/Character.js +0 -34
  38. package/dist/apis/anilist/interfaces/responses/query/ExternalLinkSourceCollection.js +0 -2
  39. package/dist/apis/anilist/interfaces/responses/query/Media.js +0 -217
  40. package/dist/apis/anilist/interfaces/responses/query/MediaList.js +0 -36
  41. package/dist/apis/anilist/interfaces/responses/query/MediaListCollectionResponse.js +0 -49
  42. package/dist/apis/anilist/interfaces/responses/query/MediaTagCollection.js +0 -2
  43. package/dist/apis/anilist/interfaces/responses/query/MediaTrend.js +0 -21
  44. package/dist/apis/anilist/interfaces/responses/query/Notification.js +0 -166
  45. package/dist/apis/anilist/interfaces/responses/query/Recommendation.js +0 -23
  46. package/dist/apis/anilist/interfaces/responses/query/Review.js +0 -30
  47. package/dist/apis/anilist/interfaces/responses/query/SiteStatistics.js +0 -31
  48. package/dist/apis/anilist/interfaces/responses/query/Staff.js +0 -59
  49. package/dist/apis/anilist/interfaces/responses/query/Studio.js +0 -60
  50. package/dist/apis/anilist/interfaces/responses/query/Thread.js +0 -44
  51. package/dist/apis/anilist/interfaces/responses/query/ThreadComment.js +0 -31
  52. package/dist/apis/anilist/interfaces/responses/query/User.js +0 -366
  53. package/dist/apis/anilist/mutation/DeleteActivity.js +0 -61
  54. package/dist/apis/anilist/mutation/DeleteActivityReply.js +0 -61
  55. package/dist/apis/anilist/mutation/DeleteCustomList.js +0 -63
  56. package/dist/apis/anilist/mutation/DeleteMediaListEntry.js +0 -61
  57. package/dist/apis/anilist/mutation/DeleteReview.js +0 -61
  58. package/dist/apis/anilist/mutation/DeleteThread.js +0 -61
  59. package/dist/apis/anilist/mutation/DeleteThreadComment.js +0 -61
  60. package/dist/apis/anilist/mutation/SaveActivityReply.js +0 -66
  61. package/dist/apis/anilist/mutation/SaveListActivity.js +0 -64
  62. package/dist/apis/anilist/mutation/SaveMediaListEntry.js +0 -97
  63. package/dist/apis/anilist/mutation/SaveMessageActivity.js +0 -68
  64. package/dist/apis/anilist/mutation/SaveRecommendation.js +0 -66
  65. package/dist/apis/anilist/mutation/SaveReview.js +0 -68
  66. package/dist/apis/anilist/mutation/SaveTextActivity.js +0 -65
  67. package/dist/apis/anilist/mutation/SaveThread.js +0 -69
  68. package/dist/apis/anilist/mutation/SaveThreadComment.js +0 -67
  69. package/dist/apis/anilist/mutation/ToggleActivityPin.js +0 -64
  70. package/dist/apis/anilist/mutation/ToggleActivitySubscription.js +0 -64
  71. package/dist/apis/anilist/mutation/ToggleFavourite.js +0 -66
  72. package/dist/apis/anilist/mutation/ToggleFollow.js +0 -62
  73. package/dist/apis/anilist/mutation/ToggleLike.js +0 -64
  74. package/dist/apis/anilist/mutation/ToggleLikeV2.js +0 -64
  75. package/dist/apis/anilist/mutation/ToggleThreadSubscription.js +0 -64
  76. package/dist/apis/anilist/mutation/UpdateAniChartHighlights.js +0 -58
  77. package/dist/apis/anilist/mutation/UpdateAniChartSettings.js +0 -58
  78. package/dist/apis/anilist/mutation/UpdateFavouriteOrder.js +0 -73
  79. package/dist/apis/anilist/mutation/UpdateMediaListEntries.js +0 -93
  80. package/dist/apis/anilist/mutation/UpdateUser.js +0 -127
  81. package/dist/apis/anilist/query/Activity.js +0 -86
  82. package/dist/apis/anilist/query/ActivityReply.js +0 -60
  83. package/dist/apis/anilist/query/AiringSchedule.js +0 -78
  84. package/dist/apis/anilist/query/AniChartUser.js +0 -56
  85. package/dist/apis/anilist/query/Character.js +0 -70
  86. package/dist/apis/anilist/query/ExternalLinkSourceCollection.js +0 -68
  87. package/dist/apis/anilist/query/Follower.js +0 -64
  88. package/dist/apis/anilist/query/Following.js +0 -64
  89. package/dist/apis/anilist/query/GenreCollection.js +0 -46
  90. package/dist/apis/anilist/query/Markdown.js +0 -57
  91. package/dist/apis/anilist/query/Media.js +0 -134
  92. package/dist/apis/anilist/query/MediaList.js +0 -90
  93. package/dist/apis/anilist/query/MediaListCollection.js +0 -76
  94. package/dist/apis/anilist/query/MediaTagCollection.js +0 -57
  95. package/dist/apis/anilist/query/MediaTrend.js +0 -84
  96. package/dist/apis/anilist/query/Notification.js +0 -65
  97. package/dist/apis/anilist/query/Recommendation.js +0 -68
  98. package/dist/apis/anilist/query/Review.js +0 -64
  99. package/dist/apis/anilist/query/SiteStatistics.js +0 -78
  100. package/dist/apis/anilist/query/Staff.js +0 -79
  101. package/dist/apis/anilist/query/Studio.js +0 -82
  102. package/dist/apis/anilist/query/Thread.js +0 -68
  103. package/dist/apis/anilist/query/ThreadComment.js +0 -63
  104. package/dist/apis/anilist/query/User.js +0 -68
  105. package/dist/apis/anilist/query/Viewer.js +0 -65
  106. package/dist/apis/anilist/query/page/Activities.js +0 -97
  107. package/dist/apis/anilist/query/page/ActivityReplies.js +0 -71
  108. package/dist/apis/anilist/query/page/AiringSchedules.js +0 -89
  109. package/dist/apis/anilist/query/page/Characters.js +0 -81
  110. package/dist/apis/anilist/query/page/Followers.js +0 -75
  111. package/dist/apis/anilist/query/page/Followings.js +0 -75
  112. package/dist/apis/anilist/query/page/Likes.js +0 -71
  113. package/dist/apis/anilist/query/page/MediaLists.js +0 -101
  114. package/dist/apis/anilist/query/page/MediaTrends.js +0 -95
  115. package/dist/apis/anilist/query/page/Medias.js +0 -143
  116. package/dist/apis/anilist/query/page/Notifications.js +0 -73
  117. package/dist/apis/anilist/query/page/Recommendations.js +0 -79
  118. package/dist/apis/anilist/query/page/Reviews.js +0 -75
  119. package/dist/apis/anilist/query/page/Staffs.js +0 -90
  120. package/dist/apis/anilist/query/page/Studios.js +0 -93
  121. package/dist/apis/anilist/query/page/ThreadCommments.js +0 -74
  122. package/dist/apis/anilist/query/page/Threads.js +0 -79
  123. package/dist/apis/anilist/query/page/Users.js +0 -79
  124. package/dist/apis/anilist/types/ActivityType.js +0 -14
  125. package/dist/apis/anilist/types/DisabledListActivity.js +0 -11
  126. package/dist/apis/anilist/types/Format.js +0 -30
  127. package/dist/apis/anilist/types/FuzzyDate.js +0 -12
  128. package/dist/apis/anilist/types/MediaListOptions.js +0 -16
  129. package/dist/apis/anilist/types/NotificationOptions.js +0 -29
  130. package/dist/apis/anilist/types/RecommendationRating.js +0 -12
  131. package/dist/apis/anilist/types/Season.js +0 -13
  132. package/dist/apis/anilist/types/Sort.js +0 -265
  133. package/dist/apis/anilist/types/Source.js +0 -25
  134. package/dist/apis/anilist/types/Status.js +0 -26
  135. package/dist/apis/anilist/types/Type.js +0 -44
  136. package/dist/apis/anilist/types/UserStaffNameLanguage.js +0 -15
  137. package/dist/apis/anilist/types/UserTitleLanguage.js +0 -15
  138. package/dist/base/APIWrapper.js +0 -17
  139. package/dist/base/RequestHandler.js +0 -48
  140. package/dist/base/ValidateVariables.js +0 -125
package/README.md CHANGED
@@ -1,241 +1,523 @@
1
- # AniLink
2
-
3
- For full documentation on methods and parameters, please refer to the [AniLink documentation](https://rlalpha49.github.io/AniLink/).
4
-
5
- AniLink is a TypeScript library for interacting with the AniList API. It provides methods for querying and mutating data, making it easier to integrate AniList's features into your own applications.
6
-
7
- ## Installation
8
-
9
- To install AniLink, you can use npm or yarn.:
10
-
11
- ```bash
12
- npm install anilink-api-wrapper
13
- ```
14
-
15
- or
16
-
17
- ```bash
18
- yarn add anilink-api-wrapper
19
- ```
20
-
21
- ## Initialization
22
-
23
- ### AniList
24
-
25
- To start using AniLink, you need to import it and initialize it with an optional authentication token. You can get an authentication token by registering your application on the AniList website.
26
-
27
- #### Typescript
28
-
29
- ```typescript
30
- import AniLink from 'anilink-api-wrapper';
31
- ```
32
-
33
- #### Javascript
34
-
35
- ```javascript
36
- const AniLink = require('anilink-api-wrapper');
37
- ```
38
-
39
- #### Initialization
40
-
41
- ```typescript
42
- const authToken = 'your-auth-token';
43
- const aniLink = new AniLink(authToken);
44
- ```
45
-
46
- You can also initialize AniLink without an authentication token
47
-
48
- ```typescript
49
- const aniLink = new AniLink();
50
- ```
51
-
52
- ## AniList API
53
-
54
- ### Structure
55
-
56
- AniLink for AniList is divided into two main sections: `anilist.query` and `anilist.mutation`. The `anilist.query` section contains methods for querying data from the AniList API, while the `anilist.mutation` section contains methods for mutating data.
57
-
58
- #### Custom Queries and Mutations
59
-
60
- If needed there is a custom section `anilist.custom` that allows the user to pass a custom query or mutation to the AniList API.
61
-
62
- The method accepts two parameters: the query or mutation string and an optional variables object.
63
-
64
- ```typescript
65
- const viewer = await aniLink.anilist.custom('query {Viewer {id}}');
66
-
67
- const mutation = 'mutation ($about: String) {UpdateUser (about: $about) {id}}';
68
- const variables = { about: "New about text" };
69
- const response = await aniLink.anilist.custom(mutation, variables);
70
- ```
71
-
72
- #### Query Methods
73
-
74
- The `anilist.query` section is further divided into main query methods and page query methods. The main query methods return a single piece of data, while the page query methods return pages of data.
75
-
76
- List of main query methods in `anilist.query`:
77
-
78
- - user
79
- - media
80
- - mediaTrend
81
- - airingSchedule
82
- - character
83
- - staff
84
- - mediaList
85
- - mediaListCollection
86
- - genreCollection
87
- - mediaTagCollection
88
- - viewer
89
- - notification
90
- - studio
91
- - review
92
- - activity
93
- - activityReply
94
- - following
95
- - follower
96
- - thread
97
- - threadComment
98
- - recommendation
99
- - markdown
100
- - aniChartUser
101
- - siteStatistics
102
- - externalLinkSourceCollection
103
-
104
- List of page query methods in `anilist.query.page`:
105
-
106
- - users
107
- - medias
108
- - characters
109
- - staffs
110
- - studios
111
- - mediaLists
112
- - airingSchedules
113
- - mediaTrends
114
- - notifications
115
- - followers
116
- - following
117
- - activities
118
- - activityReplies
119
- - threads
120
- - threadComments
121
- - reviews
122
- - recommendations
123
- - likes
124
-
125
- #### Mutation Methods
126
-
127
- List of methods in `anilist.mutation`:
128
-
129
- - updateUser
130
- - saveMediaListEntry
131
- - updateMediaListEntries
132
- - deleteMediaListEntries
133
- - deleteCustomLists
134
- - saveTextActivity
135
- - saveMessageActivity
136
- - deleteActivity
137
- - toggleActivityPin
138
- - toggleActivitySubscription
139
- - saveActivityReply
140
- - deleteActivityReply
141
- - toggleLike
142
- - toggleLikeV2
143
- - toggleFollow
144
- - toggleFavourite
145
- - updateFavouriteOrder
146
- - saveReview
147
- - deleteReview
148
- - saveRecommendation
149
- - saveThread
150
- - deleteThread
151
- - toggleThreadSubscription
152
- - saveThreadComment
153
- - deleteThreadComment
154
- - updateAniChartSettings
155
- - updateAniChartHighlights
156
-
157
- ### Error Handling
158
-
159
- AniLink will throw an error if the AniList API returns an error. You can catch these errors using a try-catch block.
160
-
161
- ```typescript
162
- try {
163
- const user = await aniLink.anilist.query.user({id: 542244});
164
- console.log(user);
165
- } catch (error) {
166
- console.error(error);
167
- }
168
- ```
169
-
170
- This includes status codes and error messages returned by the AniList API. Here is an example rate limit handler to catch the errors thrown by AniLink:
171
-
172
- ##### Typescript
173
-
174
- ```typescript
175
- async function handleRateLimit(apiCall: () => Promise<any>, retryAfter = 60) {
176
- try {
177
- let response;
178
- try {
179
- response = await apiCall();
180
- } catch (error) {
181
- throw error;
182
- }
183
- console.log(response.data);
184
- return response;
185
- } catch (error: any) {
186
- if (error.response && error.response.status === 429) {
187
- console.log('Rate limit exceeded, waiting for 1 minute before retrying...');
188
- await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
189
- console.log('Retrying...');
190
- return handleRateLimit(apiCall, retryAfter);
191
- } else {
192
- if (error.response && error.response.data) {
193
- throw error.response.data;
194
- } else {
195
- throw error.response || error;
196
- }
197
- }
198
- }
199
- }
200
- ```
201
-
202
- ##### Javascript
203
-
204
- ```javascript
205
- async function handleRateLimit(apiCall, retryAfter = 60) {
206
- // Same as above
207
- }
208
- ```
209
-
210
- The possible error codes returned by the AniList API are:
211
- - 400: Bad Request (e.g. missing variables, invalid variables, or invalid query)
212
- - 401: Unauthorized (e.g. invalid authentication token)
213
- - 404: Not Found (e.g. user not found)
214
- - 429: Too Many Requests (e.g. rate limit exceeded)
215
- - 500: Internal Server Error (e.g. AniList server error)
216
-
217
- #### Missing or Invalid Variables
218
-
219
- AniLink will also throw an error if any variables are missing or invalid. For example, if you try to query a user providing a string instead of a number for ID, AniLink will throw an error. Most variables are optional however there a few that are required.
220
- ```typescript
221
- try {
222
- const user = await aniLink.anilist.query.user({id: '542244'});
223
- console.log(user);
224
- } catch (error) {
225
- console.error(error);
226
- }
227
- ```
228
-
229
- Example Error Thrown:
230
-
231
- ```typescript
232
- Invalid id: 542244. Expected type: number
233
- ```
234
-
235
- ### Examples
236
-
237
- See the [ANILIST_API_EXAMPLES](https://github.com/RLAlpha49/AniLink/blob/master/Examples/ANILIST_API_EXAMPLES.md) for examples of how to use AniLink to interact with the AniList API.
238
-
239
- ## License
240
-
241
- This project is licensed under the MIT License - see the [LICENSE](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE) file for details.
1
+ # AniLink
2
+
3
+ [![npm version](https://img.shields.io/npm/v/anilink-api-wrapper.svg)](https://www.npmjs.com/package/anilink-api-wrapper)
4
+ [![npm downloads](https://img.shields.io/npm/dm/anilink-api-wrapper.svg)](https://www.npmjs.com/package/anilink-api-wrapper)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE)
6
+ [![CI](https://github.com/RLAlpha49/AniLink/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/RLAlpha49/AniLink/actions/workflows/ci.yml)
7
+ [![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.
27
+
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
+ ```
53
+
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
77
+
78
+ ```bash
79
+ npm install anilink-api-wrapper
80
+ ```
81
+
82
+ ### Initialize
83
+
84
+ ```typescript
85
+ import { AniLink } from "anilink-api-wrapper";
86
+
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)
91
+ const aniLink = new AniLink();
92
+ ```
93
+
94
+ Get a token by registering an application on the [AniList developer settings](https://anilist.co/settings/developer).
95
+
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
+ });
120
+ ```
121
+
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.
123
+
124
+ ### OAuth
125
+
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
477
+
478
+ ## Documentation
479
+
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
+ ```
503
+
504
+ ## Development
505
+
506
+ ```bash
507
+ 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/
514
+ ```
515
+
516
+ ## Resources
517
+
518
+ - [Contributing guide](CONTRIBUTING.md)
519
+ - [Changelog](CHANGELOG.md) and [GitHub Releases](https://github.com/RLAlpha49/AniLink/releases)
520
+
521
+ ## License
522
+
523
+ [MIT](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE)