OFFLINE FIRST IMPLEMENTATION GUIDE

Documentation for Plans

Offline-First Architecture - Complete Implementation Guide

πŸ“‹ Table of Contents

  1. Executive Summary
  2. Current Problems
  3. Target Architecture
  4. Files to Create
  5. Files to Modify
  6. Implementation Steps
  7. Testing Strategy
  8. Migration Plan
  9. Performance Metrics

πŸ“Š Executive Summary

The Goal

Transform the application from backend-first to offline-first architecture for 50-60x performance improvement while maintaining full data consistency between IndexedDB (local) and MongoDB (cloud).

The Problem

The Solution

Impact


πŸ”΄ Current Problems

Problem 1: Dual Architecture (Inconsistent)

File: libs/core-angular/src/lib/services/space.service.ts

// ❌ CURRENT BAD CODE
if (this._isOnline() && this.spaceApi.isConnected()) {
  // Backend-first mode (SLOW - 200-500ms)
  const space = await this.initializeFromApi(userId, spaceSlug);
  return space;
} else {
  // Offline mode (FAST - 5-10ms)
  const space = await this.initializeFromLocal(userId, spaceSlug);
  return space;
}

Why This is Bad:

  1. Two completely different code paths
  2. API mode always hits backend (slow!)
  3. Offline mode never syncs (data stuck locally)
  4. Switching between modes = data mismatch

Problem 2: No Sync Engine

Current State:

Result:


Problem 3: Mock Mode vs Real Mode Split

File: apps/gograb-space/src/main.ts

// ❌ CURRENT BAD CODE
provideHttpClient(
  withInterceptors(
    environment.useMockApi
      ? [mockHttpInterceptor, authInterceptor] // Completely different path
      : [authInterceptor] // Completely different path
  )
)

Why This is Bad:

  1. Mock mode data lives only in IndexedDB
  2. Real mode data lives in MongoDB
  3. No bridge between them
  4. Switching modes = starting from scratch

βœ… Target Architecture

The New Flow (Unified, Fast, Reliable)

// βœ… NEW UNIFIED FLOW

// EVERY OPERATION (Read, Write, Delete):
1. Execute on IndexedDB FIRST (5-10ms) ⚑ INSTANT
2. Update UI immediately (user sees result)
3. Queue operation for sync (non-blocking)
4. Background sync to MongoDB when online

// BACKGROUND SYNC (Happens automatically):
1. Check if online and sync enabled
2. Push queued local changes β†’ MongoDB
3. Pull remote changes β†’ IndexedDB
4. Resolve conflicts (local wins by default)
5. Update timestamps, clear queue

// MODE SWITCHING:
- Mock Mode: sync disabled, everything stays local
- Real Mode: sync enabled, background sync active
- Switching: just toggle sync flag, data preserved βœ…

πŸ“ Files to Create

1. libs/core-angular/src/lib/services/sync-engine.service.ts

Purpose: Central sync engine that handles all data synchronization

Location: libs/core-angular/src/lib/services/sync-engine.service.ts

Full Implementation:

import { Injectable, inject, signal, computed } from '@angular/core';
import { GgStorage } from '@storage-system';
import { LoggerService } from '@libs-core/log-system';
import { SpaceApiService } from './space-api.service';
import { autoFlashState } from '@storage-system';
import { firstValueFrom } from 'rxjs';

/**
 * Sync operation types
 */
export type SyncOperation = {
  id: string;
  operation: 'create' | 'update' | 'delete';
  entity: 'space' | 'universe' | 'page' | 'flow' | 'module' | 'widget';
  key: string;
  data?: any;
  timestamp: number;
  retries: number;
};

/**
 * Sync configuration
 */
export interface SyncConfig {
  enabled: boolean;
  interval: number; // milliseconds
  batchSize: number;
  retryAttempts: number;
  retryDelay: number;
  conflictResolution: 'local-wins' | 'remote-wins' | 'timestamp';
}

/**
 * Sync Engine Service
 * 
 * Handles bidirectional sync between IndexedDB (local) and MongoDB (backend)
 * 
 * Key Features:
 * - Local-first: All operations hit IndexedDB first (5-10ms)
 * - Background sync: Non-blocking sync to backend
 * - Conflict resolution: Configurable strategies
 * - Offline tolerance: Works 100% offline
 * - Mode switching: Seamless data preservation
 */
