UNPKG

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
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);