Skip to content

Commit 8ca32ae

Browse files
authored
Merge pull request #1 from Sideko-Inc/retries
Feature: Retries
2 parents 7273272 + 589dcd8 commit 8ca32ae

18 files changed

Lines changed: 1410 additions & 1087 deletions

.prettierrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"semi": true,
33
"trailingComma": "es5",
4-
"singleQuote": true,
4+
"singleQuote": false,
55
"printWidth": 80,
66
"tabWidth": 2,
77
"useTabs": false
8-
}
8+
}

CHANGELOG.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## v0.2.0
9+
10+
### Added
11+
- `RetryStrategy` configuration as part of the `CoreClient` and `RequestOptions` (prioritized)
12+
- `RetryStrategy` used to automatically retry requests according to configuration
13+
14+
### Changed
15+
n/a
16+
17+
### Deprecated
18+
n/a
19+
20+
### Removed
21+
n/a
22+
23+
### Fixed
24+
n/a
25+
26+
### Security
27+
n/a
28+
29+
## v0.1.0
930

1031
### Added
1132
- Initial release
@@ -19,11 +40,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1940
- Automated NPM publishing
2041

2142
### Changed
43+
n/a
2244

2345
### Deprecated
46+
n/a
2447

2548
### Removed
49+
n/a
2650

2751
### Fixed
52+
n/a
2853

29-
### Security
54+
### Security
55+
n/a

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# make-api-request-js
22

