Skip to content

A modern, reactive Angular service for browser storage management with AES-GCM encryption, TTL, change notifications, and Apollo-style providers

Notifications You must be signed in to change notification settings

edisonaugusthy/ng-storage

Repository files navigation

ngx-browser-storage

npm version License: MIT Angular TypeScript

πŸš€ A modern, reactive Angular storage with AES-GCM encryption, TTL support, change notifications, and signal-based reactivity.

✨ Features

  • πŸ”„ Reactive State Management - Built with Angular signals and RxJS
  • πŸ” AES-GCM Encryption - Encryption with Web Crypto API
  • ⏰ TTL Support - Automatic data expiration
  • πŸ“‘ Change Notifications - Real-time storage change watching
  • πŸͺ Multi-Storage Support - localStorage and sessionStorage
  • 🎯 Multiple Instances - Named storage configurations
  • πŸ“Š Storage Analytics - Usage statistics and monitoring
  • πŸ›‘οΈ Type Safe - Full TypeScript support with generics
  • 🌐 Cross-Browser - Graceful fallbacks for older browsers

πŸ“¦ Installation

npm install ngx-browser-storage

πŸ”’ Versioning

This library follows Angular's versioning for clear compatibility:

Angular Version ngx-browser-storage Version Status
20.0.x 20.0.x βœ… Current
20.1.x 20.1.x πŸ”„ Planned

πŸš€ Quick Start

Basic Setup

// app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideNgxBrowserStorage } from "ngx-browser-storage";

export const appConfig: ApplicationConfig = {
  providers: [
    provideNgxBrowserStorage({
      prefix: "myapp",
      storageType: "localStorage",
      defaultTTL: 60, // 1 hour
      enableLogging: false,
    }),
  ],
};

Component Usage

import { Component, inject, signal } from "@angular/core";
import { NgxBrowserStorageService } from "ngx-browser-storage";

@Component({
  selector: "app-user-profile",
  template: `
    <div>
      <input [(ngModel)]="username" placeholder="Username" />
      <button (click)="saveUser()">Save</button>
      <button (click)="saveUserSecure()">Save Encrypted</button>

      @if (currentUser(); as user) {
      <p>Welcome, {{ user.name }}!</p>
      }

      <p>Items in storage: {{ storage.stats().itemCount }}</p>
    </div>
  `,
})
export class UserProfileComponent {
  private storage = inject(NgxBrowserStorageService);

  username = "";
  currentUser = signal<User | null>(null);

  async ngOnInit() {
    // Create reactive signal for user data
    this.currentUser = await this.storage.createSignal<User>("currentUser");
  }

  async saveUser() {
    const user = { name: this.username, id: Date.now() };
    await this.storage.setData("currentUser", user);
  }

  async saveUserSecure() {
    const user = { name: this.username, id: Date.now() };
    await this.storage.setData("currentUser", user, {
      encrypt: true,
      ttlMinutes: 60,
    });
  }
}

πŸ” Security Features

AES-GCM Encryption

NgxBrowserStorage provides encryption using the Web Crypto API:

// Store sensitive data with encryption
await storage.setData("apiToken", "secret-key-123", {
  encrypt: true,
  ttlMinutes: 120, // Expires in 2 hours
});

// Retrieve encrypted data
const token = await storage.getData("apiToken", {
  decrypt: true,
  defaultValue: null,
});

// Check encryption support
if (storage.isEncryptionSupported()) {
  console.log("Using AES-GCM encryption");
} else {
  console.log("Falling back to Base64 encoding");
}

Security Comparison

Feature Base64 Fallback AES-GCM Encryption
Key Length None 256-bit
Data Integrity ❌ βœ… Authenticated
Tamper Protection ❌ βœ… Auth Tags
Browser Support Universal Modern + Fallback

βš™οΈ Configuration

Simple Configuration

import { provideNgxBrowserStorage } from "ngx-browser-storage";

