Best Practices

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Flash Persistence - Best Practices

Recommended patterns and practices for building robust applications with Flash Persistence.


Table of Contents

  1. Key Naming Conventions
  2. State Organization
  3. Performance Optimization
  4. Type Safety
  5. Testing
  6. Security
  7. Common Pitfalls

Key Naming Conventions

Use Descriptive, Hierarchical Keys

// ❌ Bad - Flat, unclear
const state1 = autoFlashState({ key: 'data', initialValue: {} });
const state2 = autoFlashState({ key: 'stuff', initialValue: {} });

// ✅ Good - Hierarchical, clear
const userProfile = autoFlashState({
  key: 'profile',
  database: 'user-data',
  collection: 'profiles',
  initialValue: {}
});

const appSettings = autoFlashState({
  key: 'settings',
  database: 'app-data',
  collection: 'config',
  initialValue: {}
});

Follow Consistent Patterns

// ✅ Consistent naming scheme
const patterns = {
  // User-specific data
  userPreferences: 'user-data:preferences:*',
  userProfile: 'user-data:profile:*',
  userHistory: 'user-data:history:*',
  
  // App-level data
  appState: 'app-data:state:*',
  appConfig: 'app-data:config:*',
  appCache: 'app-data:cache:*',
  
  // Session data
  sessionData: 'session:data:*',
  sessionTemp: 'session:temp:*'
};

Avoid Special Characters

// ❌ Bad - Special characters cause issues
autoFlashState({ key: 'user:profile', initialValue: {} });
autoFlashState({ key: 'app/state', initialValue: {} });

// ✅ Good - Use database/collection/namespace instead
autoFlashState({
  key: 'profile',
  database: 'user',
  initialValue: {}
});

State Organization

Service-Based State Management

// ✅ Centralize related state in services
@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
  // Persistent state
  private state = autoFlashState({
    key: 'preferences',
    database: 'user-data',
    collection: 'settings',
    initialValue: {
      theme: 'dark',
      language: 'en',
      notifications: true
    }
  });
  
  // Read-only signals for consumers
  theme = computed(() => this.state().theme);
  language = computed(() => this.state().language);
  notifications = computed(() => this.state().notifications);
  
  // Actions
  setTheme(theme: string) {
    this.state.update(s => ({ ...s, theme }));
  }
  
  setLanguage(language: string) {
    this.state.update(s => ({ ...s, language }));
  }
  
  toggleNotifications() {
    this.state.update(s => ({ ...s, notifications: !s.notifications }));
  }
}

Feature-Based Organization

// features/cart/cart.service.ts
@Injectable({ providedIn: 'root' })
export class CartService {
  private cart = autoFlashState<CartItem[]>({
    key: 'items',
    database: 'ecommerce',
    collection: 'cart',
    initialValue: []
  });
  
  items = this.cart.asReadonly();
  itemCount = computed(() => this.cart().length);
  total = computed(() => 
    this.cart().reduce((sum, item) => sum + item.price, 0)
  );
  
  addItem(item: CartItem) { /* ... */ }
  removeItem(id: string) { /* ... */ }
}

Composition Over Inheritance

// ✅ Compose services
@Injectable({ providedIn: 'root' })
export class CheckoutService {
  private cart = inject(CartService);
  private user = inject(UserService);
  private payments = inject(PaymentService);
  
  private checkoutState = autoFlashState({
    key: 'checkout',
    database: 'ecommerce',
    collection: 'checkout',
    initialValue: {
      step: 1,
      shippingAddress: null,
      paymentMethod: null
    }
  });
  
  // Use composition to access cart, user, payment state
  canCheckout = computed(() => 
    this.cart.itemCount() > 0 && this.user.isAuthenticated()
  );
}

Performance Optimization

Use Computed Signals for Derived State

// ❌ Bad - Recomputes on every access
class CartService {
  cart = autoFlashState<CartItem[]>({ key: 'cart', initialValue: [] });
  
  getTotal() {
    return this.cart().reduce((sum, item) => sum + item.price, 0);
  }
}

// ✅ Good - Computed once, cached
class CartService {
  cart = autoFlashState<CartItem[]>({ key: 'cart', initialValue: [] });
  
  total = computed(() =>
    this.cart().reduce((sum, item) => sum + item.price, 0)
  );
}

Batch Updates

// ❌ Bad - Multiple persistence triggers
function addMultipleItems(items: CartItem[]) {
  items.forEach(item => {
    cart.update(c => [...c, item]); // Each triggers persistence!
  });
}

// ✅ Good - Single persistence trigger
function addMultipleItems(items: CartItem[]) {
  cart.update(c => [...c, ...items]);
}

Use TTL for Temporary Data

