1 | (function (global, factory) {
|
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
3 | typeof define === 'function' && define.amd ? define(factory) :
|
4 | (global = global || self, global.ky = factory());
|
5 | }(this, (function () { 'use strict';
|
6 |
|
7 |
|
8 |
|
9 | const globals = {};
|
10 |
|
11 | const getGlobal = property => {
|
12 |
|
13 | if (typeof self !== 'undefined' && self && property in self) {
|
14 | return self;
|
15 | }
|
16 |
|
17 |
|
18 | if (typeof window !== 'undefined' && window && property in window) {
|
19 | return window;
|
20 | }
|
21 |
|
22 | if (typeof global !== 'undefined' && global && property in global) {
|
23 | return global;
|
24 | }
|
25 |
|
26 |
|
27 | if (typeof globalThis !== 'undefined' && globalThis) {
|
28 | return globalThis;
|
29 | }
|
30 | };
|
31 |
|
32 | const globalProperties = [
|
33 | 'Headers',
|
34 | 'Request',
|
35 | 'Response',
|
36 | 'ReadableStream',
|
37 | 'fetch',
|
38 | 'AbortController',
|
39 | 'FormData'
|
40 | ];
|
41 |
|
42 | for (const property of globalProperties) {
|
43 | Object.defineProperty(globals, property, {
|
44 | get() {
|
45 | const globalObject = getGlobal(property);
|
46 | const value = globalObject && globalObject[property];
|
47 | return typeof value === 'function' ? value.bind(globalObject) : value;
|
48 | }
|
49 | });
|
50 | }
|
51 |
|
52 | const isObject = value => value !== null && typeof value === 'object';
|
53 | const supportsAbortController = typeof globals.AbortController === 'function';
|
54 | const supportsStreams = typeof globals.ReadableStream === 'function';
|
55 | const supportsFormData = typeof globals.FormData === 'function';
|
56 |
|
57 | const mergeHeaders = (source1, source2) => {
|
58 | const result = new globals.Headers(source1);
|
59 | const isHeadersInstance = source2 instanceof globals.Headers;
|
60 | const source = new globals.Headers(source2);
|
61 |
|
62 | for (const [key, value] of source) {
|
63 | if ((isHeadersInstance && value === 'undefined') || value === undefined) {
|
64 | result.delete(key);
|
65 | } else {
|
66 | result.set(key, value);
|
67 | }
|
68 | }
|
69 |
|
70 | return result;
|
71 | };
|
72 |
|
73 | const deepMerge = (...sources) => {
|
74 | let returnValue = {};
|
75 | let headers = {};
|
76 |
|
77 | for (const source of sources) {
|
78 | if (Array.isArray(source)) {
|
79 | if (!(Array.isArray(returnValue))) {
|
80 | returnValue = [];
|
81 | }
|
82 |
|
83 | returnValue = [...returnValue, ...source];
|
84 | } else if (isObject(source)) {
|
85 | for (let [key, value] of Object.entries(source)) {
|
86 | if (isObject(value) && Reflect.has(returnValue, key)) {
|
87 | value = deepMerge(returnValue[key], value);
|
88 | }
|
89 |
|
90 | returnValue = {...returnValue, [key]: value};
|
91 | }
|
92 |
|
93 | if (isObject(source.headers)) {
|
94 | headers = mergeHeaders(headers, source.headers);
|
95 | }
|
96 | }
|
97 |
|
98 | returnValue.headers = headers;
|
99 | }
|
100 |
|
101 | return returnValue;
|
102 | };
|
103 |
|
104 | const requestMethods = [
|
105 | 'get',
|
106 | 'post',
|
107 | 'put',
|
108 | 'patch',
|
109 | 'head',
|
110 | 'delete'
|
111 | ];
|
112 |
|
113 | const responseTypes = {
|
114 | json: 'application/json',
|
115 | text: 'text/*',
|
116 | formData: 'multipart/form-data',
|
117 | arrayBuffer: '*/*',
|
118 | blob: '*/*'
|
119 | };
|
120 |
|
121 | const retryMethods = [
|
122 | 'get',
|
123 | 'put',
|
124 | 'head',
|
125 | 'delete',
|
126 | 'options',
|
127 | 'trace'
|
128 | ];
|
129 |
|
130 | const retryStatusCodes = [
|
131 | 408,
|
132 | 413,
|
133 | 429,
|
134 | 500,
|
135 | 502,
|
136 | 503,
|
137 | 504
|
138 | ];
|
139 |
|
140 | const retryAfterStatusCodes = [
|
141 | 413,
|
142 | 429,
|
143 | 503
|
144 | ];
|
145 |
|
146 | const stop = Symbol('stop');
|
147 |
|
148 | class HTTPError extends Error {
|
149 | constructor(response) {
|
150 |
|
151 |
|
152 | super(
|
153 | response.statusText ||
|
154 | String(
|
155 | (response.status === 0 || response.status) ?
|
156 | response.status : 'Unknown response error'
|
157 | )
|
158 | );
|
159 | this.name = 'HTTPError';
|
160 | this.response = response;
|
161 | }
|
162 | }
|
163 |
|
164 | class TimeoutError extends Error {
|
165 | constructor() {
|
166 | super('Request timed out');
|
167 | this.name = 'TimeoutError';
|
168 | }
|
169 | }
|
170 |
|
171 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
172 |
|
173 |
|
174 | const timeout = (promise, ms, abortController) =>
|
175 | new Promise((resolve, reject) => {
|
176 | const timeoutID = setTimeout(() => {
|
177 | if (abortController) {
|
178 | abortController.abort();
|
179 | }
|
180 |
|
181 | reject(new TimeoutError());
|
182 | }, ms);
|
183 |
|
184 |
|
185 | promise
|
186 | .then(resolve)
|
187 | .catch(reject)
|
188 | .then(() => {
|
189 | clearTimeout(timeoutID);
|
190 | });
|
191 |
|
192 | });
|
193 |
|
194 | const normalizeRequestMethod = input => requestMethods.includes(input) ? input.toUpperCase() : input;
|
195 |
|
196 | const defaultRetryOptions = {
|
197 | limit: 2,
|
198 | methods: retryMethods,
|
199 | statusCodes: retryStatusCodes,
|
200 | afterStatusCodes: retryAfterStatusCodes
|
201 | };
|
202 |
|
203 | const normalizeRetryOptions = (retry = {}) => {
|
204 | if (typeof retry === 'number') {
|
205 | return {
|
206 | ...defaultRetryOptions,
|
207 | limit: retry
|
208 | };
|
209 | }
|
210 |
|
211 | if (retry.methods && !Array.isArray(retry.methods)) {
|
212 | throw new Error('retry.methods must be an array');
|
213 | }
|
214 |
|
215 | if (retry.statusCodes && !Array.isArray(retry.statusCodes)) {
|
216 | throw new Error('retry.statusCodes must be an array');
|
217 | }
|
218 |
|
219 | return {
|
220 | ...defaultRetryOptions,
|
221 | ...retry,
|
222 | afterStatusCodes: retryAfterStatusCodes
|
223 | };
|
224 | };
|
225 |
|
226 |
|
227 | const maxSafeTimeout = 2147483647;
|
228 |
|
229 | class Ky {
|
230 | constructor(input, options = {}) {
|
231 | this._retryCount = 0;
|
232 | this._input = input;
|
233 | this._options = {
|
234 |
|
235 | credentials: this._input.credentials || 'same-origin',
|
236 | ...options,
|
237 | headers: mergeHeaders(this._input.headers, options.headers),
|
238 | hooks: deepMerge({
|
239 | beforeRequest: [],
|
240 | beforeRetry: [],
|
241 | afterResponse: []
|
242 | }, options.hooks),
|
243 | method: normalizeRequestMethod(options.method || this._input.method),
|
244 | prefixUrl: String(options.prefixUrl || ''),
|
245 | retry: normalizeRetryOptions(options.retry),
|
246 | throwHttpErrors: options.throwHttpErrors !== false,
|
247 | timeout: typeof options.timeout === 'undefined' ? 10000 : options.timeout
|
248 | };
|
249 |
|
250 | if (typeof this._input !== 'string' && !(this._input instanceof URL || this._input instanceof globals.Request)) {
|
251 | throw new TypeError('`input` must be a string, URL, or Request');
|
252 | }
|
253 |
|
254 | if (this._options.prefixUrl && typeof this._input === 'string') {
|
255 | if (this._input.startsWith('/')) {
|
256 | throw new Error('`input` must not begin with a slash when using `prefixUrl`');
|
257 | }
|
258 |
|
259 | if (!this._options.prefixUrl.endsWith('/')) {
|
260 | this._options.prefixUrl += '/';
|
261 | }
|
262 |
|
263 | this._input = this._options.prefixUrl + this._input;
|
264 | }
|
265 |
|
266 | if (supportsAbortController) {
|
267 | this.abortController = new globals.AbortController();
|
268 | if (this._options.signal) {
|
269 | this._options.signal.addEventListener('abort', () => {
|
270 | this.abortController.abort();
|
271 | });
|
272 | }
|
273 |
|
274 | this._options.signal = this.abortController.signal;
|
275 | }
|
276 |
|
277 | this.request = new globals.Request(this._input, this._options);
|
278 |
|
279 | if (this._options.searchParams) {
|
280 | const url = new URL(this.request.url);
|
281 | url.search = new URLSearchParams(this._options.searchParams);
|
282 |
|
283 |
|
284 | if (((supportsFormData && this._options.body instanceof globals.FormData) || this._options.body instanceof URLSearchParams) && !(this._options.headers && this._options.headers['content-type'])) {
|
285 | this.request.headers.delete('content-type');
|
286 | }
|
287 |
|
288 | this.request = new globals.Request(new globals.Request(url, this.request), this._options);
|
289 | }
|
290 |
|
291 | if (this._options.json !== undefined) {
|
292 | this._options.body = JSON.stringify(this._options.json);
|
293 | this.request.headers.set('content-type', 'application/json');
|
294 | this.request = new globals.Request(this.request, {body: this._options.body});
|
295 | }
|
296 |
|
297 | const fn = async () => {
|
298 | if (this._options.timeout > maxSafeTimeout) {
|
299 | throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
|
300 | }
|
301 |
|
302 | await delay(1);
|
303 | let response = await this._fetch();
|
304 |
|
305 | for (const hook of this._options.hooks.afterResponse) {
|
306 |
|
307 | const modifiedResponse = await hook(
|
308 | this.request,
|
309 | this._options,
|
310 | response.clone()
|
311 | );
|
312 |
|
313 | if (modifiedResponse instanceof globals.Response) {
|
314 | response = modifiedResponse;
|
315 | }
|
316 | }
|
317 |
|
318 | if (!response.ok && this._options.throwHttpErrors) {
|
319 | throw new HTTPError(response);
|
320 | }
|
321 |
|
322 |
|
323 |
|
324 | if (this._options.onDownloadProgress) {
|
325 | if (typeof this._options.onDownloadProgress !== 'function') {
|
326 | throw new TypeError('The `onDownloadProgress` option must be a function');
|
327 | }
|
328 |
|
329 | if (!supportsStreams) {
|
330 | throw new Error('Streams are not supported in your environment. `ReadableStream` is missing.');
|
331 | }
|
332 |
|
333 | return this._stream(response.clone(), this._options.onDownloadProgress);
|
334 | }
|
335 |
|
336 | return response;
|
337 | };
|
338 |
|
339 | const isRetriableMethod = this._options.retry.methods.includes(this.request.method.toLowerCase());
|
340 | const result = isRetriableMethod ? this._retry(fn) : fn();
|
341 |
|
342 | for (const [type, mimeType] of Object.entries(responseTypes)) {
|
343 | result[type] = async () => {
|
344 | this.request.headers.set('accept', this.request.headers.get('accept') || mimeType);
|
345 | const response = (await result).clone();
|
346 | return (type === 'json' && response.status === 204) ? '' : response[type]();
|
347 | };
|
348 | }
|
349 |
|
350 | return result;
|
351 | }
|
352 |
|
353 | _calculateRetryDelay(error) {
|
354 | this._retryCount++;
|
355 |
|
356 | if (this._retryCount < this._options.retry.limit && !(error instanceof TimeoutError)) {
|
357 | if (error instanceof HTTPError) {
|
358 | if (!this._options.retry.statusCodes.includes(error.response.status)) {
|
359 | return 0;
|
360 | }
|
361 |
|
362 | const retryAfter = error.response.headers.get('Retry-After');
|
363 | if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
|
364 | let after = Number(retryAfter);
|
365 | if (Number.isNaN(after)) {
|
366 | after = Date.parse(retryAfter) - Date.now();
|
367 | } else {
|
368 | after *= 1000;
|
369 | }
|
370 |
|
371 | if (typeof this._options.retry.maxRetryAfter !== 'undefined' && after > this._options.retry.maxRetryAfter) {
|
372 | return 0;
|
373 | }
|
374 |
|
375 | return after;
|
376 | }
|
377 |
|
378 | if (error.response.status === 413) {
|
379 | return 0;
|
380 | }
|
381 | }
|
382 |
|
383 | const BACKOFF_FACTOR = 0.3;
|
384 | return BACKOFF_FACTOR * (2 ** (this._retryCount - 1)) * 1000;
|
385 | }
|
386 |
|
387 | return 0;
|
388 | }
|
389 |
|
390 | async _retry(fn) {
|
391 | try {
|
392 | return await fn();
|
393 | } catch (error) {
|
394 | const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout);
|
395 | if (ms !== 0 && this._retryCount > 0) {
|
396 | await delay(ms);
|
397 |
|
398 | for (const hook of this._options.hooks.beforeRetry) {
|
399 |
|
400 | const hookResult = await hook({
|
401 | request: this.request,
|
402 | options: this._options,
|
403 | error,
|
404 | response: error.response.clone(),
|
405 | retryCount: this._retryCount
|
406 | });
|
407 |
|
408 |
|
409 | if (hookResult === stop) {
|
410 | return;
|
411 | }
|
412 | }
|
413 |
|
414 | return this._retry(fn);
|
415 | }
|
416 |
|
417 | if (this._options.throwHttpErrors) {
|
418 | throw error;
|
419 | }
|
420 | }
|
421 | }
|
422 |
|
423 | async _fetch() {
|
424 | for (const hook of this._options.hooks.beforeRequest) {
|
425 |
|
426 | const result = await hook(this.request, this._options);
|
427 |
|
428 | if (result instanceof Request) {
|
429 | this.request = result;
|
430 | break;
|
431 | }
|
432 |
|
433 | if (result instanceof Response) {
|
434 | return result;
|
435 | }
|
436 | }
|
437 |
|
438 | if (this._options.timeout === false) {
|
439 | return globals.fetch(this.request.clone());
|
440 | }
|
441 |
|
442 | return timeout(globals.fetch(this.request.clone()), this._options.timeout, this.abortController);
|
443 | }
|
444 |
|
445 |
|
446 | _stream(response, onDownloadProgress) {
|
447 | const totalBytes = Number(response.headers.get('content-length')) || 0;
|
448 | let transferredBytes = 0;
|
449 |
|
450 | return new globals.Response(
|
451 | new globals.ReadableStream({
|
452 | start(controller) {
|
453 | const reader = response.body.getReader();
|
454 |
|
455 | if (onDownloadProgress) {
|
456 | onDownloadProgress({percent: 0, transferredBytes: 0, totalBytes}, new Uint8Array());
|
457 | }
|
458 |
|
459 | async function read() {
|
460 | const {done, value} = await reader.read();
|
461 | if (done) {
|
462 | controller.close();
|
463 | return;
|
464 | }
|
465 |
|
466 | if (onDownloadProgress) {
|
467 | transferredBytes += value.byteLength;
|
468 | const percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes;
|
469 | onDownloadProgress({percent, transferredBytes, totalBytes}, value);
|
470 | }
|
471 |
|
472 | controller.enqueue(value);
|
473 | read();
|
474 | }
|
475 |
|
476 | read();
|
477 | }
|
478 | })
|
479 | );
|
480 | }
|
481 | }
|
482 |
|
483 | const validateAndMerge = (...sources) => {
|
484 | for (const source of sources) {
|
485 | if ((!isObject(source) || Array.isArray(source)) && typeof source !== 'undefined') {
|
486 | throw new TypeError('The `options` argument must be an object');
|
487 | }
|
488 | }
|
489 |
|
490 | return deepMerge({}, ...sources);
|
491 | };
|
492 |
|
493 | const createInstance = defaults => {
|
494 | const ky = (input, options) => new Ky(input, validateAndMerge(defaults, options));
|
495 |
|
496 | for (const method of requestMethods) {
|
497 | ky[method] = (input, options) => new Ky(input, validateAndMerge(defaults, options, {method}));
|
498 | }
|
499 |
|
500 | ky.HTTPError = HTTPError;
|
501 | ky.TimeoutError = TimeoutError;
|
502 | ky.create = newDefaults => createInstance(validateAndMerge(newDefaults));
|
503 | ky.extend = newDefaults => createInstance(validateAndMerge(defaults, newDefaults));
|
504 | ky.stop = stop;
|
505 |
|
506 | return ky;
|
507 | };
|
508 |
|
509 | var index = createInstance();
|
510 |
|
511 | return index;
|
512 |
|
513 | })));
|