-
Notifications
You must be signed in to change notification settings - Fork 1
/
server.ts
159 lines (134 loc) · 4.87 KB
/
server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import {
Options,
Method,
Headers as ReqHeaders,
Retry,
RequestFn,
RequestMod as RequestModule,
Response as ReqResponse,
BaseResponse,
SuccessResponse,
ErrorResponse,
ExceptionResponse,
} from './types'
const defaultStringify = require('./stringify')
// @browser-ignore
const fetch: (url: string, options?: {}) => Promise<Response> = require('node-fetch')
const AbortController: { new (): AbortController; prototype: AbortController } = require('abort-controller')
// @browser-ignore
const sleep = (ms: number): Promise<string> => {
return new Promise(resolve => setTimeout(resolve, ms))
}
const toQs = (params: {}, stringify: { (params: {}): string } = defaultStringify): string => {
const s = stringify(params)
return s ? `?${s}` : ''
}
const toObject = (headers: Headers): { [propName: string]: string } => {
const headersObject: { [key: string]: any } = {}
// https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
for (const [key, value] of Array.from(headers.entries())) {
headersObject[key] = value
}
return headersObject
}
const lowercased = (object: { [key: string]: any }): { [key: string]: any } => {
const obj: { [key: string]: any } = {}
for (const key of Object.keys(object)) obj[key.toLowerCase()] = object[key]
return obj
}
const shouldStringify = (object: {}): boolean => {
const objectType = {}.toString.call(object)
return objectType === '[object Object]' || objectType === '[object Array]'
}
const getTimeoutSignal = (ms?: number) => {
if (!ms || typeof AbortController === 'undefined') return undefined
const controller = new AbortController()
setTimeout(() => controller.abort(), ms)
return controller.signal
}
async function _request(
url: string,
{ method = 'GET', headers = {}, params = {}, body, jsonOut, stringify, ...rest }: Options,
): Promise<Response> {
const jsonHeaders: ReqHeaders = {}
let requestBody = body
if (shouldStringify(body)) {
jsonHeaders['content-type'] = 'application/json'
requestBody = JSON.stringify(body)
}
if (jsonOut) jsonHeaders.accept = 'application/json'
const requestHeaders = {
...jsonHeaders,
...lowercased(headers),
}
const options = {
method,
headers: requestHeaders,
body: requestBody,
...rest,
}
if (['GET', 'HEAD'].indexOf(method.toUpperCase()) > -1) delete options.body
return fetch(`${url}${toQs(params, stringify)}`, options)
}
/**
* Sends request to URL and returns promise that resolves to a response object.
*
* @param url - Fully qualified URL
* @param options - Basically the fetch init object (see README for differences)
*
* @returns A promise that resolves to a response object
*/
const request = (async <T = any, ET = any>(url: string, options?: Options<T, ET>): Promise<ReqResponse<T, ET>> => {
const { retry, timeout, jsonOut = true, ...rest } = options || ({} as Options)
let response: SuccessResponse<T> | ErrorResponse<ET> | ExceptionResponse, data
const signal = getTimeoutSignal(timeout)
try {
const res = await _request(url, { jsonOut, signal, ...rest })
const { status, statusText, headers: responseHeaders, url: responseUrl } = res
const fields: BaseResponse = { status, statusText, headers: toObject(responseHeaders), url: responseUrl }
if (jsonOut) {
const text = await res.text()
try {
data = JSON.parse(text)
} catch (e) {
data = text
}
} else {
data = res
}
// Any response without the data you're looking for (status >= 300) is an error
if (status < 300) response = { ...fields, data, type: 'success' }
else response = { ...fields, data, type: 'error' }
} catch (e) {
response = { data: e as Error, type: 'exception' }
}
if (retry) {
const { retries, delay, multiplier = 2, shouldRetry = res => res.type === 'exception' }: Retry<T, ET> = retry
if (retries > 0 && shouldRetry(response, { retries, delay })) {
await sleep(delay)
const nextRetry: Retry<T, ET> = {
retries: retries - 1,
delay: delay * multiplier,
multiplier,
shouldRetry,
}
return request(url, { retry: nextRetry, timeout, jsonOut, ...rest })
}
}
return response
}) as RequestModule
function requester(method: Method): RequestFn {
return <T = any, ET = any>(url: string, options?: Options<T, ET>) => request<T, ET>(url, { ...options, method })
}
request.delete = requester('DELETE')
const del = (request.del = requester('DELETE'))
const get = (request.get = requester('GET'))
const head = (request.head = requester('HEAD'))
const options = (request.options = requester('OPTIONS'))
const patch = (request.patch = requester('PATCH'))
const post = (request.post = requester('POST'))
const put = (request.put = requester('PUT'))
export { del, get, head, options, patch, post, put }
export * from './types'
export default request
module.exports = request