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:

  1. Catch all network errors - fetch() throws on network failures
  2. Set timeouts - Don't wait forever for responses
  3. Check HTTP status - response.ok verifies success
  4. Fall back to cache - Use previously fetched data
  5. Provide safe defaults - Never leave UI in broken state
  6. 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:

ScopeOffline AccessSync to CloudUse Case
device✅ Yes❌ NoDevice-specific cache
instance✅ Yes✅ YesConfiguration values
application✅ Yes✅ YesShared resources
shared(ns)✅ Yes✅ YesInter-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:

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Select "Offline" from throttling dropdown
  4. 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 npm

7. 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:

  1. TelemetryOS builds new version on server
  2. Devices download update when online
  3. Update applied at next application restart
  4. 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:

  1. Branch deployment - Deploy to test devices first
  2. Canary deployment - Roll out to subset of devices
  3. Monitor logs - Watch for errors in Studio
  4. 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:

  1. Bundle all dependencies: npm install library (not CDN links)
  2. Verify build output includes all assets
  3. Check browser console for failed resource loads
  4. 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:

  1. Use Storage API: store().device.set(key, value)
  2. Verify data is saved: const data = await store().device.get(key)
  3. 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:

  1. Optimize bundle size (code splitting, tree shaking)
  2. Load critical data first, defer non-critical requests
  3. Use async/await, avoid blocking operations
  4. Implement loading states for better perceived performance

Key Takeaways

  1. Applications run entirely from device storage - HTML/CSS/JS served locally, no internet required for core functionality

  2. Build happens on TelemetryOS servers - You push code, TelemetryOS builds and distributes to devices

  3. Network requests require internet - External APIs, third-party services, and cloud resources need connectivity

  4. Graceful degradation is essential - Handle network failures, show cached data, indicate offline state

  5. Storage API provides intelligent caching - Use device scope for local cache, automatic sync when online

  6. Test offline scenarios thoroughly - Use DevTools offline mode, verify fallback behavior

  7. Bundle critical dependencies - Don't rely on CDNs for essential resources

  8. Implement retry and queue mechanisms - Sync data when connection is restored

Next Steps


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 serve

What’s Next