Offline-First Improvements

Comprehensive offline-first architecture enhancements for error-free operation

๐Ÿ—๏ธ Offline-First Improvements

Comprehensive offline-first architecture enhancements for error-free operation when backend is unavailable.


๐Ÿ“‹ Table of Contents

  1. Overview
  2. Problem Statement
  3. Solution Architecture
  4. Implementation Details
  5. Components Modified
  6. Usage Guide
  7. Testing
  8. Expected Outcomes

๐ŸŽฏ Overview

GoGrab Space now features a sophisticated error-free offline-first architecture that:

Key Innovation

The architecture centers around a new NetworkSyncStateService in @storage-system that acts as the single source of truth for all network and backend connectivity state. All sync-related services now consume this centralized service.


๐Ÿ”ด Problem Statement

Before Implementation

When the backend-api was offline, multiple services would spam the console with errors:

Service Error Pattern Frequency
SpaceApiService ERR_CONNECTION_REFUSED Every API call
SyncEngineService Continuous retry errors Every 5 seconds
UserJourneyTrackerService Sync failures Every 30 seconds
RealtimeSyncService WebSocket connection errors Repeatedly
HttpMongoDBSyncAdapter HTTP sync errors On every batch

Root Cause

Services lacked awareness of network/backend state and attempted operations that would inevitably fail, resulting in:


๐Ÿ›๏ธ Solution Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        @storage-system                               โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚              NetworkSyncStateService (NEW)                     โ”‚  โ”‚
โ”‚  โ”‚                                                                โ”‚  โ”‚
โ”‚  โ”‚  Signals:                                                      โ”‚  โ”‚
โ”‚  โ”‚  โ€ข isOnline          - Browser network status                  โ”‚  โ”‚
โ”‚  โ”‚  โ€ข isBackendAvailable - Backend HTTP health check              โ”‚  โ”‚
โ”‚  โ”‚  โ€ข isWebSocketConnected - Socket.IO connection state           โ”‚  โ”‚
โ”‚  โ”‚  โ€ข connectionState   - Combined state enum                     โ”‚  โ”‚
โ”‚  โ”‚  โ€ข canMakeApiCalls   - Safe to call HTTP APIs                  โ”‚  โ”‚
โ”‚  โ”‚  โ€ข canUseWebSocket   - Safe to use WebSocket                   โ”‚  โ”‚
โ”‚  โ”‚  โ€ข shouldSuppressErrors - Error suppression flag               โ”‚  โ”‚
โ”‚  โ”‚                                                                โ”‚  โ”‚
โ”‚  โ”‚  Events:                                                       โ”‚  โ”‚
โ”‚  โ”‚  โ€ข onBackendAvailable$ - Backend came online                   โ”‚  โ”‚
โ”‚  โ”‚  โ€ข onBackendUnavailable$ - Backend went offline                โ”‚  โ”‚
โ”‚  โ”‚  โ€ข onReconnected$ - Reconnection completed                     โ”‚  โ”‚
โ”‚  โ”‚  โ€ข onConnectionStateChange$ - State changed                    โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                              โ”‚                                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚     Enhanced Components   โ”‚                                    โ”‚  โ”‚
โ”‚  โ”‚                           โ–ผ                                    โ”‚  โ”‚
โ”‚  โ”‚  โ€ข HttpMongoDBSyncAdapter - Uses canSync() before operations   โ”‚  โ”‚
โ”‚  โ”‚  โ€ข IntelligentSyncEngine - Backend availability aware          โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚
               Exposes signals โ”‚ via provider
                               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   @libs-foundational/core-angular                     โ”‚
โ”‚                                                                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ SyncEngineServiceโ”‚  โ”‚RealtimeSyncServiceโ”‚  โ”‚UserJourneyTracker  โ”‚  โ”‚
โ”‚  โ”‚ โ€ข Pauses sync    โ”‚  โ”‚ โ€ข Smart reconnect โ”‚  โ”‚ โ€ข Skips when       โ”‚  โ”‚
โ”‚  โ”‚ โ€ข Error suppress โ”‚  โ”‚ โ€ข WS state track  โ”‚  โ”‚   offline          โ”‚  โ”‚
โ”‚  โ”‚ โ€ข Auto-resume    โ”‚  โ”‚ โ€ข Error suppress  โ”‚  โ”‚ โ€ข Auto-resume      โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                          โ”‚
โ”‚  โ”‚ SpaceApiService  โ”‚  โ”‚ UserApiService   โ”‚                          โ”‚
โ”‚  โ”‚ โ€ข Cache fallback โ”‚  โ”‚ โ€ข Silent skip    โ”‚                          โ”‚
โ”‚  โ”‚ โ€ข Network errors โ”‚  โ”‚ โ€ข Graceful null  โ”‚                          โ”‚
โ”‚  โ”‚   โ†’ use cache    โ”‚  โ”‚   returns        โ”‚                          โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚
                               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        @libs-ui/shared-ui                             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ OfflineIndicatorComponent (NEW)                                 โ”‚  โ”‚
