-
Notifications
You must be signed in to change notification settings - Fork 4
/
net.js
74 lines (70 loc) · 3.35 KB
/
net.js
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
const { XMLHttpRequest, Blob, TextDecoder } = window
const REGEXP_HEADER_SEPARATOR = /[\r\n]+/
// TODO: later compare & check if should replace with fetch + AbortController
// fetch-like XMLHttpRequest() with timeout
// timeout in msec, result in error with status: -1, message: TIMEOUT_ERROR
const fetchLikeRequest = (url, {
method = 'GET',
headers: requestHeaders,
body,
timeout = 0, // in millisecond, 0 for no timeout, will result in error if timeout
credentials,
onUploadProgress, // ({ lengthComputable, loaded, total }) => {}
onDownloadProgress
} = {}) => new Promise((resolve, reject) => {
const getError = (message, status) => Object.assign(new Error(message), { status, url, method })
const request = new XMLHttpRequest()
request.onerror = () => reject(getError('NETWORK_ERROR', -1))
request.ontimeout = () => reject(getError('NETWORK_TIMEOUT', -1))
request.onreadystatechange = () => {
const { readyState, status } = request
if (
readyState !== 2 || // not HEADERS_RECEIVED
status === 0 // no success
) return
const responseHeaders = request.getAllResponseHeaders().split(REGEXP_HEADER_SEPARATOR).reduce((o, rawHeader) => {
const [ key, ...valueList ] = rawHeader.split(':')
if (valueList.length) o[ key.trim().toLowerCase() ] = valueList.join(':').trim()
return o
}, {})
resolve({
status,
ok: (status >= 200 && status < 300),
headers: responseHeaders,
...wrapPayload(request, getError)
})
}
if (onUploadProgress && request.upload) request.upload.onprogress = onUploadProgress
if (onDownloadProgress) request.onprogress = onDownloadProgress
request.open(method, url)
requestHeaders && Object.entries(requestHeaders).forEach(([ key, value ]) => request.setRequestHeader(key, value))
request.responseType = 'arraybuffer'
request.timeout = timeout || 0
request.withCredentials = (credentials === 'include') // Setting withCredentials has no effect on same-site requests. check: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
request.send(body || null)
})
const wrapPayload = (request, getError) => {
let payloadOutcome // KEEP|DROP
setTimeout(() => {
if (payloadOutcome) return
payloadOutcome = 'DROP'
request.abort() // drop response data
})
const arrayBuffer = () => new Promise((resolve, reject) => {
if (payloadOutcome) return reject(getError(payloadOutcome === 'KEEP' ? 'PAYLOAD_ALREADY_USED' : 'PAYLOAD_ALREADY_DROPPED', -1))
payloadOutcome = 'KEEP'
// use `onload` instead of `onreadystatechange` since `onreadystatechange` fires before `ontimeout`, thus masking the `reject` for timeout
// check: https://stackoverflow.com/questions/23940460/xmlhttprequest-timeout-case-onreadystatechange-executes-before-ontimeout/30054671#30054671
request.onload = () => resolve(request.response)
request.onerror = () => reject(getError('PAYLOAD_ERROR', -1))
request.ontimeout = () => reject(getError('PAYLOAD_TIMEOUT', -1))
})
const blob = () => arrayBuffer().then(toBlob)
const text = () => arrayBuffer().then(toText)
const json = () => text().then(parseJSON)
return { arrayBuffer, blob, text, json }
}
const toBlob = (arrayBuffer) => new Blob([ arrayBuffer ])
const toText = (arrayBuffer) => new TextDecoder().decode(arrayBuffer)
const parseJSON = (text) => JSON.parse(text)
export { fetchLikeRequest }