Files
ISA-Frontend/libs/core/storage
Lorenz Hilpert 596ae1da1b Merged PR 1969: Reward Shopping Cart Implementation with Navigation State Management and Shipping Address Integration
1. Reward Shopping Cart Implementation
  - New shopping cart with quantity control and availability checking
  - Responsive shopping cart item component with improved CSS styling
  - Shipping address integration in cart
  - Customer reward card and billing/shipping address components

  2. Navigation State Management Library (@isa/core/navigation)
  - New library with type-safe navigation context service (373 lines)
  - Navigation state service (287 lines) for temporary state between routes
  - Comprehensive test coverage (668 + 227 lines of tests)
  - Documentation (792 lines in README.md)
  - Replaces query parameters for passing temporary navigation context

  3. CRM Shipping Address Services
  - New ShippingAddressService with fetching and validation
  - CustomerShippingAddressResource and CustomerShippingAddressesResource
  - Zod schemas for data validation

  4. Additional Improvements
  - Enhanced searchbox accessibility with ARIA support
  - Availability data access rework for better fetching/mapping
  - Storybook tooltip variant support
  - Vitest JUnit and Cobertura reporting configuration

Related work items: #5382, #5383, #5384
2025-10-15 14:59:34 +00:00
..

@isa/core/storage

A powerful, type-safe storage library for Angular applications built on top of NgRx Signals. This library provides seamless integration between NgRx Signal Stores and various storage backends including localStorage, sessionStorage, IndexedDB, and server-side user state.

Features

  • 🔄 Auto-sync with NgRx Signals: Seamlessly integrates with NgRx Signal Stores
  • 🏪 Multiple Storage Providers: Support for localStorage, sessionStorage, IndexedDB, memory, and server-side storage
  • 🚀 Auto-save with Debouncing: Automatically persist state changes with configurable debouncing
  • 👤 User-scoped Storage: Automatic user-specific storage keys using OAuth identity claims
  • 🔒 Type-safe: Full TypeScript support with Zod schema validation
  • 🎛️ Configurable: Flexible configuration options for different use cases
  • 🧩 Extensible: Easy to add custom storage providers

Installation

This library is part of the ISA Frontend monorepo and is already available as @isa/core/storage.

Quick Start

import { signalStore, withState } from '@ngrx/signals';
import { withStorage, LocalStorageProvider } from '@isa/core/storage';

// Create a store with automatic localStorage persistence
const UserPreferencesStore = signalStore(
  withState({ theme: 'dark', language: 'en' }),
  withStorage('user-preferences', LocalStorageProvider)
);

// The store will automatically:
// 1. Load saved state on initialization
// 2. Provide manual save/load methods
// 3. Auto-save state changes (if enabled)

Storage Providers

LocalStorageProvider

Persists data to the browser's localStorage (survives browser restarts).

import { LocalStorageProvider } from '@isa/core/storage';

const store = signalStore(
  withState({ count: 0 }),
  withStorage('counter', LocalStorageProvider)
);

SessionStorageProvider

Persists data to sessionStorage (cleared when tab closes).

import { SessionStorageProvider } from '@isa/core/storage';

const store = signalStore(
  withState({ tempData: null }),
  withStorage('session-data', SessionStorageProvider)
);

IDBStorageProvider

Uses IndexedDB for larger data storage with better performance.

import { IDBStorageProvider } from '@isa/core/storage';

const store = signalStore(
  withState({ largeDataSet: [] }),
  withStorage('large-data', IDBStorageProvider)
);

UserStorageProvider

Server-side storage tied to the authenticated user's account.

import { UserStorageProvider } from '@isa/core/storage';

const store = signalStore(
  withState({ userSettings: {} }),
  withStorage('user-settings', UserStorageProvider)
);

MemoryStorageProvider

In-memory storage for testing or temporary data.

import { MemoryStorageProvider } from '@isa/core/storage';

const store = signalStore(
  withState({ testData: null }),
  withStorage('test-data', MemoryStorageProvider)
);

Configuration Options

The withStorage function accepts an optional configuration object:

interface WithStorageConfig {
  autosave?: boolean;     // Enable automatic state persistence (default: false)
  debounceTime?: number;  // Debounce time in milliseconds (default: 300)
  autoload?: boolean;     // Enable automatic state loading on initialization (default: true)
}

Manual Save/Load with Auto-load (Default Behavior)

const store = signalStore(
  withState({ data: 'initial' }),
  withStorage('my-data', LocalStorageProvider)
  // Default: autoload = true, autosave = false
);

// State is automatically loaded on initialization
// Manual save/load operations available
store.saveToStorage();    // Save current state
store.loadFromStorage();  // Manually reload state

Disable Auto-load for Full Manual Control

const store = signalStore(
  withState({ data: 'initial' }),
  withStorage('my-data', LocalStorageProvider, {
    autoload: false  // Disable automatic loading
  })
);

