CLEAR ALL DATA FIX

Documentation for Plans

Four Critical Fixes: Clear All Data, Galaxy Creation, Flash State & Storage Keys

Date: November 4, 2025
Issues:

  1. "Clear All Data" button (⚠️) didn't work - data reappeared after page reload
  2. Galaxy creation didn't show galaxies in UI - data saved to Flash State but UI read from IndexedDB
  3. Flash State was being cleared on EVERY page load instead of only on user/space switch
  4. Storage key mismatch - forceSave() saved to wrong keys, causing galaxies to not appear until page refresh

The REAL Problem

The SyncEngine doesn't actually sync to backend yet! Look at this code:

// libs/core-angular/src/lib/services/sync-engine.service.ts:327
private async syncOperationToBackend(operation: SyncOperation): Promise<void> {
  // TODO: Implement actual backend sync
  // For now, just log the operation
  this.logger.debug('[SyncEngine] Syncing operation to backend:', operation);
}

This meant:

  1. ✅ Delete from IndexedDB (works instantly)
  2. ❌ Queue for backend sync (does nothing!)
  3. 🔄 Page reloads after 500ms
  4. ❌ Backend still has data → Universe comes back from the dead!

The Two-Part Solution

Part 1: Initial Misdiagnosis

Initially thought the buttons were using different approaches:

Delete Universe (Trash Button) - ✅ WORKED

async deleteUniverse(universeId: string): Promise<void> {
  // ⚡ OFFLINE-FIRST: Uses SyncEngine
  await this.syncEngine.delete(`universe_${universeId}`, 'universe');
  
  // ✅ Works immediately (IndexedDB)
  // ✅ Syncs to MongoDB automatically in background
  // ✅ Works offline
  // ✅ Handles conflicts
}

Clear All Data (Warning Button) - ❌ DIDN'T WORK

async clearAllData(): Promise<void> {
  // ❌ OLD APPROACH: Direct API call
  await firstValueFrom(this.spaceApi.deleteSpace(spaceId));
  
  // ❌ Requires backend connection
  // ❌ No offline support
  // ❌ Manual storage cleanup
  // ❌ No sync queue
}

The Root Cause

clearAllData() was still using the old direct-API approach while deleteUniverse() had been migrated to the new offline-first SyncEngine architecture.

This created an inconsistency:

Part 2: The Real Solution - HYBRID APPROACH

The real fix: Use a HYBRID APPROACH that deletes from BOTH backend AND local immediately:

async clearAllData(): Promise<void> {
  // 🔥 STEP 1: Delete from BACKEND first (MongoDB)
  // This ensures data is gone from source before page reload
  if (this._isOnline() && this.spaceApi.isConnected()) {
    await firstValueFrom(this.spaceApi.deleteSpace(spaceId));
  }

  // 🔥 STEP 2: Delete from LOCAL storage (IndexedDB)
  for (const universeId of space.universeIds) {
    await this.storage.remove(`universe_${universeId}`);
  }
  await this.storage.remove(`space_${spaceId}`);

  // 🔥 STEP 3: Reset state
  this._currentSpace.set(null);
  this._universes.set([]);
}

Same fix applied to deleteUniverse():

async deleteUniverse(universeId: string): Promise<void> {
  // STEP 1: Delete from backend (MongoDB)
  if (this._isOnline() && this.spaceApi.isConnected()) {
    await firstValueFrom(this.spaceApi.deleteUniverse(space.id, universeId));
  }

  // STEP 2: Delete from local storage (IndexedDB)
  await this.storage.remove(`universe_${universeId}`);
  
  // STEP 3: Update space and signals
  // ...
}

Why This Works

The HYBRID APPROACH ensures:

  1. Backend deletion happens FIRST ✅

    • MongoDB data deleted before page reload
    • No zombie data resurrection
  2. Local deletion happens SECOND ✅

    • IndexedDB cleanup
    • Immediate UI update
  3. Page reload finds no data ✅

    • Backend: deleted
    • Local: deleted
    • Result: clean slate!

Files Changed

Issue 1: Clear All Data

apps/gograb-space/src/app/app.component.ts

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

Issue 2: Galaxy Creation

libs/space-map-lib/src/lib/components/universe-detail/universe-detail.component.ts

Backend & Storage (Previous Fixes)

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

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

Testing

Test Case 1: Clear All Data (Issue 1)

  1. Create a universe with some galaxies
  2. Click the "Clear All Data" button (⚠️)
  3. Confirm both dialogs
  4. Watch console logs:
    • ✅ Should see "Persistence flush DISABLED"
    • ✅ Should see "Flash Persistence cleared"
    • ✅ Should see backend deletion logs
    • ❌ Should NOT see "BEFOREUNLOAD Attempting to flush" (flush disabled!)
  5. Redirects to landing page
  6. Navigate back to create a new space
  7. Verify:
    • ✅ New guest user ID created
    • ✅ New space with different ID
    • ✅ No universes showing
    • ✅ No data resurrection after reload 🎯

