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
- Custom Serialization
- Advanced State Patterns
- Performance Profiling
- Migration Strategies
- Multi-Tenant Support
- SSR Considerations
- 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
- 📖 Overview - Architecture and concepts
- 🚀 Quick Start - Get started guide
- 📚 API Reference - Complete API
- 🎓 Best Practices - Recommended patterns
- 🎯 Component Integration - Usage in components
Questions? Check GitHub Discussions or open an issue.