Skip to content

Commit 5ee5285

Browse files
committed
chore: reset on top of master
1 parent 78edc8b commit 5ee5285

10 files changed

+346
-24
lines changed

package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"dist"
1313
],
1414
"browser": {
15+
"./src/http/fetch.js": "./src/http/fetch.browser.js",
16+
"./src/abort-controller.js": "./src/abort-controller.browser.js",
1517
"./src/text-encoder.js": "./src/text-encoder.browser.js",
1618
"./src/text-decoder.js": "./src/text-decoder.browser.js",
1719
"./src/temp-dir.js": "./src/temp-dir.browser.js",
@@ -34,7 +36,7 @@
3436
"license": "MIT",
3537
"dependencies": {
3638
"abort-controller": "^3.0.0",
37-
"any-signal": "^1.1.0",
39+
"any-signal": "^1.2.0",
3840
"buffer": "^5.6.0",
3941
"err-code": "^2.0.0",
4042
"fs-extra": "^9.0.1",
@@ -44,15 +46,16 @@
4446
"merge-options": "^2.0.0",
4547
"nanoid": "^3.1.3",
4648
"node-fetch": "^2.6.0",
47-
"stream-to-it": "^0.2.0"
49+
"stream-to-it": "^0.2.0",
50+
"it-to-stream": "^0.1.2"
4851
},
4952
"devDependencies": {
5053
"aegir": "^25.0.0",
5154
"delay": "^4.3.0",
5255
"it-all": "^1.0.2",
5356
"it-drain": "^1.0.1",
5457
"it-last": "^1.0.2",
55-
"it-to-stream": "^0.1.2"
58+
"uint8arrays": "^1.1.0"
5659
},
5760
"contributors": [
5861
"Hugo Dias <[email protected]>",

src/abort-controller.browser.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
'use strict'
2+
/* eslint-env browser */
3+
4+
module.exports = AbortController

src/abort-controller.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict'
2+
3+
// Electron has `AbortController` and should use that instead of custom.
4+
if (typeof AbortController === 'function') {
5+
/* eslint-env browser */
6+
module.exports = require('./abort-controller.browser')
7+
} else {
8+
module.exports = require('abort-controller')
9+
}

src/http.js

+5-20
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
11
/* eslint-disable no-undef */
22
'use strict'
33

4-
const fetch = require('node-fetch')
4+
const { fetch, Request, Headers } = require('./http/fetch')
5+
const { TimeoutError, HTTPError } = require('./http/error')
56
const merge = require('merge-options').bind({ ignoreUndefined: true })
67
const { URL, URLSearchParams } = require('iso-url')
78
const TextDecoder = require('./text-decoder')
8-
const AbortController = require('abort-controller')
9+
const AbortController = require('./abort-controller')
910
const anySignal = require('any-signal')
1011

11-
const Request = fetch.Request
12-
const Headers = fetch.Headers
13-
14-
class TimeoutError extends Error {
15-
constructor () {
16-
super('Request timed out')
17-
this.name = 'TimeoutError'
18-
}
19-
}
20-
21-
class HTTPError extends Error {
22-
constructor (response) {
23-
super(response.statusText)
24-
this.name = 'HTTPError'
25-
this.response = response
26-
}
27-
}
28-
2912
const timeout = (promise, ms, abortController) => {
3013
if (ms === undefined) {
3114
return promise
@@ -87,6 +70,8 @@ const defaults = {
8770
* @prop {function(URLSearchParams): URLSearchParams } [transformSearchParams]
8871
* @prop {function(any): any} [transform] - When iterating the response body, transform each chunk with this function.
8972
* @prop {function(Response): Promise<void>} [handleError] - Handle errors
73+
* @prop {function({total:number, loaded:number, lengthComputable:boolean}):void} [onUploadProgress] - Can be passed to track upload progress.
74+
* Note that if this option in passed undelying request will be performed using `XMLHttpRequest` and response will not be streamed.
9075
*/
9176

9277
class HTTP {

src/http/error.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict'
2+
3+
class TimeoutError extends Error {
4+
constructor (message = 'Request timed out') {
5+
super(message)
6+
this.name = 'TimeoutError'
7+
}
8+
}
9+
exports.TimeoutError = TimeoutError
10+
11+
class AbortError extends Error {
12+
constructor (message = 'The operation was aborted.') {
13+
super(message)
14+
this.name = 'AbortError'
15+
}
16+
}
17+
exports.AbortError = AbortError
18+
19+
class HTTPError extends Error {
20+
constructor (response) {
21+
super(response.statusText)
22+
this.name = 'HTTPError'
23+
this.response = response
24+
}
25+
}
26+
exports.HTTPError = HTTPError

src/http/fetch.browser.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use strict'
2+
/* eslint-env browser */
3+
4+
const { TimeoutError, AbortError } = require('./error')
5+
const { Request, Response, Headers, fetch } = require('./fetch.polyfill')
6+
7+
/**
8+
* @typedef {RequestInit & ExtraFetchOptions} FetchOptions
9+
* @typedef {Object} ExtraFetchOptions
10+
* @property {number} [timeout]
11+
* @property {URLSearchParams} [searchParams]
12+
* @property {function({total:number, loaded:number, lengthComputable:boolean}):void} [onUploadProgress]
13+
* @property {string} [overrideMimeType]
14+
* @returns {Promise<Response>}
15+
*/
16+
17+
/**
18+
* @param {string|URL} url
19+
* @param {FetchOptions} [options]
20+
* @returns {Promise<Response>}
21+
*/
22+
const fetchWithProgress = (url, options = {}) => {
23+
const request = new XMLHttpRequest()
24+
request.open(options.method || 'GET', url.toString(), true)
25+
26+
const { timeout } = options
27+
if (timeout > 0 && timeout < Infinity) {
28+
request.timeout = options.timeout
29+
}
30+
31+
if (options.overrideMimeType != null) {
32+
request.overrideMimeType(options.overrideMimeType)
33+
}
34+
35+
if (options.headers) {
36+
for (const [name, value] of options.headers.entries()) {
37+
request.setRequestHeader(name, value)
38+
}
39+
}
40+
41+
if (options.signal) {
42+
options.signal.onabort = () => request.abort()
43+
}
44+
45+
if (options.onUploadProgress) {
46+
request.upload.onprogress = options.onUploadProgress
47+
}
48+
49+
// Note: Need to use `arraybuffer` here instead of `blob` because `Blob`
50+
// instances coming from JSDOM are not compatible with `Response` from
51+
// node-fetch (which is the setup we get when testing with jest because
52+
// it uses JSDOM which does not provide a global fetch
53+
// https://github.com/jsdom/jsdom/issues/1724)
54+
request.responseType = 'arraybuffer'
55+
56+
return new Promise((resolve, reject) => {
57+
/**
58+
* @param {Event} event
59+
*/
60+
const handleEvent = (event) => {
61+
switch (event.type) {
62+
case 'error': {
63+
resolve(Response.error())
64+
break
65+
}
66+
case 'load': {
67+
resolve(
68+
new ResponseWithURL(request.responseURL, request.response, {
69+
status: request.status,
70+
statusText: request.statusText,
71+
headers: parseHeaders(request.getAllResponseHeaders())
72+
})
73+
)
74+
break
75+
}
76+
case 'timeout': {
77+
reject(new TimeoutError())
78+
break
79+
}
80+
case 'abort': {
81+
reject(new AbortError())
82+
break
83+
}
84+
default: {
85+
break
86+
}
87+
}
88+
}
89+
request.onerror = handleEvent
90+
request.onload = handleEvent
91+
request.ontimeout = handleEvent
92+
request.onabort = handleEvent
93+
94+
request.send(options.body)
95+
})
96+
}
97+
98+
const fetchWithStreaming = fetch
99+
100+
const fetchWith = (url, options = {}) =>
101+
(options.onUploadProgress != null)
102+
? fetchWithProgress(url, options)
103+
: fetchWithStreaming(url, options)
104+
105+
exports.fetch = fetchWith
106+
exports.Request = Request
107+
exports.Headers = Headers
108+
109+
/**
110+
* @param {string} input
111+
* @returns {Headers}
112+
*/
113+
const parseHeaders = (input) => {
114+
const headers = new Headers()
115+
for (const line of input.trim().split(/[\r\n]+/)) {
116+
const index = line.indexOf(': ')
117+
if (index > 0) {
118+
headers.set(line.slice(0, index), line.slice(index + 1))
119+
}
120+
}
121+
122+
return headers
123+
}
124+
125+
class ResponseWithURL extends Response {
126+
/**
127+
* @param {string} url
128+
* @param {string|Blob|ArrayBufferView|ArrayBuffer|FormData|ReadableStream<Uint8Array>} body
129+
* @param {ResponseInit} options
130+
*/
131+
constructor (url, body, options) {
132+
super(body, options)
133+
Object.defineProperty(this, 'url', { value: url })
134+
}
135+
}

src/http/fetch.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict'
2+
3+
// Electron has `XMLHttpRequest` and should get the browser implementation
4+
// instead of node.
5+
if (typeof XMLHttpRequest !== 'undefined') {
6+
module.exports = require('./fetch.browser')
7+
} else {
8+
module.exports = require('./fetch.node')
9+
}

src/http/fetch.node.js

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// @ts-check
2+
'use strict'
3+
4+
/** @type {import('node-fetch') & typeof fetch} */
5+
// @ts-ignore
6+
const nodeFetch = require('node-fetch')
7+
const toStream = require('it-to-stream')
8+
const { Buffer } = require('buffer')
9+
const { Request, Response, Headers } = nodeFetch
10+
/**
11+
* @typedef {RequestInit & ExtraFetchOptions} FetchOptions
12+
*
13+
* @typedef {import('stream').Readable} Readable
14+
* @typedef {Object} LoadProgress
15+
* @property {number} total
16+
* @property {number} loaded
17+
* @property {boolean} lengthComputable
18+
* @typedef {Object} ExtraFetchOptions
19+
* @property {number} [timeout]
20+
* @property {URLSearchParams} [searchParams]
21+
* @property {function(LoadProgress):void} [onUploadProgress]
22+
* @property {function(LoadProgress):void} [onDownloadProgress]
23+
* @property {string} [overrideMimeType]
24+
* @returns {Promise<Response>}
25+
*/
26+
27+
/**
28+
* @param {string|URL} url
29+
* @param {FetchOptions} [options]
30+
* @returns {Promise<Response>}
31+
*/
32+
const fetch = (url, options = {}) =>
33+
nodeFetch(url, withUploadProgress(options))
34+
35+
exports.fetch = fetch
36+
exports.Request = Request
37+
exports.Headers = Headers
38+
39+
/**
40+
* Takes fetch options and wraps request body to track uploda progress if
41+
* `onUploadProgress` is supplied. Otherwise returns options as is.
42+
* @param {FetchOptions} options
43+
* @returns {FetchOptions}
44+
*/
45+
const withUploadProgress = (options) => {
46+
const { onUploadProgress } = options
47+
if (onUploadProgress) {
48+
return {
49+
...options,
50+
// @ts-ignore
51+
body: bodyWithUploadProgress(options, onUploadProgress)
52+
}
53+
} else {
54+
return options
55+
}
56+
}
57+
58+
/**
59+
* Takes request `body` and `onUploadProgress` handler and returns wrapped body
60+
* that as consumed will report progress to suppled `onUploadProgress` handler.
61+
* @param {FetchOptions} init
62+
* @param {function(LoadProgress):void} onUploadProgress
63+
* @returns {Readable}
64+
*/
65+
const bodyWithUploadProgress = (init, onUploadProgress) => {
66+
// @ts-ignore - node-fetch is typed poorly
67+
const { body } = new Response(init.body, init)
68+
// @ts-ignore - Unlike standard Response, node-fetch `body` has a differnt
69+
// type see: see https://github.com/node-fetch/node-fetch/blob/master/src/body.js
70+
const source = iterateBodyWithProgress(body, onUploadProgress)
71+
return toStream.readable(source)
72+
}
73+
74+
/**
75+
* Takes body from node-fetch response as body and `onUploadProgress` handler
76+
* and returns async iterable that emits body chunks and emits
77+
* `onUploadProgress`.
78+
* @param {Buffer|null|Readable} body
79+
* @param {function(LoadProgress):void} onUploadProgress
80+
* @returns {AsyncIterable<Buffer>}
81+
*/
82+
const iterateBodyWithProgress = async function * (body, onUploadProgress) {
83+
/** @type {Buffer|null|Readable} */
84+
if (body == null) {
85+
onUploadProgress({ total: 0, loaded: 0, lengthComputable: true })
86+
} else if (Buffer.isBuffer(body)) {
87+
const total = body.byteLength
88+
const lengthComputable = true
89+
yield body
90+
onUploadProgress({ total, loaded: total, lengthComputable })
91+
} else {
92+
const total = 0
93+
const lengthComputable = false
94+
let loaded = 0
95+
for await (const chunk of body) {
96+
loaded += chunk.byteLength
97+
yield chunk
98+
onUploadProgress({ total, loaded, lengthComputable })
99+
}
100+
}
101+
}

src/http/fetch.polyfill.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
3+
/* eslint-env browser */
4+
5+
// JSDOM has `XMLHttpRequest` but it does not have a `fetch` or `Response` so
6+
// we workaround by pulling in node-fetch.
7+
// See: https://github.com/jsdom/jsdom/issues/1724
8+
exports.fetch = typeof fetch === 'function'
9+
? fetch
10+
: require('node-fetch')
11+
12+
exports.Response = typeof Response === 'function'
13+
? Response
14+
: require('node-fetch').Response
15+
16+
exports.Request = typeof Request === 'function'
17+
? Request
18+
: require('node-fetch').Response
19+
20+
exports.Headers = typeof Headers === 'function'
21+
? Headers
22+
: require('node-fetch').Headers

0 commit comments

Comments
 (0)