Test Case 2: Galaxy Creation (Issue 2)

  1. Create a universe (e.g., "Test Universe")
  2. Navigate to universe detail view
  3. Click "+ New Galaxy" button
  4. Fill in galaxy details (name, description, color)
  5. Click "Create Galaxy"
  6. Watch console logs:
    • ✅ Should see "Galaxy created successfully"
    • ✅ Should see "Reloading UnifiedDataService"
    • ✅ Should see "UnifiedDataService reloaded - galaxies updated"
  7. Verify:
    • ✅ Galaxy appears immediately in constellation view
    • ✅ Galaxy count updates in header
    • ✅ Can create pages inside the galaxy
    • ✅ Refresh page - galaxy persists

Test Case 3: Delete Individual Universe

  1. Create two universes
  2. Click trash button on one universe
  3. Verify:
    • ✅ Backend deletion happens immediately
    • ✅ Universe removed from UI
    • ✅ Other universe remains
    • ✅ Refresh page - deleted universe stays deleted

Important Notes

The Real Problem: Flash Persistence Auto-Restoration

After implementing the HYBRID approach, the universe STILL came back! The real culprit was Flash Persistence:

// libs/space-map-lib/src/lib/services/space-map-flash-state.service.ts:65
private _universes = autoFlashState<UniverseGroup[]>('space-map/universes', []);

This automatically saves universes to localStorage and restores them on page load! So even after deleting from:

The Flash Persistence would auto-restore the universe from localStorage!

The Complete Solution - Issue 1: Clear All Data

The Real Root Cause: Race Condition with Persistence Flush

Even after clearing all data, the beforeunload and visibilitychange event handlers were re-saving the data we just deleted!

// apps/gograb-space/src/app/app.component.ts
private handleBeforeUnload = () => {
  console.log('⚡ [BEFOREUNLOAD] Attempting to flush pending persistence...');
  this.flashHydrator.flushPendingPersistence(); // ⚠️ Re-saves data!
};

Timeline of the problem:

  1. ✅ User clicks "Clear All Data"
  2. ✅ Data cleared from everywhere
  3. ✅ Navigate to /landing
  4. ❌ beforeunload fires BEFORE navigation completes
  5. ❌ Flushes Flash Persistence, re-saving all data
  6. 🔄 Page reloads
  7. 🧟‍♂️ Data resurrected!

The Solution: Static Flag to Disable Flush

Added a static flag to AppComponent that can be disabled during critical clearing operations:

// apps/gograb-space/src/app/app.component.ts

// 🔥 Static flag to disable persistence flush during data clearing operations
private static disableFlushOnUnload = false;

/**
 * 🔥 PUBLIC API: Disable persistence flush (for data clearing operations)
 * Called by components that need to clear data without resurrection
 */
public static disablePersistenceFlush(): void {
  AppComponent.disableFlushOnUnload = true;
  console.log('🔥 [AppComponent] Persistence flush DISABLED to prevent data resurrection');
}

/**
 * 🔥 PUBLIC API: Re-enable persistence flush after data clearing completes
 */
public static enablePersistenceFlush(): void {
  AppComponent.disableFlushOnUnload = false;
  console.log('✅ [AppComponent] Persistence flush RE-ENABLED');
}

private handleBeforeUnload = () => {
  // 🔥 Skip flush if disabled (during data clearing operations)
  if (AppComponent.disableFlushOnUnload) {
    console.log('⚡ [BEFOREUNLOAD] Flush DISABLED - skipping to prevent data resurrection');
    return;
  }
  // ... rest of flush logic
};

Updated Clear All Data Flow

Clear data in this precise order with flush disabled:

async clearAllData(): Promise<void> {
  try {
    // 🔥 STEP 0: Disable persistence flush to prevent data resurrection
    const { AppComponent } = await import('../../../app.component');
    AppComponent.disablePersistenceFlush();

    // 🔥 STEP 1: Clear Flash Persistence
    this.flashState.clearFlashState();

    // 🔥 STEP 2: Clear backend + IndexedDB
    await this.spaceService.clearAllData();

    // 🔥 STEP 3: Clear sync queue  
    this.syncEngine.clearQueue();

    // 🔥 STEP 4: Clear micro-cache
    GgStorage.nuclearCacheReset();

    // 🔥 STEP 5: Clear guest user context
    localStorage.removeItem('guest_user_id');
    localStorage.removeItem('guest_space_slug');
    sessionStorage.clear();

    // 🔥 STEP 6: Navigate to landing (flush is disabled!)
    await this.router.navigate(['/landing']);

    // Note: We DON'T re-enable flush because we're navigating away.
    // The flag will be reset on next page load.

  } catch (error) {
    // Re-enable flush if something goes wrong
    const { AppComponent } = await import('../../../app.component');
    AppComponent.enablePersistenceFlush();
  }
}

