Advanced

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

Flash Persistence - Advanced Usage

Deep dive into advanced patterns, optimizations, and complex scenarios with Flash Persistence.


Table of Contents

  1. Custom Serialization
  2. Advanced State Patterns
  3. Performance Profiling
  4. Migration Strategies
  5. Multi-Tenant Support
  6. SSR Considerations
  7. Complex Sync Scenarios

Custom Serialization

Complex Object Serialization

For objects with methods, getters, or non-JSON types:

class User {
  constructor(
    public id: string,
    public name: string,
    private _joinDate: Date
  ) {}
  
  get joinDate(): Date {
    return this._joinDate;
  }
  
  get membershipDays(): number {
    const now = new Date();
    const diff = now.getTime() - this._joinDate.getTime();
    return Math.floor(diff / (1000 * 60 * 60 * 24));
  }
  
  // Custom serialization
  toJSON() {
    return {
      id: this.id,
      name: this.name,
      joinDate: this._joinDate.toISOString()
    };
  }
  
  // Custom deserialization
  static fromJSON(json: any): User {
    return new User(
      json.id,
      json.name,
      new Date(json.joinDate)
    );
  }
}

// Usage with Flash Persistence
const userState = autoFlashState({
  key: 'current-user',
  initialValue: new User('', '', new Date())
});

// Automatic serialization via toJSON
userState.set(new User('123', 'John', new Date()));

// Manual deserialization on load
effect(() => {
  const stored = userState();
  if (stored && !(stored instanceof User)) {
    userState.set(User.fromJSON(stored));
  }
});

Binary Data Serialization

For storing binary data (images, files):

interface ImageData {
  id: string;
  name: string;
  data: string; // Base64 encoded
  mimeType: string;
}

class ImageSerializer {
  static async fileToBase64(file: File): Promise<string> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }
  
  static base64ToBlob(base64: string, mimeType: string): Blob {
    const byteString = atob(base64.split(',')[1]);
    const ab = new ArrayBuffer(byteString.length);
    const ia = new Uint8Array(ab);
    
    for (let i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i);
    }
    
    return new Blob([ab], { type: mimeType });
  }
}

// Store images
const imageCache = autoFlashState<ImageData[]>({
  key: 'image-cache',
  initialValue: [],
  ttl: 24 * 60 * 60 * 1000 // 24 hours
});

async function cacheImage(file: File) {
  const base64 = await ImageSerializer.fileToBase64(file);
  const imageData: ImageData = {
    id: crypto.randomUUID(),
    name: file.name,
    data: base64,
    mimeType: file.type
  };
  
  imageCache.update(images => [...images, imageData]);
}

Circular Reference Handling

For data structures with circular references:

interface TreeNode {
  id: string;
  value: any;
  children: TreeNode[];
  parent?: TreeNode;
}

class CircularSafeSerializer {
  private seen = new WeakSet();
  
  serialize(obj: any): any {
    if (obj === null || typeof obj !== 'object') {
      return obj;
    }
    
    if (this.seen.has(obj)) {
      return { $ref: obj.id }; // Reference marker
    }
    
    this.seen.add(obj);
    
    if (Array.isArray(obj)) {
      return obj.map(item => this.serialize(item));
    }
    
    const serialized: any = {};
    for (const key in obj) {
      if (key !== 'parent') { // Skip parent to avoid circular ref
        serialized[key] = this.serialize(obj[key]);
      }
    }
    
    return serialized;
  }
  
  deserialize(data: any, nodeMap = new Map()): any {
    if (data === null || typeof data !== 'object') {
      return data;
    }
    
    if (data.$ref) {
      return nodeMap.get(data.$ref);
    }
    
    if (Array.isArray(data)) {
      return data.map(item => this.deserialize(item, nodeMap));
    }
    
    const obj: any = {};
    if (data.id) {
      nodeMap.set(data.id, obj);
    }
    
    for (const key in data) {
      obj[key] = this.deserialize(data[key], nodeMap);
    }
    
    return obj;
  }
}

Advanced State Patterns

State Machine Pattern

Implement a finite state machine with Flash Persistence:

type OrderState = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

interface OrderStateMachine {
  currentState: OrderState;
  history: Array<{ state: OrderState; timestamp: number }>;
  transitions: Record<OrderState, OrderState[]>;
}

const orderState = autoFlashState<OrderStateMachine>({
  key: 'order-state-machine',
  database: 'ecommerce',
  collection: 'orders',
  initialValue: {
    currentState: 'pending',
    history: [],
    transitions: {
      pending: ['processing', 'cancelled'],
      processing: ['shipped', 'cancelled'],
      shipped: ['delivered'],
      delivered: [],
      cancelled: []
    }
  }
});

