create-bablojstest
Version:
A lightweight, fast, and scalable Single Page Application framework built with vanilla JavaScript. BABLOJS provides React-like features including Virtual DOM, hooks, routing, and component-based architecture without any build step or external dependencies
646 lines (561 loc) • 19.5 kB
JavaScript
/**
* BabloHttp - A powerful, feature-rich HTTP client library for BabloJS
* Better than axios with advanced features and improved performance.
*
* Features:
* - Request/Response interceptors
* - Request cancellation (AbortController)
* - Automatic retry with exponential backoff
* - Multiple response types (JSON, text, blob, arrayBuffer)
* - Progress tracking for upload/download
* - Base URL configuration
* - Query parameters builder
* - Comprehensive error handling
* - Instance-based client creation
* - Default configuration
*
* @module babloHttp
*/
/**
* Default configuration for HTTP client
*/
const DEFAULT_CONFIG = {
timeout: 30000,
retries: 0,
retryDelay: 1000,
retryCondition: (error) => error.status >= 500 || error.code === 'NETWORK_ERROR',
baseURL: '',
headers: {
'Content-Type': 'application/json',
},
responseType: 'json', // json, text, blob, arraybuffer
validateStatus: (status) => status >= 200 && status < 300,
};
/**
* Parse query parameters from object to string
* @param {Object} params - Query parameters object
* @returns {string} - Query string
*/
function buildQueryString(params) {
if (!params || typeof params !== 'object') return '';
const query = new URLSearchParams();
Object.keys(params).forEach(key => {
const value = params[key];
if (value !== null && value !== undefined) {
if (Array.isArray(value)) {
value.forEach(v => query.append(key, v));
} else {
query.append(key, value);
}
}
});
const queryString = query.toString();
return queryString ? `?${queryString}` : '';
}
/**
* Build full URL from baseURL and path
* @param {string} baseURL - Base URL
* @param {string} url - Request URL
* @param {Object} params - Query parameters
* @returns {string} - Full URL
*/
function buildURL(baseURL, url, params) {
let fullURL = url;
// If URL is absolute, use it directly
if (url.startsWith('http://') || url.startsWith('https://')) {
fullURL = url;
} else if (baseURL) {
// Combine baseURL with path
const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
const path = url.startsWith('/') ? url : `/${url}`;
fullURL = `${base}${path}`;
}
// Add query parameters
if (params) {
const queryString = buildQueryString(params);
fullURL += queryString;
}
return fullURL;
}
/**
* Parse response based on responseType
* @param {XMLHttpRequest} xhr - XMLHttpRequest instance
* @param {string} responseType - Response type
* @returns {*} - Parsed response
*/
function parseResponse(xhr, responseType) {
switch (responseType) {
case 'json':
try {
const text = xhr.responseText || xhr.response;
return text ? JSON.parse(text) : null;
} catch (error) {
throw new Error(`Failed to parse JSON response: ${error.message}`);
}
case 'text':
return xhr.responseText || xhr.response;
case 'blob':
return new Blob([xhr.response], { type: xhr.getResponseHeader('Content-Type') });
case 'arraybuffer':
return xhr.response;
default:
return xhr.responseText || xhr.response;
}
}
/**
* Get all response headers as an object
* @param {XMLHttpRequest} xhr - XMLHttpRequest instance
* @returns {Object} - Headers object
*/
function getAllHeaders(xhr) {
const headers = {};
const headerString = xhr.getAllResponseHeaders();
if (!headerString) return headers;
const headerPairs = headerString.trim().split('\r\n');
headerPairs.forEach(headerPair => {
const [key, value] = headerPair.split(': ');
if (key && value) {
headers[key.toLowerCase()] = value;
}
});
return headers;
}
/**
* BabloHttp Client Class
*/
class BabloHttpClient {
constructor(config = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.interceptors = {
request: [],
response: [],
};
}
/**
* Add request interceptor
* @param {Function} fulfilled - Function to call when request is successful
* @param {Function} rejected - Function to call when request fails
* @returns {number} - Interceptor ID
*/
interceptRequest(fulfilled, rejected) {
const id = this.interceptors.request.length;
this.interceptors.request.push({ fulfilled, rejected });
return id;
}
/**
* Add response interceptor
* @param {Function} fulfilled - Function to call when response is successful
* @param {Function} rejected - Function to call when response fails
* @returns {number} - Interceptor ID
*/
interceptResponse(fulfilled, rejected) {
const id = this.interceptors.response.length;
this.interceptors.response.push({ fulfilled, rejected });
return id;
}
/**
* Remove interceptor
* @param {string} type - 'request' or 'response'
* @param {number} id - Interceptor ID
*/
ejectInterceptor(type, id) {
if (this.interceptors[type] && this.interceptors[type][id]) {
this.interceptors[type][id] = null;
}
}
/**
* Run request interceptors
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Processed configuration
*/
async runRequestInterceptors(config) {
let processedConfig = config;
for (const interceptor of this.interceptors.request) {
if (!interceptor) continue;
try {
if (interceptor.fulfilled) {
processedConfig = await interceptor.fulfilled(processedConfig);
}
} catch (error) {
if (interceptor.rejected) {
return await interceptor.rejected(error);
}
throw error;
}
}
return processedConfig;
}
/**
* Run response interceptors
* @param {Object} response - Response object
* @returns {Promise<Object>} - Processed response
*/
async runResponseInterceptors(response) {
let processedResponse = response;
for (const interceptor of this.interceptors.response) {
if (!interceptor) continue;
try {
if (interceptor.fulfilled) {
processedResponse = await interceptor.fulfilled(processedResponse);
}
} catch (error) {
if (interceptor.rejected) {
return await interceptor.rejected(error);
}
throw error;
}
}
return processedResponse;
}
/**
* Sleep function for retry delays
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Make HTTP request with retry logic
* @param {Object} config - Request configuration
* @param {number} attempt - Current attempt number
* @returns {Promise<Object>} - Response object
*/
async request(config, attempt = 0) {
// Merge config with defaults
const finalConfig = {
...this.config,
...config,
headers: {
...this.config.headers,
...config.headers,
},
};
// Run request interceptors
const processedConfig = await this.runRequestInterceptors(finalConfig);
return new Promise((resolve, reject) => {
// Create AbortController for cancellation
const abortController = new AbortController();
const signal = abortController.signal;
// Store abort function in config for external access
if (processedConfig.signal) {
processedConfig.signal.addEventListener('abort', () => {
abortController.abort();
});
}
// Build full URL
const fullURL = buildURL(
processedConfig.baseURL || this.config.baseURL,
processedConfig.url,
processedConfig.params
);
// Create XMLHttpRequest
const xhr = new XMLHttpRequest();
// Handle cancellation
signal.addEventListener('abort', () => {
xhr.abort();
});
// Open request
xhr.open(processedConfig.method || 'GET', fullURL, true);
// Set response type
if (processedConfig.responseType === 'blob') {
xhr.responseType = 'blob';
} else if (processedConfig.responseType === 'arraybuffer') {
xhr.responseType = 'arraybuffer';
}
// Set custom headers
const headers = processedConfig.headers || {};
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
// Set timeout
xhr.timeout = processedConfig.timeout || this.config.timeout;
// Handle progress if callback is provided
if (processedConfig.onProgress) {
xhr.upload.onprogress = (event) => {
if (event.lengthComputable && !signal.aborted) {
processedConfig.onProgress({
type: 'upload',
loaded: event.loaded,
total: event.total,
progress: (event.loaded / event.total) * 100,
});
}
};
xhr.onprogress = (event) => {
if (event.lengthComputable && !signal.aborted) {
processedConfig.onProgress({
type: 'download',
loaded: event.loaded,
total: event.total,
progress: (event.loaded / event.total) * 100,
});
}
};
}
// Handle response
xhr.onload = async () => {
if (signal.aborted) return;
try {
const responseData = parseResponse(xhr, processedConfig.responseType || 'json');
const responseHeaders = getAllHeaders(xhr);
const response = {
data: responseData,
status: xhr.status,
statusText: xhr.statusText,
headers: responseHeaders,
config: processedConfig,
request: xhr,
};
// Validate status
const isValidStatus = (processedConfig.validateStatus || this.config.validateStatus)(xhr.status);
if (isValidStatus) {
// Run response interceptors
const processedResponse = await this.runResponseInterceptors(response);
// Call success hooks
if (processedConfig.onSuccess) {
processedConfig.onSuccess(processedResponse);
}
resolve(processedResponse);
} else {
const error = {
message: `Request failed with status code ${xhr.status}`,
code: 'HTTP_ERROR',
status: xhr.status,
statusText: xhr.statusText,
response: response,
config: processedConfig,
request: xhr,
};
// Call error hooks
if (processedConfig.onError) {
processedConfig.onError(error);
}
// Check if we should retry
const shouldRetry = processedConfig.retries > attempt &&
(processedConfig.retryCondition || this.config.retryCondition)(error);
if (shouldRetry) {
const delay = processedConfig.retryDelay || this.config.retryDelay;
await this.sleep(delay * (attempt + 1)); // Exponential backoff
return this.request(config, attempt + 1).then(resolve).catch(reject);
}
reject(error);
}
} catch (error) {
const parseError = {
message: error.message || 'Failed to parse response',
code: 'PARSE_ERROR',
config: processedConfig,
request: xhr,
};
if (processedConfig.onError) {
processedConfig.onError(parseError);
}
reject(parseError);
} finally {
if (processedConfig.onComplete) {
processedConfig.onComplete();
}
}
};
// Handle network errors
xhr.onerror = async () => {
if (signal.aborted) return;
const networkError = {
message: 'Network Error',
code: 'NETWORK_ERROR',
config: processedConfig,
request: xhr,
};
if (processedConfig.onError) {
processedConfig.onError(networkError);
}
// Check if we should retry
const shouldRetry = processedConfig.retries > attempt &&
(processedConfig.retryCondition || this.config.retryCondition)(networkError);
if (shouldRetry) {
const delay = processedConfig.retryDelay || this.config.retryDelay;
await this.sleep(delay * (attempt + 1)); // Exponential backoff
return this.request(config, attempt + 1).then(resolve).catch(reject);
}
reject(networkError);
};
// Handle timeout
xhr.ontimeout = async () => {
if (signal.aborted) return;
const timeoutError = {
message: `Timeout of ${processedConfig.timeout}ms exceeded`,
code: 'TIMEOUT_ERROR',
config: processedConfig,
request: xhr,
};
if (processedConfig.onError) {
processedConfig.onError(timeoutError);
}
// Check if we should retry
const shouldRetry = processedConfig.retries > attempt &&
(processedConfig.retryCondition || this.config.retryCondition)(timeoutError);
if (shouldRetry) {
const delay = processedConfig.retryDelay || this.config.retryDelay;
await this.sleep(delay * (attempt + 1)); // Exponential backoff
return this.request(config, attempt + 1).then(resolve).catch(reject);
}
reject(timeoutError);
};
// Handle abort
xhr.onabort = () => {
const abortError = {
message: 'Request aborted',
code: 'ABORT_ERROR',
config: processedConfig,
request: xhr,
};
if (processedConfig.onError) {
processedConfig.onError(abortError);
}
reject(abortError);
};
// Send request
try {
if (processedConfig.beforeSend) {
processedConfig.beforeSend();
}
let requestBody = null;
if (processedConfig.data || processedConfig.body) {
const body = processedConfig.data || processedConfig.body;
if (typeof body === 'string') {
requestBody = body;
} else if (body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer) {
requestBody = body;
} else {
requestBody = JSON.stringify(body);
}
}
xhr.send(requestBody);
} catch (error) {
const requestError = {
message: error.message || 'Failed to send request',
code: 'REQUEST_ERROR',
config: processedConfig,
error: error,
};
if (processedConfig.onError) {
processedConfig.onError(requestError);
}
reject(requestError);
}
});
}
/**
* GET request
* @param {string} url - Request URL
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
get(url, config = {}) {
return this.request({ ...config, url, method: 'GET' });
}
/**
* POST request
* @param {string} url - Request URL
* @param {*} data - Request data
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
post(url, data, config = {}) {
return this.request({ ...config, url, method: 'POST', data });
}
/**
* PUT request
* @param {string} url - Request URL
* @param {*} data - Request data
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
put(url, data, config = {}) {
return this.request({ ...config, url, method: 'PUT', data });
}
/**
* PATCH request
* @param {string} url - Request URL
* @param {*} data - Request data
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
patch(url, data, config = {}) {
return this.request({ ...config, url, method: 'PATCH', data });
}
/**
* DELETE request
* @param {string} url - Request URL
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
delete(url, config = {}) {
return this.request({ ...config, url, method: 'DELETE' });
}
/**
* HEAD request
* @param {string} url - Request URL
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
head(url, config = {}) {
return this.request({ ...config, url, method: 'HEAD' });
}
/**
* OPTIONS request
* @param {string} url - Request URL
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object
*/
options(url, config = {}) {
return this.request({ ...config, url, method: 'OPTIONS' });
}
}
/**
* Create a new BabloHttp client instance
* @param {Object} config - Default configuration
* @returns {BabloHttpClient} - BabloHttp client instance
*/
export function createBabloHttp(config = {}) {
return new BabloHttpClient(config);
}
/**
* Default BabloHttp client instance
*/
const defaultClient = new BabloHttpClient();
/**
* Request function (backward compatible)
* @param {Object} config - Request configuration
* @returns {Promise<Object>} - Response object (returns data directly for backward compatibility)
*/
export async function babloRequest(config) {
const response = await defaultClient.request(config);
// Return data directly for backward compatibility
return response.data !== undefined ? response.data : response;
}
/**
* Export convenience methods from default client
*/
export const babloHttp = {
request: (config) => defaultClient.request(config),
get: (url, config) => defaultClient.get(url, config).then(r => r.data),
post: (url, data, config) => defaultClient.post(url, data, config).then(r => r.data),
put: (url, data, config) => defaultClient.put(url, data, config).then(r => r.data),
patch: (url, data, config) => defaultClient.patch(url, data, config).then(r => r.data),
delete: (url, config) => defaultClient.delete(url, config).then(r => r.data),
head: (url, config) => defaultClient.head(url, config).then(r => r.data),
options: (url, config) => defaultClient.options(url, config).then(r => r.data),
create: createBabloHttp,
interceptRequest: (fulfilled, rejected) => defaultClient.interceptRequest(fulfilled, rejected),
interceptResponse: (fulfilled, rejected) => defaultClient.interceptResponse(fulfilled, rejected),
ejectInterceptor: (type, id) => defaultClient.ejectInterceptor(type, id),
get interceptors() {
return defaultClient.interceptors;
},
};
/**
* Export default BabloHttp client instance
*/
export default babloHttp;