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 lines 110 kB
{"version":3,"file":"ng-http-spring.mjs","sources":["../../../projects/ng-http-spring/src/lib/services/http.service.ts","../../../projects/ng-http-spring/src/lib/services/memory-cache.service.ts","../../../projects/ng-http-spring/src/lib/decorators/parameter.decorators.ts","../../../projects/ng-http-spring/src/lib/decorators/http.decorators.ts","../../../projects/ng-http-spring/src/lib/interceptors/logging.interceptor.ts","../../../projects/ng-http-spring/src/lib/models/error.model.ts","../../../projects/ng-http-spring/src/lib/interceptors/error.interceptor.ts","../../../projects/ng-http-spring/src/lib/interceptors/auth.interceptor.ts","../../../projects/ng-http-spring/src/lib/interceptors/index.ts","../../../projects/ng-http-spring/src/lib/services/file-upload.service.ts","../../../projects/ng-http-spring/src/lib/services/request-queue.service.ts","../../../projects/ng-http-spring/src/lib/services/response-transformer.service.ts","../../../projects/ng-http-spring/src/lib/services/offline.service.ts","../../../projects/ng-http-spring/src/lib/services/websocket.service.ts","../../../projects/ng-http-spring/src/lib/services/graphql.service.ts","../../../projects/ng-http-spring/src/lib/ng-http-spring.module.ts","../../../projects/ng-http-spring/src/lib/providers.ts","../../../projects/ng-http-spring/src/public-api.ts","../../../projects/ng-http-spring/src/ng-http-spring.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\r\nimport { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';\r\nimport { Observable, retry, timer } from 'rxjs';\r\nimport { HttpConfig } from '../models/http-config.model';\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class HttpService {\r\n constructor(private readonly http: HttpClient) {}\r\n\r\n executeRequest(config: HttpConfig): Observable<any> {\r\n let headers = new HttpHeaders();\r\n let params = new HttpParams();\r\n\r\n // Add config headers\r\n if (config.headers) {\r\n Object.entries(config.headers).forEach(([key, value]) => {\r\n headers = headers.set(key, String(value));\r\n });\r\n }\r\n\r\n // Add config params\r\n if (config.params) {\r\n Object.entries(config.params).forEach(([key, value]) => {\r\n params = params.set(key, String(value));\r\n });\r\n }\r\n\r\n const method = config.method?.toUpperCase() || 'GET';\r\n const options = {\r\n headers,\r\n params,\r\n responseType: config.responseType || 'json',\r\n withCredentials: config.withCredentials,\r\n observe: config.observe || 'body'\r\n } as any; // Cast to any to avoid type issues with observe property\r\n\r\n let request = this.createRequest(method, config, options);\r\n\r\n if (config.retryConfig) {\r\n const retryConfig = config.retryConfig;\r\n request = request.pipe(\r\n retry({\r\n count: retryConfig.maxRetries,\r\n delay: (error) => {\r\n if (retryConfig.retryCondition && !retryConfig.retryCondition(error)) {\r\n throw error;\r\n }\r\n return timer(retryConfig.delayMs);\r\n }\r\n })\r\n );\r\n }\r\n\r\n return request;\r\n }\r\n\r\n private createRequest(method: string, config: HttpConfig, options: any): Observable<any> {\r\n switch (method) {\r\n case 'GET':\r\n return this.http.get(config.url, options);\r\n case 'POST':\r\n return this.http.post(config.url, config.body, options);\r\n case 'PUT':\r\n return this.http.put(config.url, config.body, options);\r\n case 'DELETE':\r\n return this.http.delete(config.url, options);\r\n case 'PATCH':\r\n return this.http.patch(config.url, config.body, options);\r\n default:\r\n throw new Error(`Unsupported HTTP method: ${method}`);\r\n }\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { CacheStorage, CacheEntry } from '../models/cache.model';\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class MemoryCacheService implements CacheStorage {\r\n private cache = new Map<string, CacheEntry>();\r\n\r\n get<T>(key: string): CacheEntry<T> | undefined {\r\n const entry = this.cache.get(key);\r\n if (!entry) return undefined;\r\n\r\n if (entry.expiry && entry.expiry < Date.now()) {\r\n this.cache.delete(key);\r\n return undefined;\r\n }\r\n\r\n return entry as CacheEntry<T>;\r\n }\r\n\r\n set<T>(key: string, entry: CacheEntry<T>): void {\r\n this.cache.set(key, entry);\r\n }\r\n\r\n delete(key: string): void {\r\n this.cache.delete(key);\r\n }\r\n\r\n clear(group?: string): void {\r\n if (group) {\r\n for (const [key, entry] of this.cache.entries()) {\r\n if (entry.group === group) {\r\n this.cache.delete(key);\r\n }\r\n }\r\n } else {\r\n this.cache.clear();\r\n }\r\n }\r\n}\r\n","import { MetadataKeys } from '../models/metadata.model';\r\n\r\ninterface ParamMetadata {\r\n paramName: string;\r\n index: number;\r\n}\r\n\r\nfunction ensureParamMetadataArray(target: Object, key: string | symbol, metadataKey: string): ParamMetadata[] {\r\n const existingMetadata = Reflect.getMetadata(metadataKey, target, key) || [];\r\n return Array.isArray(existingMetadata) ? existingMetadata : [];\r\n}\r\n\r\nexport function PathVariable(paramName: string) {\r\n return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\r\n const pathParams = ensureParamMetadataArray(target, propertyKey, MetadataKeys.PATH_PARAMS);\r\n pathParams.push({ paramName, index: parameterIndex });\r\n Reflect.defineMetadata(MetadataKeys.PATH_PARAMS, pathParams, target, propertyKey);\r\n };\r\n}\r\n\r\nexport function RequestParam(paramName: string) {\r\n return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\r\n const queryParams = ensureParamMetadataArray(target, propertyKey, MetadataKeys.QUERY_PARAMS);\r\n queryParams.push({ paramName, index: parameterIndex });\r\n Reflect.defineMetadata(MetadataKeys.QUERY_PARAMS, queryParams, target, propertyKey);\r\n };\r\n}\r\n\r\nexport function RequestBody() {\r\n return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\r\n Reflect.defineMetadata(MetadataKeys.BODY_PARAM, parameterIndex, target, propertyKey);\r\n };\r\n}\r\n\r\nexport function Header(headerName: string) {\r\n return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\r\n const headerParams = ensureParamMetadataArray(target, propertyKey, MetadataKeys.HEADER_PARAMS);\r\n headerParams.push({ paramName: headerName, index: parameterIndex });\r\n Reflect.defineMetadata(MetadataKeys.HEADER_PARAMS, headerParams, target, propertyKey);\r\n };\r\n}\r\n","import { HttpHeaders, HttpParams } from '@angular/common/http';\r\nimport { inject } from '@angular/core';\r\nimport { Observable, of } from 'rxjs';\r\nimport { catchError, tap } from 'rxjs/operators';\r\nimport { HttpConfig } from '../models/http-config.model';\r\nimport { HttpService } from '../services/http.service';\r\nimport { CacheConfig } from '../models/cache.model';\r\nimport { ErrorConfig } from '../models/error.model';\r\nimport { MemoryCacheService } from '../services/memory-cache.service';\r\n\r\nexport interface RequestMetadata {\r\n path: string;\r\n method: string;\r\n cache?: CacheConfig;\r\n error?: ErrorConfig;\r\n config?: HttpConfig;\r\n}\r\n\r\nfunction processMetadata(target: any, propertyKey: string): RequestMetadata {\r\n return Reflect.getMetadata('requestMetadata', target, propertyKey) || {};\r\n}\r\n\r\nfunction processRequestConfig(path: string, target: any, propertyKey: string, args: any[]): HttpConfig {\r\n const config: HttpConfig = {\r\n url: path,\r\n headers: {},\r\n params: {}\r\n };\r\n\r\n // Path variables\r\n const pathParams = Reflect.getMetadata('pathParams', target, propertyKey) || [];\r\n pathParams.forEach((param: { paramName: string; index: number }) => {\r\n config.url = config.url.replace(`{${param.paramName}}`, String(args[param.index]));\r\n });\r\n\r\n // Query parameters\r\n const queryParams = Reflect.getMetadata('queryParams', target, propertyKey) || [];\r\n queryParams.forEach((param: { paramName: string; index: number }) => {\r\n if (args[param.index] !== undefined) {\r\n config.params![param.paramName] = String(args[param.index]);\r\n }\r\n });\r\n\r\n return config;\r\n}\r\n\r\nfunction handleCacheResponse(response: any, cacheKey: string, cacheConfig?: CacheConfig, cacheService?: MemoryCacheService) {\r\n if (!cacheConfig || !cacheService) return;\r\n\r\n cacheService.set(cacheKey, {\r\n data: response,\r\n expiry: cacheConfig.ttl ? Date.now() + cacheConfig.ttl : 0,\r\n group: cacheConfig.group\r\n });\r\n}\r\n\r\nfunction handleError(error: any, errorConfig?: ErrorConfig): Observable<any> {\r\n if (errorConfig?.handler) {\r\n errorConfig.handler.handleError(error);\r\n }\r\n if (errorConfig?.transform) {\r\n return of(errorConfig.transform(error));\r\n }\r\n throw error;\r\n}\r\n\r\nfunction checkCache(cacheConfig: CacheConfig | undefined, cacheKey: string, cacheService: MemoryCacheService): Observable<any> | undefined {\r\n if (!cacheConfig) return undefined;\r\n\r\n const cached = cacheService.get(cacheKey);\r\n if (cached && (!cached.expiry || cached.expiry > Date.now())) {\r\n return of(cached.data);\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction buildConfig(path: string, method: string, args: any[], target: any, propertyKey: string, metadata: RequestMetadata): HttpConfig {\r\n const requestConfig = processRequestConfig(path, target, propertyKey, args);\r\n return {\r\n ...requestConfig,\r\n method,\r\n ...metadata.config,\r\n body: method !== 'GET' && method !== 'DELETE' ? args[0] : undefined,\r\n headers: { ...requestConfig.headers, ...metadata.config?.headers },\r\n params: { ...requestConfig.params, ...metadata.config?.params }\r\n };\r\n}\r\n\r\nfunction createHttpDecorator(method: string) {\r\n return (path: string, config?: HttpConfig & { cache?: CacheConfig; error?: ErrorConfig }) => {\r\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\r\n const metadata: RequestMetadata = {\r\n path,\r\n method,\r\n cache: config?.cache,\r\n error: config?.error,\r\n config\r\n };\r\n\r\n Reflect.defineMetadata('requestMetadata', metadata, target, propertyKey);\r\n\r\n descriptor.value = function (...args: any[]): Observable<any> {\r\n const httpService = inject(HttpService);\r\n const cacheService = inject(MemoryCacheService);\r\n const metadata = processMetadata(target, propertyKey);\r\n const cacheKey = metadata.cache?.key || `${path}-${JSON.stringify(args)}`;\r\n\r\n const cachedResult = checkCache(metadata.cache, cacheKey, cacheService);\r\n if (cachedResult) return cachedResult;\r\n\r\n const finalConfig = buildConfig(path, method, args, target, propertyKey, metadata);\r\n\r\n return httpService.executeRequest(finalConfig).pipe(\r\n tap(response => handleCacheResponse(response, cacheKey, metadata.cache, cacheService)),\r\n catchError(error => handleError(error, metadata.error))\r\n );\r\n };\r\n\r\n return descriptor;\r\n };\r\n };\r\n}\r\n\r\nexport const Get = createHttpDecorator('GET');\r\nexport const Post = createHttpDecorator('POST');\r\nexport const Put = createHttpDecorator('PUT');\r\nexport const Delete = createHttpDecorator('DELETE');\r\nexport const Patch = createHttpDecorator('PATCH');\r\n\r\nexport * from './parameter.decorators';\r\n","import { Injectable, inject } from '@angular/core';\r\nimport { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpEventType } from '@angular/common/http';\r\nimport { Observable } from 'rxjs';\r\nimport { tap, finalize } from 'rxjs/operators';\r\nimport { v4 as uuidv4 } from 'uuid';\r\n\r\nexport enum LogLevel {\r\n DEBUG = 'DEBUG',\r\n INFO = 'INFO',\r\n WARN = 'WARN',\r\n ERROR = 'ERROR'\r\n}\r\n\r\nexport interface LogConfig {\r\n level: LogLevel;\r\n maxBodySize?: number;\r\n includeHeaders?: boolean;\r\n includeTiming?: boolean;\r\n includeMetrics?: boolean;\r\n formatter?: (message: string, level: LogLevel, data?: any) => string;\r\n}\r\n\r\n@Injectable()\r\nexport class LoggingInterceptor implements HttpInterceptor {\r\n private config: LogConfig = {\r\n level: LogLevel.INFO,\r\n maxBodySize: 1024 * 10, // 10KB\r\n includeHeaders: true,\r\n includeTiming: true,\r\n includeMetrics: true\r\n };\r\n\r\n private metrics = new Map<string, {\r\n count: number;\r\n totalDuration: number;\r\n failures: number;\r\n avgResponseSize: number;\r\n }>();\r\n\r\n setConfig(config: Partial<LogConfig>) {\r\n this.config = { ...this.config, ...config };\r\n }\r\n\r\n intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\r\n const startTime = Date.now();\r\n const requestId = uuidv4();\r\n const requestSize = this.calculateRequestSize(request);\r\n\r\n this.log(LogLevel.INFO, `[HTTP] Request ${requestId} - ${request.method} ${request.url} started`, {\r\n requestId,\r\n method: request.method,\r\n url: request.url,\r\n headers: this.config.includeHeaders ? request.headers.keys().reduce((acc, key) => ({\r\n ...acc,\r\n [key]: request.headers.getAll(key)\r\n }), {}) : undefined,\r\n body: this.truncateBody(request.body),\r\n size: requestSize\r\n });\r\n\r\n return next.handle(request).pipe(\r\n tap({\r\n next: (event: HttpEvent<any>) => {\r\n if (event.type === HttpEventType.Response) {\r\n const response = event as HttpResponse<any>;\r\n const duration = Date.now() - startTime;\r\n const responseSize = this.calculateResponseSize(response);\r\n\r\n this.updateMetrics(request.url, duration, responseSize, false);\r\n\r\n this.log(LogLevel.INFO, `[HTTP] Request ${requestId} completed`, {\r\n requestId,\r\n duration,\r\n status: response.status,\r\n headers: this.config.includeHeaders ? response.headers.keys().reduce((acc, key) => ({\r\n ...acc,\r\n [key]: response.headers.getAll(key)\r\n }), {}) : undefined,\r\n body: this.truncateBody(response.body),\r\n size: responseSize\r\n });\r\n }\r\n },\r\n error: (error) => {\r\n const duration = Date.now() - startTime;\r\n this.updateMetrics(request.url, duration, 0, true);\r\n\r\n this.log(LogLevel.ERROR, `[HTTP] Request ${requestId} failed`, {\r\n requestId,\r\n duration,\r\n error: error.message,\r\n stack: error.stack\r\n });\r\n }\r\n }),\r\n finalize(() => {\r\n if (this.config.includeMetrics) {\r\n this.logMetrics(request.url);\r\n }\r\n })\r\n );\r\n }\r\n\r\n private calculateRequestSize(request: HttpRequest<any>): number {\r\n let size = 0;\r\n size += request.url.length;\r\n size += JSON.stringify(request.body || '').length;\r\n request.headers.keys().forEach(key => {\r\n size += key.length;\r\n size += request.headers.getAll(key)?.join(',').length || 0;\r\n });\r\n return size;\r\n }\r\n\r\n private calculateResponseSize(response: HttpResponse<any>): number {\r\n let size = 0;\r\n size += JSON.stringify(response.body || '').length;\r\n response.headers.keys().forEach(key => {\r\n size += key.length;\r\n size += response.headers.getAll(key)?.join(',').length || 0;\r\n });\r\n return size;\r\n }\r\n\r\n private truncateBody(body: any): any {\r\n if (!body) return body;\r\n const stringified = JSON.stringify(body);\r\n if (stringified.length <= this.config.maxBodySize!) {\r\n return body;\r\n }\r\n return stringified.substring(0, this.config.maxBodySize!) + '... (truncated)';\r\n }\r\n\r\n private updateMetrics(url: string, duration: number, responseSize: number, isFailure: boolean) {\r\n const current = this.metrics.get(url) || {\r\n count: 0,\r\n totalDuration: 0,\r\n failures: 0,\r\n avgResponseSize: 0\r\n };\r\n\r\n current.count++;\r\n current.totalDuration += duration;\r\n if (isFailure) current.failures++;\r\n current.avgResponseSize = (current.avgResponseSize * (current.count - 1) + responseSize) / current.count;\r\n\r\n this.metrics.set(url, current);\r\n }\r\n\r\n private logMetrics(url: string) {\r\n const metrics = this.metrics.get(url);\r\n if (metrics) {\r\n this.log(LogLevel.DEBUG, '[HTTP] Metrics', {\r\n url,\r\n totalRequests: metrics.count,\r\n avgDuration: metrics.totalDuration / metrics.count,\r\n failureRate: (metrics.failures / metrics.count) * 100,\r\n avgResponseSize: metrics.avgResponseSize\r\n });\r\n }\r\n }\r\n\r\n private log(level: LogLevel, message: string, data?: any) {\r\n if (this.isLevelEnabled(level)) {\r\n const formattedMessage = this.config.formatter\r\n ? this.config.formatter(message, level, data)\r\n : `${new Date().toISOString()} ${level} ${message} ${data ? JSON.stringify(data) : ''}`;\r\n\r\n switch (level) {\r\n case LogLevel.DEBUG:\r\n console.debug(formattedMessage);\r\n break;\r\n case LogLevel.INFO:\r\n console.info(formattedMessage);\r\n break;\r\n case LogLevel.WARN:\r\n console.warn(formattedMessage);\r\n break;\r\n case LogLevel.ERROR:\r\n console.error(formattedMessage);\r\n break;\r\n }\r\n }\r\n }\r\n\r\n private isLevelEnabled(level: LogLevel): boolean {\r\n const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];\r\n return levels.indexOf(level) >= levels.indexOf(this.config.level);\r\n }\r\n}\r\n","import { HttpErrorResponse } from '@angular/common/http';\r\n\r\nexport class HttpError extends Error {\r\n constructor(\r\n public status: number,\r\n public override message: string,\r\n public error: any,\r\n public path: string\r\n ) {\r\n super(message);\r\n this.name = 'HttpError';\r\n }\r\n\r\n static from(error: HttpErrorResponse): HttpError {\r\n return new HttpError(\r\n error.status,\r\n error.message,\r\n error.error,\r\n error.url || 'unknown'\r\n );\r\n }\r\n}\r\n\r\nexport interface ErrorHandler {\r\n handleError(error: HttpError): void;\r\n}\r\n\r\nexport interface ErrorConfig {\r\n handler?: ErrorHandler;\r\n retryable?: boolean;\r\n transform?: (error: HttpError) => any;\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';\r\nimport { Observable, throwError } from 'rxjs';\r\nimport { catchError } from 'rxjs/operators';\r\nimport { HttpError } from '../models/error.model';\r\n\r\n@Injectable()\r\nexport class ErrorInterceptor implements HttpInterceptor {\r\n intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\r\n return next.handle(request).pipe(\r\n catchError((error: HttpErrorResponse) => {\r\n return throwError(() => HttpError.from(error));\r\n })\r\n );\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';\r\nimport { Observable, throwError, from, lastValueFrom } from 'rxjs';\r\nimport { catchError, switchMap } from 'rxjs/operators';\r\nimport { AuthConfig } from '../models/auth.model';\r\n\r\n@Injectable()\r\nexport class AuthInterceptor implements HttpInterceptor {\r\n private isRefreshing = false;\r\n private authConfig: AuthConfig;\r\n\r\n constructor() {\r\n // Default config, should be overridden\r\n this.authConfig = {\r\n tokenProvider: () => '',\r\n headerName: 'Authorization',\r\n scheme: 'Bearer'\r\n };\r\n }\r\n\r\n setConfig(config: AuthConfig) {\r\n this.authConfig = { ...this.authConfig, ...config };\r\n }\r\n\r\n intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\r\n return from(this.handleRequest(request, next));\r\n }\r\n\r\n private async handleRequest(request: HttpRequest<any>, next: HttpHandler): Promise<HttpEvent<any>> {\r\n try {\r\n const token = await this.authConfig.tokenProvider();\r\n const authReq = this.addAuthHeader(request, token);\r\n return await lastValueFrom(next.handle(authReq));\r\n } catch (error) {\r\n if (error instanceof HttpErrorResponse && error.status === 401 && this.authConfig.refreshToken) {\r\n return await this.handleAuthError(request, next, error);\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n private async handleAuthError(request: HttpRequest<any>, next: HttpHandler, error: HttpErrorResponse): Promise<HttpEvent<any>> {\r\n if (!this.isRefreshing && this.authConfig.refreshToken) {\r\n this.isRefreshing = true;\r\n try {\r\n const newToken = await this.authConfig.refreshToken();\r\n this.isRefreshing = false;\r\n const authReq = this.addAuthHeader(request, newToken);\r\n return await lastValueFrom(next.handle(authReq));\r\n } catch (refreshError) {\r\n this.isRefreshing = false;\r\n if (this.authConfig.onAuthError) {\r\n this.authConfig.onAuthError(refreshError);\r\n }\r\n throw refreshError;\r\n }\r\n }\r\n throw error;\r\n }\r\n\r\n private addAuthHeader(request: HttpRequest<any>, token: string): HttpRequest<any> {\r\n return request.clone({\r\n headers: request.headers.set(\r\n this.authConfig.headerName!,\r\n `${this.authConfig.scheme} ${token}`\r\n )\r\n });\r\n }\r\n}\r\n","import { HttpHandlerFn, HttpInterceptorFn } from '@angular/common/http';\r\nimport { inject } from '@angular/core';\r\nimport { LoggingInterceptor } from './logging.interceptor';\r\nimport { ErrorInterceptor } from './error.interceptor';\r\nimport { AuthInterceptor } from './auth.interceptor';\r\n\r\nexport const loggingInterceptor: HttpInterceptorFn = (req, next) => {\r\n const handler = { handle: next } as any;\r\n return inject(LoggingInterceptor).intercept(req, handler);\r\n};\r\n\r\nexport const errorInterceptor: HttpInterceptorFn = (req, next) => {\r\n const handler = { handle: next } as any;\r\n return inject(ErrorInterceptor).intercept(req, handler);\r\n};\r\n\r\nexport const authInterceptor: HttpInterceptorFn = (req, next) => {\r\n const handler = { handle: next } as any;\r\n return inject(AuthInterceptor).intercept(req, handler);\r\n};\r\n","import { Injectable } from '@angular/core';\r\nimport { HttpClient, HttpEventType, HttpRequest, HttpHeaders } from '@angular/common/http';\r\nimport { Observable, Subject, of, throwError } from 'rxjs';\r\nimport { catchError, map, tap } from 'rxjs/operators';\r\n\r\nexport interface UploadConfig {\r\n url: string;\r\n file: File;\r\n chunkSize?: number;\r\n headers?: { [key: string]: string };\r\n allowedTypes?: string[];\r\n maxSize?: number;\r\n withCredentials?: boolean;\r\n metadata?: { [key: string]: any };\r\n}\r\n\r\nexport interface UploadProgress {\r\n loaded: number;\r\n total: number;\r\n percentage: number;\r\n status: 'pending' | 'uploading' | 'completed' | 'failed' | 'paused';\r\n chunk?: number;\r\n totalChunks?: number;\r\n}\r\n\r\nexport interface ChunkMetadata {\r\n chunkIndex: number;\r\n totalChunks: number;\r\n fileId: string;\r\n fileName: string;\r\n fileSize: number;\r\n mimeType: string;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class FileUploadService {\r\n private readonly DEFAULT_CHUNK_SIZE = 1024 * 1024; // 1MB\r\n private activeUploads = new Map<string, Subject<UploadProgress>>();\r\n\r\n constructor(private http: HttpClient) {}\r\n\r\n upload(config: UploadConfig): Observable<UploadProgress> {\r\n // Validate file\r\n if (!this.validateFile(config.file, config.allowedTypes, config.maxSize)) {\r\n return throwError(() => new Error('Invalid file type or size'));\r\n }\r\n\r\n const uploadId = Math.random().toString(36).substring(7);\r\n const progress = new Subject<UploadProgress>();\r\n this.activeUploads.set(uploadId, progress);\r\n\r\n if (config.chunkSize && config.file.size > config.chunkSize) {\r\n this.uploadInChunks(config, progress);\r\n } else {\r\n this.uploadFile(config, progress);\r\n }\r\n\r\n return progress.asObservable();\r\n }\r\n\r\n pauseUpload(uploadId: string) {\r\n const progress = this.activeUploads.get(uploadId);\r\n if (progress) {\r\n progress.next({\r\n loaded: 0,\r\n total: 0,\r\n percentage: 0,\r\n status: 'paused'\r\n });\r\n }\r\n }\r\n\r\n resumeUpload(uploadId: string) {\r\n // Resume logic would go here\r\n }\r\n\r\n cancelUpload(uploadId: string) {\r\n const progress = this.activeUploads.get(uploadId);\r\n if (progress) {\r\n progress.complete();\r\n this.activeUploads.delete(uploadId);\r\n }\r\n }\r\n\r\n private validateFile(file: File, allowedTypes?: string[], maxSize?: number): boolean {\r\n if (allowedTypes && !allowedTypes.includes(file.type)) {\r\n return false;\r\n }\r\n\r\n if (maxSize && file.size > maxSize) {\r\n return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n private uploadFile(config: UploadConfig, progress: Subject<UploadProgress>) {\r\n const formData = new FormData();\r\n formData.append('file', config.file);\r\n\r\n if (config.metadata) {\r\n formData.append('metadata', JSON.stringify(config.metadata));\r\n }\r\n\r\n const req = new HttpRequest('POST', config.url, formData, {\r\n reportProgress: true,\r\n withCredentials: config.withCredentials,\r\n headers: new HttpHeaders(config.headers || {})\r\n });\r\n\r\n this.http.request(req).pipe(\r\n map(event => this.getProgress(event, config.file.size)),\r\n catchError(error => {\r\n progress.next({\r\n loaded: 0,\r\n total: config.file.size,\r\n percentage: 0,\r\n status: 'failed'\r\n });\r\n return throwError(() => error);\r\n })\r\n ).subscribe(\r\n currentProgress => progress.next(currentProgress),\r\n error => progress.error(error),\r\n () => progress.complete()\r\n );\r\n }\r\n\r\n private uploadInChunks(config: UploadConfig, progress: Subject<UploadProgress>) {\r\n const chunkSize = config.chunkSize || this.DEFAULT_CHUNK_SIZE;\r\n const totalChunks = Math.ceil(config.file.size / chunkSize);\r\n const fileId = Math.random().toString(36).substring(7);\r\n let uploadedChunks = 0;\r\n let uploadedBytes = 0;\r\n\r\n const uploadNextChunk = (chunkIndex: number) => {\r\n const start = chunkIndex * chunkSize;\r\n const end = Math.min(start + chunkSize, config.file.size);\r\n const chunk = config.file.slice(start, end);\r\n\r\n const formData = new FormData();\r\n formData.append('chunk', chunk);\r\n formData.append('metadata', JSON.stringify({\r\n chunkIndex,\r\n totalChunks,\r\n fileId,\r\n fileName: config.file.name,\r\n fileSize: config.file.size,\r\n mimeType: config.file.type\r\n } as ChunkMetadata));\r\n\r\n const req = new HttpRequest('POST', config.url, formData, {\r\n reportProgress: true,\r\n withCredentials: config.withCredentials,\r\n headers: new HttpHeaders(config.headers || {})\r\n });\r\n\r\n this.http.request(req).pipe(\r\n tap(event => {\r\n if (event.type === HttpEventType.UploadProgress) {\r\n uploadedBytes = (chunkIndex * chunkSize) + (event.loaded || 0);\r\n progress.next({\r\n loaded: uploadedBytes,\r\n total: config.file.size,\r\n percentage: Math.round((uploadedBytes * 100) / config.file.size),\r\n status: 'uploading',\r\n chunk: chunkIndex + 1,\r\n totalChunks\r\n });\r\n }\r\n }),\r\n catchError(error => {\r\n progress.next({\r\n loaded: uploadedBytes,\r\n total: config.file.size,\r\n percentage: Math.round((uploadedBytes * 100) / config.file.size),\r\n status: 'failed',\r\n chunk: chunkIndex + 1,\r\n totalChunks\r\n });\r\n return throwError(() => error);\r\n })\r\n ).subscribe(\r\n event => {\r\n if (event.type === HttpEventType.Response) {\r\n uploadedChunks++;\r\n if (uploadedChunks === totalChunks) {\r\n // All chunks uploaded, complete the upload\r\n progress.next({\r\n loaded: config.file.size,\r\n total: config.file.size,\r\n percentage: 100,\r\n status: 'completed',\r\n chunk: totalChunks,\r\n totalChunks\r\n });\r\n progress.complete();\r\n } else {\r\n // Upload next chunk\r\n uploadNextChunk(chunkIndex + 1);\r\n }\r\n }\r\n },\r\n error => progress.error(error)\r\n );\r\n };\r\n\r\n // Start uploading chunks\r\n uploadNextChunk(0);\r\n }\r\n\r\n private getProgress(event: any, totalSize: number): UploadProgress {\r\n switch (event.type) {\r\n case HttpEventType.UploadProgress:\r\n const loaded = event.loaded || 0;\r\n return {\r\n loaded,\r\n total: totalSize,\r\n percentage: Math.round((loaded * 100) / totalSize),\r\n status: 'uploading'\r\n };\r\n\r\n case HttpEventType.Response:\r\n return {\r\n loaded: totalSize,\r\n total: totalSize,\r\n percentage: 100,\r\n status: 'completed'\r\n };\r\n\r\n default:\r\n return {\r\n loaded: 0,\r\n total: totalSize,\r\n percentage: 0,\r\n status: 'pending'\r\n };\r\n }\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { HttpRequest } from '@angular/common/http';\r\nimport { Observable, Subject, from, timer } from 'rxjs';\r\nimport { concatMap, tap } from 'rxjs/operators';\r\n\r\nexport interface QueueConfig {\r\n maxConcurrent?: number;\r\n priorityLevels?: number;\r\n defaultPriority?: number;\r\n throttleMs?: number;\r\n retryDelayMs?: number;\r\n}\r\n\r\nexport interface QueuedRequest<T = any> {\r\n id: string;\r\n request: HttpRequest<T>;\r\n priority: number;\r\n timestamp: number;\r\n execute: () => Observable<any>;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class RequestQueueService {\r\n private config: QueueConfig = {\r\n maxConcurrent: 4,\r\n priorityLevels: 3,\r\n defaultPriority: 1,\r\n throttleMs: 0,\r\n retryDelayMs: 1000\r\n };\r\n\r\n private queues: QueuedRequest[][] = [];\r\n private activeRequests = new Set<string>();\r\n private queueSubject = new Subject<QueuedRequest>();\r\n private requestComplete = new Subject<string>();\r\n\r\n constructor() {\r\n // Initialize priority queues\r\n for (let i = 0; i < (this.config.priorityLevels || 1); i++) {\r\n this.queues[i] = [];\r\n }\r\n\r\n // Process queue items\r\n this.queueSubject.pipe(\r\n concatMap(request => {\r\n if (this.activeRequests.size >= (this.config.maxConcurrent || 1)) {\r\n return timer(100).pipe(tap(() => this.queueSubject.next(request)));\r\n }\r\n\r\n return this.processRequest(request);\r\n })\r\n ).subscribe();\r\n\r\n // Handle completed requests\r\n this.requestComplete.subscribe(id => {\r\n this.activeRequests.delete(id);\r\n this.processNextRequest();\r\n });\r\n }\r\n\r\n setConfig(config: Partial<QueueConfig>) {\r\n this.config = { ...this.config, ...config };\r\n // Reinitialize queues if priority levels changed\r\n if (config.priorityLevels && config.priorityLevels !== this.queues.length) {\r\n this.queues = Array(config.priorityLevels).fill([]);\r\n }\r\n }\r\n\r\n enqueue<T>(request: HttpRequest<T>, execute: () => Observable<any>, priority?: number): Observable<any> {\r\n const id = Math.random().toString(36).substring(7);\r\n const queuedRequest: QueuedRequest = {\r\n id,\r\n request,\r\n priority: priority ?? this.config.defaultPriority!,\r\n timestamp: Date.now(),\r\n execute\r\n };\r\n\r\n const priorityIndex = Math.min(\r\n Math.max(0, queuedRequest.priority),\r\n this.queues.length - 1\r\n );\r\n this.queues[priorityIndex].push(queuedRequest);\r\n\r\n return new Observable(observer => {\r\n this.processNextRequest();\r\n\r\n const subscription = this.requestComplete\r\n .pipe(\r\n tap(completedId => {\r\n if (completedId === id) {\r\n observer.complete();\r\n }\r\n })\r\n )\r\n .subscribe();\r\n\r\n return () => {\r\n subscription.unsubscribe();\r\n this.removeFromQueue(id);\r\n };\r\n });\r\n }\r\n\r\n private processNextRequest() {\r\n if (this.activeRequests.size >= (this.config.maxConcurrent || 1)) {\r\n return;\r\n }\r\n\r\n const nextRequest = this.getNextRequest();\r\n if (nextRequest) {\r\n this.queueSubject.next(nextRequest);\r\n }\r\n }\r\n\r\n private processRequest(queuedRequest: QueuedRequest): Observable<any> {\r\n this.activeRequests.add(queuedRequest.id);\r\n\r\n return from(\r\n new Promise(resolve => {\r\n if (this.config.throttleMs) {\r\n setTimeout(resolve, this.config.throttleMs);\r\n } else {\r\n resolve(true);\r\n }\r\n })\r\n ).pipe(\r\n concatMap(() => queuedRequest.execute()),\r\n tap({\r\n complete: () => this.requestComplete.next(queuedRequest.id),\r\n error: () => this.requestComplete.next(queuedRequest.id)\r\n })\r\n );\r\n }\r\n\r\n private getNextRequest(): QueuedRequest | undefined {\r\n for (const queue of this.queues) {\r\n if (queue.length > 0) {\r\n return queue.shift();\r\n }\r\n }\r\n return undefined;\r\n }\r\n\r\n private removeFromQueue(id: string) {\r\n for (const queue of this.queues) {\r\n const index = queue.findIndex(req => req.id === id);\r\n if (index !== -1) {\r\n queue.splice(index, 1);\r\n break;\r\n }\r\n }\r\n }\r\n\r\n getQueueStatus() {\r\n return {\r\n activeRequests: this.activeRequests.size,\r\n queueLengths: this.queues.map(q => q.length),\r\n config: { ...this.config }\r\n };\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\n\r\nexport interface TransformConfig {\r\n dateFields?: string[];\r\n caseStyle?: 'camelCase' | 'snakeCase' | 'kebabCase';\r\n transforms?: { [key: string]: (value: any) => any };\r\n removeFields?: string[];\r\n addFields?: { [key: string]: any | ((data: any) => any) };\r\n xmlToJson?: boolean;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class ResponseTransformerService {\r\n transform(data: any, config: TransformConfig): any {\r\n if (!data) return data;\r\n\r\n let result = this.clone(data);\r\n\r\n if (config.xmlToJson && typeof data === 'string' && data.trim().startsWith('<')) {\r\n result = this.xmlToJson(data);\r\n }\r\n\r\n if (config.dateFields) {\r\n result = this.transformDates(result, config.dateFields);\r\n }\r\n\r\n if (config.caseStyle) {\r\n result = this.transformCase(result, config.caseStyle);\r\n }\r\n\r\n if (config.transforms) {\r\n result = this.applyCustomTransforms(result, config.transforms);\r\n }\r\n\r\n if (config.removeFields) {\r\n result = this.removeFields(result, config.removeFields);\r\n }\r\n\r\n if (config.addFields) {\r\n result = this.addFields(result, config.addFields);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n private clone(obj: any): any {\r\n return JSON.parse(JSON.stringify(obj));\r\n }\r\n\r\n private transformDates(data: any, dateFields: string[]): any {\r\n if (Array.isArray(data)) {\r\n return data.map(item => this.transformDates(item, dateFields));\r\n }\r\n\r\n if (data && typeof data === 'object') {\r\n const result = { ...data };\r\n for (const key in result) {\r\n if (dateFields.includes(key) && typeof result[key] === 'string') {\r\n result[key] = new Date(result[key]);\r\n } else if (typeof result[key] === 'object') {\r\n result[key] = this.transformDates(result[key], dateFields);\r\n }\r\n }\r\n return result;\r\n }\r\n\r\n return data;\r\n }\r\n\r\n private transformCase(data: any, style: 'camelCase' | 'snakeCase' | 'kebabCase'): any {\r\n if (Array.isArray(data)) {\r\n return data.map(item => this.transformCase(item, style));\r\n }\r\n\r\n if (data && typeof data === 'object') {\r\n const result: any = {};\r\n for (const key in data) {\r\n const transformedKey = this.transformKey(key, style);\r\n result[transformedKey] = typeof data[key] === 'object'\r\n ? this.transformCase(data[key], style)\r\n : data[key];\r\n }\r\n return result;\r\n }\r\n\r\n return data;\r\n }\r\n\r\n private transformKey(key: string, style: 'camelCase' | 'snakeCase' | 'kebabCase'): string {\r\n switch (style) {\r\n case 'camelCase':\r\n return key.replace(/([-_][a-z])/g, group =>\r\n group.toUpperCase().replace('-', '').replace('_', '')\r\n );\r\n case 'snakeCase':\r\n return key\r\n .replace(/([A-Z])/g, '_$1')\r\n .toLowerCase()\r\n .replace(/^_/, '');\r\n case 'kebabCase':\r\n return key\r\n .replace(/([A-Z])/g, '-$1')\r\n .toLowerCase()\r\n .replace(/^-/, '');\r\n default:\r\n return key;\r\n }\r\n }\r\n\r\n private applyCustomTransforms(data: any, transforms: { [key: string]: (value: any) => any }): any {\r\n if (Array.isArray(data)) {\r\n return data.map(item => this.applyCustomTransforms(item, transforms));\r\n }\r\n\r\n if (data && typeof data === 'object') {\r\n const result = { ...data };\r\n for (const key in result) {\r\n if (transforms[key]) {\r\n result[key] = transforms[key](result[key]);\r\n } else if (typeof result[key] === 'object') {\r\n result[key] = this.applyCustomTransforms(result[key], transforms);\r\n }\r\n }\r\n return result;\r\n }\r\n\r\n return data;\r\n }\r\n\r\n private removeFields(data: any, fields: string[]): any {\r\n if (Array.isArray(data)) {\r\n return data.map(item => this.removeFields(item, fields));\r\n }\r\n\r\n if (data && typeof data === 'object') {\r\n const result = { ...data };\r\n fields.forEach(field => delete result[field]);\r\n for (const key in result) {\r\n if (typeof result[key] === 'object') {\r\n result[key] = this.removeFields(result[key], fields);\r\n }\r\n }\r\n return result;\r\n }\r\n\r\n return data;\r\n }\r\n\r\n private addFields(data: any, fields: { [key: string]: any | ((data: any) => any) }): any {\r\n if (Array.isArray(data)) {\r\n return data.map(item => this.addFields(item, fields));\r\n }\r\n\r\n if (data && typeof data === 'object') {\r\n const result = { ...data };\r\n for (const key in fields) {\r\n result[key] = typeof fields[key] === 'function'\r\n ? fields[key](data)\r\n : fields[key];\r\n }\r\n return result;\r\n }\r\n\r\n return data;\r\n }\r\n\r\n private xmlToJson(xml: string): any {\r\n const parser = new DOMParser();\r\n const xmlDoc = parser.parseFromString(xml, 'text/xml');\r\n return this.xmlNodeToJson(xmlDoc.documentElement);\r\n }\r\n\r\n private xmlNodeToJson(node: Element): any {\r\n const obj: any = {};\r\n\r\n if (node.nodeType === Node.TEXT_NODE) {\r\n return node.nodeValue?.trim();\r\n }\r\n\r\n // Handle attributes\r\n if (node.attributes) {\r\n for (let i = 0; i < node.attributes.length; i++) {\r\n const attr = node.attributes[i];\r\n obj[`@${attr.nodeName}`] = attr.nodeValue;\r\n }\r\n }\r\n\r\n // Handle child nodes\r\n node.childNodes.forEach(child => {\r\n if (child.nodeType === Node.TEXT_NODE) {\r\n const text = child.nodeValue?.trim();\r\n if (text) obj['#text'] = text;\r\n } else if (child.nodeType === Node.ELEMENT_NODE) {\r\n const childElement = child as Element;\r\n const childObj = this.xmlNodeToJson(childElement);\r\n\r\n if (obj[childElement.nodeName]) {\r\n if (!Array.isArray(obj[childElement.nodeName])) {\r\n obj[childElement.nodeName] = [obj[childElement.nodeName]];\r\n }\r\n obj[childElement.nodeName].push(childObj);\r\n } else {\r\n obj[childElement.nodeName] = childObj;\r\n }\r\n }\r\n });\r\n\r\n return obj;\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { Observable, Subject, fromEvent, merge, of } from 'rxjs';\r\nimport { map, startWith } from 'rxjs/operators';\r\nimport { openDB, DBSchema, IDBPDatabase } from 'idb';\r\n\r\ninterface OfflineRequestStore {\r\n id?: number;\r\n url: string;\r\n method: string;\r\n body?: any;\r\n headers?: { [key: string]: string };\r\n timestamp: number;\r\n retryCount: number;\r\n priority: number;\r\n}\r\n\r\ninterface OfflineDB extends DBSchema {\r\n requests: {\r\n key: number;\r\n value: OfflineRequestStore;\r\n indexes: { 'by-timestamp': number };\r\n };\r\n responses: {\r\n key: string;\r\n value: any;\r\n indexes: { 'by-timestamp': number };\r\n };\r\n}\r\n\r\nexport interface OfflineConfig {\r\n dbName?: string;\r\n maxRetries?: number;\r\n retryDelay?: number;\r\n maxCacheAge?: number;\r\n syncStrategy?: 'immediate' | 'periodic' | 'manual';\r\n syncInterval?: number;\r\n priorityLevels?: number;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class OfflineService {\r\n private db!: IDBPDatabase<OfflineDB>;\r\n private online$ = merge(\r\n of(navigator.onLine),\r\n fromEvent(window, 'online').pipe(map(() => true)),\r\n fromEvent(window, 'offline').pipe(map(() => false))\r\n ).pipe(startWith(navigator.onLine));\r\n\r\n private config: OfflineConfig = {\r\n dbName: 'ng-http-spring-offline',\r\n maxRetries: 3,\r\n retryDelay: 5000,\r\n maxCacheAge: 24 * 60 * 60 * 1000, // 24 hours\r\n syncStrategy: 'immediate',\r\n syncInterval: 30000, // 30 seconds\r\n priorityLevels: 3\r\n };\r\n\r\n private syncInProgress = false;\r\n private syncSubject = new Subject<void>();\r\n readonly sync$ = this.syncSubject.asObservable();\r\n\r\n constructor() {\r\n this.initializeDB();\r\n this.setupSync();\r\n }\r\n\r\n setConfig(config: Partial<OfflineConfig>) {\r\n this.config = { ...this.config, ...config };\r\n this.setupSync();\r\n }\r\n\r\n private async initializeDB() {\r\n this.db = await openDB<OfflineDB>(this.config.dbName!, 1, {\r\n upgrade(db) {\r\n const requestStore = db.createObjectStore('requests', {\r\n keyPath: 'id',\r\n autoIncrement: true\r\n });\r\n requestStore.createIndex('by-timestamp', 'timestamp');\r\n\r\n const responseStore = db.createObjectStore('responses', {\r\n keyPath: 'url'\r\n });\r\n responseStore.createIndex('by-timestamp', 'timestamp');\r\n }\r\n });\r\n }\r\n\r\n private setupSync() {\r\n if (this.config.syncStrategy === 'periodic') {\r\n setInterval(() => this.sync(), this.config.syncInterval);\r\n }\r\n\r\n this.online$.subscribe(isOnline => {\r\n if (isOnline && this.config.syncStrategy === 'immediate') {\r\n this.sync();\r\n }\r\n });\r\n }\r\n\r\n async queueRequest(request: Omit<OfflineRequestStore, 'timestamp' | 'retryCount'>) {\r\n const offlineRequest: OfflineRequestStore = {\r\n ...request,\r\n timestamp: Date.now(),\r\n retryCount: 0\r\n };\r\n\r\n await this.db.add('requests', offlineRequest);\r\n\r\n if (navigator.onLine && this.config.syncStrategy === 'immediate') {\r\n this.sync();\r\n }\r\n }\r\n\r\n async cacheResponse(url: string, response: any) {\r\n await this.db.put('responses', {\r\n url,\r\n data: response,\r\n timestamp: Date.now()\r\n });\r\n }\r\n\r\n async getCachedResponse(url: string): Promise<any | null> {\r\n const cached = await this.db.get('responses', url);\r\n if (!cached) return null;\r\n\r\n if (Date.now() - cached.timestamp > this.config.maxCacheAge!) {\r\n await this.db.delete('responses', url);\r\n return null;\r\n }\r\n\r\n return cached.data;\r\n }\r\n\r\n async sync(): Promise<void> {\r\n if (this.syncInProgress || !navigator.onLine) return;\r\n\r\n this.syncInProgress = true;\r\n try {\r\n const requests = await this.db.getAllFromIndex('requests', 'by-timestamp');\r\n\r\n // Sort by priority and timestamp\r\n requests.sort((a, b) => {\r\n if (a.priority !== b.priority) {\r\n return b.priority - a.priority; // Higher priority first\r\n }\r\n return a.timestamp - b.timestamp; // Older first\r\n });\r\n\r\n for (const request of requests) {\r\n try {\r\n const response = await fetch(request.url, {\r\n method: request.method,\r\n body: request.body ? JSON.stringify(request.body) : undefined,\r\n headers: request.headers\r\n });\r\n\r\n if (response.ok) {\r\n await this.db.delete('requests', request.id!);\r\n const data = await response.json();\r\n await this.cacheResponse(request.url, data);\r\n } else {\r\n request.retryCount++;\r\n if (request.retryCount >= this.config.maxRetries!) {\r\n await this.db.delete('requests', request.id!);\r\n } else {\r\n await this.db.put('requests', request);\r\n }\r\n }\r\n } catch (error) {\r\n request.retryCount++;\r\n if (request.retryCount >= this.config.maxRetries!) {\r\n await this.db.delete('requests', request.id!);\r\n } else {\r\n await this.db.put('requests', request);\r\n }\r\n }\r\n\r\n // Add delay between retries\r\n if (request.retryCount > 0) {\r\n await new Promise(resolve =>\r\n setTimeout(resolve, this.config.retryDelay! * request.retryCount)\r\n );\r\n }\r\n }\r\n\r\n this.syncSubject.next();\r\n } finally {\r\n this.syncInProgress = false;\r\n }\r\n }\r\n\r\n async getPendingRequests(): Promise<OfflineRequestStore[]> {\r\n return this.db.getAllFromIndex('requests', 'by-timestamp');\r\n }\r\n\r\n async clearCache(): Promise<void> {\r\n await this.db.clear('responses');\r\n }\r\n\r\n async clearPendingRequests(): Promise<void> {\r\n await this.db.clear('requests');\r\n }\r\n\r\n isOnline(): Observable<boolean> {\r\n return this.online$;\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { BehaviorSubject, Subject } from 'rxjs';\r\nimport { webSocket, WebSocketSubject } from 'rxjs/webSocket';\r\nimport { retryWhen, delay, tap } from 'rxjs/operators';\r\n\r\nexport interface WebSocketConfig {\r\n url: string;\r\n protocols?: string | string[];\r\n reconnectAttempts?: number;\r\n reconnectInterval?: number;\r\n heartbeatInterval?: number;\r\n heartbeatMessage?: any;\r\n serializer?: (data: any) => string;\r\n deserializer?: (e: MessageEvent) => any;\r\n}\r\n\r\nexport interface WebSocketMessage<T = any> {\r\n type: string;\r\n data: T;\r\n timestamp?: number;\r\n}\r\n\r\nexport interface WebSocketStatus {\r\n connected: boolean;\r\n attempting: boolean;\r\n attemptCount: number;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class WebSocketService {\r\n private config: WebSocketConfig = {\r\n url: '',\r\n reconnectAttempts: 5,\r\n reconnectInterval: 3000,\r\n heartbeatInterval: 30000\r\n };\r\n\r\n private wsSubject?: WebSocketSubject<WebSocketMessage>;\r\n private messagesSubject = new Subject<WebSocketMessage>();\r\n private statusSubject = new BehaviorSubject<WebSocketStatus>({\r\n connected: false,\r\n attempting: false,\r\n attemptCount: 0\r\n });\r\n\r\n private heartbeatTimer?: ReturnType<typeof setInterval>;\r\n private reconnectCount = 0;\r\n\r\n readonly messages$ = this.messagesSubject.asObservable();\r\n readonly status$ = this.statusSubject.asObservable();\r\n\r\n connect(config: Partial<WebSocketConfig>) {\r\n this.config = { ...this.config, ...config };\r\n this.initializeWebSocket();\r\n }\r\n\r\n send(message: WebSocketMessage) {\r\n if (this.wsSubject &&