@lylajs/core
Version:
630 lines (629 loc) • 27 kB
JavaScript
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);
}
};
}