Offline

Understanding how TelemetryOS applications run locally on devices with graceful offline handling

Offline

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 code pushes to a Git repository, TelemetryOS performs a complete build on the server:

Developer pushes code → GitHub repository
     ↓
TelemetryOS build servers compile the application
     ↓
Build artifacts (HTML, CSS, JS, assets) created
     ↓
Packaged application distributed to devices
     ↓
Devices download and cache locally

The build process produces all the artifacts needed for the application to run independently on a device. This includes JavaScript bundles (transpiled and minified), CSS stylesheets (processed and optimized), HTML entry points, static assets such as images, fonts, and media files, and source maps for debugging.

TelemetryOS respects your existing build tooling throughout this process. Vite, Webpack, Parcel, and Rollup configurations are all honored. Dependencies from npm, yarn, or pnpm are installed, custom build scripts are executed, and environment variables are injected into the final output.

Device Download & Local Storage

Once built, the application is downloaded to devices and stored locally:

Device filesystem:
/telemetryos/applications/example-app/
  ├── index.html
  ├── assets/
  │   ├── main.js (bundled application code)
  │   ├── styles.css
  │   └── images/
  └── manifest.json

Every device receives a complete local copy of all application files. These files are versioned, so multiple versions can be cached for rollback if needed. The local copy persists across reboots, meaning files survive device restarts without re-downloading. When a new version becomes available, the device automatically downloads the update.

Local Execution Model

Application Runs Entirely from Device

Application code is served from local storage, not fetched from the internet:

Device powers on
  ↓
TelemetryOS Player loads
  ↓
Playlist schedules the 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)
  • ✅ The 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.

Your entire application core runs locally: HTML rendering and layout, CSS styling and animations, all JavaScript execution and logic, and any framework operations from React, Vue, or others. Event handlers, user interaction, and graphics through Canvas, WebGL, or SVG all function without a network connection.

The TelemetryOS SDK APIs also work fully offline. This includes the Storage API across all scopes, the Platform API for device, user, and account information, the Playlist API for navigation and current page data, and the Media API for accessing the TelemetryOS media library.

Standard browser APIs and locally cached data are equally available. You can use LocalStorage, SessionStorage, IndexedDB, the Cache API with Service Workers, Web Workers, and Canvas or Audio/Video playback. Any data you have previously stored through the Storage API, IndexedDB databases, LocalStorage values, or Service Worker caches remains accessible regardless of connectivity.

What Requires Internet

These operations require network connectivity.

Any call to an external service needs an active internet connection. This covers REST API requests via fetch() or axios, GraphQL queries, WebSocket connections, and Server-Sent Events. Third-party service integrations such as weather APIs, payment gateways, CRM/ERP systems, social media feeds, and real-time data sources all depend on network availability.

Cloud-hosted resources that were not bundled with your application also require connectivity. External images and videos, CDN-hosted libraries, remote fonts, and streaming media will fail to load when the device is offline.

Finally, certain TelemetryOS cloud services need internet access to function: storage synchronization to the cloud, remote device management, screenshot uploads, and log aggregation all communicate with TelemetryOS servers and cannot operate in isolation.

Graceful Degradation Patterns

Applications 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
    };
  }
}

Good error handling follows a consistent set of principles. Always catch network errors, since fetch() throws on network failures. Set timeouts so your application does not wait indefinitely for a response. Check the HTTP status with response.ok to verify success before consuming the body. When a request fails, fall back to previously cached data so the user still sees something useful. Provide safe defaults to ensure the UI is never left in a broken state, and show offline indicators so users understand when they are viewing stale or unavailable data.

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();

// 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 applications in offline mode:

Chrome DevTools:

Offline testing is available through DevTools (F12) > Network tab > "Offline" throttling mode. This simulates network disconnection for verifying 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.jsdelivr.net/npm/[email protected]/lodash.min.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 the 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 code changes are pushed, TelemetryOS builds the new version on its servers and distributes it to devices. Each device downloads the update when it is online, and the update is applied the next time the application restarts. The old version remains cached on the device so it can be used as a rollback if anything goes wrong.

Updates happen automatically in the background without interrupting the current session. The new version loads the next time the application starts, and no user action is required.

Version Rollback

If an update causes issues, rollback to previous version:

Studio → Applications → [App Name] → Versions → Rollback

Rollback is instant - devices immediately revert to previous working version.

Testing Updates

Before deploying an update across your entire fleet, validate it in stages. Start with a branch deployment to a set of test devices, then use a canary deployment to roll it out to a small subset of production devices. Monitor the logs in Studio for errors during the rollout, and use screenshot verification for visual confirmation that the application is rendering correctly.

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

TelemetryOS applications run entirely from device storage. All HTML, CSS, and JavaScript is served locally, so no internet connection is required for core functionality. The build process happens on TelemetryOS servers -- when you push code, the platform compiles your application and distributes it to devices automatically.

While the application itself runs offline, any external network requests still require internet connectivity. This includes calls to third-party APIs, cloud-hosted resources that are not bundled with the application, and TelemetryOS cloud services like storage synchronization and log aggregation. Graceful degradation is therefore essential: applications should handle network failures cleanly, fall back to cached data, and clearly indicate offline state to users.

The TelemetryOS Storage API is your primary tool for intelligent caching, providing automatic synchronization when online and reliable local access when offline. Pair this with sensible retry and queue mechanisms to sync data once connectivity is restored, bundle all critical dependencies so you never rely on CDNs for essential resources, and test offline scenarios thoroughly using DevTools before deploying to production devices.

Next Steps


What’s Next