โ”‚  โ”‚ โ€ข Shows "Offline Mode" banner                                   โ”‚  โ”‚
โ”‚  โ”‚ โ€ข Shows "Reconnecting..." state                                 โ”‚  โ”‚
โ”‚  โ”‚ โ€ข Auto-hides when online                                        โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Why This Architecture?

  1. Single Source of Truth - @storage-system owns all sync/network state
  2. No Duplication - Services consume NetworkSyncStateService, don't recreate
  3. Clean Dependency Flow - storage-system โ†’ core-angular (not circular)
  4. Centralized Error Suppression - One place to control logging behavior

๐Ÿ”ง Implementation Details

1. NetworkSyncStateService

Location: libs/libs-foundational/storage-system/src/sync/services/network-sync-state.service.ts

The foundation for ALL offline-first behavior.

// Connection states
export type ConnectionState = 
  | 'online'              // Backend available, full sync
  | 'offline'             // Browser offline, local-only
  | 'backend-unavailable' // Browser online but backend down
  | 'reconnecting';       // Attempting to reconnect

// Key API
readonly isOnline = signal<boolean>(navigator.onLine);
readonly isBackendAvailable = signal<boolean>(false);
readonly isWebSocketConnected = signal<boolean>(false);

readonly connectionState = computed<ConnectionState>(() => {
  if (!this._isOnline()) return 'offline';
  if (this._reconnecting) return 'reconnecting';
  if (!this._isBackendAvailable()) return 'backend-unavailable';
  return 'online';
});

readonly canMakeApiCalls = computed(() => 
  this._isOnline() && this._isBackendAvailable()
);

readonly canUseWebSocket = computed(() => 
  this.canMakeApiCalls() && this.isWebSocketConnected()
);

// Events for coordinated recovery
readonly onBackendAvailable$: Observable<void>;
readonly onBackendUnavailable$: Observable<void>;
readonly onReconnected$: Observable<void>;

Health Check with Exponential Backoff

// Backoff intervals: 5s โ†’ 10s โ†’ 20s โ†’ 40s โ†’ 60s (max)
private healthCheckIntervals = [5000, 10000, 20000, 40000, 60000];

private async performHealthCheck(): Promise<boolean> {
  try {
    const response = await firstValueFrom(
      this.http.get<{ status: string }>(this.healthCheckUrl, {
        headers: { 'X-Health-Check': 'true' }
      }).pipe(timeout(5000))
    );
    return response?.status === 'ok';
  } catch {
    return false;
  }
}

2. Provider Configuration

Location: libs/libs-foundational/storage-system/src/sync/providers/provide-network-sync-state.ts

// In app.config.ts
provideNetworkSyncState({
  apiBaseUrl: environment.backendApiUrl,
  healthCheckUrl: '/api/health',
  maxErrorLogs: 3,          // Suppress after 3 errors
  verbose: isDevMode(),     // Verbose logging in dev
  autoInitialize: true      // Start health checks on init
})

3. Error Suppression Strategy

The service tracks error count and suppresses repetitive logging:

// Only log first N errors, then suppress
recordError(): boolean {
  this._errorCount.update(c => c + 1);
  return this._errorCount() <= this.config.maxErrorLogs;
}

shouldSuppressErrors(): boolean {
  return this._errorCount() >= this.config.maxErrorLogs;
}

// Reset on successful connection
resetErrorCount(): void {
  this._errorCount.set(0);
}

๐Ÿ“ฆ Components Modified

Storage System (@storage-system)

Component Changes
NetworkSyncStateService NEW - Central network state management
provideNetworkSyncState NEW - Angular provider with auto-init
IntelligentSyncEngine Added canSync() computed, uses NetworkSyncState
HttpMongoDBSyncAdapter Added network checks, error suppression

Core Angular (@libs-foundational/core-angular)