function canTransition(from: OrderState, to: OrderState): boolean {
  return orderState().transitions[from].includes(to);
}

function transitionTo(newState: OrderState) {
  const current = orderState();
  
  if (!canTransition(current.currentState, newState)) {
    throw new Error(`Invalid transition: ${current.currentState} → ${newState}`);
  }
  
  orderState.update(state => ({
    ...state,
    currentState: newState,
    history: [
      ...state.history,
      { state: newState, timestamp: Date.now() }
    ]
  }));
}

Command Pattern

Implement undo/redo with Flash Persistence:

interface Command {
  id: string;
  type: string;
  execute(): void;
  undo(): void;
  timestamp: number;
}

interface CommandHistory {
  past: Command[];
  future: Command[];
}

class TextEditCommand implements Command {
  id = crypto.randomUUID();
  type = 'text-edit';
  timestamp = Date.now();
  
  constructor(
    private textState: WritableSignal<string>,
    private oldValue: string,
    private newValue: string
  ) {}
  
  execute() {
    this.textState.set(this.newValue);
  }
  
  undo() {
    this.textState.set(this.oldValue);
  }
}

const commandHistory = autoFlashState<CommandHistory>({
  key: 'command-history',
  initialValue: { past: [], future: [] }
});

function executeCommand(command: Command) {
  command.execute();
  
  commandHistory.update(history => ({
    past: [...history.past, command],
    future: [] // Clear redo stack
  }));
}

function undo() {
  const history = commandHistory();
  if (history.past.length === 0) return;
  
  const command = history.past[history.past.length - 1];
  command.undo();
  
  commandHistory.update(h => ({
    past: h.past.slice(0, -1),
    future: [command, ...h.future]
  }));
}

function redo() {
  const history = commandHistory();
  if (history.future.length === 0) return;
  
  const command = history.future[0];
  command.execute();
  
  commandHistory.update(h => ({
    past: [...h.past, command],
    future: h.future.slice(1)
  }));
}

Observer Pattern

Implement a publish-subscribe system:

interface Subscription<T> {
  id: string;
  callback: (data: T) => void;
  filter?: (data: T) => boolean;
}

class PubSubState<T> {
  private state = autoFlashState<T[]>({
    key: 'pubsub-events',
    initialValue: []
  });
  
  private subscriptions = new Map<string, Subscription<T>>();
  
  constructor() {
    // React to state changes
    effect(() => {
      const events = this.state();
      if (events.length > 0) {
        const latest = events[events.length - 1];
        this.notifySubscribers(latest);
      }
    });
  }
  
  publish(data: T) {
    this.state.update(events => [...events, data]);
  }
  
  subscribe(callback: (data: T) => void, filter?: (data: T) => boolean): string {
    const id = crypto.randomUUID();
    this.subscriptions.set(id, { id, callback, filter });
    return id;
  }
  
  unsubscribe(id: string) {
    this.subscriptions.delete(id);
  }
  
  private notifySubscribers(data: T) {
    this.subscriptions.forEach(sub => {
      if (!sub.filter || sub.filter(data)) {
        sub.callback(data);
      }
    });
  }
}

Performance Profiling

Monitoring State Performance

interface PerformanceMetric {
  operation: 'read' | 'write' | 'update';
  duration: number;
  timestamp: number;
  key: string;
}

class FlashPerformanceProfiler {
  private metrics: PerformanceMetric[] = [];
  private enabled = false;
  
  enable() {
    this.enabled = true;
  }
  
  disable() {
    this.enabled = false;
  }
  
  trackRead<T>(key: string, fn: () => T): T {
    if (!this.enabled) return fn();
    
    const start = performance.now();
    const result = fn();
    const duration = performance.now() - start;
    
    this.metrics.push({
      operation: 'read',
      duration,
      timestamp: Date.now(),
      key
    });
    
    return result;
  }
  
  getMetrics() {
    return {
      total: this.metrics.length,
      avgReadTime: this.calculateAverage('read'),
      avgWriteTime: this.calculateAverage('write'),
      avgUpdateTime: this.calculateAverage('update'),
      slowestOperations: this.getSlowest(10)
    };
  }
  
  private calculateAverage(operation: 'read' | 'write' | 'update'): number {
    const filtered = this.metrics.filter(m => m.operation === operation);
    if (filtered.length === 0) return 0;
    
    const sum = filtered.reduce((acc, m) => acc + m.duration, 0);
    return sum / filtered.length;
  }
  
  private getSlowest(count: number): PerformanceMetric[] {
    return [...this.metrics]
      .sort((a, b) => b.duration - a.duration)
      .slice(0, count);
  }
  
