ng-http-spring
Version:
A Spring Boot-like HTTP library for Angular with decorators, caching, request queuing, file upload, WebSocket, and GraphQL support
1,315 lines (1,299 loc) • 54.8 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, inject, NgModule } from '@angular/core';
import { retry, timer, of, throwError, from, lastValueFrom, Subject, Observable, merge, fromEvent, BehaviorSubject } from 'rxjs';
import { tap, catchError, finalize, map, concatMap, startWith, retryWhen, delay } from 'rxjs/operators';
import * as i1 from '@angular/common/http';
import { HttpHeaders, HttpParams, HttpEventType, HttpErrorResponse, HttpRequest, HttpClient, HTTP_INTERCEPTORS, HttpClientModule, provideHttpClient, withInterceptors } from '@angular/common/http';
import { v4 } from 'uuid';
import { openDB } from 'idb';
import { webSocket } from 'rxjs/webSocket';
import 'reflect-metadata';
class HttpService {
http;
constructor(http) {
this.http = http;
}
executeRequest(config) {
let headers = new HttpHeaders();
let params = new HttpParams();
// Add config headers
if (config.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
headers = headers.set(key, String(value));
});
}
// Add config params
if (config.params) {
Object.entries(config.params).forEach(([key, value]) => {
params = params.set(key, String(value));
});
}
const method = config.method?.toUpperCase() || 'GET';
const options = {
headers,
params,
responseType: config.responseType || 'json',
withCredentials: config.withCredentials,
observe: config.observe || 'body'
}; // Cast to any to avoid type issues with observe property
let request = this.createRequest(method, config, options);
if (config.retryConfig) {
const retryConfig = config.retryConfig;
request = request.pipe(retry({
count: retryConfig.maxRetries,
delay: (error) => {
if (retryConfig.retryCondition && !retryConfig.retryCondition(error)) {
throw error;
}
return timer(retryConfig.delayMs);
}
}));
}
return request;
}
createRequest(method, config, options) {
switch (method) {
case 'GET':
return this.http.get(config.url, options);
case 'POST':
return this.http.post(config.url, config.body, options);
case 'PUT':
return this.http.put(config.url, config.body, options);
case 'DELETE':
return this.http.delete(config.url, options);
case 'PATCH':
return this.http.patch(config.url, config.body, options);
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: HttpService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: HttpService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: HttpService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i1.HttpClient }] });
class MemoryCacheService {
cache = new Map();
get(key) {
const entry = this.cache.get(key);
if (!entry)
return undefined;
if (entry.expiry && entry.expiry < Date.now()) {
this.cache.delete(key);
return undefined;
}
return entry;
}
set(key, entry) {
this.cache.set(key, entry);
}
delete(key) {
this.cache.delete(key);
}
clear(group) {
if (group) {
for (const [key, entry] of this.cache.entries()) {
if (entry.group === group) {
this.cache.delete(key);
}
}
}
else {
this.cache.clear();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: MemoryCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: MemoryCacheService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: MemoryCacheService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
function ensureParamMetadataArray(target, key, metadataKey) {
const existingMetadata = Reflect.getMetadata(metadataKey, target, key) || [];
return Array.isArray(existingMetadata) ? existingMetadata : [];
}
function PathVariable(paramName) {
return function (target, propertyKey, parameterIndex) {
const pathParams = ensureParamMetadataArray(target, propertyKey, "path:params" /* MetadataKeys.PATH_PARAMS */);
pathParams.push({ paramName, index: parameterIndex });
Reflect.defineMetadata("path:params" /* MetadataKeys.PATH_PARAMS */, pathParams, target, propertyKey);
};
}
function RequestParam(paramName) {
return function (target, propertyKey, parameterIndex) {
const queryParams = ensureParamMetadataArray(target, propertyKey, "query:params" /* MetadataKeys.QUERY_PARAMS */);
queryParams.push({ paramName, index: parameterIndex });
Reflect.defineMetadata("query:params" /* MetadataKeys.QUERY_PARAMS */, queryParams, target, propertyKey);
};
}
function RequestBody() {
return function (target, propertyKey, parameterIndex) {
Reflect.defineMetadata("body:param" /* MetadataKeys.BODY_PARAM */, parameterIndex, target, propertyKey);
};
}
function Header(headerName) {
return function (target, propertyKey, parameterIndex) {
const headerParams = ensureParamMetadataArray(target, propertyKey, "header:params" /* MetadataKeys.HEADER_PARAMS */);
headerParams.push({ paramName: headerName, index: parameterIndex });
Reflect.defineMetadata("header:params" /* MetadataKeys.HEADER_PARAMS */, headerParams, target, propertyKey);
};
}
function processMetadata(target, propertyKey) {
return Reflect.getMetadata('requestMetadata', target, propertyKey) || {};
}
function processRequestConfig(path, target, propertyKey, args) {
const config = {
url: path,
headers: {},
params: {}
};
// Path variables
const pathParams = Reflect.getMetadata('pathParams', target, propertyKey) || [];
pathParams.forEach((param) => {
config.url = config.url.replace(`{${param.paramName}}`, String(args[param.index]));
});
// Query parameters
const queryParams = Reflect.getMetadata('queryParams', target, propertyKey) || [];
queryParams.forEach((param) => {
if (args[param.index] !== undefined) {
config.params[param.paramName] = String(args[param.index]);
}
});
return config;
}
function handleCacheResponse(response, cacheKey, cacheConfig, cacheService) {
if (!cacheConfig || !cacheService)
return;
cacheService.set(cacheKey, {
data: response,
expiry: cacheConfig.ttl ? Date.now() + cacheConfig.ttl : 0,
group: cacheConfig.group
});
}
function handleError(error, errorConfig) {
if (errorConfig?.handler) {
errorConfig.handler.handleError(error);
}
if (errorConfig?.transform) {
return of(errorConfig.transform(error));
}
throw error;
}
function checkCache(cacheConfig, cacheKey, cacheService) {
if (!cacheConfig)
return undefined;
const cached = cacheService.get(cacheKey);
if (cached && (!cached.expiry || cached.expiry > Date.now())) {
return of(cached.data);
}
return undefined;
}
function buildConfig(path, method, args, target, propertyKey, metadata) {
const requestConfig = processRequestConfig(path, target, propertyKey, args);
return {
...requestConfig,
method,
...metadata.config,
body: method !== 'GET' && method !== 'DELETE' ? args[0] : undefined,
headers: { ...requestConfig.headers, ...metadata.config?.headers },
params: { ...requestConfig.params, ...metadata.config?.params }
};
}
function createHttpDecorator(method) {
return (path, config) => {
return function (target, propertyKey, descriptor) {
const metadata = {
path,
method,
cache: config?.cache,
error: config?.error,
config
};
Reflect.defineMetadata('requestMetadata', metadata, target, propertyKey);
descriptor.value = function (...args) {
const httpService = inject(HttpService);
const cacheService = inject(MemoryCacheService);
const metadata = processMetadata(target, propertyKey);
const cacheKey = metadata.cache?.key || `${path}-${JSON.stringify(args)}`;
const cachedResult = checkCache(metadata.cache, cacheKey, cacheService);
if (cachedResult)
return cachedResult;
const finalConfig = buildConfig(path, method, args, target, propertyKey, metadata);
return httpService.executeRequest(finalConfig).pipe(tap(response => handleCacheResponse(response, cacheKey, metadata.cache, cacheService)), catchError(error => handleError(error, metadata.error)));
};
return descriptor;
};
};
}
const Get = createHttpDecorator('GET');
const Post = createHttpDecorator('POST');
const Put = createHttpDecorator('PUT');
const Delete = createHttpDecorator('DELETE');
const Patch = createHttpDecorator('PATCH');
var LogLevel;
(function (LogLevel) {
LogLevel["DEBUG"] = "DEBUG";
LogLevel["INFO"] = "INFO";
LogLevel["WARN"] = "WARN";
LogLevel["ERROR"] = "ERROR";
})(LogLevel || (LogLevel = {}));
class LoggingInterceptor {
config = {
level: LogLevel.INFO,
maxBodySize: 1024 * 10, // 10KB
includeHeaders: true,
includeTiming: true,
includeMetrics: true
};
metrics = new Map();
setConfig(config) {
this.config = { ...this.config, ...config };
}
intercept(request, next) {
const startTime = Date.now();
const requestId = v4();
const requestSize = this.calculateRequestSize(request);
this.log(LogLevel.INFO, `[HTTP] Request ${requestId} - ${request.method} ${request.url} started`, {
requestId,
method: request.method,
url: request.url,
headers: this.config.includeHeaders ? request.headers.keys().reduce((acc, key) => ({
...acc,
[key]: request.headers.getAll(key)
}), {}) : undefined,
body: this.truncateBody(request.body),
size: requestSize
});
return next.handle(request).pipe(tap({
next: (event) => {
if (event.type === HttpEventType.Response) {
const response = event;
const duration = Date.now() - startTime;
const responseSize = this.calculateResponseSize(response);
this.updateMetrics(request.url, duration, responseSize, false);
this.log(LogLevel.INFO, `[HTTP] Request ${requestId} completed`, {
requestId,
duration,
status: response.status,
headers: this.config.includeHeaders ? response.headers.keys().reduce((acc, key) => ({
...acc,
[key]: response.headers.getAll(key)
}), {}) : undefined,
body: this.truncateBody(response.body),
size: responseSize
});
}
},
error: (error) => {
const duration = Date.now() - startTime;
this.updateMetrics(request.url, duration, 0, true);
this.log(LogLevel.ERROR, `[HTTP] Request ${requestId} failed`, {
requestId,
duration,
error: error.message,
stack: error.stack
});
}
}), finalize(() => {
if (this.config.includeMetrics) {
this.logMetrics(request.url);
}
}));
}
calculateRequestSize(request) {
let size = 0;
size += request.url.length;
size += JSON.stringify(request.body || '').length;
request.headers.keys().forEach(key => {
size += key.length;
size += request.headers.getAll(key)?.join(',').length || 0;
});
return size;
}
calculateResponseSize(response) {
let size = 0;
size += JSON.stringify(response.body || '').length;
response.headers.keys().forEach(key => {
size += key.length;
size += response.headers.getAll(key)?.join(',').length || 0;
});
return size;
}
truncateBody(body) {
if (!body)
return body;
const stringified = JSON.stringify(body);
if (stringified.length <= this.config.maxBodySize) {
return body;
}
return stringified.substring(0, this.config.maxBodySize) + '... (truncated)';
}
updateMetrics(url, duration, responseSize, isFailure) {
const current = this.metrics.get(url) || {
count: 0,
totalDuration: 0,
failures: 0,
avgResponseSize: 0
};
current.count++;
current.totalDuration += duration;
if (isFailure)
current.failures++;
current.avgResponseSize = (current.avgResponseSize * (current.count - 1) + responseSize) / current.count;
this.metrics.set(url, current);
}
logMetrics(url) {
const metrics = this.metrics.get(url);
if (metrics) {
this.log(LogLevel.DEBUG, '[HTTP] Metrics', {
url,
totalRequests: metrics.count,
avgDuration: metrics.totalDuration / metrics.count,
failureRate: (metrics.failures / metrics.count) * 100,
avgResponseSize: metrics.avgResponseSize
});
}
}
log(level, message, data) {
if (this.isLevelEnabled(level)) {
const formattedMessage = this.config.formatter
? this.config.formatter(message, level, data)
: `${new Date().toISOString()} ${level} ${message} ${data ? JSON.stringify(data) : ''}`;
switch (level) {
case LogLevel.DEBUG:
console.debug(formattedMessage);
break;
case LogLevel.INFO:
console.info(formattedMessage);
break;
case LogLevel.WARN:
console.warn(formattedMessage);
break;
case LogLevel.ERROR:
console.error(formattedMessage);
break;
}
}
}
isLevelEnabled(level) {
const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];
return levels.indexOf(level) >= levels.indexOf(this.config.level);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LoggingInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LoggingInterceptor });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LoggingInterceptor, decorators: [{
type: Injectable
}] });
class HttpError extends Error {
status;
message;
error;
path;
constructor(status, message, error, path) {
super(message);
this.status = status;
this.message = message;
this.error = error;
this.path = path;
this.name = 'HttpError';
}
static from(error) {
return new HttpError(error.status, error.message, error.error, error.url || 'unknown');
}
}
class ErrorInterceptor {
intercept(request, next) {
return next.handle(request).pipe(catchError((error) => {
return throwError(() => HttpError.from(error));
}));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ErrorInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ErrorInterceptor });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ErrorInterceptor, decorators: [{
type: Injectable
}] });
class AuthInterceptor {
isRefreshing = false;
authConfig;
constructor() {
// Default config, should be overridden
this.authConfig = {
tokenProvider: () => '',
headerName: 'Authorization',
scheme: 'Bearer'
};
}
setConfig(config) {
this.authConfig = { ...this.authConfig, ...config };
}
intercept(request, next) {
return from(this.handleRequest(request, next));
}
async handleRequest(request, next) {
try {
const token = await this.authConfig.tokenProvider();
const authReq = this.addAuthHeader(request, token);
return await lastValueFrom(next.handle(authReq));
}
catch (error) {
if (error instanceof HttpErrorResponse && error.status === 401 && this.authConfig.refreshToken) {
return await this.handleAuthError(request, next, error);
}
throw error;
}
}
async handleAuthError(request, next, error) {
if (!this.isRefreshing && this.authConfig.refreshToken) {
this.isRefreshing = true;
try {
const newToken = await this.authConfig.refreshToken();
this.isRefreshing = false;
const authReq = this.addAuthHeader(request, newToken);
return await lastValueFrom(next.handle(authReq));
}
catch (refreshError) {
this.isRefreshing = false;
if (this.authConfig.onAuthError) {
this.authConfig.onAuthError(refreshError);
}
throw refreshError;
}
}
throw error;
}
addAuthHeader(request, token) {
return request.clone({
headers: request.headers.set(this.authConfig.headerName, `${this.authConfig.scheme} ${token}`)
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: AuthInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: AuthInterceptor });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: AuthInterceptor, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
const loggingInterceptor = (req, next) => {
const handler = { handle: next };
return inject(LoggingInterceptor).intercept(req, handler);
};
const errorInterceptor = (req, next) => {
const handler = { handle: next };
return inject(ErrorInterceptor).intercept(req, handler);
};
const authInterceptor = (req, next) => {
const handler = { handle: next };
return inject(AuthInterceptor).intercept(req, handler);
};
class FileUploadService {
http;
DEFAULT_CHUNK_SIZE = 1024 * 1024; // 1MB
activeUploads = new Map();
constructor(http) {
this.http = http;
}
upload(config) {
// Validate file
if (!this.validateFile(config.file, config.allowedTypes, config.maxSize)) {
return throwError(() => new Error('Invalid file type or size'));
}
const uploadId = Math.random().toString(36).substring(7);
const progress = new Subject();
this.activeUploads.set(uploadId, progress);
if (config.chunkSize && config.file.size > config.chunkSize) {
this.uploadInChunks(config, progress);
}
else {
this.uploadFile(config, progress);
}
return progress.asObservable();
}
pauseUpload(uploadId) {
const progress = this.activeUploads.get(uploadId);
if (progress) {
progress.next({
loaded: 0,
total: 0,
percentage: 0,
status: 'paused'
});
}
}
resumeUpload(uploadId) {
// Resume logic would go here
}
cancelUpload(uploadId) {
const progress = this.activeUploads.get(uploadId);
if (progress) {
progress.complete();
this.activeUploads.delete(uploadId);
}
}
validateFile(file, allowedTypes, maxSize) {
if (allowedTypes && !allowedTypes.includes(file.type)) {
return false;
}
if (maxSize && file.size > maxSize) {
return false;
}
return true;
}
uploadFile(config, progress) {
const formData = new FormData();
formData.append('file', config.file);
if (config.metadata) {
formData.append('metadata', JSON.stringify(config.metadata));
}
const req = new HttpRequest('POST', config.url, formData, {
reportProgress: true,
withCredentials: config.withCredentials,
headers: new HttpHeaders(config.headers || {})
});
this.http.request(req).pipe(map(event => this.getProgress(event, config.file.size)), catchError(error => {
progress.next({
loaded: 0,
total: config.file.size,
percentage: 0,
status: 'failed'
});
return throwError(() => error);
})).subscribe(currentProgress => progress.next(currentProgress), error => progress.error(error), () => progress.complete());
}
uploadInChunks(config, progress) {
const chunkSize = config.chunkSize || this.DEFAULT_CHUNK_SIZE;
const totalChunks = Math.ceil(config.file.size / chunkSize);
const fileId = Math.random().toString(36).substring(7);
let uploadedChunks = 0;
let uploadedBytes = 0;
const uploadNextChunk = (chunkIndex) => {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, config.file.size);
const chunk = config.file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('metadata', JSON.stringify({
chunkIndex,
totalChunks,
fileId,
fileName: config.file.name,
fileSize: config.file.size,
mimeType: config.file.type
}));
const req = new HttpRequest('POST', config.url, formData, {
reportProgress: true,
withCredentials: config.withCredentials,
headers: new HttpHeaders(config.headers || {})
});
this.http.request(req).pipe(tap(event => {
if (event.type === HttpEventType.UploadProgress) {
uploadedBytes = (chunkIndex * chunkSize) + (event.loaded || 0);
progress.next({
loaded: uploadedBytes,
total: config.file.size,
percentage: Math.round((uploadedBytes * 100) / config.file.size),
status: 'uploading',
chunk: chunkIndex + 1,
totalChunks
});
}
}), catchError(error => {
progress.next({
loaded: uploadedBytes,
total: config.file.size,
percentage: Math.round((uploadedBytes * 100) / config.file.size),
status: 'failed',
chunk: chunkIndex + 1,
totalChunks
});
return throwError(() => error);
})).subscribe(event => {
if (event.type === HttpEventType.Response) {
uploadedChunks++;
if (uploadedChunks === totalChunks) {
// All chunks uploaded, complete the upload
progress.next({
loaded: config.file.size,
total: config.file.size,
percentage: 100,
status: 'completed',
chunk: totalChunks,
totalChunks
});
progress.complete();
}
else {
// Upload next chunk
uploadNextChunk(chunkIndex + 1);
}
}
}, error => progress.error(error));
};
// Start uploading chunks
uploadNextChunk(0);
}
getProgress(event, totalSize) {
switch (event.type) {
case HttpEventType.UploadProgress:
const loaded = event.loaded || 0;
return {
loaded,
total: totalSize,
percentage: Math.round((loaded * 100) / totalSize),
status: 'uploading'
};
case HttpEventType.Response:
return {
loaded: totalSize,
total: totalSize,
percentage: 100,
status: 'completed'
};
default:
return {
loaded: 0,
total: totalSize,
percentage: 0,
status: 'pending'
};
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: FileUploadService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: FileUploadService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: FileUploadService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i1.HttpClient }] });
class RequestQueueService {
config = {
maxConcurrent: 4,
priorityLevels: 3,
defaultPriority: 1,
throttleMs: 0,
retryDelayMs: 1000
};
queues = [];
activeRequests = new Set();
queueSubject = new Subject();
requestComplete = new Subject();
constructor() {
// Initialize priority queues
for (let i = 0; i < (this.config.priorityLevels || 1); i++) {
this.queues[i] = [];
}
// Process queue items
this.queueSubject.pipe(concatMap(request => {
if (this.activeRequests.size >= (this.config.maxConcurrent || 1)) {
return timer(100).pipe(tap(() => this.queueSubject.next(request)));
}
return this.processRequest(request);
})).subscribe();
// Handle completed requests
this.requestComplete.subscribe(id => {
this.activeRequests.delete(id);
this.processNextRequest();
});
}
setConfig(config) {
this.config = { ...this.config, ...config };
// Reinitialize queues if priority levels changed
if (config.priorityLevels && config.priorityLevels !== this.queues.length) {
this.queues = Array(config.priorityLevels).fill([]);
}
}
enqueue(request, execute, priority) {
const id = Math.random().toString(36).substring(7);
const queuedRequest = {
id,
request,
priority: priority ?? this.config.defaultPriority,
timestamp: Date.now(),
execute
};
const priorityIndex = Math.min(Math.max(0, queuedRequest.priority), this.queues.length - 1);
this.queues[priorityIndex].push(queuedRequest);
return new Observable(observer => {
this.processNextRequest();
const subscription = this.requestComplete
.pipe(tap(completedId => {
if (completedId === id) {
observer.complete();
}
}))
.subscribe();
return () => {
subscription.unsubscribe();
this.removeFromQueue(id);
};
});
}
processNextRequest() {
if (this.activeRequests.size >= (this.config.maxConcurrent || 1)) {
return;
}
const nextRequest = this.getNextRequest();
if (nextRequest) {
this.queueSubject.next(nextRequest);
}
}
processRequest(queuedRequest) {
this.activeRequests.add(queuedRequest.id);
return from(new Promise(resolve => {
if (this.config.throttleMs) {
setTimeout(resolve, this.config.throttleMs);
}
else {
resolve(true);
}
})).pipe(concatMap(() => queuedRequest.execute()), tap({
complete: () => this.requestComplete.next(queuedRequest.id),
error: () => this.requestComplete.next(queuedRequest.id)
}));
}
getNextRequest() {
for (const queue of this.queues) {
if (queue.length > 0) {
return queue.shift();
}
}
return undefined;
}
removeFromQueue(id) {
for (const queue of this.queues) {
const index = queue.findIndex(req => req.id === id);
if (index !== -1) {
queue.splice(index, 1);
break;
}
}
}
getQueueStatus() {
return {
activeRequests: this.activeRequests.size,
queueLengths: this.queues.map(q => q.length),
config: { ...this.config }
};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RequestQueueService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RequestQueueService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RequestQueueService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class ResponseTransformerService {
transform(data, config) {
if (!data)
return data;
let result = this.clone(data);
if (config.xmlToJson && typeof data === 'string' && data.trim().startsWith('<')) {
result = this.xmlToJson(data);
}
if (config.dateFields) {
result = this.transformDates(result, config.dateFields);
}
if (config.caseStyle) {
result = this.transformCase(result, config.caseStyle);
}
if (config.transforms) {
result = this.applyCustomTransforms(result, config.transforms);
}
if (config.removeFields) {
result = this.removeFields(result, config.removeFields);
}
if (config.addFields) {
result = this.addFields(result, config.addFields);
}
return result;
}
clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
transformDates(data, dateFields) {
if (Array.isArray(data)) {
return data.map(item => this.transformDates(item, dateFields));
}
if (data && typeof data === 'object') {
const result = { ...data };
for (const key in result) {
if (dateFields.includes(key) && typeof result[key] === 'string') {
result[key] = new Date(result[key]);
}
else if (typeof result[key] === 'object') {
result[key] = this.transformDates(result[key], dateFields);
}
}
return result;
}
return data;
}
transformCase(data, style) {
if (Array.isArray(data)) {
return data.map(item => this.transformCase(item, style));
}
if (data && typeof data === 'object') {
const result = {};
for (const key in data) {
const transformedKey = this.transformKey(key, style);
result[transformedKey] = typeof data[key] === 'object'
? this.transformCase(data[key], style)
: data[key];
}
return result;
}
return data;
}
transformKey(key, style) {
switch (style) {
case 'camelCase':
return key.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', '').replace('_', ''));
case 'snakeCase':
return key
.replace(/([A-Z])/g, '_$1')
.toLowerCase()
.replace(/^_/, '');
case 'kebabCase':
return key
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, '');
default:
return key;
}
}
applyCustomTransforms(data, transforms) {
if (Array.isArray(data)) {
return data.map(item => this.applyCustomTransforms(item, transforms));
}
if (data && typeof data === 'object') {
const result = { ...data };
for (const key in result) {
if (transforms[key]) {
result[key] = transforms[key](result[key]);
}
else if (typeof result[key] === 'object') {
result[key] = this.applyCustomTransforms(result[key], transforms);
}
}
return result;
}
return data;
}
removeFields(data, fields) {
if (Array.isArray(data)) {
return data.map(item => this.removeFields(item, fields));
}
if (data && typeof data === 'object') {
const result = { ...data };
fields.forEach(field => delete result[field]);
for (const key in result) {
if (typeof result[key] === 'object') {
result[key] = this.removeFields(result[key], fields);
}
}
return result;
}
return data;
}
addFields(data, fields) {
if (Array.isArray(data)) {
return data.map(item => this.addFields(item, fields));
}
if (data && typeof data === 'object') {
const result = { ...data };
for (const key in fields) {
result[key] = typeof fields[key] === 'function'
? fields[key](data)
: fields[key];
}
return result;
}
return data;
}
xmlToJson(xml) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xml, 'text/xml');
return this.xmlNodeToJson(xmlDoc.documentElement);
}
xmlNodeToJson(node) {
const obj = {};
if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue?.trim();
}
// Handle attributes
if (node.attributes) {
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes[i];
obj[`@${attr.nodeName}`] = attr.nodeValue;
}
}
// Handle child nodes
node.childNodes.forEach(child => {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.nodeValue?.trim();
if (text)
obj['#text'] = text;
}
else if (child.nodeType === Node.ELEMENT_NODE) {
const childElement = child;
const childObj = this.xmlNodeToJson(childElement);
if (obj[childElement.nodeName]) {
if (!Array.isArray(obj[childElement.nodeName])) {
obj[childElement.nodeName] = [obj[childElement.nodeName]];
}
obj[childElement.nodeName].push(childObj);
}
else {
obj[childElement.nodeName] = childObj;
}
}
});
return obj;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ResponseTransformerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ResponseTransformerService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ResponseTransformerService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class OfflineService {
db;
online$ = merge(of(navigator.onLine), fromEvent(window, 'online').pipe(map(() => true)), fromEvent(window, 'offline').pipe(map(() => false))).pipe(startWith(navigator.onLine));
config = {
dbName: 'ng-http-spring-offline',
maxRetries: 3,
retryDelay: 5000,
maxCacheAge: 24 * 60 * 60 * 1000, // 24 hours
syncStrategy: 'immediate',
syncInterval: 30000, // 30 seconds
priorityLevels: 3
};
syncInProgress = false;
syncSubject = new Subject();
sync$ = this.syncSubject.asObservable();
constructor() {
this.initializeDB();
this.setupSync();
}
setConfig(config) {
this.config = { ...this.config, ...config };
this.setupSync();
}
async initializeDB() {
this.db = await openDB(this.config.dbName, 1, {
upgrade(db) {
const requestStore = db.createObjectStore('requests', {
keyPath: 'id',
autoIncrement: true
});
requestStore.createIndex('by-timestamp', 'timestamp');
const responseStore = db.createObjectStore('responses', {
keyPath: 'url'
});
responseStore.createIndex('by-timestamp', 'timestamp');
}
});
}
setupSync() {
if (this.config.syncStrategy === 'periodic') {
setInterval(() => this.sync(), this.config.syncInterval);
}
this.online$.subscribe(isOnline => {
if (isOnline && this.config.syncStrategy === 'immediate') {
this.sync();
}
});
}
async queueRequest(request) {
const offlineRequest = {
...request,
timestamp: Date.now(),
retryCount: 0
};
await this.db.add('requests', offlineRequest);
if (navigator.onLine && this.config.syncStrategy === 'immediate') {
this.sync();
}
}
async cacheResponse(url, response) {
await this.db.put('responses', {
url,
data: response,
timestamp: Date.now()
});
}
async getCachedResponse(url) {
const cached = await this.db.get('responses', url);
if (!cached)
return null;
if (Date.now() - cached.timestamp > this.config.maxCacheAge) {
await this.db.delete('responses', url);
return null;
}
return cached.data;
}
async sync() {
if (this.syncInProgress || !navigator.onLine)
return;
this.syncInProgress = true;
try {
const requests = await this.db.getAllFromIndex('requests', 'by-timestamp');
// Sort by priority and timestamp
requests.sort((a, b) => {
if (a.priority !== b.priority) {
return b.priority - a.priority; // Higher priority first
}
return a.timestamp - b.timestamp; // Older first
});
for (const request of requests) {
try {
const response = await fetch(request.url, {
method: request.method,
body: request.body ? JSON.stringify(request.body) : undefined,
headers: request.headers
});
if (response.ok) {
await this.db.delete('requests', request.id);
const data = await response.json();
await this.cacheResponse(request.url, data);
}
else {
request.retryCount++;
if (request.retryCount >= this.config.maxRetries) {
await this.db.delete('requests', request.id);
}
else {
await this.db.put('requests', request);
}
}
}
catch (error) {
request.retryCount++;
if (request.retryCount >= this.config.maxRetries) {
await this.db.delete('requests', request.id);
}
else {
await this.db.put('requests', request);
}
}
// Add delay between retries
if (request.retryCount > 0) {
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay * request.retryCount));
}
}
this.syncSubject.next();
}
finally {
this.syncInProgress = false;
}
}
async getPendingRequests() {
return this.db.getAllFromIndex('requests', 'by-timestamp');
}
async clearCache() {
await this.db.clear('responses');
}
async clearPendingRequests() {
await this.db.clear('requests');
}
isOnline() {
return this.online$;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: OfflineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: OfflineService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: OfflineService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class WebSocketService {
config = {
url: '',
reconnectAttempts: 5,
reconnectInterval: 3000,
heartbeatInterval: 30000
};
wsSubject;
messagesSubject = new Subject();
statusSubject = new BehaviorSubject({
connected: false,
attempting: false,
attemptCount: 0
});
heartbeatTimer;
reconnectCount = 0;
messages$ = this.messagesSubject.asObservable();
status$ = this.statusSubject.asObservable();
connect(config) {
this.config = { ...this.config, ...config };
this.initializeWebSocket();
}
send(message) {
if (this.wsSubject && !this.wsSubject.closed) {
this.wsSubject.next(message);
}
else {
console.warn('WebSocket is not connected. Message not sent:', message);
}
}
close() {
this.stopHeartbeat();
if (this.wsSubject) {
this.wsSubject.complete();
}
}
initializeWebSocket() {
if (this.wsSubject) {
this.wsSubject.complete();
}
this.wsSubject = webSocket({
url: this.config.url,
protocol: this.config.protocols,
serializer: this.config.serializer || this.defaultSerializer,
deserializer: this.config.deserializer || ((e) => this.defaultDeserializer(e.data))
});
this.wsSubject.pipe(retryWhen(errors => errors.pipe(tap(err => {
this.reconnectCount++;
this.updateStatus(false, true);
console.warn('WebSocket error, attempting reconnect:', err);
}), delay(this.config.reconnectInterval), tap(() => {
if (this.reconnectCount > this.config.reconnectAttempts) {
throw new Error('WebSocket reconnection failed');
}
})))).subscribe({
next: (message) => {
this.reconnectCount = 0;
this.updateStatus(true, false);
this.messagesSubject.next(message);
},
error: (error) => {
this.updateStatus(false, false);
console.error('WebSocket error:', error);
this.stopHeartbeat();
},
complete: () => {
this.updateStatus(false, false);
this.stopHeartbeat();
}
});
this.startHeartbeat();
}
startHeartbeat() {
if (this.config.heartbeatInterval && this.config.heartbeatMessage) {
this.heartbeatTimer = setInterval(() => {
this.send({
type: 'heartbeat',
data: this.config.heartbeatMessage
});
}, this.config.heartbeatInterval);
}
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
}
updateStatus(connected, attempting) {
this.statusSubject.next({
connected,
attempting,
attemptCount: this.reconnectCount
});
}
defaultSerializer(data) {
return JSON.stringify(data);
}
defaultDeserializer(data) {
try {
return JSON.parse(data);
}
catch {
return data;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WebSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WebSocketService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WebSocketService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class GraphQLService {
http;
config = {
url: '',
batchInterval: 10,
batchMax: 10
};
batchQueue = [];
batchTimeout;
constructor(http) {
this.http = http;
}
setConfig(config) {
this.config = { ...this.config, ...config };
}
query(query, variables) {
return this.request({ query, variables });
}
mutation(mutation, variables) {
return this.request({ query: mutation, variables });
}
batchQuery(query, variables) {
const request = {
id: Math.random().toString(36).substring(7),
query,
variables
};
return new Observable(observer => {
this.batchQueue.push(request);
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => {
this.processBatch();
}, this.config.batchInterval);
}
if (this.batchQueue.length >= this.config.batchMax) {
this.processBatch();
}
const subscription = this.processBatchedRequest(request).subscribe(result => observer.next(result), error => observer.error(error), () => observer.complete());
return () => {
subscription.unsubscribe();
this.batchQueue = this.batchQueue.filter(req => req.id !== request.id);
};
});
}
request(graphQLRequest) {
return this.http.post(this.config.url, this.prepareRequest(graphQLRequest), {
headers: new HttpHeaders({
'Content-Type': 'application/json',
...this.config.headers
})
}).pipe(map(response => {
if (response.errors?.length) {
throw new Error(response.errors.map(e => e.message).join('\n'));
}
return response.data;
}));
}
processBatch() {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);