@Injectable({
  providedIn: 'root'
})
export class SyncEngineService {
  private storage = inject(GgStorage);
  private logger = inject(LoggerService);
  private spaceApi = inject(SpaceApiService);

  // Sync configuration
  private config = signal<SyncConfig>({
    enabled: false,
    interval: 5000, // 5 seconds
    batchSize: 10,
    retryAttempts: 3,
    retryDelay: 1000,
    conflictResolution: 'local-wins'
  });

  // Sync state
  private syncEnabled = computed(() => this.config().enabled);
  private syncInProgress = signal(false);
  private lastSyncTime = autoFlashState<number>('sync-last-sync', 0);
  
  // Sync queue (persisted across sessions)
  private syncQueue = autoFlashState<SyncOperation[]>('sync-queue', []);
  
  // Statistics
  private syncStats = signal({
    totalSyncs: 0,
    successfulSyncs: 0,
    failedSyncs: 0,
    lastSyncDuration: 0
  });

  // Computed
  readonly queueSize = computed(() => this.syncQueue().length);
  readonly isSyncing = this.syncInProgress.asReadonly();
  readonly stats = this.syncStats.asReadonly();

  private syncIntervalId?: number;

  /**
   * Initialize sync engine
   */
  async initialize(config: Partial<SyncConfig>): Promise<void> {
    this.logger.info('[SyncEngine] Initializing...', config);
    
    this.config.update(current => ({ ...current, ...config }));
    
    if (this.config().enabled) {
      this.startBackgroundSync();
      // Immediate sync on startup
      this.performSync();
    }
    
    this.logger.info('[SyncEngine] βœ… Initialized', {
      enabled: this.config().enabled,
      queueSize: this.syncQueue().length
    });
  }

  /**
   * Enable or disable sync
   */
  setSyncEnabled(enabled: boolean): void {
    this.logger.info(`[SyncEngine] Sync ${enabled ? 'ENABLED' : 'DISABLED'}`);
    
    this.config.update(c => ({ ...c, enabled }));
    
    if (enabled) {
      this.startBackgroundSync();
      // Immediate sync when enabled
      this.performSync();
    } else {
      this.stopBackgroundSync();
    }
  }

  /**
   * Write operation - ALWAYS local first
   */
  async write<T>(key: string, data: T, entity: SyncOperation['entity']): Promise<void> {
    this.logger.debug(`[SyncEngine] Write: ${key}`);
    
    // 1. Write to IndexedDB FIRST (instant!)
    await this.storage.put(key, data);
    
    // 2. Queue for backend sync (if enabled)
    if (this.syncEnabled()) {
      this.queueOperation({
        id: `${entity}-${key}-${Date.now()}`,
        operation: 'update',
        entity,
        key,
        data,
        timestamp: Date.now(),
        retries: 0
      });
    }
    
    this.logger.debug(`[SyncEngine] βœ… Write complete: ${key}`);
  }

  /**
   * Read operation - ALWAYS from local
   */
  async read<T>(key: string): Promise<T | null> {
    // Always read from IndexedDB (instant!)
    const data = await this.storage.get<T>(key);
    
    // If stale or missing, trigger background fetch (don't block!)
    if (this.syncEnabled() && this.isStale(key)) {
      this.fetchFromBackendInBackground(key);
    }
    
    return data;
  }

  /**
   * Delete operation - ALWAYS local first
   */
  async delete(key: string, entity: SyncOperation['entity']): Promise<void> {
    this.logger.debug(`[SyncEngine] Delete: ${key}`);
    
    // 1. Delete from IndexedDB FIRST
    await this.storage.remove(key);
    
    // 2. Queue for backend sync
    if (this.syncEnabled()) {
      this.queueOperation({
        id: `${entity}-${key}-${Date.now()}`,
        operation: 'delete',
        entity,
        key,
        timestamp: Date.now(),
        retries: 0
      });
    }
    
    this.logger.debug(`[SyncEngine] βœ… Delete complete: ${key}`);
  }

