offline-detector
Version:
A lightweight TypeScript library for detecting online/offline status in browsers with modern bundler support. Minimal dependencies, tree-shakable, and works with any bundler.
222 lines (216 loc) • 6.35 kB
JavaScript
function createDebounce(options) {
const { delay, callback } = options;
let timer = null;
return {
call() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(callback, delay);
},
cancel() {
if (timer) {
clearTimeout(timer);
timer = null;
}
},
};
}
function createNativeEventHandlers(handlers) {
const { onOnline, onOffline } = handlers;
const handleOnline = () => {
onOnline();
};
const handleOffline = () => {
onOffline();
};
return {
addListeners() {
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
},
removeListeners() {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
},
};
}
async function testConnectivity(options) {
const { testUrl, timeout } = options;
try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeout);
});
const fetchPromise = fetch(testUrl, {
method: 'HEAD',
mode: 'no-cors',
cache: 'no-cache',
});
await Promise.race([fetchPromise, timeoutPromise]);
return true;
}
catch {
return false;
}
}
function createPolling(options) {
const { interval, callback } = options;
let timer = null;
return {
start() {
if (timer)
return;
timer = setInterval(callback, interval);
},
stop() {
if (timer) {
clearInterval(timer);
timer = null;
}
},
};
}
function createOfflineDetector(options = {}) {
const { onOnline, onOffline, stateChangeDebounceDelay = 1000, networkVerification = {}, nativeEvents: nativeEventsConfig = {}, } = options;
const { enabled: useNetworkTest = true, url: testUrl = 'https://www.google.com/favicon.ico', requestTimeout: timeout = 5000, interval: checkInterval = 60000, maxFailures: failureThreshold = 3, } = networkVerification;
const { enabled: useNativeEvents = true } = nativeEventsConfig;
let isListening = false;
let isOnline = true;
let consecutiveFailures = 0;
const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';
if (!isBrowser) {
throw new Error('OfflineDetector can only be used in browser environments');
}
const debounceAction = createDebounce({
delay: stateChangeDebounceDelay,
callback: () => {
if (isOnline) {
onOnline?.();
}
else {
onOffline?.();
}
},
});
const handleStateChange = (newState) => {
if (isOnline === newState)
return;
const wasOnline = isOnline;
isOnline = newState;
if (useNetworkTest && isListening) {
if (newState && !wasOnline) {
offlinePolling.stop();
polling.start();
}
else if (!newState && wasOnline) {
polling.stop();
offlinePolling.start();
}
}
debounceAction.call();
};
const verifyConnectivity = async () => {
if (!useNetworkTest) {
return navigator.onLine;
}
try {
const isConnected = await testConnectivity({
testUrl,
timeout,
});
return isConnected;
}
catch {
return false;
}
};
const performConnectivityCheck = async () => {
if (!isListening)
return;
const isConnected = await verifyConnectivity();
if (isConnected) {
consecutiveFailures = 0;
if (!isOnline) {
handleStateChange(true);
}
}
else {
if (isOnline) {
consecutiveFailures++;
if (consecutiveFailures >= failureThreshold) {
handleStateChange(false);
}
}
}
};
const nativeEvents = createNativeEventHandlers({
onOnline: () => {
if (!useNativeEvents)
return;
if (useNetworkTest) {
verifyConnectivity().then(isActuallyOnline => {
if (isActuallyOnline) {
consecutiveFailures = 0;
handleStateChange(true);
}
});
}
else {
consecutiveFailures = 0;
handleStateChange(true);
}
},
onOffline: () => {
if (!useNativeEvents)
return;
handleStateChange(false);
},
});
const polling = createPolling({
interval: checkInterval,
callback: performConnectivityCheck,
});
const offlinePolling = createPolling({
interval: Math.min(checkInterval / 4, 15000),
callback: performConnectivityCheck,
});
return {
start() {
if (isListening)
return;
isListening = true;
isOnline = navigator.onLine;
if (useNativeEvents) {
nativeEvents.addListeners();
}
if (useNetworkTest) {
if (isOnline) {
polling.start();
}
else {
offlinePolling.start();
}
performConnectivityCheck();
}
},
stop() {
if (!isListening)
return;
isListening = false;
if (useNativeEvents) {
nativeEvents.removeListeners();
}
polling.stop();
offlinePolling.stop();
debounceAction.cancel();
},
isOnline() {
return isOnline;
},
destroy() {
this.stop();
},
};
}
export { createOfflineDetector };
//# sourceMappingURL=index.js.map