@@ -13,21 +13,42 @@ class {{config.name}} {
1313 */
1414 readonly #token?: string;
1515
16+
1617 /**
17- * Base URL of the API
18+ * API client options
1819 * @readonly
1920 * @private
2021 */
21- readonly #baseUrl: string;
22+ readonly #options: {{config.name} }.Options;
23+
24+ /**
25+ * Default options
26+ * @readonly
27+ * @private
28+ * @static
29+ * @internal
30+ */
31+ static readonly #defaultOptions: { {config.name} }.Options = {
32+ baseUrl: " {{{config.baseUrl}}}" ,
33+ autoRetry: true ,
34+ maxRetryDelay: 5,
35+ maxRetries: 3
36+ } ;
2237
2338 /**
2439 * Construct a new { {config.name} } API client
2540 * @param token API token to use for requests
26- * @param [baseUrl=" { {{config.baseUrl } }}"] Base URL of the API
41+ * @param [options] Options for the API client
2742 */
28- public constructor(token?: string, baseUrl = "{ {{config.baseUrl} }}") {
43+ public constructor(token?: string, options: Partial<{ {config.name} }.Options> = { {config.name} }.#defaultOptions) {
44+ const fullOptions = {{config.name} }.#defaultOptions;
45+ fullOptions.baseUrl = fullOptions.baseUrl ?? { {config.name} }.#defaultOptions.baseUrl;
46+ fullOptions.autoRetry = fullOptions.autoRetry ?? { {config.name} }.#defaultOptions.autoRetry;
47+ fullOptions.maxRetryDelay = fullOptions.maxRetryDelay ?? { {config.name} }.#defaultOptions.maxRetryDelay;
48+ fullOptions.maxRetries = fullOptions.maxRetries ?? { {config.name} }.#defaultOptions.maxRetries;
49+
2950 this.#token = token;
30- this.#baseUrl = baseUrl ;
51+ this.#options = fullOptions ;
3152 }
3253
3354 /**
@@ -39,8 +60,8 @@ class {{config.name}} {
3960 * @internal
4061 * @private
4162 */
42- async #sendRequest <T >(operation: Schema.Operation, pathParams: Record<string , string >, queryParams: Record<string , string >, body?: any): Promise<Cloudnode .ApiResponse <T >> {
43- const url = new URL(operation.path.replace(/^\/+/, " " ), this.#baseUrl);
63+ async #sendRawRequest <T >(operation: Schema.Operation, pathParams: Record<string , string >, queryParams: Record<string , string >, body?: any): Promise<{ {config.name } } .ApiResponse<T >> {
64+ const url = new URL(operation.path.replace(/^\/+/, " " ), this.#options. baseUrl);
4465 for (const [key, value] of Object.entries(pathParams))
4566 url.pathname = url.pathname.replaceAll(`/:${key} `, `/${ value} `);
4667 for (const [key, value] of Object.entries(queryParams))
@@ -76,19 +97,53 @@ class {{config.name}} {
7697 });
7798 }
7899 else data = text as any;
79- const res = Cloudnode. makeApiResponse(data, new Cloudnode .RawResponse(response, { operation, pathParams, queryParams, body} as const));
100+ const res = { {config.name } }. makeApiResponse(data, new { {config.name } } .RawResponse(response, { operation, pathParams, queryParams, body} as const));
80101 if (response.ok) return res;
81102 else throw res;
82103 }
83104
105+ /**
106+ * Send a request to the API with support for auto-retry
107+ * @param operation The operation to call
108+ * @param pathParams Path parameters to use in the request
109+ * @param queryParams Query parameters to use in the request
110+ * @param options API client options. Overrides the client's options
111+ * @internal
112+ * @private
113+ */
114+ #sendRequest<T >(operation: Schema.Operation, pathParams: Record<string , string >, queryParams: Record<string , string >, body?: any, options?: { {config.name} }.Options): Promise<{ {config.name} }.ApiResponse<T >> {
115+ return new Promise(async (resolve, reject) => {
116+ const send = (i: number = 0) => {
117+ this.#sendRawRequest< T> (operation, pathParams, queryParams, body)
118+ .then(response => resolve(response))
119+ .catch(e => {
120+ options ??= this.#options;
121+ options.baseUrl ??= this.#options.baseUrl;
122+ options.autoRetry ??= this.#options.autoRetry;
123+ options.maxRetries ??= this.#options.maxRetries;
124+ options.maxRetryDelay ??= this.#options.maxRetryDelay;
125+
126+ if (options.autoRetry && i < options.maxRetries && e instanceof {{config.name} }.R.ApiResponse) {
127+ const res: {{config.name} }.R.ApiResponse = e;
128+ const retryAfter: number = Number(res._response.status !== 429 ? res._response.headers["x-retry-after"] ?? res._response.headers["retry-after"] : res._response.headers["x-ratelimit-reset"] ?? res._response.headers["x-rate-limit-reset"] ?? res._response.headers["ratelimit-reset"] ?? res._response.headers["rate-limit-reset"] ?? res._response.headers["retry-after"] ?? res._response.headers["x-retry-after"]);
129+ if (Number.isNaN(retryAfter) || retryAfter > options.maxRetryDelay) return reject(e);
130+ setTimeout(send, Number(retryAfter) * 1000, ++i);
131+ }
132+ else reject(e);
133+ });
134+ }
135+ send(0);
136+ });
137+ }
138+
84139 /**
85140 * Get another page of paginated results
86141 * @param response Response to get a different page of
87142 * @param page Page to get
88143 * @returns The new page or null if the page is out of bounds
89144 * @throws { Cloudnode.Error} Error returned by the API
90145 */
91- async getPage<T >(response: Cloudnode. ApiResponse<Cloudnode . PaginatedData <T >>, page: number): Promise<Cloudnode . ApiResponse <Cloudnode .PaginatedData <T >> | null> {
146+ async getPage<T >(response: { {config.name } }. ApiResponse<{ {config.name } }. PaginatedData<T >>, page: number): Promise<{ {config.name } }. ApiResponse<{ {config.name } } .PaginatedData<T >> | null> {
92147 if (page * response.limit > response.total || page < 1) return null;
93148 const query = Object.assign ({} , response._response.request.queryParams);
94149 query.page = page.toString();
@@ -101,7 +156,7 @@ class {{config.name}} {
101156 * @returns The next page or null if this is the last page
102157 * @throws { Cloudnode.Error} Error returned by the API
103158 */
104- async getNextPage<T >(response: Cloudnode. ApiResponse<Cloudnode . PaginatedData <T >>): Promise<Cloudnode . ApiResponse <Cloudnode .PaginatedData <T >> | null> {
159+ async getNextPage<T >(response: { {config.name } }. ApiResponse<{ {config.name } }. PaginatedData<T >>): Promise<{ {config.name } }. ApiResponse<{ {config.name } } .PaginatedData<T >> | null> {
105160 return await this.getPage(response, response.page + 1);
106161 }
107162
@@ -111,7 +166,7 @@ class {{config.name}} {
111166 * @returns The previous page or null if this is the first page
112167 * @throws { Cloudnode.Error} Error returned by the API
113168 */
114- async getPreviousPage<T >(response: Cloudnode. ApiResponse<Cloudnode . PaginatedData <T >>): Promise<Cloudnode . ApiResponse <Cloudnode .PaginatedData <T >> | null> {
169+ async getPreviousPage<T >(response: { {config.name } }. ApiResponse<{ {config.name } }. PaginatedData<T >>): Promise<{ {config.name } }. ApiResponse<{ {config.name } } .PaginatedData<T >> | null> {
115170 return await this.getPage(response, response.page - 1);
116171 }
117172
@@ -122,13 +177,13 @@ class {{config.name}} {
122177 * @returns All of the data in 1 page
123178 * @throws { Cloudnode.Error} Error returned by the API
124179 */
125- async getAllPages<T >(response: Cloudnode. ApiResponse<Cloudnode . PaginatedData <T >>): Promise<Cloudnode .PaginatedData <T >> {
180+ async getAllPages<T >(response: { {config.name } }. ApiResponse<{ {config.name } }. PaginatedData<T >>): Promise<{ {config.name } } .PaginatedData<T >> {
126181 const pages: (true | null)[] = new Array(Math.ceil(response.total / response.limit)).fill(null);
127182 pages[response.page - 1] = true ;
128- const promises: (Promise< Cloudnode. ApiResponse< Cloudnode .PaginatedData< T>> | null> | true )[] = pages.map((page, i) => page === null ? this.getPage(response, i + 1) : true );
183+ const promises: (Promise< {{config.name } }. ApiResponse<{ {config.name } } .PaginatedData<T >> | null> | true)[] = pages.map((page, i) => page === null ? this.getPage(response, i + 1) : true);
129184 const newPages = await Promise.all(promises.filter(page => page !== true));
130185 newPages.splice(response.page - 1, 0, response);
131- const allPages = newPages.filter(p => p !== null) as Cloudnode. ApiResponse< Cloudnode .PaginatedData< T>> [];
186+ const allPages = newPages.filter(p => p !== null) as { {config.name } }. ApiResponse<{ {config.name } } .PaginatedData<T >>[];
132187 return {
133188 items: allPages.map(p => p.items).flat(),
134189 total: response.total,
@@ -261,7 +316,7 @@ namespace {{config.name}} {
261316 } ;
262317
263318 public constructor(response: import("node-fetch").Response, request: { operation: Schema.Operation, pathParams: Record< string, string> , queryParams: Record< string, string> , body: any} ) {
264- this.headers = Object.fromEntries(response.headers.entries());
319+ this.headers = Object.fromEntries([... response.headers.entries()].map(([k, v]) = > [k.toLowerCase(), v] ));
265320 this.ok = response.ok;
266321 this.redirected = response.redirected;
267322 this.status = response.status;
@@ -271,7 +326,7 @@ namespace {{config.name}} {
271326 }
272327 }
273328
274- namespace R {
329+ export namespace R {
275330 export class ApiResponse {
276331 /**
277332 * API response
@@ -299,6 +354,34 @@ namespace {{config.name}} {
299354 export function makeApiResponse<T >(data: T, response: RawResponse): ApiResponse<T > {
300355 return Object.assign (new R.ApiResponse(response), data);
301356 }
357+
358+ /**
359+ * API client options
360+ */
361+ export interface Options {
362+ /**
363+ * The base URL of the API
364+ */
365+ baseUrl: string;
366+
367+ /**
368+ * Whether to automatically retry requests that fail temporarily.
369+ * If enabled, when a request fails due to a temporary error, such as a rate limit, the request will be retried after the specified delay.
370+ */
371+ autoRetry: boolean;
372+
373+ /**
374+ * The maximum number of seconds that is acceptable to wait before retrying a failed request.
375+ * This requires {@link Options.autoRetry} to be enabled.
376+ */
377+ maxRetryDelay: number;
378+
379+ /**
380+ * The maximum number of times to retry a failed request.
381+ * This requires { @link Options.autoRetry} to be enabled.
382+ */
383+ maxRetries: number;
384+ }
302385}
303386
304387export default { {config.name} };
0 commit comments