  /**
   * Queue an operation for sync
   */
  private queueOperation(operation: SyncOperation): void {
    this.syncQueue.update(queue => {
      // Check for duplicate operations on same key
      const filtered = queue.filter(op => op.key !== operation.key);
      return [...filtered, operation];
    });
    
    this.logger.debug(`[SyncEngine] Queued operation: ${operation.operation} ${operation.key}`, {
      queueSize: this.syncQueue().length
    });
  }

  /**
   * Start background sync
   */
  private startBackgroundSync(): void {
    if (this.syncIntervalId) {
      this.stopBackgroundSync();
    }
    
    this.syncIntervalId = window.setInterval(() => {
      if (!this.syncInProgress() && navigator.onLine) {
        this.performSync();
      }
    }, this.config().interval);
    
    this.logger.info('[SyncEngine] πŸ”„ Background sync started', {
      interval: this.config().interval
    });
  }

  /**
   * Stop background sync
   */
  private stopBackgroundSync(): void {
    if (this.syncIntervalId) {
      clearInterval(this.syncIntervalId);
      this.syncIntervalId = undefined;
      this.logger.info('[SyncEngine] ⏸️  Background sync stopped');
    }
  }

  /**
   * Perform bidirectional sync
   */
  private async performSync(): Promise<void> {
    if (this.syncInProgress()) {
      this.logger.debug('[SyncEngine] Sync already in progress, skipping');
      return;
    }
    
    if (!navigator.onLine) {
      this.logger.debug('[SyncEngine] Offline, skipping sync');
      return;
    }
    
    this.syncInProgress.set(true);
    const startTime = Date.now();
    
    this.logger.info('[SyncEngine] πŸ”„ Starting sync...', {
      queueSize: this.syncQueue().length
    });
    
    try {
      // 1. Push local changes to backend
      await this.pushToBackend();
      
      // 2. Pull remote changes from backend
      await this.pullFromBackend();
      
      // 3. Update stats
      const duration = Date.now() - startTime;
      this.lastSyncTime.set(Date.now());
      this.syncStats.update(stats => ({
        totalSyncs: stats.totalSyncs + 1,
        successfulSyncs: stats.successfulSyncs + 1,
        failedSyncs: stats.failedSyncs,
        lastSyncDuration: duration
      }));
      
      this.logger.info('[SyncEngine] βœ… Sync complete', {
        duration: `${duration}ms`,
        queueSize: this.syncQueue().length
      });
      
    } catch (error) {
      this.logger.error('[SyncEngine] ❌ Sync failed', error);
      this.syncStats.update(stats => ({
        ...stats,
        failedSyncs: stats.failedSyncs + 1
      }));
    } finally {
      this.syncInProgress.set(false);
    }
  }

  /**
   * Push queued operations to backend
   */
  private async pushToBackend(): Promise<void> {
    const queue = this.syncQueue();
    if (queue.length === 0) {
      return;
    }
    
    this.logger.info(`[SyncEngine] πŸ“€ Pushing ${queue.length} operations to backend`);
    
    const config = this.config();
    const batch = queue.slice(0, config.batchSize);
    
    for (const operation of batch) {
      try {
        await this.syncOperationToBackend(operation);
        
        // Remove from queue on success
        this.syncQueue.update(q => q.filter(op => op.id !== operation.id));
        
      } catch (error) {
        this.logger.error('[SyncEngine] Failed to sync operation', operation, error);
        
        // Retry logic
        if (operation.retries < config.retryAttempts) {
          this.syncQueue.update(q => q.map(op => 
            op.id === operation.id 
              ? { ...op, retries: op.retries + 1 }
              : op
          ));
        } else {
          // Max retries reached, remove from queue
          this.logger.error('[SyncEngine] Max retries reached, removing from queue', operation);
          this.syncQueue.update(q => q.filter(op => op.id !== operation.id));
        }
      }
    }
  }