// ✅ Automatic cleanup
const searchResults = autoFlashState({
  key: 'search-results',
  initialValue: [],
  ttl: 5 * 60 * 1000 // 5 minutes
});

const sessionData = autoFlashState({
  key: 'session',
  initialValue: null,
  ttl: 30 * 60 * 1000 // 30 minutes
});

Optimize Large Arrays

// ❌ Bad - Stores entire array
const largeList = autoFlashState({
  key: 'list',
  initialValue: [] // Could be thousands of items
});

// ✅ Good - Paginate or limit
const recentItems = autoFlashState({
  key: 'recent',
  initialValue: [] // Keep only last 50
});

function addRecentItem(item: any) {
  recentItems.update(items => [item, ...items].slice(0, 50));
}

Debounce User Input

@Component({
  template: `
    <input [ngModel]="searchQuery()"
           (ngModelChange)="updateSearch($event)" />
  `
})
export class SearchComponent {
  searchQuery = autoFlashState({
    key: 'search-query',
    initialValue: ''
  });
  
  private updateDebounced = debounce((query: string) => {
    this.searchQuery.set(query);
  }, 300);
  
  updateSearch(query: string) {
    this.updateDebounced(query);
  }
}

Type Safety

Use Generic Types

// ✅ Fully typed
interface UserPreferences {
  theme: 'light' | 'dark';
  language: 'en' | 'es' | 'fr';
  notifications: boolean;
}

const preferences = autoFlashState<UserPreferences>({
  key: 'preferences',
  initialValue: {
    theme: 'dark',
    language: 'en',
    notifications: true
  }
});

// TypeScript enforces valid values
preferences.set({
  theme: 'light', // ✅ Valid
  language: 'invalid', // ❌ Type error
  notifications: true
});

Create Type-Safe Builders

// ✅ Type-safe builder pattern
class PreferencesBuilder {
  static create<T extends Record<string, any>>(
    key: string,
    initialValue: T
  ) {
    return autoFlashState<T>({
      key,
      database: 'user-data',
      collection: 'preferences',
      initialValue
    });
  }
}

// Usage with full type inference
const prefs = PreferencesBuilder.create('my-prefs', {
  theme: 'dark',
  autoSave: true
});

prefs().theme; // ✅ Type: string
prefs().autoSave; // ✅ Type: boolean

Avoid Type Assertions

// ❌ Bad - Loses type safety
const state = autoFlashState({
  key: 'data',
  initialValue: {} as any
});

// ✅ Good - Explicit interface
interface AppData {
  loaded: boolean;
  error: string | null;
  data: any[];
}

const state = autoFlashState<AppData>({
  key: 'data',
  initialValue: {
    loaded: false,
    error: null,
    data: []
  }
});

Testing

Test State Persistence

describe('CartService', () => {
  let service: CartService;
  let storage: GgStorage;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideFlashPersistence()]
    });
    
    service = TestBed.inject(CartService);
    storage = TestBed.inject(GgStorage);
  });
  
  it('should persist cart items', async () => {
    const item = { id: '1', name: 'Test', price: 10 };
    service.addItem(item);
    
    // Wait for debounce + persistence
    await new Promise(resolve => setTimeout(resolve, 400));
    
    // Verify persistence
    const stored = await storage.get('ecommerce:cart:items');
    expect(stored).toContainEqual(item);
  });
  
  it('should restore cart on reload', async () => {
    const items = [
      { id: '1', name: 'Item 1', price: 10 },
      { id: '2', name: 'Item 2', price: 20 }
    ];
    
    // Store directly
    await storage.put('ecommerce:cart:items', items);
    
    // Recreate service (simulates page reload)
    service = TestBed.inject(CartService);
    
    // Should restore from storage
    expect(service.cart()).toEqual(items);
  });
});

Mock Flash Persistence

// test-utils/flash-persistence-mock.ts
export function provideMockFlashPersistence() {
  return {
    provide: FlashStateHydratorService,
    useValue: {
      updateState: jasmine.createSpy('updateState'),
      getInstantState: jasmine.createSpy('getInstantState'),
      clearState: jasmine.createSpy('clearState')
    }
  };
}

// Usage in tests
TestBed.configureTestingModule({
  providers: [provideMockFlashPersistence()]
});

Test Computed Signals

it('should compute total correctly', () => {
  service.cart.set([
    { id: '1', price: 10, quantity: 2 },
    { id: '2', price: 20, quantity: 1 }
  ]);
  
  expect(service.total()).toBe(40); // (10*2) + (20*1)
});

Security

Avoid Storing Sensitive Data

// ❌ Bad - Never store in browser storage
const passwords = autoFlashState({
  key: 'passwords',
  initialValue: []
});

const creditCards = autoFlashState({
  key: 'payment-methods',
  initialValue: []
});