export const appConfig: ApplicationConfig = {
  providers: [
    provideNgxBrowserStorage({
      prefix: "myapp",
      storageType: "localStorage",
      defaultTTL: 0, // No expiration
      enableLogging: false,
      caseSensitive: false,
    }),
  ],
};

Advanced Configuration with Factories

import { provideNgxBrowserStorage } from "ngx-browser-storage";
import { environment } from "./environments/environment";

export const appConfig: ApplicationConfig = {
  providers: [
    provideNgxBrowserStorage(
      () => ({
        prefix: environment.production ? "prod-app" : "dev-app",
        storageType: environment.production ? "localStorage" : "sessionStorage",
        defaultTTL: environment.production ? 60 : 30,
        enableLogging: !environment.production,
        caseSensitive: false,
      }),
      {
        autoCleanup: true,
        strictMode: environment.production,
        enableMetrics: !environment.production,
      }
    ),
  ],
};

Multiple Named Storage Instances

import { provideNamedNgxBrowserStorage, NgxBrowserStorageManager } from "ngx-browser-storage";

export const appConfig: ApplicationConfig = {
  providers: [
    provideNamedNgxBrowserStorage(() => ({
      user: {
        prefix: "user-data",
        storageType: "localStorage",
        defaultTTL: 0, // Persistent
        enableLogging: false,
      },
      cache: {
        prefix: "app-cache",
        storageType: "localStorage",
        defaultTTL: 60, // 1 hour
        enableLogging: false,
      },
      session: {
        prefix: "session-data",
        storageType: "sessionStorage",
        defaultTTL: 30, // 30 minutes
        enableLogging: true,
      },
    })),
    NgxBrowserStorageManager,
  ],
};

πŸ“š API Reference

Core Methods

setData<T>(key: string, value: T, options?): Promise<boolean>

Store data with optional encryption and TTL.

// Basic storage
await storage.setData("user", { name: "John", age: 30 });

// With encryption and TTL
await storage.setData("session", userData, {
  encrypt: true,
  ttlMinutes: 60,
});

getData<T>(key: string, options?): Promise<T | null>

Retrieve data with optional decryption.

// Basic retrieval
const user = await storage.getData<User>("user");

// With decryption and default value
const theme = await storage.getData("theme", {
  decrypt: true,
  defaultValue: "light",
});

hasKey(key: string): Promise<boolean>

Check if a key exists in storage.

const userExists = await storage.hasKey("currentUser");

removeData(key: string): boolean

Remove a specific key from storage.

storage.removeData("temporaryData");

removeAll(): boolean

Clear all storage data with the current prefix.

storage.removeAll();

Reactive Features

createSignal<T>(key: string, defaultValue?): Promise<Signal<T | null>>

Create a reactive signal that automatically updates when storage changes.

// Create reactive signals
const userSignal = await storage.createSignal<User>("currentUser");
const themeSignal = await storage.createSignal("theme", "light");

// Use in template
@Component({
  template: `<p>Hello {{ userSignal()?.name }}!</p>`,
})
export class MyComponent {
  userSignal = signal<User | null>(null);

  async ngOnInit() {
    this.userSignal = await inject(NgxBrowserStorageService).createSignal<User>("user");
  }
}

watch<T>(key: string): Observable<T | null>

Watch for changes to a specific key.

// Watch single key
storage.watch<string>("theme").subscribe((theme) => {
  document.body.className = theme || "light";
});

watchAll(): Observable<StorageChangeEvent>

Watch for all storage changes.

storage.watchAll().subscribe((event) => {
  console.log(`${event.action} on ${event.key}:`, event.newValue);
});

watchKeys<T>(keys: string[]): Observable<{key: string, value: T}>

Watch multiple specific keys.

storage.watchKeys(["user", "settings", "theme"]).subscribe(({ key, value }) => {
  console.log(`${key} changed:`, value);
});

