151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import { ServiceWorker } from './classes.serviceworker.js';
|
|
import { logger } from './logging.js';
|
|
|
|
export class NetworkManager {
|
|
public serviceWorkerRef: ServiceWorker;
|
|
public webRequest: plugins.webrequest.WebrequestClient;
|
|
private isOffline: boolean = false;
|
|
private lastOnlineCheck: number = 0;
|
|
private readonly ONLINE_CHECK_INTERVAL = 30000; // 30 seconds
|
|
|
|
public previousState: string;
|
|
|
|
constructor(serviceWorkerRefArg: ServiceWorker) {
|
|
this.serviceWorkerRef = serviceWorkerRefArg;
|
|
this.webRequest = new plugins.webrequest.WebrequestClient();
|
|
|
|
// Listen for connection changes
|
|
this.getConnection()?.addEventListener('change', () => {
|
|
this.updateConnectionStatus();
|
|
});
|
|
|
|
// Listen for online/offline events
|
|
self.addEventListener('online', () => {
|
|
this.isOffline = false;
|
|
logger.log('info', 'Device is now online');
|
|
this.updateConnectionStatus();
|
|
});
|
|
|
|
self.addEventListener('offline', () => {
|
|
this.isOffline = true;
|
|
logger.log('warn', 'Device is now offline');
|
|
this.updateConnectionStatus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* gets the connection
|
|
*/
|
|
public getConnection() {
|
|
const navigatorLocal: any = self.navigator;
|
|
return navigatorLocal?.connection;
|
|
}
|
|
|
|
public getEffectiveType() {
|
|
return this.getConnection()?.effectiveType || '4g';
|
|
}
|
|
|
|
public updateConnectionStatus() {
|
|
const currentType = this.getEffectiveType();
|
|
logger.log('info', `Connection type changed from ${this.previousState} to ${currentType}`);
|
|
this.previousState = currentType;
|
|
}
|
|
|
|
/**
|
|
* Checks if the device is currently online by attempting to contact the server
|
|
* @returns Promise<boolean> true if online, false if offline
|
|
*/
|
|
public async checkOnlineStatus(): Promise<boolean> {
|
|
const now = Date.now();
|
|
// Only check if enough time has passed since last check
|
|
if (now - this.lastOnlineCheck < this.ONLINE_CHECK_INTERVAL) {
|
|
return !this.isOffline;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/typedrequest', {
|
|
method: 'HEAD',
|
|
cache: 'no-cache'
|
|
});
|
|
this.isOffline = false;
|
|
this.lastOnlineCheck = now;
|
|
return true;
|
|
} catch (error) {
|
|
this.isOffline = true;
|
|
this.lastOnlineCheck = now;
|
|
logger.log('warn', 'Device appears to be offline');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes a network request with offline handling
|
|
* @param request The request to make
|
|
* @param options Additional options
|
|
* @returns Promise<Response>
|
|
*/
|
|
public async makeRequest<T>(request: Request | string, options: {
|
|
timeoutMs?: number;
|
|
retries?: number;
|
|
backoffMs?: number;
|
|
} = {}): Promise<Response> {
|
|
const {
|
|
timeoutMs = 5000,
|
|
retries = 1,
|
|
backoffMs = 1000
|
|
} = options;
|
|
|
|
let lastError: Error | unknown;
|
|
for (let i = 0; i <= retries; i++) {
|
|
let timeoutId: number | undefined;
|
|
const controller = new AbortController();
|
|
|
|
try {
|
|
const isOnline = await this.checkOnlineStatus();
|
|
if (!isOnline) {
|
|
throw new Error('Device is offline');
|
|
}
|
|
|
|
// Set up timeout
|
|
timeoutId = setTimeout(() => controller.abort(), timeoutMs) as unknown as number;
|
|
|
|
const response = await fetch(request, {
|
|
...typeof request === 'string' ? {} : request,
|
|
signal: controller.signal
|
|
});
|
|
|
|
// Clear timeout on successful response
|
|
clearTimeout(timeoutId);
|
|
return response;
|
|
} catch (error) {
|
|
// Always clear timeout, even on error
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
|
|
lastError = error;
|
|
logger.log('warn', `Request attempt ${i+1}/${retries+1} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
// Check if this was an abort error (timeout)
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
logger.log('warn', `Request timed out after ${timeoutMs}ms`);
|
|
}
|
|
|
|
// Retry with backoff if we have retries left
|
|
if (i < retries) {
|
|
const backoffTime = backoffMs * (i + 1);
|
|
logger.log('info', `Retrying in ${backoffTime}ms...`);
|
|
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert lastError to Error if it isn't already
|
|
const finalError = lastError instanceof Error
|
|
? lastError
|
|
: new Error(typeof lastError === 'string' ? lastError : 'Unknown error during request');
|
|
|
|
throw finalError;
|
|
}
|
|
} |