Component Changes
SyncEngineService Uses canMakeApiCalls(), auto-resume on reconnect
SpaceApiService Returns cached data when offline, exposes isOfflineMode
UserApiService Silent skip when offline, graceful null returns
RealtimeSyncService Smart reconnection, WS state tracking
UserJourneyTrackerService Skips sync when offline, auto-resume

UI Components (@libs-ui/shared-ui)

Component Changes
OfflineIndicatorComponent NEW - Visual connection status banner

App Configuration (gograb-space)

Component Changes
app.config.ts Added provideNetworkSyncState() early in providers

๐Ÿ“– Usage Guide

Consuming NetworkSyncStateService

import { NetworkSyncStateService } from '@storage-system';

@Injectable({ providedIn: 'root' })
export class MyService {
  private readonly networkState = inject(NetworkSyncStateService);

  async doSomethingWithBackend(): Promise<void> {
    // Check before making API calls
    if (!this.networkState.canMakeApiCalls()) {
      console.log('Backend unavailable - queuing operation');
      return;
    }

    try {
      await this.http.get('/api/data').toPromise();
    } catch (error) {
      // Only log if not suppressed
      if (!this.networkState.shouldSuppressErrors()) {
        console.error('API call failed:', error);
      }
    }
  }
}

Listening for Reconnection Events

export class MySyncService {
  private readonly networkState = inject(NetworkSyncStateService);
  private readonly destroyRef = inject(DestroyRef);

  constructor() {
    // Trigger sync when backend becomes available
    this.networkState.onBackendAvailable$.pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(() => {
      console.log('Backend available - syncing now');
      this.networkState.resetErrorCount();
      this.performSync();
    });
  }
}

Using the Offline Indicator Component

<!-- Add to app.component.html or layout -->
<ux-offline-indicator></ux-offline-indicator>

<!-- Rest of your app -->
<router-outlet></router-outlet>
import { OfflineIndicatorComponent } from '@shared-ui';

@Component({
  standalone: true,
  imports: [OfflineIndicatorComponent, RouterOutlet],
  template: `
    <ux-offline-indicator></ux-offline-indicator>
    <router-outlet></router-outlet>
  `
})
export class AppComponent {}

Exposing Connection State in UI

@Component({
  template: `
    @if (isOffline()) {
      <div class="offline-notice">
        Working offline - changes saved locally
      </div>
    }
  `
})
export class MyComponent {
  private networkState = inject(NetworkSyncStateService);
  isOffline = computed(() => !this.networkState.canMakeApiCalls());
}

๐Ÿงช Testing

Manual Testing Checklist

  1. Start app with backend offline

    • No console errors on startup
    • Offline indicator shows
    • App loads and is usable
  2. Work offline

    • Can navigate between pages
    • Can modify data (saved locally)
    • No repeated error messages
  3. Start backend while app running

    • Backend detected within 60 seconds
    • Sync automatically resumes
    • Offline indicator disappears
  4. Kill backend while working

    • Graceful handling (no crash)
    • Offline indicator appears
    • Work can continue

Simulating Offline Mode

// In browser console:
// Simulate offline
window.__networkState__.setBackendUnavailable();

// Simulate online
window.__networkState__.setBackendAvailable();

โœ… Expected Outcomes

Metric Before After
Console errors when offline 100+ per minute 0
User experience Confusing errors Clear feedback
Data loss risk Possible None (local storage)
Recovery time Manual refresh Automatic
Backend load Unnecessary retries Smart polling

Connection State Flow

Browser Online + Backend Up = "online" (full functionality)
                    โ”‚
                    โ–ผ Backend goes down
                    โ”‚
Browser Online + Backend Down = "backend-unavailable"
                    โ”‚         (local storage works,
                    โ”‚          no sync attempts)
                    โ”‚
                    โ–ผ Health check succeeds
                    โ”‚
              "reconnecting" (brief state)
                    โ”‚
                    โ–ผ Sync completes
                    โ”‚
              "online" (back to normal)

๐Ÿ“ File Reference

New Files Created

Files Modified


๐Ÿ”ฎ Future Enhancements

  1. Offline Data Queue Visualization - Show users pending changes
  2. Conflict Resolution UI - When local and server data conflict
  3. Background Sync API - Use Service Worker for more robust sync
  4. Selective Sync - Allow users to choose what syncs when
  5. Bandwidth-Aware Sync - Adjust sync based on connection quality

๐Ÿ“š Related Documentation


Last Updated: December 2025
Implementation Status: โœ… Complete
Session: 7