182 lines
7.7 KiB
Markdown
182 lines
7.7 KiB
Markdown
# 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:
|
|
1. **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**).
|
|
2. **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.
|
|
3. **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:
|
|
|
|
```typescript
|
|
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.
|
|
|