Why Guest Context Clearing is Critical

Without clearing guest context:

  1. ✅ Delete all data
  2. 🔄 Page reloads
  3. ❌ Same guest user ID restored from localStorage
  4. ❌ Same space ID recreated: space_temp-9e793dc8-...
  5. ❌ Orphaned universe records still in IndexedDB
  6. 🧟‍♂️ Universe comes back!

With guest context clearing:

  1. ✅ Delete all data
  2. ✅ Clear guest user ID
  3. ✅ Navigate to /landing
  4. ✅ Next time: New guest user with new space ID
  5. ✅ No orphaned records = clean slate!

Issue 2: Galaxy Creation Not Showing in UI

The Problem

When creating a galaxy:

  1. ✅ Galaxy created via SpaceMapFlashStateService.createGalaxy()
  2. ✅ Saved to Flash State (localStorage): 'flash:space-map/galaxies'
  3. ❌ UI doesn't update - still shows 0 galaxies
  4. ❌ Component reads from UnifiedDataService.galaxies() which is stale

Root Cause: The UniverseDetailComponent uses UnifiedDataService for data:

// libs/space-map-lib/src/lib/components/universe-detail/universe-detail.component.ts:140
readonly galaxies = this.unifiedDataService.galaxies; // From GgStorage

When a galaxy is created:

The Solution: Reload UnifiedDataService After Creation

After creating a galaxy, explicitly reload UnifiedDataService to pick up the new data:

// libs/space-map-lib/src/lib/components/universe-detail/universe-detail.component.ts

async onGalaxyCreated(galaxyData: { name: string, description?: string, color?: string }) {
  try {
    const currentUniverseId = this.currentWorldId();
    
    // Create galaxy using flash state service
    await this.flashState.createGalaxy({
      name: galaxyData.name,
      description: galaxyData.description,
      colors: [galaxyData.color || '#7a68f3'],
      universeId: currentUniverseId
    });

    console.log('✅ Galaxy created successfully via flash state');

    // 🔄 CRITICAL: Reload UnifiedDataService to pick up new galaxy from storage
    console.log('🔄 Reloading UnifiedDataService to refresh galaxies...');
    const currentSpace = this.spaceService.currentSpace();
    if (currentSpace) {
      await this.unifiedDataService.loadSpaceData(currentSpace.spaceSlug);
      console.log('✅ UnifiedDataService reloaded - galaxies updated');
    }

    // 🔄 Clear cache and force refresh
    this.clearGalaxyPagesCache();
    this.refreshUniverseData();

    this.showCreateGalaxyDialog = false;
    this.cdr.detectChanges();

  } catch (error) {
    console.error('❌ Error creating galaxy:', error);
  }
}

Why This Works

  1. Galaxy Created: Saved to Flash State + IndexedDB
  2. UnifiedDataService Reloaded: Reads fresh data from storage
  3. Component Signals Update: Reactive signals pick up new galaxy
  4. UI Updates: Galaxy appears in the constellation view

Alternative Approaches Considered

Option A: Direct Signal Update (Not chosen)

// Manually update UnifiedDataService signals
this.unifiedDataService._galaxies.update(galaxies => [...galaxies, newGalaxy]);

❌ Violates encapsulation - signals are private for a reason

Option B: Event-Based Sync (Not chosen)

// Emit event, UnifiedDataService listens and reloads
this.flashState.galaxyCreated$.emit(newGalaxy);

❌ Adds complexity with event system

Option C: Reload Data (Chosen) ✅

// Simply reload all data for the space
await this.unifiedDataService.loadSpaceData(spaceSlug);

✅ Simple, reliable, ensures consistency ✅ Works for all data types (galaxies, pages, flows) ✅ No side effects or hidden state

Why SyncEngine Isn't Enough

The SyncEngine is great for most operations, but has a critical limitation:

// SyncEngine queues operations but doesn't sync yet
private async syncOperationToBackend(operation: SyncOperation): Promise<void> {
  // TODO: Implement actual backend sync ⚠️
  this.logger.debug('[SyncEngine] Syncing operation to backend:', operation);
}

Until SyncEngine implements actual backend sync, deletion operations MUST:

  1. Use the HYBRID approach (delete backend + local)
  2. Clear Flash Persistence before reload

Future Improvement

