Files
travelplanning/OFFLINE_FIRST.md

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:

  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:

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.