  clear() {
    this.metrics = [];
  }
}

// Usage
const profiler = new FlashPerformanceProfiler();
profiler.enable();

// Profile reads
const value = profiler.trackRead('my-key', () => myState());

// Get results
console.log(profiler.getMetrics());

Memory Usage Tracking

class MemoryTracker {
  private baseline = 0;
  
  startTracking() {
    if (performance.memory) {
      this.baseline = (performance as any).memory.usedJSHeapSize;
    }
  }
  
  getMemoryUsage(): number {
    if (!performance.memory) {
      console.warn('Memory API not available');
      return 0;
    }
    
    const current = (performance as any).memory.usedJSHeapSize;
    return (current - this.baseline) / 1024 / 1024; // MB
  }
  
  logMemoryImpact(operation: string) {
    const before = this.getMemoryUsage();
    
    return () => {
      const after = this.getMemoryUsage();
      const delta = after - before;
      console.log(`[${operation}] Memory impact: ${delta.toFixed(2)}MB`);
    };
  }
}

// Usage
const tracker = new MemoryTracker();
tracker.startTracking();

const logImpact = tracker.logMemoryImpact('large-state-update');
largeState.set(massiveDataArray);
logImpact();

Migration Strategies

Migrating from NGXS

Complete migration pattern from NGXS to Flash Persistence:

// Before: NGXS
@State<PreferencesStateModel>({
  name: 'preferences',
  defaults: {
    theme: 'light',
    language: 'en',
    notifications: true
  }
})
@Injectable()
export class PreferencesState {
  @Selector()
  static theme(state: PreferencesStateModel) {
    return state.theme;
  }
  
  @Action(SetTheme)
  setTheme(ctx: StateContext<PreferencesStateModel>, action: SetTheme) {
    ctx.patchState({ theme: action.theme });
  }
}

// After: Flash Persistence
@Injectable({ providedIn: 'root' })
export class PreferencesService {
  private state = autoFlashState({
    key: 'preferences',
    database: 'user-data',
    initialValue: {
      theme: 'light' as const,
      language: 'en' as const,
      notifications: true
    }
  });
  
  // Selectors → Computed
  theme = computed(() => this.state().theme);
  language = computed(() => this.state().language);
  notifications = computed(() => this.state().notifications);
  
  // Actions → Methods
  setTheme(theme: 'light' | 'dark') {
    this.state.update(s => ({ ...s, theme }));
  }
  
  setLanguage(language: 'en' | 'es' | 'fr') {
    this.state.update(s => ({ ...s, language }));
  }
  
  toggleNotifications() {
    this.state.update(s => ({ ...s, notifications: !s.notifications }));
  }
}

Data Migration Script

Migrate existing localStorage data:

interface MigrationStrategy {
  from: string;
  to: string;
  transform?: (data: any) => any;
}

class DataMigrator {
  async migrate(strategies: MigrationStrategy[]): Promise<void> {
    for (const strategy of strategies) {
      const oldData = localStorage.getItem(strategy.from);
      if (!oldData) continue;
      
      try {
        let parsed = JSON.parse(oldData);
        
        if (strategy.transform) {
          parsed = strategy.transform(parsed);
        }
        
        // Store in new Flash Persistence format
        const newState = autoFlashState({
          key: strategy.to,
          initialValue: parsed
        });
        
        newState.set(parsed);
        
        // Optionally remove old data
        localStorage.removeItem(strategy.from);
        
        console.log(`Migrated: ${strategy.from} → ${strategy.to}`);
      } catch (error) {
        console.error(`Migration failed for ${strategy.from}:`, error);
      }
    }
  }
}

// Usage
const migrator = new DataMigrator();

await migrator.migrate([
  {
    from: 'ngxs_preferences',
    to: 'preferences',
    transform: (data) => ({
      theme: data.theme || 'light',
      language: data.lang || 'en', // Rename field
      notifications: data.notifs ?? true // Default value
    })
  },
  {
    from: 'old_cart',
    to: 'cart'
  }
]);

Multi-Tenant Support

Tenant-Scoped State

@Injectable({ providedIn: 'root' })
export class MultiTenantStateService {
  private currentTenantId = signal<string | null>(null);
  private stateCache = new Map<string, WritableSignal<any>>();
  
  setTenant(tenantId: string) {
    this.currentTenantId.set(tenantId);
  }
  
