Offline Capabilities & Local Execution
Understanding how TelemetryOS applications run locally on devices with graceful offline handling
Offline Capabilities & Local Execution
TelemetryOS applications are designed for reliability in real-world deployment environments where internet connectivity cannot be guaranteed. Understanding the build-deploy-run architecture is essential for creating applications that gracefully handle network conditions.
Build & Deploy Architecture
Server-Side Build Process
When you push code to your Git repository, TelemetryOS performs a complete build on the server:
Developer pushes code → GitHub repository
↓
TelemetryOS build servers compile your application
↓
Build artifacts (HTML, CSS, JS, assets) created
↓
Packaged application distributed to devices
↓
Devices download and cache locally
What gets built:
- JavaScript bundles (transpiled, minified)
- CSS stylesheets (processed, optimized)
- HTML entry points
- Static assets (images, fonts, media)
- Source maps (for debugging)
Build tools respected:
- Vite, Webpack, Parcel, Rollup configurations
- npm/yarn/pnpm dependencies installed
- Custom build scripts executed
- Environment variables injected
Device Download & Local Storage
Once built, your application is downloaded to devices and stored locally:
Device filesystem:
/telemetryos/applications/your-app/
├── index.html
├── assets/
│ ├── main.js (bundled application code)
│ ├── styles.css
│ └── images/
└── manifest.json
Key characteristics:
- Complete local copy - All application files stored on device
- Versioned storage - Multiple versions cached for rollback
- Persistent across reboots - Files survive device restarts
- Update on change - New versions automatically downloaded when available
Local Execution Model
Application Runs Entirely from Device
Your application code is served from local storage, not fetched from the internet:
Device powers on
↓
TelemetryOS Player loads
↓
Playlist schedules your application
↓
Application loads from local filesystem
↓
HTML/CSS/JS served locally (FAST!)
↓
Application renders in Chrome iframe
What this means:
- ✅ Application code loads instantly (no network latency)
- ✅ Your application starts and runs without internet
- ✅ All bundled assets available immediately
- ✅ No concerns about CDN availability or DNS failures
What Works Offline
These features are fully available without internet connectivity:
Application Core
- HTML rendering and layout
- CSS styling and animations
- JavaScript execution and logic
- React/Vue/framework operations
- Event handlers and user interaction
- Canvas, WebGL, SVG graphics
TelemetryOS SDK APIs
- Storage API (all scopes)
- Platform API (device, user, account info)
- Playlist API (navigation, current page)
- Media API (TelemetryOS media library)
Browser APIs
- LocalStorage, SessionStorage
- IndexedDB
- Cache API (Service Workers)
- Web Workers
- Canvas, Audio/Video playback
Locally Cached Data
- Data stored via Storage API
- IndexedDB databases
- LocalStorage values
- Service Worker caches
What Requires Internet
These operations require network connectivity:
External API Calls
- REST API requests (
fetch(),axios) - GraphQL queries
- WebSocket connections
- Server-Sent Events (SSE)
Third-Party Services
- Weather APIs
- Payment gateways
- CRM/ERP integrations
- Social media feeds
- Real-time data sources
Cloud Resources
- External images/videos (not bundled)
- CDN-hosted libraries (if not bundled)
- Remote fonts (if not local)
- Streaming media
TelemetryOS Cloud Services
- Storage synchronization (to cloud)
- Remote device management
- Screenshot uploads
- Log aggregation
Graceful Degradation Patterns
Your application must handle network failures gracefully. Users should never see broken interfaces or cryptic errors when internet connectivity is lost.
Detecting Connectivity
Use browser APIs to detect online/offline state:
// Check current connectivity status
if (navigator.onLine) {
console.log('Device is online');
} else {
console.log('Device is offline');
}
// Listen for connectivity changes
window.addEventListener('online', () => {
console.log('Connection restored');
syncPendingData();
hideOfflineBanner();
});
window.addEventListener('offline', () => {
console.log('Connection lost');
showOfflineBanner();
pauseDataSync();
});Important: navigator.onLine detects network interface status, not actual internet connectivity. A device can report "online" but still fail to reach external services.
Handling Failed Network Requests
Always wrap external API calls with error handling:
async function fetchWeatherData(city) {
try {
const response = await fetch(
`https://api.weather.com/v1/current?city=${city}`,
{ timeout: 5000 } // Set reasonable timeout
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Cache successful response
await cacheWeatherData(city, data);
return data;
} catch (error) {
console.warn('Weather fetch failed:', error);
// Fall back to cached data
const cached = await getCachedWeatherData(city);
if (cached) {
return cached;
}
// Return safe default
return {
city,
temperature: '--',
conditions: 'Data unavailable',
offline: true
};
}
}Error handling principles:
- Catch all network errors -
fetch()throws on network failures - Set timeouts - Don't wait forever for responses
- Check HTTP status -
response.okverifies success - Fall back to cache - Use previously fetched data
- Provide safe defaults - Never leave UI in broken state
- Show offline indicators - Make offline state visible to users
UI Patterns for Offline State
Offline Banner
function OfflineBanner() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (isOnline) return null;
return (
<div className="offline-banner">
⚠️ No internet connection. Displaying cached data.
</div>
);
}Stale Data Indicators
function DataDisplay({ data, lastUpdated }) {
const isStale = Date.now() - lastUpdated > 60000; // 1 minute
return (
<div>
<h2>{data.title}</h2>
{isStale && (
<span className="stale-indicator">
Last updated: {formatTimestamp(lastUpdated)}
</span>
)}
<div>{data.content}</div>
</div>
);
}Retry Mechanisms
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
} catch (error) {
if (attempt === maxRetries) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Storage & Caching Strategies
TelemetryOS Storage API
The Storage API provides intelligent caching with automatic synchronization:
import { configure, store } from '@telemetryos/sdk';
configure('weather-app');
// Store data - automatically cached on device
await store().device.set('weatherData', {
city: 'New York',
temperature: 72,
timestamp: Date.now()
});
// Retrieve data - works offline
const cached = await store().device.get('weatherData');Storage scope characteristics:
| Scope | Offline Access | Sync to Cloud | Use Case |
|---|---|---|---|
| device | ✅ Yes | ❌ No | Device-specific cache |
| instance | ✅ Yes | ✅ Yes | Configuration values |
| application | ✅ Yes | ✅ Yes | Shared resources |
| shared(ns) | ✅ Yes | ✅ Yes | Inter-app communication |
Key benefits:
- Data persists across application restarts
- Synchronizes to cloud when online
- Conflicts resolved automatically
- No explicit "save" or "sync" calls needed
IndexedDB for Large Datasets
For large datasets, use IndexedDB directly:
// Open database
const db = await openDB('weather-cache', 1, {
upgrade(db) {
db.createObjectStore('forecasts', { keyPath: 'city' });
}
});
// Store data offline
await db.put('forecasts', {
city: 'New York',
forecast: [/* 7-day forecast */],
timestamp: Date.now()
});
// Retrieve cached data
const cached = await db.get('forecasts', 'New York');
if (cached && Date.now() - cached.timestamp < 3600000) {
// Use cached data (less than 1 hour old)
displayForecast(cached.forecast);
} else {
// Fetch fresh data
try {
const fresh = await fetchForecast('New York');
await db.put('forecasts', {
city: 'New York',
forecast: fresh,
timestamp: Date.now()
});
displayForecast(fresh);
} catch (error) {
// Fall back to stale cache
if (cached) {
displayForecast(cached.forecast, { stale: true });
}
}
}Service Workers for Advanced Caching
Implement Service Workers for request-level caching:
// service-worker.js
const CACHE_NAME = 'weather-app-v1';
const CACHE_URLS = [
'/',
'/index.html',
'/main.js',
'/styles.css'
];
// Cache on install
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(CACHE_URLS);
})
);
});
// Serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response; // Cache hit
}
// Clone the request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
// Cache successful responses
if (response && response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
}).catch(() => {
// Network failed, return offline page
return caches.match('/offline.html');
});
})
);
});Best Practices
1. Cache Aggressively
Store data locally whenever possible:
async function loadData() {
// Try cache first (instant)
const cached = await store().device.get('data');
if (cached) {
displayData(cached);
}
// Then fetch fresh data in background
try {
const fresh = await fetchFromAPI();
await store().device.set('data', fresh);
displayData(fresh);
} catch (error) {
// Already showing cached data, log error silently
console.error('Background fetch failed:', error);
}
}2. Set Reasonable Timeouts
Don't wait indefinitely for network requests:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.warn('Request timeout');
}
throw error;
}3. Implement Queue-Based Sync
Queue operations when offline, sync when online:
const pendingQueue = [];
async function saveData(data) {
// Always save locally first
await store().device.set(data.id, data);
// Try to sync to server
if (navigator.onLine) {
try {
await syncToServer(data);
} catch (error) {
pendingQueue.push(data);
}
} else {
pendingQueue.push(data);
}
}
// Sync queue when online
window.addEventListener('online', async () => {
while (pendingQueue.length > 0) {
const data = pendingQueue.shift();
try {
await syncToServer(data);
} catch (error) {
// Re-queue on failure
pendingQueue.unshift(data);
break;
}
}
});4. Show Data Freshness
Always indicate when data might be stale:
function DataTimestamp({ timestamp }) {
const age = Date.now() - timestamp;
const minutes = Math.floor(age / 60000);
let indicator;
if (minutes < 5) {
indicator = '🟢 Live';
} else if (minutes < 60) {
indicator = `🟡 ${minutes}m ago`;
} else {
indicator = `🔴 ${Math.floor(minutes / 60)}h ago`;
}
return <span className="freshness">{indicator}</span>;
}5. Test Offline Scenarios
Always test your application in offline mode:
Chrome DevTools:
- Open DevTools (F12)
- Go to Network tab
- Select "Offline" from throttling dropdown
- Verify application behavior
Automated Testing:
// Cypress test
it('handles offline mode gracefully', () => {
cy.visit('/');
// Go offline
cy.window().then(win => {
win.dispatchEvent(new Event('offline'));
});
// Verify offline banner appears
cy.contains('No internet connection').should('be.visible');
// Verify cached data still displays
cy.get('[data-testid="weather-data"]').should('exist');
});6. Bundle Critical Assets
Don't rely on CDNs for critical resources:
// ❌ Bad - CDN dependency
<script src="https://cdn.example.com/library.js"></script>
// ✅ Good - bundled dependency
import library from 'library'; // Bundled via npm7. Provide Offline Fallbacks
Design UI components with offline states:
function WeatherDisplay() {
const [weather, setWeather] = useState(null);
const [isOffline, setIsOffline] = useState(!navigator.onLine);
if (isOffline && !weather) {
return (
<div className="offline-state">
<Icon name="cloud-off" />
<h3>Unable to load weather data</h3>
<p>Check your internet connection and try again.</p>
</div>
);
}
return (
<div className="weather-data">
{isOffline && <OfflineBadge />}
{/* Weather data display */}
</div>
);
}Common Patterns
Cache-First Strategy
Load from cache immediately, update in background:
async function loadWithCacheFirst(key, fetchFn) {
// 1. Load from cache immediately
const cached = await store().device.get(key);
if (cached) {
displayData(cached.data);
}
// 2. Fetch fresh data in background
try {
const fresh = await fetchFn();
await store().device.set(key, {
data: fresh,
timestamp: Date.now()
});
displayData(fresh);
} catch (error) {
// Keep showing cached data
if (!cached) {
showErrorState();
}
}
}Network-First Strategy
Try network first, fall back to cache:
async function loadWithNetworkFirst(key, fetchFn) {
try {
// 1. Try network first
const fresh = await fetchFn();
await store().device.set(key, {
data: fresh,
timestamp: Date.now()
});
return fresh;
} catch (error) {
// 2. Fall back to cache
console.warn('Network failed, using cache:', error);
const cached = await store().device.get(key);
if (cached) {
return cached.data;
}
throw new Error('No data available');
}
}Stale-While-Revalidate
Show stale data, update when fresh data arrives:
async function loadWithSWR(key, fetchFn, maxAge = 300000) {
// 1. Load cached data
const cached = await store().device.get(key);
const isStale = !cached || (Date.now() - cached.timestamp > maxAge);
// 2. Display cached data immediately if available
if (cached) {
displayData(cached.data, { stale: isStale });
}
// 3. Revalidate if stale
if (isStale) {
try {
const fresh = await fetchFn();
await store().device.set(key, {
data: fresh,
timestamp: Date.now()
});
displayData(fresh, { stale: false });
} catch (error) {
// Keep showing cached data
console.error('Revalidation failed:', error);
}
}
}Deployment Considerations
Application Updates
When you push code changes:
- TelemetryOS builds new version on server
- Devices download update when online
- Update applied at next application restart
- Old version cached for rollback if needed
Update behavior:
- Updates happen automatically in the background
- Current session continues uninterrupted
- New version loads next time application starts
- No user action required
Version Rollback
If an update causes issues, rollback to previous version:
Studio → Applications → Your App → Versions → Rollback
Rollback is instant - devices immediately revert to previous working version.
Testing Updates
Test updates before fleet-wide deployment:
- Branch deployment - Deploy to test devices first
- Canary deployment - Roll out to subset of devices
- Monitor logs - Watch for errors in Studio
- Screenshot verification - Visual confirmation of correct rendering
Troubleshooting
Application Won't Load Offline
Symptoms: Application shows "Unable to load" when internet is disconnected.
Causes:
- External dependencies not bundled (CDN resources)
- Service Worker misconfiguration
- HTML/CSS/JS not properly cached
Solutions:
- Bundle all dependencies:
npm install library(not CDN links) - Verify build output includes all assets
- Check browser console for failed resource loads
- Test with DevTools offline mode before deployment
Data Not Persisting
Symptoms: Data disappears when application restarts.
Causes:
- Using SessionStorage instead of LocalStorage
- Not using Storage API properly
- Clearing cache on restart
Solutions:
- Use Storage API:
store().device.set(key, value) - Verify data is saved:
const data = await store().device.get(key) - Check browser storage limits (IndexedDB: ~50-100MB typical)
Slow Application Performance
Symptoms: Application loads slowly even with internet connection.
Causes:
- Large bundle sizes
- Too many external API calls on startup
- Blocking network requests
Solutions:
- Optimize bundle size (code splitting, tree shaking)
- Load critical data first, defer non-critical requests
- Use async/await, avoid blocking operations
- Implement loading states for better perceived performance
Key Takeaways
-
Applications run entirely from device storage - HTML/CSS/JS served locally, no internet required for core functionality
-
Build happens on TelemetryOS servers - You push code, TelemetryOS builds and distributes to devices
-
Network requests require internet - External APIs, third-party services, and cloud resources need connectivity
-
Graceful degradation is essential - Handle network failures, show cached data, indicate offline state
-
Storage API provides intelligent caching - Use device scope for local cache, automatic sync when online
-
Test offline scenarios thoroughly - Use DevTools offline mode, verify fallback behavior
-
Bundle critical dependencies - Don't rely on CDNs for essential resources
-
Implement retry and queue mechanisms - Sync data when connection is restored
Next Steps
- Application Creation - Learn how to create and deploy applications
- Storage Methods - Complete storage methods reference
- Local Development - Test applications locally before deployment
- Code Examples - See complete offline handling patterns
Ready to build offline-capable applications? Start with the CLI and implement graceful degradation from day one:
npm install -g @telemetryos/cli
tos init my-offline-app
cd my-offline-app
tos serveUpdated 5 days ago