|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +Object.defineProperty(exports, '__esModule', { value: true }); |
| 4 | + |
| 5 | +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } |
| 6 | + |
| 7 | +var get = _interopDefault(require('simple-get')); |
| 8 | + |
| 9 | +/** |
| 10 | + * @typedef {Object} GitProgressEvent |
| 11 | + * @property {string} phase |
| 12 | + * @property {number} loaded |
| 13 | + * @property {number} total |
| 14 | + */ |
| 15 | + |
| 16 | +/** |
| 17 | + * @callback ProgressCallback |
| 18 | + * @param {GitProgressEvent} progress |
| 19 | + * @returns {void | Promise<void>} |
| 20 | + */ |
| 21 | + |
| 22 | +/** |
| 23 | + * @typedef {Object} GitHttpRequest |
| 24 | + * @property {string} url - The URL to request |
| 25 | + * @property {string} [method='GET'] - The HTTP method to use |
| 26 | + * @property {Object<string, string>} [headers={}] - Headers to include in the HTTP request |
| 27 | + * @property {Object} [agent] - An HTTP or HTTPS agent that manages connections for the HTTP client (Node.js only) |
| 28 | + * @property {AsyncIterableIterator<Uint8Array>} [body] - An async iterator of Uint8Arrays that make up the body of POST requests |
| 29 | + * @property {ProgressCallback} [onProgress] - Reserved for future use (emitting `GitProgressEvent`s) |
| 30 | + * @property {object} [signal] - Reserved for future use (canceling a request) |
| 31 | + */ |
| 32 | + |
| 33 | +/** |
| 34 | + * @typedef {Object} GitHttpResponse |
| 35 | + * @property {string} url - The final URL that was fetched after any redirects |
| 36 | + * @property {string} [method] - The HTTP method that was used |
| 37 | + * @property {Object<string, string>} [headers] - HTTP response headers |
| 38 | + * @property {AsyncIterableIterator<Uint8Array>} [body] - An async iterator of Uint8Arrays that make up the body of the response |
| 39 | + * @property {number} statusCode - The HTTP status code |
| 40 | + * @property {string} statusMessage - The HTTP status message |
| 41 | + */ |
| 42 | + |
| 43 | +/** |
| 44 | + * @callback HttpFetch |
| 45 | + * @param {GitHttpRequest} request |
| 46 | + * @returns {Promise<GitHttpResponse>} |
| 47 | + */ |
| 48 | + |
| 49 | +/** |
| 50 | + * @typedef {Object} HttpClient |
| 51 | + * @property {HttpFetch} request |
| 52 | + */ |
| 53 | + |
| 54 | +// Convert a value to an Async Iterator |
| 55 | +// This will be easier with async generator functions. |
| 56 | +function fromValue(value) { |
| 57 | + let queue = [value]; |
| 58 | + return { |
| 59 | + next() { |
| 60 | + return Promise.resolve({ done: queue.length === 0, value: queue.pop() }) |
| 61 | + }, |
| 62 | + return() { |
| 63 | + queue = []; |
| 64 | + return {} |
| 65 | + }, |
| 66 | + [Symbol.asyncIterator]() { |
| 67 | + return this |
| 68 | + }, |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +function getIterator(iterable) { |
| 73 | + if (iterable[Symbol.asyncIterator]) { |
| 74 | + return iterable[Symbol.asyncIterator]() |
| 75 | + } |
| 76 | + if (iterable[Symbol.iterator]) { |
| 77 | + return iterable[Symbol.iterator]() |
| 78 | + } |
| 79 | + if (iterable.next) { |
| 80 | + return iterable |
| 81 | + } |
| 82 | + return fromValue(iterable) |
| 83 | +} |
| 84 | + |
| 85 | +// Currently 'for await' upsets my linters. |
| 86 | +async function forAwait(iterable, cb) { |
| 87 | + const iter = getIterator(iterable); |
| 88 | + while (true) { |
| 89 | + const { value, done } = await iter.next(); |
| 90 | + if (value) await cb(value); |
| 91 | + if (done) break |
| 92 | + } |
| 93 | + if (iter.return) iter.return(); |
| 94 | +} |
| 95 | + |
| 96 | +function asyncIteratorToStream(iter) { |
| 97 | + const { PassThrough } = require('readable-stream'); |
| 98 | + const stream = new PassThrough(); |
| 99 | + setTimeout(async () => { |
| 100 | + await forAwait(iter, chunk => stream.write(chunk)); |
| 101 | + stream.end(); |
| 102 | + }, 1); |
| 103 | + return stream |
| 104 | +} |
| 105 | + |
| 106 | +async function collect(iterable) { |
| 107 | + let size = 0; |
| 108 | + const buffers = []; |
| 109 | + // This will be easier once `for await ... of` loops are available. |
| 110 | + await forAwait(iterable, value => { |
| 111 | + buffers.push(value); |
| 112 | + size += value.byteLength; |
| 113 | + }); |
| 114 | + const result = new Uint8Array(size); |
| 115 | + let nextIndex = 0; |
| 116 | + for (const buffer of buffers) { |
| 117 | + result.set(buffer, nextIndex); |
| 118 | + nextIndex += buffer.byteLength; |
| 119 | + } |
| 120 | + return result |
| 121 | +} |
| 122 | + |
| 123 | +// Convert a Node stream to an Async Iterator |
| 124 | +function fromNodeStream(stream) { |
| 125 | + // Use native async iteration if it's available. |
| 126 | + const asyncIterator = Object.getOwnPropertyDescriptor( |
| 127 | + stream, |
| 128 | + Symbol.asyncIterator |
| 129 | + ); |
| 130 | + if (asyncIterator && asyncIterator.enumerable) { |
| 131 | + return stream |
| 132 | + } |
| 133 | + // Author's Note |
| 134 | + // I tried many MANY ways to do this. |
| 135 | + // I tried two npm modules (stream-to-async-iterator and streams-to-async-iterator) with no luck. |
| 136 | + // I tried using 'readable' and .read(), and .pause() and .resume() |
| 137 | + // It took me two loooong evenings to get to this point. |
| 138 | + // So if you are horrified that this solution just builds up a queue with no backpressure, |
| 139 | + // and turns Promises inside out, too bad. This is the first code that worked reliably. |
| 140 | + let ended = false; |
| 141 | + const queue = []; |
| 142 | + let defer = {}; |
| 143 | + stream.on('data', chunk => { |
| 144 | + queue.push(chunk); |
| 145 | + if (defer.resolve) { |
| 146 | + defer.resolve({ value: queue.shift(), done: false }); |
| 147 | + defer = {}; |
| 148 | + } |
| 149 | + }); |
| 150 | + stream.on('error', err => { |
| 151 | + if (defer.reject) { |
| 152 | + defer.reject(err); |
| 153 | + defer = {}; |
| 154 | + } |
| 155 | + }); |
| 156 | + stream.on('end', () => { |
| 157 | + ended = true; |
| 158 | + if (defer.resolve) { |
| 159 | + defer.resolve({ done: true }); |
| 160 | + defer = {}; |
| 161 | + } |
| 162 | + }); |
| 163 | + return { |
| 164 | + next() { |
| 165 | + return new Promise((resolve, reject) => { |
| 166 | + if (queue.length === 0 && ended) { |
| 167 | + return resolve({ done: true }) |
| 168 | + } else if (queue.length > 0) { |
| 169 | + return resolve({ value: queue.shift(), done: false }) |
| 170 | + } else if (queue.length === 0 && !ended) { |
| 171 | + defer = { resolve, reject }; |
| 172 | + } |
| 173 | + }) |
| 174 | + }, |
| 175 | + return() { |
| 176 | + stream.removeAllListeners(); |
| 177 | + if (stream.destroy) stream.destroy(); |
| 178 | + }, |
| 179 | + [Symbol.asyncIterator]() { |
| 180 | + return this |
| 181 | + }, |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +/** |
| 186 | + * HttpClient |
| 187 | + * |
| 188 | + * @param {GitHttpRequest} request |
| 189 | + * @returns {Promise<GitHttpResponse>} |
| 190 | + */ |
| 191 | +async function request({ |
| 192 | + onProgress, |
| 193 | + url, |
| 194 | + method = 'GET', |
| 195 | + headers = {}, |
| 196 | + agent, |
| 197 | + body, |
| 198 | +}) { |
| 199 | + // If we can, we should send it as a single buffer so it sets a Content-Length header. |
| 200 | + if (body && Array.isArray(body)) { |
| 201 | + body = Buffer.from(await collect(body)); |
| 202 | + } else if (body) { |
| 203 | + body = asyncIteratorToStream(body); |
| 204 | + } |
| 205 | + return new Promise((resolve, reject) => { |
| 206 | + get( |
| 207 | + { |
| 208 | + url, |
| 209 | + method, |
| 210 | + headers, |
| 211 | + agent, |
| 212 | + body, |
| 213 | + }, |
| 214 | + (err, res) => { |
| 215 | + if (err) return reject(err) |
| 216 | + try { |
| 217 | + const iter = fromNodeStream(res); |
| 218 | + resolve({ |
| 219 | + url: res.url, |
| 220 | + method: res.method, |
| 221 | + statusCode: res.statusCode, |
| 222 | + statusMessage: res.statusMessage, |
| 223 | + body: iter, |
| 224 | + headers: res.headers, |
| 225 | + }); |
| 226 | + } catch (e) { |
| 227 | + reject(e); |
| 228 | + } |
| 229 | + } |
| 230 | + ); |
| 231 | + }) |
| 232 | +} |
| 233 | + |
| 234 | +var index = { request }; |
| 235 | + |
| 236 | +exports.default = index; |
| 237 | +exports.request = request; |
0 commit comments