7.7 KiB
To AI Agent: Implement Offline-First Architecture for Mobile App & Web Browsers (Offline Photo Upload & Offline Map Routing)
1. Context & Architectural Strategy
We are implementing an Offline-First Capabilities Layer for both browser environments (Chrome Android, iOS Safari) and mobile application wrappers. The application must remain functional when network connectivity is lost (Network = 0).
Core Requirements:
- Offline Photo Upload Queue: When a user uploads a photo without an active internet connection, the system must not throw a network exception. Instead, it must store the image file (Blob/ArrayBuffer) and its accompanying metadata (GPS, Timestamp) into a browser-native transactional database (IndexedDB).
- Background Sync Resume: As soon as the device regains cellular/Wi-Fi data telemetry, a background synchronization worker must automatically trigger, reading the IndexedDB queue and completing the multi-part upload streams to the Debian server sequentially.
- Offline Map Navigation Routing: Cache critical operational Map Layout Tiles and routing geometries via a persistent client-side caching framework (Service Workers + Cache API).
2. Technical Offline System Stack Overview
[Offline User Interaction] │ ├──➔ (Photo Upload) ──➔ Save Blob & EXIF Metadata ──➔ [ IndexedDB Storage ] │ │ (Device re-connects) │ ▼ │ [ Background Sync ] ──➔ POST to Server │ └──➔ (Map Routing) ──➔ Request Map Assets ──➔ [ Service Worker Cache API ] ──➔ Render View
3. Code Refactoring Blueprint
Step 1: Implement Offline Storage Queue Service (offlineQueue.ts)
Create a localized database management layer at frontend/src/utils/offlineQueue.ts leveraging IndexedDB to hold pending image uploads:
import { openDB, IDBPDatabase } from 'idb';
const DB_NAME = 'yotrip-offline-db';
const STORE_NAME = 'pending-uploads';
interface OfflinePhoto {
id: string;
fileBlob: Blob;
fileName: string;
latitude: string | null;
longitude: string | null;
capturedAt: string | null;
timestamp: number;
}
// Initialize the local IndexedDB container safely
const getDB = (): Promise<IDBPDatabase> => {
return openDB(DB_NAME, 1, {
upgrade(db) {
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
},
});
};
// 1. STASH UPLOAD METADATA INTO OFFLINE STORAGE
export const queueOfflineUpload = async (photoData: Omit<OfflinePhoto, 'id' | 'timestamp'>) => {
const db = await getDB();
const id = crypto.randomUUID();
const item: OfflinePhoto = {
...photoData,
id,
timestamp: Date.now(),
};
await db.put(STORE_NAME, item);
console.log(`📦 Photo [${id}] stashed safely into local IndexedDB queue for offline background sync.`);
return id;
};
// 2. RETRIEVE ALL PENDING FILES ONCE ONLINE
export const getPendingUploads = async (): Promise<OfflinePhoto[]> => {
const db = await getDB();
return db.getAll(STORE_NAME);
};
// 3. REMOVE COMPLETED TRANSACTION FROM QUEUE
export const removePendingUpload = async (id: string) => {
const db = await getDB();
await db.delete(STORE_NAME, id);
};
### Step 2: Implement Background Synchronization Sync Engine (backgroundSync.ts)
Create an active network monitoring service that intercepts reconnection signals and synchronizes the local database records:
import { getPendingUploads, removePendingUpload } from './offlineQueue';
import { api } from '../services/api';
export const executeBackgroundSyncEngine = async (refreshMapPins: () => void) => {
const pendingItems = await getPendingUploads();
if (pendingItems.length === 0) return;
console.log(`🔄 Internet restored! Syncing [${pendingItems.length}] pending items to yotrip.labz.io.vn...`);
for (const item of pendingItems) {
const formData = new FormData();
formData.append('photo', item.fileBlob, item.fileName);
if (item.latitude && item.longitude) {
formData.append('latitude', item.latitude);
formData.append('longitude', item.longitude);
}
if (item.capturedAt) {
formData.append('capturedAt', item.capturedAt);
}
try {
// Dispatch payload to Debian production server endpoint
const response = await api.post('/photos/upload-with-meta', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if (response.status === 200 || response.status === 201) {
// Remove item from local DB once server confirms safe receipt
await removePendingUpload(item.id);
console.log(`✅ Offline photo [${item.id}] synchronized successfully.`);
}
} catch (error) {
console.error(`✖ Failed to synchronize item [${item.id}]. Will retry on next connectivity cycle:`, error);
break; // Stop loop if server goes down mid-transit to protect execution queues
}
}
// Trigger reactive map layer update
refreshMapPins();
};
// Global Connectivity Listener initialization hook
export const initNetworkStatusListeners = (refreshMapPins: () => void) => {
window.addEventListener('online', () => executeBackgroundSyncEngine(refreshMapPins));
// Guard check on application startup in case network recovered while app was closed
if (navigator.onLine) {
executeBackgroundSyncEngine(refreshMapPins);
}
};
### Step 3: Configure Service Worker for Offline Map Tiles Cache (sw.js)
Configure your service worker script block (e.g., public/sw.js) to intercept and cache vector navigation maps or stylesheet assets statically:
const CACHE_NAME = 'yotrip-static-map-v1';
const MAP_TILE_PATTERN = /tiles\.maps\.lincoln|openstreetmap|mapbox/;
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/src/main.tsx',
'/manifest.json',
'/assets/offline-map-placeholder.png' // Fallback image asset
]);
})
);
});
// NETWORK-FIRST FALLBACK TO CACHE STRATEGY FOR OFFLINE ROUTING MAPS
self.addEventListener('fetch', (event) => {
const requestUrl = event.request.url;
if (MAP_TILE_PATTERN.test(requestUrl)) {
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone and update cache with newly acquired dynamic tile data layers
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// If offline, serve map tiles seamlessly straight from client-side Cache API
return caches.match(event.request);
})
);
}
});
## 4. Quality Verification & Acceptance Criteria for AI Agent[ ] Airplane Mode Upload Stability: Toggle Airplane mode ($Network = 0$) inside the Android Emulator/iOS Safari responsive inspector. Upload an image. The UI must transition seamlessly, displaying an "Ảnh đã được lưu tạm ngoại tuyến" notification banner without runtime exceptions.[ ] Automatic Queue Flushing Check: Disable Airplane mode. Verify through the browser network inspector tool that a series of asynchronous multi-part POST network requests fire automatically towards https://yotrip.labz.io.vn/api/ without requiring user interaction.[ ] Offline Map Cache Test: Clear your browser network connectivity states. Navigate around the map interface canvas. Previously inspected map sections and routing data blocks must remain fully rendered from the Service Worker cache layer.