UNPKG

ng-speed-test

Version:

Angular library for testing internet connection speed

394 lines (386 loc) 15.7 kB
import * as i0 from '@angular/core'; import { Injectable, NgModule } from '@angular/core'; import { of, Observable, throwError, merge, fromEvent } from 'rxjs'; import { switchMap, mergeMap, map, timeout, catchError, startWith } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; class SpeedTestFileModel { constructor(file) { this.path = 'https://raw.githubusercontent.com/jrquick17/ng-speed-test/02c59e4afde67c35a5ba74014b91d44b33c0b3fe/demo/src/assets/5mb.jpg'; this.shouldBustCache = true; this.size = 4952221; if (file) { if (file.path !== undefined) { this.path = file.path; } if (file.size !== undefined) { this.size = file.size; } if (file.shouldBustCache !== undefined) { this.shouldBustCache = file.shouldBustCache; } } } } class SpeedTestResultsModel { constructor(fileSize) { this.fileSize = fileSize; this.duration = 0; this.hasEnded = false; this.startTime = null; this.endTime = null; this.speedBps = 0; } get speedKbps() { return this.speedBps / 1024; } get speedMbps() { return this.speedKbps / 1024; } _update() { if (this.endTime !== null && this.startTime !== null) { const milliseconds = this.endTime - this.startTime; if (milliseconds !== 0) { this.duration = milliseconds / 1000; } const bitsLoaded = this.fileSize * 8; this.speedBps = bitsLoaded / this.duration; } } end() { if (!this.hasEnded) { this.hasEnded = true; this.endTime = Date.now(); this._update(); } } error() { if (!this.hasEnded) { this.hasEnded = true; this.endTime = null; this._update(); } } start() { this.startTime = Date.now(); } } class SpeedTestSettingsModel { constructor(settings) { this.iterations = 3; this.file = new SpeedTestFileModel(); this.retryDelay = 500; if (settings) { if (settings.iterations !== undefined) { this.iterations = settings.iterations; } if (settings.retryDelay !== undefined) { this.retryDelay = settings.retryDelay; } if (settings.file) { this.file = new SpeedTestFileModel(); if (settings.file.path !== undefined) { this.file.path = settings.file.path; } if (settings.file.size !== undefined) { this.file.size = settings.file.size; } if (settings.file.shouldBustCache !== undefined) { this.file.shouldBustCache = settings.file.shouldBustCache; } } } } } class SpeedTestService { constructor() { this.DEFAULT_TIMEOUT = 15000; // Reduced from 30s to 15s this.OFFLINE_CHECK_TIMEOUT = 3000; // Quick offline check } applyCacheBuster(path) { const separator = path.includes('?') ? '&' : '?'; return `${path}${separator}cache_bust=${Date.now()}_${Math.random()}`; } /** * Quick connectivity check before running speed test */ checkConnectivity() { // First check navigator.onLine if (!navigator.onLine) { return of(false); } // Then do a quick network request to verify actual connectivity return new Observable(observer => { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); observer.next(false); observer.complete(); }, this.OFFLINE_CHECK_TIMEOUT); // Use a small, fast endpoint for connectivity check fetch('https://httpbin.org/get?minimal=true', { method: 'HEAD', mode: 'no-cors', signal: controller.signal, cache: 'no-cache' }) .then(() => { clearTimeout(timeoutId); observer.next(true); observer.complete(); }) .catch(() => { clearTimeout(timeoutId); observer.next(false); observer.complete(); }); return () => { clearTimeout(timeoutId); controller.abort(); }; }); } downloadTest(settings, allResults = []) { // Quick connectivity check first return this.checkConnectivity().pipe(switchMap(isConnected => { if (!isConnected) { return throwError(() => new Error('No internet connection available')); } return new Observable(observer => { const testResult = new SpeedTestResultsModel(settings.file.size); const abortController = new AbortController(); let filePath = settings.file.path; if (settings.file.shouldBustCache) { filePath = this.applyCacheBuster(filePath); } testResult.start(); // Set a more aggressive timeout for the fetch request const fetchTimeout = setTimeout(() => { abortController.abort(); testResult.error(); observer.next(testResult); observer.complete(); }, this.DEFAULT_TIMEOUT); fetch(filePath, { method: 'GET', signal: abortController.signal, cache: 'no-cache' }) .then(response => { clearTimeout(fetchTimeout); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.blob(); }) .then(() => { testResult.end(); observer.next(testResult); observer.complete(); }) .catch(error => { clearTimeout(fetchTimeout); console.warn('Speed test download failed:', error); testResult.error(); const delay = settings.iterations !== 1 ? settings.retryDelay : 0; setTimeout(() => { observer.next(testResult); observer.complete(); }, delay); }); // Cleanup function return () => { clearTimeout(fetchTimeout); abortController.abort(); }; }); }), mergeMap((testResult) => { allResults.push(testResult); if (settings.iterations === 1) { // Calculate average speed from all valid results const validResults = allResults.filter(result => result.speedBps > 0); if (validResults.length === 0) { return throwError(() => new Error('All speed test iterations failed - no internet connection or server unreachable')); } const totalSpeed = validResults.reduce((sum, result) => sum + result.speedBps, 0); const averageSpeed = totalSpeed / validResults.length; return of(averageSpeed); } else { settings.iterations--; return this.downloadTest(settings, allResults); } })); } validateSettings(settings) { if (!settings.file?.path) { throw new Error('ng-speed-test: File path is required'); } if (!settings.file?.size || settings.file.size <= 0) { throw new Error('ng-speed-test: Valid file size is required'); } if (settings.iterations && settings.iterations < 1) { throw new Error('ng-speed-test: Iterations must be at least 1'); } } /** * Get internet speed in bits per second (bps) * Fails fast if no internet connection is available */ getBps(customSettings) { return new Observable(observer => { // Check connectivity immediately if (!navigator.onLine) { observer.error(new Error('No internet connection - browser reports offline')); return; } // Small delay to ensure proper initialization setTimeout(() => { // Create settings with proper merging const defaultSettings = new SpeedTestSettingsModel(); const settings = this.mergeSettings(defaultSettings, customSettings); try { this.validateSettings(settings); this.downloadTest(settings).subscribe({ next: (speedBps) => { observer.next(speedBps); observer.complete(); }, error: (error) => { observer.error(error); } }); } catch (error) { observer.error(error); } }, 1); }); } /** * Properly merge custom settings with defaults */ mergeSettings(defaultSettings, customSettings) { if (!customSettings) { return defaultSettings; } const mergedSettings = new SpeedTestSettingsModel(); // Merge iterations mergedSettings.iterations = customSettings.iterations !== undefined ? customSettings.iterations : defaultSettings.iterations; // Merge retryDelay mergedSettings.retryDelay = customSettings.retryDelay !== undefined ? customSettings.retryDelay : defaultSettings.retryDelay; // Merge file settings if (customSettings.file) { mergedSettings.file = new SpeedTestFileModel(); // Merge file path mergedSettings.file.path = customSettings.file.path !== undefined ? customSettings.file.path : defaultSettings.file.path; // Merge file size mergedSettings.file.size = customSettings.file.size !== undefined ? customSettings.file.size : defaultSettings.file.size; // Merge shouldBustCache mergedSettings.file.shouldBustCache = customSettings.file.shouldBustCache !== undefined ? customSettings.file.shouldBustCache : defaultSettings.file.shouldBustCache; } else { mergedSettings.file = defaultSettings.file; } return mergedSettings; } /** * Get internet speed in kilobits per second (Kbps) */ getKbps(settings) { return this.getBps(settings).pipe(map(bps => bps / 1024)); } /** * Get internet speed in megabits per second (Mbps) */ getMbps(settings) { return this.getKbps(settings).pipe(map(kbps => kbps / 1024)); } /** * Get comprehensive speed test results with fast failure for offline scenarios */ getSpeedTestResult(settings) { const startTime = Date.now(); return this.getBps(settings).pipe(map(bps => ({ bps, kbps: bps / 1024, mbps: bps / (1024 * 1024), duration: (Date.now() - startTime) / 1000 })), timeout(this.DEFAULT_TIMEOUT + 5000), // Overall timeout slightly longer than individual request timeout catchError(error => { if (error.name === 'TimeoutError') { return throwError(() => new Error('Speed test timed out - please check your internet connection')); } return throwError(() => error); })); } /** * Check if the browser is online with enhanced detection */ isOnline() { return merge(fromEvent(window, 'offline').pipe(map(() => false)), fromEvent(window, 'online').pipe(map(() => true)), of(navigator.onLine)).pipe(startWith(navigator.onLine), // Verify actual connectivity for online state switchMap(browserOnline => { if (!browserOnline) { return of(false); } // Quick connectivity verification return this.checkConnectivity(); })); } /** * Monitor network connection status with enhanced detection */ getNetworkStatus() { const getConnectionInfo = () => { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; return { isOnline: navigator.onLine, effectiveType: connection?.effectiveType, downlink: connection?.downlink }; }; return merge(fromEvent(window, 'offline').pipe(map(() => ({ ...getConnectionInfo(), isOnline: false }))), fromEvent(window, 'online').pipe(map(() => getConnectionInfo()), // Verify actual connectivity when browser reports online switchMap(info => this.checkConnectivity().pipe(map(actuallyOnline => ({ ...info, isOnline: actuallyOnline }))))), of(getConnectionInfo()).pipe(switchMap(info => info.isOnline ? this.checkConnectivity().pipe(map(actuallyOnline => ({ ...info, isOnline: actuallyOnline }))) : of(info)))); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class SpeedTestModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestModule, imports: [CommonModule, FormsModule] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestModule, providers: [SpeedTestService], imports: [CommonModule, FormsModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SpeedTestModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule, FormsModule], providers: [SpeedTestService] }] }] }); /** * Generated bundle index. Do not edit. */ export { SpeedTestFileModel, SpeedTestModule, SpeedTestResultsModel, SpeedTestService, SpeedTestSettingsModel }; //# sourceMappingURL=ng-speed-test.mjs.map