Skip to main content

Offline Support Examples

Examples of building offline-first applications with Strata Storage.

Basic Offline Detection

import { Strata } from 'strata-storage';

class OfflineManager {
private storage: Strata;
private isOnline = navigator.onLine;

constructor() {
this.storage = new Strata();
this.setupListeners();
}

private setupListeners() {
window.addEventListener('online', () => {
this.isOnline = true;
this.onOnline();
});

window.addEventListener('offline', () => {
this.isOnline = false;
this.onOffline();
});
}

private async onOnline() {
console.log('Back online - syncing data...');
await this.syncPendingChanges();
}

private async onOffline() {
console.log('Gone offline - using local storage');
await this.storage.set('offline-since', Date.now());
}
}

Offline Queue

class OfflineQueue {
private storage: Strata;
private queueKey = 'offline-queue';

async add(action: QueueAction) {
const queue = await this.getQueue();
queue.push({
...action,
id: generateId(),
timestamp: Date.now()
});

await this.storage.set(this.queueKey, queue);
}

async process() {
const queue = await this.getQueue();
const pending = [...queue];
const failed: QueueAction[] = [];

for (const action of pending) {
try {
await this.executeAction(action);
} catch (error) {
console.error('Failed to process:', action, error);
failed.push(action);
}
}

// Keep only failed actions in queue
await this.storage.set(this.queueKey, failed);

return {
processed: pending.length - failed.length,
failed: failed.length
};
}

private async executeAction(action: QueueAction) {
switch (action.type) {
case 'api-call':
return fetch(action.url, action.options);
case 'sync-data':
return this.syncData(action.data);
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
}

Service Worker Integration

// service-worker.js
const storage = new Strata({
namespace: 'sw-cache',
defaultStorages: ['cache', 'indexedDB']
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(async (response) => {
if (response) {
return response;
}

try {
const networkResponse = await fetch(event.request);

// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open('v1');
cache.put(event.request, networkResponse.clone());

// Store metadata
await storage.set(`cache:${event.request.url}`, {
cached: Date.now(),
size: networkResponse.headers.get('content-length')
});
}

return networkResponse;
} catch (error) {
// Offline - try to serve from cache
const cached = await storage.get(`fallback:${event.request.url}`);
if (cached) {
return new Response(cached.data, cached.options);
}

throw error;
}
})
);
});

Conflict Resolution

class ConflictResolver {
private storage: Strata;

async sync(localData: any, remoteData: any) {
const conflicts: Conflict[] = [];

// Compare timestamps
if (localData.updatedAt > remoteData.updatedAt) {
// Local is newer
return this.resolveLocalWins(localData, remoteData);
} else if (remoteData.updatedAt > localData.updatedAt) {
// Remote is newer
return this.resolveRemoteWins(localData, remoteData);
} else {
// Same timestamp - need manual resolution
conflicts.push({
key: localData.id,
local: localData,
remote: remoteData
});
}

if (conflicts.length > 0) {
await this.storage.set('conflicts', conflicts);
return this.mergeData(localData, remoteData);
}

return localData;
}

private mergeData(local: any, remote: any) {
// Custom merge strategy
return {
...remote,
...local,
merged: true,
mergedAt: Date.now()
};
}
}

Progressive Data Loading

class ProgressiveLoader {
private storage: Strata;

async loadData(key: string) {
// 1. Check memory cache
const cached = await this.storage.get(key, { storage: 'memory' });
if (cached) return { data: cached, source: 'memory' };

// 2. Check local storage
const local = await this.storage.get(key, { storage: 'indexedDB' });
if (local) {
// Background refresh if stale
if (this.isStale(local)) {
this.refreshInBackground(key);
}
return { data: local, source: 'local' };
}

// 3. Fetch from network
try {
const fresh = await this.fetchFromNetwork(key);
await this.storage.set(key, fresh);
return { data: fresh, source: 'network' };
} catch (error) {
// 4. Use stale data if available
const stale = await this.storage.get(`stale:${key}`);
if (stale) {
return { data: stale, source: 'stale', warning: 'Using stale data' };
}

throw error;
}
}
}

Background Sync

class BackgroundSync {
private storage: Strata;

async register(tag: string, data: any) {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const reg = await navigator.serviceWorker.ready;

// Store data for sync
await this.storage.set(`sync:${tag}`, data);

// Register background sync
await reg.sync.register(tag);
} else {
// Fallback to periodic sync
this.fallbackSync(tag, data);
}
}

private fallbackSync(tag: string, data: any) {
const attemptSync = async () => {
if (navigator.onLine) {
try {
await this.performSync(tag, data);
await this.storage.remove(`sync:${tag}`);
} catch (error) {
// Retry later
setTimeout(attemptSync, 30000);
}
} else {
// Check again in 10 seconds
setTimeout(attemptSync, 10000);
}
};

attemptSync();
}
}

Offline Analytics

class OfflineAnalytics {
private storage: Strata;
private batchSize = 50;

async track(event: AnalyticsEvent) {
const events = await this.storage.get('analytics-queue') || [];
events.push({
...event,
timestamp: Date.now(),
offline: !navigator.onLine
});

await this.storage.set('analytics-queue', events);

// Try to flush if online
if (navigator.onLine && events.length >= this.batchSize) {
this.flush();
}
}

async flush() {
const events = await this.storage.get('analytics-queue') || [];
if (events.length === 0) return;

try {
await fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(events)
});

// Clear queue on success
await this.storage.remove('analytics-queue');
} catch (error) {
console.error('Failed to flush analytics:', error);
}
}
}

Data Prefetching

class DataPrefetcher {
private storage: Strata;
private prefetchQueue = new Set<string>();

async prefetch(urls: string[]) {
for (const url of urls) {
if (!this.prefetchQueue.has(url)) {
this.prefetchQueue.add(url);
this.prefetchUrl(url);
}
}
}

private async prefetchUrl(url: string) {
try {
const response = await fetch(url);
const data = await response.json();

await this.storage.set(`prefetch:${url}`, data, {
ttl: 3600000 // 1 hour
});
} catch (error) {
console.error(`Failed to prefetch ${url}:`, error);
} finally {
this.prefetchQueue.delete(url);
}
}

async get(url: string) {
// Check prefetched data first
const prefetched = await this.storage.get(`prefetch:${url}`);
if (prefetched) return prefetched;

// Fetch normally
const response = await fetch(url);
return response.json();
}
}

See Also