3-
[![CI](https://github.com/sideko-inc/make-api-request-js/workflows/CI/badge.svg)](https://github.com/sideko-inc/make-api-request-js/actions)
3+
[![CI](https://github.com/sideko-inc/make-request-js/workflows/CI/badge.svg)](https://github.com/sideko-inc/make-request-js/actions)
44
[![npm version](https://badge.fury.io/js/make-api-request-js.svg)](https://badge.fury.io/js/make-api-request-js)
55
[![codecov](https://codecov.io/gh/sideko-inc/make-api-request-js/branch/main/graph/badge.svg)](https://codecov.io/gh/sideko-inc/make-api-request-js)
66

src/api-error.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
import type { RequestConfig } from "./core-client";
42

53
export class ApiError extends Error {
@@ -8,7 +6,7 @@ export class ApiError extends Error {
86

97
constructor(request: RequestConfig, response: Response) {
108
super(
11-
`${response.status} was returned from ${request.method} ${request.path}`,
9+
`${response.status} was returned from ${request.method} ${request.path}`
1210
);
1311
this.request = request;
1412
this.response = response;

src/api-promise.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ export class ApiPromise<T>
6262
default:
6363
return new BinaryResponse(
6464
await response.blob(),
65-
contentType,
65+
contentType
6666
) as unknown as T;
6767
}
6868
}
6969

7070
private async *handleNodeStream(
71-
stream: NodeJS.ReadableStream,
71+
stream: NodeJS.ReadableStream
7272
): AsyncIterableIterator<T> {
7373
const { responseSchema } = this.responseProps;
7474
const parser = new EventSourceParser();
@@ -87,7 +87,7 @@ export class ApiPromise<T>
8787
}
8888

8989
private async *handleWebStream(
90-
stream: ReadableStream<Uint8Array>,
90+
stream: ReadableStream<Uint8Array>
9191
): AsyncIterableIterator<T> {
9292
const { responseSchema } = this.responseProps;
9393
const reader = stream.getReader();
@@ -177,7 +177,7 @@ export class ApiPromise<T>
177177
onrejected?:
178178
| ((reason: any) => Result2 | PromiseLike<Result2>)
179179
| undefined
180-
| null,
180+
| null
181181
): Promise<Result1 | Result2> {
182182
if (this.responseProps.responseStream) {
183183
return Promise.resolve(this.asEventStream() as unknown as Result1);
@@ -189,7 +189,7 @@ export class ApiPromise<T>
189189
onrejected?:
190190
| ((reason: any) => Result | PromiseLike<Result>)
191191
| undefined
192-
| null,
192+
| null
193193
): Promise<T | Result> {
194194
return this.parseResponse().catch(onrejected);
195195
}

src/auth.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class AuthKey implements AuthProvider {
7171
constructor(
7272
name: string,
7373
location: "query" | "header" | "cookie",
74-
key?: string,
74+
key?: string
7575
) {
7676
this.name = name;
7777
this.location = location;
@@ -176,7 +176,7 @@ export class OAuth2 implements AuthProvider {
176176
}
177177

178178
async refresh(
179-
form: OAuth2Password | OAuth2ClientCredentials,
179+
form: OAuth2Password | OAuth2ClientCredentials
180180
): Promise<{ accessToken: string; expiresAt: Date }> {
181181
const {
182182
baseUrl,
@@ -247,7 +247,7 @@ export class OAuth2 implements AuthProvider {
247247
body: reqData,
248248
contentType: reqHeaders["content-type"],
249249
},
250-
tokenRes as any,
250+
tokenRes as any
251251
);
252252
}
253253

@@ -277,6 +277,8 @@ export class OAuth2 implements AuthProvider {
277277
}
278278

279279
setValue(_val?: string | undefined): void {
280-
throw new Error("an OAuth2 auth provider cannot be used as a requestMutator");
280+
throw new Error(
281+
"an OAuth2 auth provider cannot be used as a requestMutator"
282+
);
281283
}
282284
}

src/binary-response.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
import type { Blob as NodeBlob } from "node-fetch";
42

53
export class BinaryResponse {

src/core-client.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import {
1414
MULTIPART_FORM,
1515
URL_FORM,
1616
} from "./content-type";
17+
import { RetryStrategy, RetryConfig, sleep } from "./retry";
1718

1819
export interface CoreClientProps {
1920
baseUrl: string | Record<string, string | undefined>;
2021
timeout?: number | undefined;
2122
auths?: Record<string, AuthProvider>;
23+
retries?: RetryStrategy;
2224
}
2325

2426
export type ApiResponse = Response | NodeResponse;
@@ -48,6 +50,7 @@ export interface RequestOptions {
4850
timeout?: number;
4951
additionalHeaders?: Record<string, string>;
5052
additionalQuery?: Record<string, string>;
53+
retries?: RetryStrategy;
5154
}
5255

5356
const _DEFAULT_SERVICE_NAME = "__default_service__";
@@ -56,6 +59,7 @@ export class CoreClient {
5659
private baseUrl: Record<string, string | undefined>;
5760
private auths: Record<string, AuthProvider>;
5861
private timeout: number | undefined;
62+
private retries?: RetryStrategy | undefined;
5963

6064
constructor(props: CoreClientProps) {
6165
this.baseUrl =
@@ -64,6 +68,7 @@ export class CoreClient {
6468
: props.baseUrl;
6569
this.auths = props.auths ?? {};
6670
this.timeout = props.timeout;
71+
this.retries = props.retries;
6772
}
6873

6974
private async applyAuths(cfg: RequestConfig): Promise<RequestConfig> {
@@ -116,7 +121,7 @@ export class CoreClient {
116121

117122
private encodeBodyByContentType(
118123
cfg: RequestConfig,
119-
reqInit: RequestInit,
124+
reqInit: RequestInit
120125
): RequestInit {
121126
const contentTypeOverride =
122127
cfg.opts?.additionalHeaders?.["content-type"] ??
@@ -136,9 +141,8 @@ export class CoreClient {
136141

137142
if (RUNTIME.type === "node") {
138143
// explicitly set boundary
139-
headers[
140-
"content-type"
141-
] = `${MULTIPART_FORM}; boundary=${form.getBoundary()}`;
144+
headers["content-type"] =
145+
`${MULTIPART_FORM}; boundary=${form.getBoundary()}`;
142146
} else {
143147
// the browser should automatically set the content type
144148
delete headers["content-type"];
@@ -149,7 +153,7 @@ export class CoreClient {
149153
} else if (contentType === URL_FORM) {
150154
if (typeof cfg.body !== "object") {
151155
throw new TypeError(
152-
"x-www-form-urlencoded data must be an object at the top level",
156+
"x-www-form-urlencoded data must be an object at the top level"
153157
);
154158
}
155159

@@ -182,29 +186,56 @@ export class CoreClient {
182186
return reqInit;
183187
}
184188

185-
private async request(cfg: RequestConfig): Promise<ApiResponse> {
186-
const fetcherFn =
187-
RUNTIME.type === "node" || typeof fetch !== "function"
188-
? nodeFetch
189-
: fetch;
190-
191-
cfg = await this.applyAuths(cfg);
192-
const reqInit = this.buildRequestInit(cfg);
193-
const url = this.buildUrlFromCfg(cfg);
194-
195-
const timeout = cfg.opts?.timeout ?? this.timeout;
189+
private async sendRequest({
190+
url,
191+
reqInit,
192+
timeout,
193+
}: {
194+
url: string;
195+
reqInit: RequestInit;
196+
timeout?: number | undefined;
197+
}): Promise<ApiResponse> {
196198
const controller = new AbortController();
197199
let timeoutId;
198200
if (typeof timeout !== "undefined") {
199201
timeoutId = setTimeout(() => controller.abort(), timeout);
200202
}
201203
reqInit.signal = controller.signal;
202204

205+
const fetcherFn =
206+
RUNTIME.type === "node" || typeof fetch !== "function"
207+
? nodeFetch
208+
: fetch;
203209
const response = await fetcherFn(url, reqInit as any);
204210

205211
if (timeoutId) {
206212
clearTimeout(timeoutId);
207213
}
214+
return response;
215+
}
216+
217+
private async request(cfg: RequestConfig): Promise<ApiResponse> {
218+
cfg = await this.applyAuths(cfg);
219+
const reqInit = this.buildRequestInit(cfg);
220+
const url = this.buildUrlFromCfg(cfg);
221+
const timeout = cfg.opts?.timeout ?? this.timeout;
222+
const sendRequestData = { url, reqInit, timeout };
223+
224+
let response = await this.sendRequest(sendRequestData);
225+
if (cfg.opts?.retries || this.retries) {
226+
const retry = new RetryConfig({
227+
override: cfg.opts?.retries,
228+
base: this.retries,
229+
});
230+
let attempt = 1;
231+
let delay = retry.initialDelay;
232+
while (retry.shouldRetry({ attempt, statusCode: response.status })) {
233+
await sleep(delay);
234+
response = await this.sendRequest(sendRequestData);
235+
delay = retry.calcNextDelay({ currDelay: delay });
236+
attempt++;
237+
}
238+
}
208239

209240
if (!response.ok) {
210241
throw new ApiError(cfg, response as any);

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export {
2121
export { createForm, isUploadFile, UploadFile } from "./form-data";
2222
export { encodeQueryParam } from "./query";
2323
export { RUNTIME } from "./runtime";
24+
export { RetryStrategy } from "./retry";
2425
export { zodRequiredAny, zodTransform, zodUploadFile } from "./zod";

src/query.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function encodeForm(name: string, value: any, explode: boolean): string {
3636
if (Array.isArray(value)) {
3737
return qs.stringify(
3838
{ [name]: value },
39-
{ arrayFormat: explode ? "repeat" : "comma" },
39+
{ arrayFormat: explode ? "repeat" : "comma" }
4040
);
4141
} else if (typeof value === "object" && value != null) {
4242
if (explode) {
@@ -49,7 +49,7 @@ function encodeForm(name: string, value: any, explode: boolean): string {
4949
// non-explode form objects should be encoded like /users?id=key0,val0,key1,val1
5050
return qs.stringify(
5151
{ [name]: Object.entries(value).flat() },
52-
{ arrayFormat: "comma" },
52+
{ arrayFormat: "comma" }
5353
);
5454
}
5555
} else {
@@ -60,7 +60,7 @@ function encodeForm(name: string, value: any, explode: boolean): string {
6060
function encodeSpaceDelimited(
6161
name: string,
6262
value: any,
63-
explode: boolean,
63+
explode: boolean
6464
): string {
6565
if (Array.isArray(value) && !explode) {
6666
// non-explode spaceDelimited arrays should be encoded like /users?id=3%204%205
@@ -76,7 +76,7 @@ function encodeSpaceDelimited(
7676
function encodePipeDelimited(
7777
name: string,
7878
value: any,
79-
explode: boolean,
79+
explode: boolean
8080
): string {
8181
if (Array.isArray(value) && !explode) {
8282
// non-explode pipeDelimited arrays should be encoded like /users?id=3|4|5

0 commit comments

Comments
 (0)