// Manual operations only
store.saveToStorage();    // Save current state
store.loadFromStorage();  // Load and restore state

Auto-save with Auto-load

const store = signalStore(
  withState({ count: 0 }),
  withStorage('counter', LocalStorageProvider, {
    autosave: true
    // autoload: true (default) - state loaded on initialization
    // Will auto-save with 300ms debounce
  })
);

Auto-save with Custom Debouncing

const store = signalStore(
  withState({ settings: {} }),
  withStorage('settings', LocalStorageProvider, {
    autosave: true,
    autoload: true,     // Load saved settings on initialization (default)
    debounceTime: 1000  // Save 1 second after last change
  })
);

Real-World Examples

Tab Management with Auto-save

// From libs/core/tabs/src/lib/tab.ts
export const TabService = signalStore(
  { providedIn: 'root' },
  withDevtools('TabService'),
  withStorage('tabs', UserStorageProvider), // Server-side user storage
  withState<{ activatedTabId: number | null }>({
    activatedTabId: null,
  }),
  withEntities<Tab>(),
  // ... other features
);

Shopping Cart with Auto-persistence

const ShoppingCartStore = signalStore(
  withState<{ items: CartItem[], total: number }>({
    items: [],
    total: 0
  }),
  withStorage('shopping-cart', LocalStorageProvider, {
    autosave: true,
    debounceTime: 500  // Save 500ms after changes stop
  }),
  withMethods((store) => ({
    addItem(item: CartItem) {
      const items = [...store.items(), item];
      const total = items.reduce((sum, item) => sum + item.price, 0);
      patchState(store, { items, total });
      // State automatically saved after 500ms
    },
    removeItem(id: string) {
      const items = store.items().filter(item => item.id !== id);
      const total = items.reduce((sum, item) => sum + item.price, 0);
      patchState(store, { items, total });
      // State automatically saved after 500ms
    }
  }))
);

User Preferences with Manual Control

const UserPreferencesStore = signalStore(
  withState({
    theme: 'light' as 'light' | 'dark',
    language: 'en',
    notifications: true
  }),
  withStorage('user-preferences', LocalStorageProvider),
  withMethods((store) => ({
    updateTheme(theme: 'light' | 'dark') {
      patchState(store, { theme });
      store.saveToStorage(); // Manual save
    },
    resetToDefaults() {
      patchState(store, {
        theme: 'light',
        language: 'en',
        notifications: true
      });
      store.saveToStorage(); // Manual save
    }
  }))
);

Advanced Usage

User-scoped Storage Keys

The library automatically creates user-specific storage keys when using any storage provider:

// Internal key generation (user sub: "user123", key: "settings")
// Results in: "user123:a1b2c3" (where a1b2c3 is a hash of "settings")

This ensures that different users' data never conflicts, even on shared devices.

Schema Validation with Zod

The underlying Storage class supports optional Zod schema validation:

import { z } from 'zod';
import { injectStorage, LocalStorageProvider } from '@isa/core/storage';

const UserSchema = z.object({
  name: z.string(),
  age: z.number()
});

// In a service or component
const storage = injectStorage(LocalStorageProvider);

// Type-safe get with validation
const userData = storage.get('user', UserSchema);
// userData is properly typed as z.infer<typeof UserSchema>

Custom Storage Provider

Create your own storage provider by implementing the StorageProvider interface:

import { Injectable } from '@angular/core';
import { StorageProvider } from '@isa/core/storage';

@Injectable({ providedIn: 'root' })
export class CustomStorageProvider implements StorageProvider {
  async init?(): Promise<void> {
    // Optional initialization logic
  }

  async reload?(): Promise<void> {
    // Optional reload logic
  }

  set(key: string, value: unknown): void {
    // Your storage implementation
    console.log(`Saving ${key}:`, value);
  }

  get(key: string): unknown {
    // Your retrieval implementation
    console.log(`Loading ${key}`);
    return null;
  }

  clear(key: string): void {
    // Your clear implementation
    console.log(`Clearing ${key}`);
  }
}

API Reference

withStorage(storageKey, storageProvider, config?)

NgRx Signals store feature that adds storage capabilities.

Parameters:

  • storageKey: string - Unique key for storing data
  • storageProvider: Type<StorageProvider> - Storage provider class
  • config?: WithStorageConfig - Optional configuration

Returns:

  • SignalStoreFeature with added methods:
    • saveToStorage() - Manually save current state
    • loadFromStorage() - Manually load and apply stored state

injectStorage(storageProvider)

Injectable function to get a storage instance.

Parameters:

  • storageProvider: Type<StorageProvider> - Storage provider class

Returns:

  • Storage instance with methods:
    • set<T>(key, value) - Store value
    • get<T>(key, schema?) - Retrieve value with optional validation
    • clear(key) - Remove value

Storage Providers

All storage providers implement the StorageProvider interface:

interface StorageProvider {
  init?(): Promise<void>;           // Optional initialization
  reload?(): Promise<void>;         // Optional reload
  set(key: string, value: unknown): void;      // Store value
  get(key: string): unknown;                   // Retrieve value
  clear(key: string): void;                    // Remove value
}

Available Providers:

  • LocalStorageProvider - Browser localStorage
  • SessionStorageProvider - Browser sessionStorage
  • IDBStorageProvider - IndexedDB
  • UserStorageProvider - Server-side user storage
  • MemoryStorageProvider - In-memory storage

Best Practices

1. Choose the Right Storage Provider

  • LocalStorageProvider: User preferences, settings that should persist across sessions
  • SessionStorageProvider: Temporary data that should be cleared when tab closes
  • IDBStorageProvider: Large datasets, complex objects, better performance needs
  • UserStorageProvider: Cross-device synchronization, server-backed user data
  • MemoryStorageProvider: Testing, temporary data during app lifecycle

2. Configure Auto-save and Auto-load Wisely

// For frequent changes (like form inputs)
withStorage('form-draft', LocalStorageProvider, {
  autosave: true,
  autoload: true,     // Restore draft on page reload
  debounceTime: 1000  // Longer debounce
})

// For infrequent changes (like settings)
withStorage('user-settings', LocalStorageProvider, {
  autosave: true,
  autoload: true,     // Load saved settings immediately
  debounceTime: 100   // Shorter debounce
})

// For critical data that needs manual control
withStorage('important-data', LocalStorageProvider, {
  autoload: false     // Disable automatic loading
})
// Use manual saveToStorage() and loadFromStorage() for precise control

3. Handle Storage Errors

Storage operations can fail (quota exceeded, network issues, etc.). The library handles errors gracefully:

  • Failed saves are logged to console but don't throw
  • Failed loads return undefined/null
  • State continues to work in memory even if storage fails

4. Consider Storage Size Limits

  • localStorage/sessionStorage: ~5-10MB per domain
  • IndexedDB: Much larger, varies by browser and device
  • UserStorageProvider: Depends on server configuration

5. Test with Different Storage Providers

Use MemoryStorageProvider in tests for predictable, isolated behavior:

// In tests
const testStore = signalStore(
  withState({ data: 'test' }),
  withStorage('test-key', MemoryStorageProvider)
);

Architecture Notes

The library consists of several key components:

  1. Storage Class: Core storage abstraction with user-scoping
  2. StorageProvider Interface: Pluggable storage backends
  3. withStorage Feature: NgRx Signals integration
  4. Hash Utilities: Efficient key generation
  5. User Token: OAuth-based user identification

The architecture promotes:

  • Separation of Concerns: Storage logic separate from business logic
  • Type Safety: Full TypeScript support throughout
  • Extensibility: Easy to add new storage providers
  • User Privacy: Automatic user-scoping prevents data leaks
  • Performance: Debounced saves prevent excessive writes

Migration Guide

From Manual localStorage

// Before
localStorage.setItem('settings', JSON.stringify(settings));
const settings = JSON.parse(localStorage.getItem('settings') || '{}');

// After
const SettingsStore = signalStore(
  withState(defaultSettings),
  withStorage('settings', LocalStorageProvider, { autosave: true })
);

From Custom Storage Services

// Before
@Injectable()
class SettingsService {
  private settings = signal(defaultSettings);

  save() {
    localStorage.setItem('settings', JSON.stringify(this.settings()));
  }

  load() {
    const data = localStorage.getItem('settings');
    if (data) this.settings.set(JSON.parse(data));
  }
}

// After
const SettingsStore = signalStore(
  withState(defaultSettings),
  withStorage('settings', LocalStorageProvider, { autosave: true })
);

Troubleshooting

Common Issues

  1. Storage not persisting: Check if storage provider supports your environment
  2. Data not loading: Verify storage key consistency
  3. Performance issues: Adjust debounce time or switch storage providers
  4. User data conflicts: Ensure USER_SUB token is properly configured

Debug Mode

The storage feature uses the centralized @isa/core/logging system. All storage operations are logged with appropriate context including the storage key, autosave settings, and operation details.

To see debug logs, configure the logging system at the application level:

// The storage feature automatically logs:
// - Debug: Successful operations, state loading/saving
// - Warn: Validation failures, data type issues
// - Error: Storage failures, fallback application errors

const store = signalStore(
  withState({ data: null }),
  withStorage('my-data', LocalStorageProvider)
);
// All operations will be logged with context: { module: 'storage', storageKey: 'my-data', ... }

Testing

Run unit tests with:

nx test core-storage

License

This library is part of the ISA Frontend project and follows the project's licensing terms.