UNPKG

@lylajs/core

Version:

630 lines (629 loc) 27 kB
import { defineLylaError, isLylaError as _isLylaError, LYLA_ERROR } from './error'; import { mergeUrl, mergeHeaders, mergeOptions } from './utils'; import { getStatusText } from './status'; function isOkStatus(status) { return 200 <= status && status < 300; } export function createLyla(adapter, lylaOptions, ...overrides) { const mergedLylaOptions = mergeOptions(lylaOptions, ...overrides); async function request(options) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const resolvedContext = options.context === undefined ? mergedLylaOptions.context : options.context; let optionsWithContext = Object.assign({}, options, { context: typeof resolvedContext === 'object' ? JSON.parse(JSON.stringify(resolvedContext)) : resolvedContext }); async function handleNonResponseError(error, shouldRejectOriginalRequest) { var _a, _b; if ((_a = _options.hooks) === null || _a === void 0 ? void 0 : _a.onNonResponseError) { try { for (const hook of (_b = _options.hooks) === null || _b === void 0 ? void 0 : _b.onNonResponseError) { const maybePromise = hook(error); if (maybePromise instanceof Promise) { await maybePromise; } } if (shouldRejectOriginalRequest) { _reject(error); } } catch (e) { _reject(defineLylaError({ type: LYLA_ERROR.BROKEN_ON_NON_RESPONSE_ERROR, error: e, message: '`onNonResponseError` hook throws error', detail: undefined, response: undefined, context: error.context, requestOptions: _options }, undefined)); return; } } else { if (shouldRejectOriginalRequest) { _reject(error); } } } try { if ((_a = mergedLylaOptions === null || mergedLylaOptions === void 0 ? void 0 : mergedLylaOptions.hooks) === null || _a === void 0 ? void 0 : _a.onInit) { for (const hook of mergedLylaOptions.hooks.onInit) { const maybeOptionsWithContextPromise = hook(optionsWithContext); if (maybeOptionsWithContextPromise instanceof Promise) { optionsWithContext = await maybeOptionsWithContextPromise; } else { optionsWithContext = maybeOptionsWithContextPromise; } } } if ((_b = options === null || options === void 0 ? void 0 : options.hooks) === null || _b === void 0 ? void 0 : _b.onInit) { for (const hook of options.hooks.onInit) { const maybeOptionsWithContextPromise = hook(optionsWithContext); if (maybeOptionsWithContextPromise instanceof Promise) { optionsWithContext = await maybeOptionsWithContextPromise; } else { optionsWithContext = maybeOptionsWithContextPromise; } } } } catch (e) { const brokenOnInitError = defineLylaError({ type: LYLA_ERROR.BROKEN_ON_INIT, message: '`onInit` hook throws error', detail: undefined, error: e, response: undefined, context: optionsWithContext.context, requestOptions: optionsWithContext }, undefined); handleNonResponseError(brokenOnInitError, true); throw brokenOnInitError; } let _options = mergeOptions(mergedLylaOptions, optionsWithContext); _options.method = (_c = _options.method) === null || _c === void 0 ? void 0 : _c.toUpperCase(); _options.responseType = options.responseType || 'text'; _options.url = _options.url || ''; if (_options.baseUrl) { _options.url = mergeUrl(_options.baseUrl, _options.url); } let stack; try { stack = (_d = new Error().stack) === null || _d === void 0 ? void 0 : _d.replace(/^Error/, ''); } catch (_) { } // Resolve query string, patch it to URL if (_options.query) { const urlSearchParams = new URLSearchParams(); for (const [key, value] of Object.entries(_options.query)) { if (Array.isArray(value)) { for (const v of value) { urlSearchParams.append(key, v.toString()); } } else if (value !== undefined && value !== null) { urlSearchParams.append(key, value.toString()); } } const queryString = urlSearchParams.toString(); if (_options.url.includes('?')) { const badRequestError = defineLylaError({ type: LYLA_ERROR.BAD_REQUEST, message: "`options.query` can't be set if `options.url` contains '?'", detail: undefined, error: undefined, response: undefined, context: _options.context, requestOptions: _options }, undefined); handleNonResponseError(badRequestError, true); throw badRequestError; } if (queryString.length) { _options.url = _options.url + '?' + queryString; } } if ((_e = _options.hooks) === null || _e === void 0 ? void 0 : _e.onBeforeRequest) { try { for (const hook of (_f = _options.hooks) === null || _f === void 0 ? void 0 : _f.onBeforeRequest) { const maybeOptionsPromise = hook(_options); if (maybeOptionsPromise instanceof Promise) { _options = await maybeOptionsPromise; } else { _options = maybeOptionsPromise; } } } catch (e) { const brokenOnBeforeRequestError = defineLylaError({ type: LYLA_ERROR.BROKEN_ON_BEFORE_REQUEST, message: '`onBeforeRequest` hook throws error', detail: undefined, error: e, response: undefined, context: optionsWithContext.context, requestOptions: _options }, undefined); handleNonResponseError(brokenOnBeforeRequestError, true); throw brokenOnBeforeRequestError; } } // Move json data to body as string if (_options.json !== undefined) { if (_options.body !== undefined) { const badRequestError = defineLylaError({ type: LYLA_ERROR.BAD_REQUEST, message: "`options.json` can't be used together with `options.body`. If you want to use `options.json`, you should left `options.body` as `undefined`", detail: undefined, error: undefined, response: undefined, context: _options.context, requestOptions: _options }, undefined); handleNonResponseError(badRequestError, true); throw badRequestError; } _options.body = JSON.stringify(_options.json); } const { timeout, url = '', method = 'get', body, responseType = 'text', withCredentials = false, signal, onUploadProgress, onDownloadProgress } = _options; async function handleResponseError(error) { var _a, _b; if ((_a = _options.hooks) === null || _a === void 0 ? void 0 : _a.onResponseError) { try { for (const hook of (_b = _options.hooks) === null || _b === void 0 ? void 0 : _b.onResponseError) { const maybePromise = hook(error, _customReject); if (maybePromise instanceof Promise) { await maybePromise; } } _reject(error); } catch (e) { const brokenOnResponseErrorError = defineLylaError({ type: LYLA_ERROR.BROKEN_ON_RESPONSE_ERROR, error: e, message: '`onResponseError` hook throws error', detail: undefined, response: undefined, context: error.context, requestOptions: _options }, undefined); handleNonResponseError(brokenOnResponseErrorError, true); return; } } else { _reject(error); } } let _resolve; let _reject; let _customReject; // make request headers const requestHeaders = {}; mergeHeaders(requestHeaders, mergedLylaOptions.headers); mergeHeaders(requestHeaders, _options.headers); // Set 'content-type' header if (_options.json !== undefined) { requestHeaders['content-type'] = (_g = requestHeaders['content-type']) !== null && _g !== void 0 ? _g : 'application/json'; } requestHeaders['accept'] = (_h = requestHeaders.accept) !== null && _h !== void 0 ? _h : '*/*'; _options.headers = requestHeaders; let settled = false; let abortSignalListenerOff = false; function cleanup() { settled = true; if (!abortSignalListenerOff) { stopListeningAbortSignal(); } } function stopListeningAbortSignal() { abortSignalListenerOff = true; if (signal) { signal.removeEventListener('abort', onAbortSignalReceived); } } const requestPromise = new Promise((resolve, reject) => { _resolve = resolve; _reject = (reason) => { if (settled) return; cleanup(); reject(reason); }; _customReject = (reason) => { if (settled) return; cleanup(); reject(reason); }; }); let aborted = false; function onAbortSignalReceived() { if (aborted) return; aborted = true; const abortError = defineLylaError({ type: LYLA_ERROR.ABORTED, message: 'Request aborted', detail: undefined, error: undefined, response: undefined, context: _options.context, requestOptions: _options }, stack); const settled = () => adapterHandle.abort(); handleResponseError(abortError).then(settled).catch(settled); } if (signal) { signal.addEventListener('abort', onAbortSignalReceived); } let hasNetworkError = false; const onHeadersReceived = (_j = _options.hooks) === null || _j === void 0 ? void 0 : _j.onHeadersReceived; const adapterHandle = adapter({ url, method, body, json: _options.json, headers: requestHeaders, responseType, withCredentials, extraOptions: _options.extraOptions, onNetworkError(detail) { hasNetworkError = true; stopListeningAbortSignal(); const networkError = defineLylaError({ type: LYLA_ERROR.NETWORK, message: 'Network error', detail, error: undefined, response: undefined, context: _options.context, requestOptions: _options }, stack); handleResponseError(networkError); }, onDownloadProgress: onDownloadProgress ? (progress) => { onDownloadProgress(Object.assign(Object.assign({}, progress), { requestOptions: _options })); } : undefined, onUploadProgress: onUploadProgress ? (progress) => { onUploadProgress(Object.assign(Object.assign({}, progress), { requestOptions: _options })); } : undefined, onHeadersReceived: onHeadersReceived ? (_headers, originalRequest) => { if (aborted) return; if (hasNetworkError) return; if (onHeadersReceived) { const headers = mergeHeaders({}, _headers); try { for (const hook of onHeadersReceived) { hook({ headers, requestOptions: _options, originalRequest }, _customReject); } } catch (error) { const brokenOnHeadersReceived = defineLylaError({ type: LYLA_ERROR.BROKEN_ON_HEADERS_RECEIVED, message: '`onHeadersReceived` hook throws error', detail: undefined, response: undefined, error, context: _options.context, requestOptions: _options }, undefined); handleNonResponseError(brokenOnHeadersReceived, true); return; } } } : undefined, async onResponse(resp, detail) { var _a; if (aborted) return; if (hasNetworkError) return; stopListeningAbortSignal(); let _json; let _jsonIsSet = false; let _cachedJson; let _cachedJsonParsingError; const statusText = getStatusText(resp.status); let response = { context: _options.context, requestOptions: _options, status: resp.status, statusText, headers: mergeHeaders({}, resp.headers), body: resp.body, detail, set json(value) { _jsonIsSet = true; _json = value; }, get json() { if (_jsonIsSet) return _json; if (responseType !== 'text') { const dataConversionError = defineLylaError({ type: LYLA_ERROR.INVALID_CONVERSION, message: `Can not convert ${responseType} to JSON`, detail: undefined, error: undefined, response, context: response.context, requestOptions: _options }, undefined); handleNonResponseError(dataConversionError, false); throw dataConversionError; } if (_cachedJson === undefined) { try { return (_cachedJson = JSON.parse(resp.body)); } catch (e) { _cachedJsonParsingError = e; } } else { return _cachedJson; } if (_cachedJsonParsingError) { const dataConversionError = defineLylaError({ type: LYLA_ERROR.INVALID_JSON, message: _cachedJsonParsingError.message, detail: undefined, error: _cachedJsonParsingError, context: response.context, response, requestOptions: _options }, undefined); handleNonResponseError(dataConversionError, false); throw dataConversionError; } } }; if (!isOkStatus(resp.status)) { const reason = `${resp.status} ${statusText}`; const httpError = defineLylaError({ type: LYLA_ERROR.HTTP, message: `Request failed with ${reason}`, detail: undefined, error: undefined, response, context: _options.context, requestOptions: _options }, stack); handleResponseError(httpError); return; } if ((_a = _options.hooks) === null || _a === void 0 ? void 0 : _a.onAfterResponse) { try { for (const hook of _options.hooks.onAfterResponse) { const maybeResponsePromise = hook(response, _customReject); if (maybeResponsePromise instanceof Promise) { response = await maybeResponsePromise; } else { response = maybeResponsePromise; } } } catch (error) { const brokenOnAfterResponseErrorError = defineLylaError({ type: LYLA_ERROR.BROKEN_ON_AFTER_RESPONSE, message: '`onAfterResponse` hook throws error', detail: undefined, response, error, context: response.context, requestOptions: _options }, undefined); handleNonResponseError(brokenOnAfterResponseErrorError, true); return; } } _resolve(response); } }); if (timeout) { setTimeout(() => { if (settled) return; adapterHandle.abort(); aborted = true; const timeoutError = defineLylaError({ type: LYLA_ERROR.TIMEOUT, message: timeout ? `Timeout of ${timeout}ms exceeded` : 'Timeout exceeded', detail: undefined, error: undefined, response: undefined, context: _options.context, requestOptions: _options }, stack); handleResponseError(timeoutError); }, timeout); } if (!_options.allowGetBody && method === 'GET' && body) { const badRequestError = defineLylaError({ type: LYLA_ERROR.BAD_REQUEST, message: "Can not send a request with body in 'GET' method.", error: undefined, response: undefined, detail: undefined, context: _options.context, requestOptions: _options }, undefined); handleNonResponseError(badRequestError, true); throw badRequestError; } return requestPromise; } function createRequestShortcut(method) { return (url, options) => { return request(Object.assign(Object.assign({}, options), { method, url })); }; } const lyla = Object.assign(request, { get: createRequestShortcut('get'), post: createRequestShortcut('post'), put: createRequestShortcut('put'), patch: createRequestShortcut('patch'), head: createRequestShortcut('head'), delete: createRequestShortcut('delete'), options: createRequestShortcut('options'), trace: createRequestShortcut('trace'), connect: createRequestShortcut('connect'), get errorType() { return {}; }, withRetry: null }); const withRetry = ({ onResolved, onRejected, createState }) => { async function requestWithRetry(options) { const state = createState(); let retryOptionsResolver = undefined; const makeBrokenRetryError = (error) => defineLylaError({ type: LYLA_ERROR.BROKEN_RETRY, error: error, message: 'Retry process throws an unexpected error', detail: undefined, response: undefined, context: undefined, isRetryError: true, requestOptions: options }, undefined); const makeRetryRejectedError = (error) => defineLylaError({ type: LYLA_ERROR.RETRY_REJECTED_BY_NON_LYLA_ERROR, error: error, message: 'Retry process is rejected by user with an non-lyla error', detail: undefined, response: undefined, context: undefined, isRetryError: true, requestOptions: options }, undefined); while (true) { let response; let finalOptions; // options resolving throws an error try { finalOptions = retryOptionsResolver ? await retryOptionsResolver() : options; } catch (e) { throw makeBrokenRetryError(e); } try { response = await request(finalOptions); } catch (e) { let rejected; // onRejected throws an error try { rejected = await onRejected({ options: finalOptions, state, lyla, // The error can be a lyla error, or a custom error thrown by user // in lyla hooks. error: e }); } catch (e) { throw makeBrokenRetryError(e); } // expected error switch (rejected.action) { case 'reject': if (_isLylaError(rejected.value)) { throw rejected.value; } else { throw makeRetryRejectedError(rejected.value); } case 'retry': retryOptionsResolver = rejected.value; continue; } } let resolved; try { resolved = await onResolved({ options: finalOptions, response, state }); } catch (e) { throw makeBrokenRetryError(e); } // expected error switch (resolved.action) { case 'resolve': return resolved.value; case 'reject': if (_isLylaError(resolved.value)) { throw resolved.value; } else { throw makeRetryRejectedError(resolved.value); } case 'retry': retryOptionsResolver = resolved.value; continue; } } } function createRequestWithRetryShortcut(method) { return (url, options) => { return requestWithRetry(Object.assign(Object.assign({}, options), { method, url })); }; } return Object.assign(requestWithRetry, { get: createRequestWithRetryShortcut('get'), post: createRequestWithRetryShortcut('post'), put: createRequestWithRetryShortcut('put'), patch: createRequestWithRetryShortcut('patch'), head: createRequestWithRetryShortcut('head'), delete: createRequestWithRetryShortcut('delete'), options: createRequestWithRetryShortcut('options'), trace: createRequestWithRetryShortcut('trace'), connect: createRequestWithRetryShortcut('connect'), get errorType() { return {}; }, withRetry() { throw new Error("Can't call `withRetry` on a lyla instance with created by `withRetry`"); } }); }; lyla.withRetry = withRetry; return { lyla, isLylaError(e) { return _isLylaError(e); }, isLylaErrorWithRetry(e) { return _isLylaError(e); } }; }