  /**
   * Sync a single operation to backend
   */
  private async syncOperationToBackend(operation: SyncOperation): Promise<void> {
    switch (operation.entity) {
      case 'space':
        if (operation.operation === 'update') {
          await firstValueFrom(
            this.spaceApi.updateSpace(operation.key.replace('space_', ''), operation.data)
          );
        } else if (operation.operation === 'delete') {
          await firstValueFrom(
            this.spaceApi.deleteSpace(operation.key.replace('space_', ''))
          );
        }
        break;
        
      case 'universe':
        // TODO: Implement universe sync API
        this.logger.warn('[SyncEngine] Universe sync not implemented yet');
        break;
        
      // TODO: Add other entity types
      default:
        this.logger.warn(`[SyncEngine] Unknown entity type: ${operation.entity}`);
    }
  }

  /**
   * Pull changes from backend
   */
  private async pullFromBackend(): Promise<void> {
    const lastSync = this.lastSyncTime();
    
    this.logger.info('[SyncEngine] πŸ“₯ Pulling changes from backend', {
      since: new Date(lastSync).toISOString()
    });
    
    // TODO: Implement change detection API
    // For now, skip pull to avoid conflicts during initial implementation
    this.logger.debug('[SyncEngine] Pull from backend not implemented yet');
  }

  /**
   * Check if data is stale
   */
  private isStale(key: string): boolean {
    const lastSync = this.lastSyncTime();
    const now = Date.now();
    const staleThreshold = 60000; // 1 minute
    
    return (now - lastSync) > staleThreshold;
  }

  /**
   * Fetch from backend in background (non-blocking)
   */
  private fetchFromBackendInBackground(key: string): void {
    // Fire and forget - don't wait for result
    setTimeout(async () => {
      try {
        // TODO: Implement fetch from backend API
        this.logger.debug('[SyncEngine] Background fetch not implemented yet:', key);
      } catch (error) {
        this.logger.debug('[SyncEngine] Background fetch failed:', error);
      }
    }, 0);
  }

  /**
   * Manually trigger sync
   */
  async manualSync(): Promise<void> {
    this.logger.info('[SyncEngine] Manual sync triggered');
    await this.performSync();
  }

  /**
   * Clear sync queue
   */
  clearQueue(): void {
    this.logger.warn('[SyncEngine] Clearing sync queue');
    this.syncQueue.set([]);
  }

  /**
   * Get sync status
   */
  getStatus(): {
    enabled: boolean;
    queueSize: number;
    isSyncing: boolean;
    lastSync: Date | null;
    stats: typeof this.syncStats;
  } {
    return {
      enabled: this.syncEnabled(),
      queueSize: this.queueSize(),
      isSyncing: this.syncInProgress(),
      lastSync: this.lastSyncTime() ? new Date(this.lastSyncTime()) : null,
      stats: this.syncStats()
    };
  }
}

2. libs/core-angular/src/lib/services/sync-engine.service.spec.ts

Purpose: Unit tests for SyncEngine

Location: libs/core-angular/src/lib/services/sync-engine.service.spec.ts

import { TestBed } from '@angular/core/testing';
import { SyncEngineService } from './sync-engine.service';