  getTenantState<T>(key: string, initialValue: T): WritableSignal<T> {
    const tenantId = this.currentTenantId();
    if (!tenantId) {
      throw new Error('No tenant set');
    }
    
    const cacheKey = `${tenantId}:${key}`;
    
    if (!this.stateCache.has(cacheKey)) {
      const state = autoFlashState({
        key,
        database: tenantId,
        collection: 'tenant-data',
        initialValue
      });
      this.stateCache.set(cacheKey, state);
    }
    
    return this.stateCache.get(cacheKey)!;
  }
  
  clearTenantData(tenantId: string) {
    // Remove all cached states for tenant
    for (const [key, state] of this.stateCache.entries()) {
      if (key.startsWith(`${tenantId}:`)) {
        this.stateCache.delete(key);
      }
    }
  }
}

// Usage
const multiTenant = inject(MultiTenantStateService);

// Switch tenant
multiTenant.setTenant('tenant-123');

// Get tenant-specific state
const preferences = multiTenant.getTenantState('preferences', {
  theme: 'dark'
});

SSR Considerations

Server-Side Rendering Support

import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class SSRSafeStateService {
  private platformId = inject(PLATFORM_ID);
  private isBrowser = isPlatformBrowser(this.platformId);
  
  createState<T>(options: AutoFlashStateOptions<T>): WritableSignal<T> {
    if (!this.isBrowser) {
      // SSR: Return non-persistent signal
      return signal(options.initialValue);
    }
    
    // Browser: Return persistent signal
    return autoFlashState(options);
  }
}

// Usage
const ssrSafe = inject(SSRSafeStateService);

const state = ssrSafe.createState({
  key: 'data',
  initialValue: { value: 0 }
});

Hydration Strategy

@Injectable({ providedIn: 'root' })
export class HydrationService {
  private platformId = inject(PLATFORM_ID);
  
  async hydrateFromServer<T>(
    key: string,
    serverData: T
  ): Promise<WritableSignal<T>> {
    if (isPlatformBrowser(this.platformId)) {
      // Check if we have local data
      const hydrator = inject(FlashStateHydratorService);
      const localData = hydrator.getInstantState<T>(key);
      
      if (localData) {
        // Use local data (offline-first)
        return autoFlashState({ key, initialValue: localData });
      }
    }
    
    // Use server data
    return autoFlashState({ key, initialValue: serverData });
  }
}

Complex Sync Scenarios

Conflict Resolution with Operational Transform

interface TextOperation {
  type: 'insert' | 'delete' | 'retain';
  position: number;
  text?: string;
  length?: number;
}

class OTResolver {
  transform(op1: TextOperation, op2: TextOperation): [TextOperation, TextOperation] {
    // Simplified OT algorithm
    if (op1.type === 'insert' && op2.type === 'insert') {
      if (op1.position < op2.position) {
        return [
          op1,
          { ...op2, position: op2.position + (op1.text?.length || 0) }
        ];
      }
      return [
        { ...op1, position: op1.position + (op2.text?.length || 0) },
        op2
      ];
    }
    
    // Add more cases for delete, retain...
    return [op1, op2];
  }
  
  apply(text: string, op: TextOperation): string {
    switch (op.type) {
      case 'insert':
        return text.slice(0, op.position) + op.text + text.slice(op.position);
      case 'delete':
        return text.slice(0, op.position) + text.slice(op.position + (op.length || 0));
      default:
        return text;
    }
  }
}

// Usage with Flash Persistence
const collaborativeDoc = autoFlashState({
  key: 'collaborative-doc',
  database: 'collab',
  priority: 'critical',
  initialValue: ''
});

const otResolver = new OTResolver();

function handleRemoteOperation(remoteOp: TextOperation) {
  const current = collaborativeDoc();
  const transformed = otResolver.transform(localPendingOp, remoteOp);
  const newText = otResolver.apply(current, transformed[0]);
  collaborativeDoc.set(newText);
}

Multi-Device Sync with Conflict Detection

interface SyncMetadata {
  lastSyncTime: number;
  deviceId: string;
  version: number;
}

const syncState = autoFlashState<SyncMetadata>({
  key: 'sync-metadata',
  initialValue: {
    lastSyncTime: 0,
    deviceId: crypto.randomUUID(),
    version: 0
  }
});

async function detectConflicts<T>(
  localData: T & { version: number },
  remoteData: T & { version: number }
): Promise<'local' | 'remote' | 'conflict'> {
  if (localData.version === remoteData.version) {
    return 'local'; // No conflict
  }
  
  if (localData.version > remoteData.version) {
    return 'local'; // Local is newer
  }
  
  if (remoteData.version > localData.version) {
    return 'remote'; // Remote is newer
  }
  
  return 'conflict'; // Versions diverged
}

Next Steps


Questions? Check GitHub Discussions or open an issue.