watchPattern<T>(pattern: string): Observable<{key: string, value: T}>

Watch keys matching a pattern.

// Watch all user-related keys
storage.watchPattern("user.*").subscribe(({ key, value }) => {
  console.log(`User data ${key} changed:`, value);
});

Advanced Methods

updateData<T>(key, updateFn, options?): Promise<boolean>

Update existing data using a function.

await storage.updateData("cart", (current: CartItem[] = []) => [...current, newItem], { encrypt: true });

setIfNotExists<T>(key, value, options?): Promise<boolean>

Set data only if key doesn't exist.

const wasSet = await storage.setIfNotExists("config", defaultConfig);

getStorageStats(): Promise<StorageStats>

Get detailed storage statistics.

const stats = await storage.getStorageStats();
console.log(`Total: ${stats.totalItems} items, ${stats.totalSize} bytes`);

Security Methods

isEncryptionSupported(): boolean

Check if AES-GCM encryption is available.

if (storage.isEncryptionSupported()) {
  // Use full encryption
} else {
  // Handle fallback
}

clearEncryptionKey(): void

Clear cached encryption key (useful for logout).

// Clear encryption key for security
storage.clearEncryptionKey();

πŸ’Ό Real-World Examples

User Authentication Service

@Injectable({ providedIn: "root" })
export class AuthService {
  private storage = inject(NgxBrowserStorageService);

  // Reactive authentication state
  isAuthenticated = computed(() => this.storage.stats().keys.includes("auth"));
  currentUser = signal<User | null>(null);

  async ngOnInit() {
    this.currentUser = await this.storage.createSignal<User>("currentUser");
  }

  async login(credentials: LoginCredentials): Promise<void> {
    const result = await this.authApi.login(credentials);

    // Store encrypted auth token with 8-hour TTL
    await this.storage.setData("auth", result.token, {
      encrypt: true,
      ttlMinutes: 8 * 60,
    });

    await this.storage.setData("currentUser", result.user);
  }

  async logout(): Promise<void> {
    this.storage.removeMultiple(["auth", "currentUser"]);
    this.storage.clearEncryptionKey();
  }
}

Shopping Cart Service

@Injectable({ providedIn: "root" })
export class CartService {
  private storage = inject(NgxBrowserStorageService);

  // Reactive cart state
  items = signal<CartItem[]>([]);
  itemCount = computed(() => this.items().reduce((sum, item) => sum + item.quantity, 0));
  total = computed(() => this.items().reduce((sum, item) => sum + item.price * item.quantity, 0));

  async ngOnInit() {
    this.items = await this.storage.createSignal<CartItem[]>("cart", []);
  }

  async addItem(product: Product, quantity = 1): Promise<void> {
    await this.storage.updateData(
      "cart",
      (current: CartItem[] = []) => {
        const existing = current.find((item) => item.id === product.id);
        if (existing) {
          existing.quantity += quantity;
          return [...current];
        }
        return [...current, { ...product, quantity }];
      },
      { encrypt: true, ttlMinutes: 60 }
    );
  }

  async removeItem(productId: string): Promise<void> {
    await this.storage.updateData("cart", (current: CartItem[] = []) => current.filter((item) => item.id !== productId), { encrypt: true });
  }

  clearCart(): void {
    this.storage.removeData("cart");
  }
}

User Preferences Service

@Injectable({ providedIn: "root" })
export class PreferencesService {
  private storage = inject(NgxBrowserStorageService);

  // Reactive preferences
  theme = signal<"light" | "dark">("light");
  language = signal<string>("en");
  notifications = signal<boolean>(true);

  async ngOnInit() {
    // Initialize reactive preferences
    this.theme = await this.storage.createSignal("theme", "light");
    this.language = await this.storage.createSignal("language", "en");
    this.notifications = await this.storage.createSignal("notifications", true);

    // Auto-apply theme changes
    effect(() => {
      document.body.setAttribute("data-theme", this.theme());
    });
  }