describe('SyncEngineService', () => {
  let service: SyncEngineService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(SyncEngineService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  // TODO: Add comprehensive tests
  // - Test queue operations
  // - Test sync cycle
  // - Test conflict resolution
  // - Test offline/online transitions
  // - Test mode switching
});

πŸ“ Files to Modify

1. libs/core-angular/src/lib/services/space.service.ts

Changes Required:

  1. Inject SyncEngineService
  2. Replace all direct storage calls with syncEngine.write() / syncEngine.read()
  3. Remove dual code paths (API vs local)
  4. Simplify to single unified path

Before:

// ❌ OLD CODE (lines 125-200)
async initializeSpace(userId: string, spaceSlug?: string): Promise<Space> {
  if (this._isOnline() && this.spaceApi.isConnected()) {
    let space = await this.initializeFromApi(userId, spaceSlug);
    // ... complex API logic
  } else {
    const space = await this.initializeFromLocal(userId, spaceSlug);
    // ... complex local logic
  }
}

After:

// βœ… NEW CODE
async initializeSpace(userId: string, spaceSlug?: string): Promise<Space> {
  this._isLoading.set(true);
  
  try {
    // Try to load from local first (instant!)
    const spaceKey = `space_${userId}`;
    let space = await this.syncEngine.read<Space>(spaceKey);
    
    if (!space) {
      // Create new space
      space = await this.createNewSpace(userId, spaceSlug);
    }
    
    // Load universes
    const universes = await this.loadUniverses(space.id);
    
    // Update state
    this._currentSpace.set(space);
    this._universes.set(universes);
    
    this.logger.info('[SpaceService] βœ… Space initialized:', space.id);
    return space;
    
  } finally {
    this._isLoading.set(false);
  }
}

private async createNewSpace(userId: string, spaceSlug?: string): Promise<Space> {
  const isGuestUser = userId.startsWith('temp-');
  const spaceId = isGuestUser ? `space_${userId}` : `space_${Date.now()}`;
  
  const space: Space = {
    id: spaceId,
    name: spaceSlug || 'My Space',
    spaceSlug: spaceSlug || `space-${Date.now()}`,
    ownerId: userId,
    universeIds: [],
    settings: {
      defaultTheme: 'dark',
      aiProvider: 'anthropic',
      storageLocation: 'local',
      autoSave: true,
      autoBackup: false
    },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  
  // Write to local + queue for sync
  await this.syncEngine.write(`space_${userId}`, space, 'space');
  
  return space;
}

Specific Changes:

  1. Add import:
import { SyncEngineService } from './sync-engine.service';
  1. Inject service:
private readonly syncEngine = inject(SyncEngineService);
  1. Replace createUniverse method (lines 780-850):
async createUniverse(data: { name: string; description?: string }): Promise<UniverseWithDeployment> {
  const space = this._currentSpace();
  if (!space) {
    throw new Error('No space initialized. Call initializeSpace() first.');
  }

  const universe: UniverseWithDeployment = {
    id: `universe_${Date.now()}`,
    name: data.name,
    description: data.description || '',
    slug: this.generateSlug(data.name),
    deploymentStatus: 'draft',
    galaxyIds: [],
    metadata: {
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      createdBy: space.ownerId
    }
  };

  // Add spaceId for validation
  (universe as any).spaceId = space.id;

  // βœ… Write to local + queue for sync
  await this.syncEngine.write(`universe_${universe.id}`, universe, 'universe');

  // Update space's universeIds
  space.universeIds.push(universe.id);
  space.updatedAt = new Date().toISOString();
  await this.syncEngine.write(`space_${space.ownerId}`, space, 'space');

  // Update state
  this._universes.update(u => [...u, universe]);

  this.logger.info('[SpaceService] βœ… Universe created:', universe.id);
  return universe;
}
  1. Replace loadUniverses method (lines 700-750):
async loadUniverses(spaceId: string): Promise<UniverseWithDeployment[]> {
  const space = await this.syncEngine.read<Space>(`space_${spaceId}`);
  if (!space || !space.universeIds) {
    return [];
  }

  // Load all universes from local storage
  const universes: UniverseWithDeployment[] = [];
  
  for (const universeId of space.universeIds) {
    const universe = await this.syncEngine.read<UniverseWithDeployment>(`universe_${universeId}`);
    if (universe) {
      // Validate universe belongs to this space
      const universeSpaceId = (universe as any).spaceId;
      if (!universeSpaceId || universeSpaceId === space.id) {
        universes.push(universe);
      } else {
        this.logger.warn('[SpaceService] Skipping universe from different space', {
          universeId,
          expectedSpace: space.id,
          actualSpace: universeSpaceId
        });
      }
    }
  }

  this.logger.info('[SpaceService] Loaded universes:', {
    count: universes.length,
    spaceId
  });

  return universes;
}
  1. Replace updateSpace method (lines 488-545):
async updateSpace(updates: Partial<Space>): Promise<Space> {
  const space = this._currentSpace();
  if (!space) {
    throw new Error('No space initialized.');
  }

  const updatedSpace: Space = {
    ...space,
    ...updates,
    updatedAt: new Date().toISOString()
  };

  // Write to local + queue for sync
  await this.syncEngine.write(`space_${space.ownerId}`, updatedSpace, 'space');

  // Update state
  this._currentSpace.set(updatedSpace);

  this.logger.info('[SpaceService] βœ… Space updated:', updatedSpace.id);
  return updatedSpace;
}
  1. Replace deleteUniverse method (lines 916-950):
async deleteUniverse(universeId: string): Promise<void> {
  const space = this._currentSpace();
  if (!space) {
    throw new Error('No space initialized');
  }

  // Delete from local + queue for sync
  await this.syncEngine.delete(`universe_${universeId}`, 'universe');

  // Update space
  space.universeIds = space.universeIds.filter(id => id !== universeId);
  space.updatedAt = new Date().toISOString();
  await this.syncEngine.write(`space_${space.ownerId}`, space, 'space');

  // Update state
  this._universes.update(u => u.filter(universe => universe.id !== universeId));

  this.logger.info('[SpaceService] βœ… Universe deleted:', universeId);
}

2. apps/gograb-space/src/main.ts

Changes Required:

  1. Initialize SyncEngineService at app startup
  2. Configure based on environment
  3. Remove conditional interceptor logic (keep unified)

Before:

// ❌ OLD CODE (lines 120-126)
provideHttpClient(
  withInterceptors(
    environment.useMockApi
      ? [mockHttpInterceptor, authInterceptor]
      : [authInterceptor]
  )
)

After:

// βœ… NEW CODE - Add after app bootstraps
bootstrapApplication(AppComponent, {
  providers: [
    // ... existing providers
    
    // Initialize SyncEngine
    provideAppInitializer(() => {
      const syncEngine = inject(SyncEngineService);
      const logger = inject(LoggerService);
      
      return async () => {
        await syncEngine.initialize({
          enabled: !environment.useMockApi,  // Sync enabled in real mode
          interval: 5000,  // 5 seconds
          batchSize: 10,
          retryAttempts: 3,
          retryDelay: 1000,
          conflictResolution: 'local-wins'
        });
        
        logger.info('[App] πŸ”„ SyncEngine initialized', {
          mode: environment.useMockApi ? 'MOCK (sync disabled)' : 'REAL (sync enabled)',
          queueSize: syncEngine.queueSize()
        });
      };
    }),
    
    // Keep both interceptors always active (unified path)
    provideHttpClient(
      withInterceptors([mockHttpInterceptor, authInterceptor])
    )
  ]
})

3. apps/gograb-space/src/environments/environment.ts

Changes Required:

  1. Add sync configuration
  2. Document new behavior

Add:

export const environment = {
  production: false,
  useMockApi: false,
  backendApiUrl: BACKEND_URLS.local,
  deploymentMode: DEPLOYMENT_MODES.development,
  
  // βœ… NEW: Sync configuration
  sync: {
    // Sync is automatically enabled based on useMockApi:
    // - useMockApi = false β†’ sync enabled (background sync to MongoDB)
    // - useMockApi = true β†’ sync disabled (100% local, no backend calls)
    interval: 5000,  // Sync every 5 seconds
    batchSize: 10,  // Sync 10 operations at a time
    retryAttempts: 3,  // Retry failed operations 3 times
    conflictResolution: 'local-wins' as const,  // Local changes win by default
  },
  
  ...SHARED_CONFIG,
};

4. libs/core-angular/src/lib/services/space-api.service.ts

Changes Required:

  1. Add comments about new usage pattern
  2. Keep existing API methods (used by SyncEngine)

Add at top of file:

/**
 * Space API Service
 * 
 * ⚠️ IMPORTANT: With offline-first architecture, this service should ONLY be used by:
 * 1. SyncEngineService - for background sync operations
 * 2. Direct API operations that don't involve data persistence
 * 
 * ❌ DO NOT use this service directly in components/services for CRUD operations!
 * βœ… USE SyncEngineService instead for all data operations
 * 
 * This ensures:
 * - All operations are local-first (fast)
 * - Data is properly queued for sync
 * - Offline mode works correctly
 */

5. apps/gograb-space/src/app/components/space-components/g-space-dashboard/g-space-dashboard.component.ts

Changes Required:

  1. Update clearAllData to use sync engine
  2. Update nuclearReset to clear sync queue

Replace clearAllData method:

async clearAllData(): Promise<void> {
  if (!confirm('⚠️ WARNING: This will delete ALL data. Are you sure?')) {
    return;
  }

  try {
    console.log('🧹 Clearing all data...');

    // Clear via space service (which uses sync engine)
    await this.spaceService.clearAllData();
    
    // Clear sync queue
    const syncEngine = inject(SyncEngineService);
    syncEngine.clearQueue();

    console.log('βœ… All data cleared successfully! Reloading...');
    window.location.reload();

  } catch (error) {
    console.error('Failed to clear data:', error);
    alert('❌ Failed to clear data.');
  }
}

Add to nuclearReset method (after step 4):

// Step 5: Clear sync queue
console.log('🧹 Step 5: Clearing sync queue...');
const syncEngine = inject(SyncEngineService);
syncEngine.clearQueue();

πŸš€ Implementation Steps

Phase 1: Foundation (Day 1-2)

Goal: Create sync engine and basic infrastructure

  1. Create SyncEngineService

    • Create file: libs/core-angular/src/lib/services/sync-engine.service.ts
    • Copy full implementation from above
    • Run: npm run lint to check for errors
  2. Export from barrel file

    • Edit: libs/core-angular/src/index.ts
    • Add: export * from './lib/services/sync-engine.service';
  3. Add sync config to environment

    • Edit: apps/gograb-space/src/environments/environment.ts
    • Add sync configuration section
  4. Test compilation

    • Run: npx nx build core-angular
    • Fix any TypeScript errors

Checkpoint: SyncEngine compiles without errors


Phase 2: SpaceService Integration (Day 3-4)

Goal: Refactor SpaceService to use SyncEngine

  1. Inject SyncEngine into SpaceService

    private readonly syncEngine = inject(SyncEngineService);
    
  2. Refactor initializeSpace method

    • Replace dual API/local path with unified local-first path
    • Use syncEngine.read() for loading
  3. Refactor createUniverse method

    • Replace storage.put() with syncEngine.write()
    • Remove API call logic
  4. Refactor loadUniverses method

    • Use syncEngine.read() for all universe loading
  5. Refactor updateSpace method

    • Use syncEngine.write()
  6. Refactor deleteUniverse method

    • Use syncEngine.delete()
  7. Test in isolation

    • Run: npx nx test core-angular
    • Fix failing tests

Checkpoint: SpaceService uses SyncEngine for all operations


Phase 3: App Integration (Day 5)

Goal: Initialize SyncEngine at app startup

  1. Update main.ts

    • Add provideAppInitializer for SyncEngine
    • Configure based on environment
  2. Update dashboard component

    • Update clearAllData() to clear sync queue
    • Update nuclearReset() to clear sync queue
  3. Test app startup

    • Run: npx nx serve gograb-space
    • Check console for sync initialization logs
    • Verify no errors

Checkpoint: App starts with SyncEngine initialized


Phase 4: Testing & Validation (Day 6-7)

Goal: Verify everything works in both modes

  1. Test Mock Mode

    • Set useMockApi: true
    • Create universe β†’ should be instant
    • Check: No API calls in network tab
    • Check: Data in IndexedDB
    • Refresh β†’ data should persist
  2. Test Real Mode

    • Set useMockApi: false
    • Create universe β†’ should be instant
    • Check: Sync queue in console
    • Check: API call happens in background
    • Check: Data in IndexedDB
    • Refresh β†’ data should persist
  3. Test Mode Switching

    • Start in Mock mode
    • Create universe
    • Switch to Real mode
    • Verify: Universe still there
    • Verify: Sync queue starts
    • Check backend: Universe synced
  4. Test Offline Mode

    • Start in Real mode
    • Go offline (Chrome DevTools β†’ Network β†’ Offline)
    • Create universe β†’ should still work
    • Go online
    • Verify: Sync happens automatically
  5. Test Conflict Resolution

    • Create universe on device A
    • Create universe on device B (same space)
    • Go online on both
    • Verify: Both universes appear on both devices

Checkpoint: All modes work correctly


Phase 5: Performance Testing (Day 8)

Goal: Measure and validate performance improvements

  1. Measure current performance (before refactor)

    • Time: Create universe
    • Time: Load dashboard
    • Time: Open universe
    • Document baseline
  2. Measure new performance (after refactor)

    • Same operations
    • Compare results
    • Should see 50-60x improvement
  3. Create performance report

    • Document metrics
    • Create graphs/charts
    • Add to documentation

Checkpoint: Performance targets met


Phase 6: Documentation & Polish (Day 9-10)

Goal: Document the new architecture

  1. Update README

    • Document offline-first architecture
    • Add sync configuration docs
    • Add troubleshooting guide
  2. Add JSDoc comments

    • Document all public methods
    • Add usage examples
  3. Create migration guide

    • For existing users with data
    • Step-by-step instructions
  4. Code review

    • Review all changes
    • Clean up debug logs
    • Remove commented code

Checkpoint: Documentation complete


πŸ§ͺ Testing Strategy

Unit Tests

Test File: libs/core-angular/src/lib/services/sync-engine.service.spec.ts

describe('SyncEngineService', () => {
  describe('write operations', () => {
    it('should write to IndexedDB first');
    it('should queue operation for sync when enabled');
    it('should NOT queue when sync disabled');
  });
  
  describe('read operations', () => {
    it('should read from IndexedDB');
    it('should trigger background fetch when stale');
  });
  
  describe('sync cycle', () => {
    it('should push queued operations to backend');
    it('should pull changes from backend');
    it('should resolve conflicts correctly');
  });
  
  describe('offline handling', () => {
    it('should work offline');
    it('should queue operations offline');
    it('should sync when online');
  });
});

Integration Tests

describe('Offline-First Integration', () => {
  it('should create universe instantly in mock mode');
  it('should create universe instantly in real mode');
  it('should sync to backend in background');
  it('should work offline');
  it('should preserve data when switching modes');
});

E2E Tests

describe('Offline-First E2E', () => {
  it('should create and persist universe');
  it('should load dashboard quickly');
  it('should work offline');
  it('should sync when online');
  it('should handle mode switching');
});

πŸ“¦ Migration Plan

For Existing Users with Data

Problem: Users have data in current format (either all-local or all-backend)

Solution: One-time migration on first load

Create migration service:

libs/core-angular/src/lib/services/migration.service.ts

@Injectable({ providedIn: 'root' })
export class MigrationService {
  async migrateToOfflineFirst(): Promise<void> {
    const migrationKey = 'migration-offline-first-v1';
    const migrated = localStorage.getItem(migrationKey);
    
    if (migrated === 'true') {
      return; // Already migrated
    }
    
    console.log('[Migration] Starting offline-first migration...');
    
    // 1. Detect old data format
    // 2. Copy to new format
    // 3. Mark as migrated
    
    localStorage.setItem(migrationKey, 'true');
    console.log('[Migration] βœ… Migration complete');
  }
}

Call in main.ts:

provideAppInitializer(() => {
  const migration = inject(MigrationService);
  return () => migration.migrateToOfflineFirst();
})

πŸ“Š Performance Metrics

Target Performance

Operation Current (Backend-First) Target (Offline-First) Improvement
Create Space 300-600ms 5-10ms 60x faster
Create Universe 300-600ms 5-10ms 60x faster
Load Dashboard 1000-2000ms 50-100ms 20x faster
Open Universe 200-400ms 10-20ms 20x faster
Update Space 200-500ms 5-10ms 40x faster
Offline Mode ❌ Broken βœ… Works ∞

How to Measure

// Add to code
const start = performance.now();
await operation();
const duration = performance.now() - start;
console.log(`Operation took: ${duration}ms`);

βœ… Success Criteria


πŸ”— Additional Resources

Documentation References

Existing Code References


🚨 Important Notes

  1. Backwards Compatibility:

    • Users with existing data will need migration
    • Plan for gradual rollout
  2. Testing Priority:

    • Test mode switching extensively
    • Test offlineβ†’online transitions
    • Test conflict scenarios
  3. Performance Monitoring:

    • Add performance metrics
    • Monitor sync queue size
    • Track sync success/failure rates
  4. Rollback Plan:

    • Keep old code commented for 1 release
    • Feature flag for enabling/disabling
    • Easy rollback path if issues arise

This is the complete implementation guide. Start with Phase 1 and work through each phase systematically. πŸš€

Estimated Timeline: 7-10 days
Priority: CRITICAL (50-60x performance improvement)
Risk Level: MEDIUM (well-tested approach, but significant refactor)


Date Created: November 3, 2025
Author: AI Assistant
Status: READY FOR IMPLEMENTATION