When SyncEngineService.syncOperationToBackend() is implemented:

  1. Keep HYBRID approach for critical operations (clear all data)
  2. Consider pure offline-first for non-critical operations (individual deletes)
  3. Add "force sync" flag for operations that need immediate backend sync

Multi-Tenant Readiness

Current State: Tenant-Scoped Clearing ✅

The current implementation already supports multi-tenant architectures:

  1. Space-Scoped Deletion: clearAllData() only deletes the current space (tenant)

    const space = this._currentSpace();
    for (const universeId of space.universeIds) {
      await this.storage.remove(`universe_${universeId}`);
    }
    
  2. Flash Persistence Clearing: Uses clearFlashState() which is user/space aware

    // Only clears Flash Persistence for current space
    this.flashState.clearFlashState();
    
  3. Micro-Cache: Uses nuclearCacheReset() which clears all namespaces

    • ⚠️ Note: This is global, not tenant-scoped
    • Future improvement: Add clearForUser(userId) or clearForSpace(spaceId)

Future Enhancements for Multi-Tenant

When fully multi-tenant:

  1. Tenant-Scoped Micro-Cache Clearing

    // Instead of:
    GgStorage.nuclearCacheReset();
    
    // Use:
    GgStorage.clearForSpace(currentSpaceId);
    // or
    GgStorage.clearForUser(currentUserId);
    
  2. Tenant-Isolated Flash Persistence Keys

    // Keys should include tenant/space ID
    autoFlashState(`space-${spaceId}/universes`, []);
    // Instead of:
    autoFlashState('space-map/universes', []);
    
  3. Backend Soft Deletes for Multi-Tenant

    • Add deletedAt timestamp instead of hard delete
    • Support tenant-level restore operations
    • Audit trail for compliance

Testing Multi-Tenant Clearing

// Test scenario:
1. Login as User A → Create universe in Space A
2. Login as User B → Create universe in Space B  
3. As User B: Click "Clear All Data"
4. Verify:
   ✅ User B's data is cleared
   ✅ User A's data is PRESERVED
   ✅ No cross-tenant contamination

Issue 4: Storage Key Mismatch - Galaxy Not Appearing Until Refresh

🐛 The Problem

Symptom: Galaxy was created successfully but didn't appear in the UI until page refresh.

Root Cause: Storage key mismatch between save and load operations.

What Was Happening:

  1. Galaxy Creation:

    // SpaceMapFlashStateService.createGalaxy()
    this._galaxies.update(galaxies => [...galaxies, newGalaxy]);  // ← Updates signal
    await this.forceSave();  // ← Saves to storage
    
  2. Force Save (BUGGY):

    // SpaceMapFlashStateService.forceSave()
    await this.saveToStorage('space-map/galaxies', galaxies);  // ← NO "flash:" prefix!
    
  3. UnifiedDataService Load (Looking for wrong key):

    // UnifiedDataService.loadGalaxies()
    const galaxiesState = await this.storage.get<any>('flash:space-map/galaxies');  // ← Has "flash:" prefix!
    

Keys didn't match!

✅ The Fix

Update SpaceMapFlashStateService.forceSave() to use the correct flash: prefix:

// libs/space-map-lib/src/lib/services/space-map-flash-state.service.ts
async forceSave(): Promise<void> {
  try {
    // 🔥 CRITICAL: Use "flash:" prefix to match autoFlashState storage keys
    // This ensures UnifiedDataService can read the galaxies immediately after save
    await Promise.all([
      this.saveToStorage('flash:space-map/worlds', pages),
      this.saveToStorage('flash:space-map/galaxies', galaxies),       // ← Fixed!
      this.saveToStorage('flash:space-map/universes', universes),
      this.saveToStorage('flash:space-map/ui-state', uiState)
    ]);
  } catch (error) {
    console.error('❌ Failed to force save:', error);
    throw error;
  }
}

Also updated clearAllSpaceMapData() to use the correct keys:

async clearAllSpaceMapData(): Promise<void> {
  // Clear storage (use "flash:" prefix to match autoFlashState keys)
  await Promise.all([
    this.storage.remove('flash:space-map/worlds'),
    this.storage.remove('flash:space-map/galaxies'),       // ← Fixed!
    this.storage.remove('flash:space-map/universes'),
    this.storage.remove('flash:space-map/ui-state')
  ]);
}

Why the flash: Prefix?

autoFlashState automatically adds the flash: prefix to all storage keys. This is why:

// When you declare:
private _galaxies = autoFlashState<GalaxyGroup[]>('space-map/galaxies', []);

// autoFlashState saves to:
'flash:space-map/galaxies'  // ← Automatic prefix!

Manual saveToStorage() calls must use the same prefix to match.

Files Changed

Result

✅ NOW: Galaxy appears immediately after creation (no page refresh required!)


Related Documentation