  async updatePreference<T>(key: string, value: T): Promise<void> {
    await this.storage.setData(key, value, { encrypt: true });
  }

  async resetToDefaults(): Promise<void> {
    await this.updatePreference("theme", "light");
    await this.updatePreference("language", "en");
    await this.updatePreference("notifications", true);
  }
}

Form Auto-Save Service

@Injectable({ providedIn: "root" })
export class FormAutoSaveService {
  private storage = inject(NgxBrowserStorageService);
  private saveTimeouts = new Map<string, number>();

  async autoSave<T>(formId: string, data: T, delayMs = 1000): Promise<void> {
    // Debounce saves
    const existingTimeout = this.saveTimeouts.get(formId);
    if (existingTimeout) {
      clearTimeout(existingTimeout);
    }

    const timeoutId = setTimeout(async () => {
      await this.storage.setData(
        `form_${formId}`,
        {
          data,
          savedAt: Date.now(),
        },
        {
          encrypt: true,
          ttlMinutes: 60,
        }
      );
      this.saveTimeouts.delete(formId);
    }, delayMs);

    this.saveTimeouts.set(formId, timeoutId);
  }

  async getSavedData<T>(formId: string): Promise<{ data: T; savedAt: number } | null> {
    return await this.storage.getData(`form_${formId}`, { decrypt: true });
  }

  clearSavedData(formId: string): void {
    const timeoutId = this.saveTimeouts.get(formId);
    if (timeoutId) {
      clearTimeout(timeoutId);
      this.saveTimeouts.delete(formId);
    }
    this.storage.removeData(`form_${formId}`);
  }
}

Multiple Storage Instances

@Component({
  selector: "app-dashboard",
})
export class DashboardComponent {
  private storageManager = inject(NgxBrowserStorageManager);

  // Different storage instances for different purposes
  private userStorage = this.storageManager.getStorage("user");
  private cacheStorage = this.storageManager.getStorage("cache");
  private sessionStorage = this.storageManager.getStorage("session");

  async ngOnInit() {
    // Load persistent user data
    const userProfile = await this.userStorage.getData("profile");

    // Load cached application data
    const cachedData = await this.cacheStorage.getData("dashboard-metrics");

    // Save current session state
    await this.sessionStorage.setData("current-view", "dashboard", {
      encrypt: true,
      ttlMinutes: 30,
    });
  }
}

πŸ§ͺ Testing

Test Configuration

import { provideNgxBrowserStorage } from "ngx-browser-storage";

// test-setup.ts
export function provideStorageForTesting() {
  return provideNgxBrowserStorage(
    {
      prefix: "test",
      storageType: "sessionStorage",
      defaultTTL: 0,
      enableLogging: true,
      caseSensitive: false,
    },
    {
      autoCleanup: false,
      strictMode: true,
      enableMetrics: false,
    }
  );
}

// In test files
describe("NgxBrowserStorageService", () => {
  let service: NgxBrowserStorageService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [...provideStorageForTesting()],
    });
    service = TestBed.inject(NgxBrowserStorageService);
  });

  it("should store and retrieve data", async () => {
    await service.setData("test", "value");
    const result = await service.getData("test");
    expect(result).toBe("value");
  });

  it("should handle encryption", async () => {
    await service.setData("secret", "encrypted-data", { encrypt: true });
    const result = await service.getData("secret", { decrypt: true });
    expect(result).toBe("encrypted-data");
  });

  it("should create reactive signals", async () => {
    const signal = await service.createSignal<string>("reactive-test", "default");
    expect(signal()).toBe("default");

    await service.setData("reactive-test", "updated");
    expect(signal()).toBe("updated");
  });
});

πŸ”„ Migration Guide

From ng7-storage to ngx-browser-storage

This is a breaking change that requires updates:

1. Package Installation

# Remove old package
npm uninstall ng7-storage

