CLEAR ALL DATA FIX
Documentation for Plans
Four Critical Fixes: Clear All Data, Galaxy Creation, Flash State & Storage Keys
Date: November 4, 2025
Issues:
- "Clear All Data" button (⚠️) didn't work - data reappeared after page reload
- Galaxy creation didn't show galaxies in UI - data saved to Flash State but UI read from IndexedDB
- Flash State was being cleared on EVERY page load instead of only on user/space switch
- 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:
- ✅ Delete from IndexedDB (works instantly)
- ❌ Queue for backend sync (does nothing!)
- 🔄 Page reloads after 500ms
- ❌ 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:
- Single universe deletion: Offline-first ✅
- Bulk data deletion: Old approach ❌
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:
Backend deletion happens FIRST ✅
- MongoDB data deleted before page reload
- No zombie data resurrection
Local deletion happens SECOND ✅
- IndexedDB cleanup
- Immediate UI update
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
- Added
disableFlushOnUnloadstatic flag - Added
disablePersistenceFlush()public static method - Added
enablePersistenceFlush()public static method - Updated
handleBeforeUnload()to check flag before flushing - Updated
handleVisibilityChange()to check flag before flushing
apps/gograb-space/src/app/components/space-components/g-space-dashboard/g-space-dashboard.component.ts
- Updated
clearAllData()method:- STEP 0: Disable persistence flush via
AppComponent.disablePersistenceFlush() - STEP 1-5: Clear data as before
- STEP 6: Navigate to landing without re-enabling flush
- Error handling: Re-enable flush if something goes wrong
- STEP 0: Disable persistence flush via
Issue 2: Galaxy Creation
libs/space-map-lib/src/lib/components/universe-detail/universe-detail.component.ts
- Updated
onGalaxyCreated()method:- Added
UnifiedDataService.loadSpaceData()call after galaxy creation - Ensures UI picks up newly created galaxy from storage
- Maintains reactive signal updates
- Added
Backend & Storage (Previous Fixes)
libs/core-angular/src/lib/services/space.service.ts
Updated
clearAllData()method (lines 896-946)- Added direct backend deletion BEFORE local deletion
- Uses
spaceApi.deleteSpace()for immediate sync
Updated
deleteUniverse()method (lines 776-818)- Added direct backend deletion BEFORE local deletion
- Uses
spaceApi.deleteUniverse()for immediate sync
libs/core-angular/src/lib/services/space-api.service.ts
- Added
deleteUniverse()method (lines 240-258)- New API method for deleting individual universes
DELETE /api/spaces/:spaceId/universes/:universeId
Testing
Test Case 1: Clear All Data (Issue 1)
- Create a universe with some galaxies
- Click the "Clear All Data" button (⚠️)
- Confirm both dialogs
- 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!)
- Redirects to landing page
- Navigate back to create a new space
- 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)
- Create a universe (e.g., "Test Universe")
- Navigate to universe detail view
- Click "+ New Galaxy" button
- Fill in galaxy details (name, description, color)
- Click "Create Galaxy"
- Watch console logs:
- ✅ Should see "Galaxy created successfully"
- ✅ Should see "Reloading UnifiedDataService"
- ✅ Should see "UnifiedDataService reloaded - galaxies updated"
- 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
- Create two universes
- Click trash button on one universe
- 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:
- ✅ Backend (MongoDB)
- ✅ IndexedDB
- ✅ Micro-cache
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:
- ✅ User clicks "Clear All Data"
- ✅ Data cleared from everywhere
- ✅ Navigate to
/landing - ❌
beforeunloadfires BEFORE navigation completes - ❌ Flushes Flash Persistence, re-saving all data
- 🔄 Page reloads
- 🧟♂️ 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:
- ✅ Delete all data
- 🔄 Page reloads
- ❌ Same guest user ID restored from localStorage
- ❌ Same space ID recreated:
space_temp-9e793dc8-... - ❌ Orphaned universe records still in IndexedDB
- 🧟♂️ Universe comes back!
With guest context clearing:
- ✅ Delete all data
- ✅ Clear guest user ID
- ✅ Navigate to /landing
- ✅ Next time: New guest user with new space ID
- ✅ No orphaned records = clean slate!
Issue 2: Galaxy Creation Not Showing in UI
The Problem
When creating a galaxy:
- ✅ Galaxy created via
SpaceMapFlashStateService.createGalaxy() - ✅ Saved to Flash State (localStorage):
'flash:space-map/galaxies' - ❌ UI doesn't update - still shows 0 galaxies
- ❌ 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:
- ✅ Saved to Flash State (localStorage via
autoFlashState) - ✅ Saved to GgStorage (IndexedDB)
- ❌ UnifiedDataService cached data NOT refreshed
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
- Galaxy Created: Saved to Flash State + IndexedDB
- UnifiedDataService Reloaded: Reads fresh data from storage
- Component Signals Update: Reactive signals pick up new galaxy
- 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:
- Use the HYBRID approach (delete backend + local)
- Clear Flash Persistence before reload
Future Improvement
When SyncEngineService.syncOperationToBackend() is implemented:
- Keep HYBRID approach for critical operations (clear all data)
- Consider pure offline-first for non-critical operations (individual deletes)
- 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:
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}`); }Flash Persistence Clearing: Uses
clearFlashState()which is user/space aware// Only clears Flash Persistence for current space this.flashState.clearFlashState();Micro-Cache: Uses
nuclearCacheReset()which clears all namespaces- ⚠️ Note: This is global, not tenant-scoped
- Future improvement: Add
clearForUser(userId)orclearForSpace(spaceId)
Future Enhancements for Multi-Tenant
When fully multi-tenant:
Tenant-Scoped Micro-Cache Clearing
// Instead of: GgStorage.nuclearCacheReset(); // Use: GgStorage.clearForSpace(currentSpaceId); // or GgStorage.clearForUser(currentUserId);Tenant-Isolated Flash Persistence Keys
// Keys should include tenant/space ID autoFlashState(`space-${spaceId}/universes`, []); // Instead of: autoFlashState('space-map/universes', []);Backend Soft Deletes for Multi-Tenant
- Add
deletedAttimestamp instead of hard delete - Support tenant-level restore operations
- Audit trail for compliance
- Add
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:
Galaxy Creation:
// SpaceMapFlashStateService.createGalaxy() this._galaxies.update(galaxies => [...galaxies, newGalaxy]); // ← Updates signal await this.forceSave(); // ← Saves to storageForce Save (BUGGY):
// SpaceMapFlashStateService.forceSave() await this.saveToStorage('space-map/galaxies', galaxies); // ← NO "flash:" prefix!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!
- Saved to:
'space-map/galaxies' - Loading from:
'flash:space-map/galaxies'
✅ 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
libs/space-map-lib/src/lib/services/space-map-flash-state.service.ts- Updated
forceSave()to use'flash:space-map/*'keys (lines 865-872) - Updated
clearAllSpaceMapData()to use'flash:space-map/*'keys (lines 899-904)
- Updated
Result
✅ NOW: Galaxy appears immediately after creation (no page refresh required!)