// ✅ Good - Store only non-sensitive identifiers
const authToken = autoFlashState({
  key: 'auth-token',
  initialValue: null,
  ttl: 60 * 60 * 1000 // 1 hour, auto-expire
});

const paymentMethodIds = autoFlashState({
  key: 'saved-payment-ids',
  initialValue: [] // Just IDs, actual data stays server-side
});

Sanitize User Input

// ✅ Sanitize before storing
function updateUserProfile(input: any) {
  const sanitized = {
    name: sanitizeString(input.name),
    bio: sanitizeString(input.bio),
    website: sanitizeUrl(input.website)
  };
  
  userProfile.set(sanitized);
}

Use TTL for Session Data

// ✅ Auto-expire sensitive session data
const sessionData = autoFlashState({
  key: 'session',
  initialValue: null,
  ttl: 30 * 60 * 1000 // 30 minutes
});

Encrypt Sensitive Data

// ✅ Encrypt before storing (if absolutely necessary)
import { encrypt, decrypt } from './crypto-utils';

const encryptedData = autoFlashState({
  key: 'encrypted',
  initialValue: null
});

function storeSecure(data: any) {
  const encrypted = encrypt(JSON.stringify(data));
  encryptedData.set(encrypted);
}

function retrieveSecure(): any {
  const encrypted = encryptedData();
  if (!encrypted) return null;
  return JSON.parse(decrypt(encrypted));
}

Common Pitfalls

1. Forgetting Initial Values

// ❌ Bad - Undefined initial value
const state = autoFlashState({
  key: 'data',
  initialValue: undefined // Will cause issues
});

// ✅ Good - Explicit initial value
const state = autoFlashState({
  key: 'data',
  initialValue: null // or {}, [], etc.
});

2. Mutating State Directly

// ❌ Bad - Mutates signal value
const cart = autoFlashState<CartItem[]>({
  key: 'cart',
  initialValue: []
});

cart().push(newItem); // ❌ Doesn't trigger updates!

// ✅ Good - Immutable update
cart.update(items => [...items, newItem]);

3. Not Using Computed for Derived Values

// ❌ Bad - Recomputes every time
function getExpensiveValue() {
  return cart().map(item => {
    // Expensive computation
    return processItem(item);
  });
}

// ✅ Good - Computed once
const expensiveValue = computed(() =>
  cart().map(item => processItem(item))
);

4. Over-Persisting

// ❌ Bad - Persists every mouse move
@HostListener('mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
  mousePosition.set({ x: event.clientX, y: event.clientY });
  // This triggers persistence every frame!
}

// ✅ Good - Don't persist transient UI state
mousePosition = signal({ x: 0, y: 0 });
// Or use debounce/throttle if persistence is needed

5. Circular Dependencies

// ❌ Bad - Circular effect
const a = autoFlashState({ key: 'a', initialValue: 0 });
const b = autoFlashState({ key: 'b', initialValue: 0 });

effect(() => {
  b.set(a() + 1); // Sets b based on a
});

effect(() => {
  a.set(b() + 1); // Sets a based on b - circular!
});

// ✅ Good - Unidirectional data flow
const a = autoFlashState({ key: 'a', initialValue: 0 });
const b = computed(() => a() + 1); // Derived, not stored

6. Ignoring TTL for Cache

// ❌ Bad - Cache never expires
const apiCache = autoFlashState({
  key: 'api-cache',
  initialValue: {}
});

// ✅ Good - Set appropriate TTL
const apiCache = autoFlashState({
  key: 'api-cache',
  initialValue: {},
  ttl: 5 * 60 * 1000 // 5 minutes
});

Architecture Patterns

Repository Pattern

@Injectable({ providedIn: 'root' })
export class UserRepository {
  private cache = autoFlashState<User[]>({
    key: 'users-cache',
    database: 'cache',
    initialValue: [],
    ttl: 5 * 60 * 1000
  });
  
  async getUsers(): Promise<User[]> {
    const cached = this.cache();
    if (cached.length > 0) return cached;
    
    const users = await this.api.fetchUsers();
    this.cache.set(users);
    return users;
  }
}

Facade Pattern

@Injectable({ providedIn: 'root' })
export class AppFacade {
  private userService = inject(UserService);
  private cartService = inject(CartService);
  private prefsService = inject(PreferencesService);
  
  // Unified interface
  readonly user = this.userService.currentUser;
  readonly cart = this.cartService.items;
  readonly theme = this.prefsService.theme;
  
  // Unified actions
  updateTheme(theme: string) {
    this.prefsService.setTheme(theme);
  }
  
  addToCart(item: CartItem) {
    this.cartService.addItem(item);
  }
}

Next Steps


Questions? Join our GitHub Discussions.