# Install new package
npm install ngx-browser-storage

2. Update Imports

// Before
import { provideNgStorageConfig } from "ng7-storage";

// After
import { provideNgxBrowserStorage } from "ngx-browser-storage";

3. Update Method Calls

// Before (synchronous)
const user = storage.getData("user");
storage.setData("user", newUser);

// After (asynchronous)
const user = await storage.getData("user");
await storage.setData("user", newUser);

4. Update Reactive Features

// Before
const signal = storage.createSignal("user");

// After
const signal = await storage.createSignal("user");

5. Storage Prefix Changes

The default prefix changed from ng-storage to ngx-browser-storage. Existing data with the old prefix won't be automatically migrated. You can:

  • Keep using a custom prefix: prefix: 'ng-storage'
  • Or migrate data manually in your application

Breaking Changes Summary

  • ⚠️ Package Name: ng7-storage β†’ ngx-browser-storage
  • ⚠️ Method Signatures: Core methods now async
  • ⚠️ Signal Creation: Now async
  • ⚠️ Default Prefix: ng-storage β†’ ngx-browser-storage
  • βœ… Enhanced: Much stronger AES-GCM encryption
  • βœ… Compatible: Existing stored data still readable

🌐 Browser Compatibility

Browser Version Storage Support AES-GCM Encryption
Chrome 37+ βœ… βœ…
Firefox 34+ βœ… βœ…
Safari 7+ βœ… βœ…
Edge 12+ βœ… βœ…
IE 11 βœ… ❌ (Base64 fallback)
IE 8-10 βœ… ❌ (Base64 fallback)

🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

# Clone repository
git clone https://github.com/edisonaugusthy/ng-storage.git

# Install dependencies
npm install

# Run tests
npm test

# Build library
npm run build

# Check Angular version alignment
npm run check-angular-version

πŸ“ Changelog

v20.0.3 - Major Rebranding & Enhancement

🚨 Breaking Changes

  • Package renamed: ng7-storage β†’ ngx-browser-storage
  • Methods now async: setData(), getData(), hasKey() for encryption support
  • Signal creation async: createSignal() now returns Promise<Signal<T>>
  • Default prefix changed: ng-storage β†’ ngx-browser-storage

✨ New Features

  • AES-GCM Encryption: Military-grade security with Web Crypto API
  • PBKDF2 Key Derivation: 100,000 iterations for secure key generation
  • Version Alignment: Now follows Angular version numbers (20.0.3)
  • Enhanced Security: Data integrity protection with authentication tags
  • Better Fallbacks: Automatic Base64 fallback for older browsers
  • Improved TypeScript: Better type safety and generics

πŸ›‘οΈ Security Enhancements

  • Unique Encryption: Random IVs ensure unique encryption per data item
  • Tamper Protection: Authentication tags prevent data modification
  • Key Management: Secure key caching and rotation support
  • Browser Detection: Automatic encryption capability detection

πŸ”§ Improvements

  • Performance: Optimized key caching and encryption operations
  • Error Handling: Better error messages and fallback strategies
  • Documentation: Comprehensive guides and examples
  • Testing: Enhanced test coverage and utilities

Legacy Versions

  • v19.0.0: Previous version with basic Base64 encoding
  • v1.0.0: Initial release

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Angular team for the amazing framework and signals
  • Web Crypto API for enabling secure client-side encryption
  • RxJS team for reactive programming utilities
  • Community contributors and users

πŸ“ž Support

  • πŸ› Bug Reports: GitHub Issues
  • πŸ’¬ Discussions: GitHub Discussions
  • πŸ” Security Issues: Please report security vulnerabilities privately via email

Made with ❀️ for the Angular community

⭐ Star this repo | 🍴 Fork it | πŸ“‹ Report Issues

About

A modern, reactive Angular service for browser storage management with AES-GCM encryption, TTL, change notifications, and Apollo-style providers

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •