npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@inputless/storage

v1.0.6

Published

Data persistence and storage management for behavioral events

Readme

@inputless/storage

Storage management system for behavioral events with multi-backend support, automatic cleanup, and quota management.

Purpose

Manages data persistence, retrieval, and cleanup for behavioral analytics data in the browser. Provides unified API across localStorage, IndexedDB, and SessionStorage with automatic retention policies, compression, and quota management.

Features

  • Multiple Storage Backends: localStorage, IndexedDB, SessionStorage with adapter pattern
  • Automatic Cleanup: Configurable retention policies with automatic expiration
  • Batch Operations: Efficient batch read/write operations
  • Data Compression: Optional compression for space efficiency (gzip/brotli)
  • Storage Quota Management: Graceful handling of storage limits with automatic space freeing
  • Type Safety: Full TypeScript type safety with @inputless/events
  • Performance: Optimized for fast operations (<10ms localStorage, <50ms IndexedDB)

Installation

npm install @inputless/storage

Dependencies

  • @inputless/events - Type definitions for behavioral events

Quick Start (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';
import { generateEventId } from '@inputless/events';

// Initialize storage manager (local only - no API endpoints)
const storage = new StorageManager({
  adapter: new LocalStorageAdapter(),
  maxSize: 10000,
  retentionDays: 30,
  compression: false,
  debug: true,
});

// Store events
const events: BaseEvent[] = [
  {
    id: generateEventId(),
    type: 'ui.click',
    timestamp: Date.now(),
    sessionId: 'session-123',
    // ... other event properties
  },
];
await storage.storeEvents(events);

// Retrieve events by session
const sessionEvents = await storage.getEvents('session-123');

// Get storage statistics
const stats = await storage.getStatistics();
console.log(`Total events: ${stats.totalEvents}`);
console.log(`Storage usage: ${stats.totalSize} bytes`);
console.log(`Events by session:`, stats.eventsBySession);
console.log(`Events by type:`, stats.eventsByType);

Usage Examples

Example 1: Basic Setup with LocalStorage (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';

// Initialize storage manager with localStorage (local only)
const storage = new StorageManager({
  adapter: new LocalStorageAdapter({ prefix: 'myapp_' }),
  maxSize: 5000,
  retentionDays: 7,
  batchSize: 100,
  keyPrefix: 'myapp_',
  debug: true,
});

// Store single event
const event: BaseEvent = {
  id: 'event-1',
  type: 'ui.click',
  timestamp: Date.now(),
  sessionId: 'session-123',
};
await storage.storeEvent(event);

// Store multiple events
const events: BaseEvent[] = [event1, event2, event3];
await storage.storeEvents(events);

Example 2: Using IndexedDB for Large Datasets (Local Only)

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';

// Initialize storage manager with IndexedDB (local only - for large datasets)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'myapp_analytics',
    storeName: 'events',
    version: 1,
  }),
  maxSize: 50000, // Larger capacity
  retentionDays: 30,
  compression: true,
  compressionAlgorithm: 'gzip', // 'gzip' | 'brotli'
  batchSize: 100,
  debug: true,
});

// IndexedDB adapter provides optimized queries
// Uses IndexedDB indexes if available
const sessionEvents = await storage.getEvents('session-123');

// Uses IndexedDB date index if available
const dateRangeEvents = await storage.getEventsByDateRange(
  new Date('2024-01-01'),
  new Date('2024-01-31')
);

Example 3: Session Storage for Temporary Data (Local Only)

import { StorageManager, SessionStorageAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';

// Initialize storage manager with SessionStorage (local only - session-scoped)
const storage = new StorageManager({
  adapter: new SessionStorageAdapter(),
  maxSize: 1000,
  retentionDays: 0, // No retention (session-scoped)
  debug: true,
});

// Store events (cleared when tab closes)
const event: BaseEvent = {
  id: 'event-1',
  type: 'ui.click',
  timestamp: Date.now(),
  sessionId: 'session-123',
};
await storage.storeEvent(event);

// Events are automatically cleared when browser tab closes

Example 4: Storing Events (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';
import { generateEventId } from '@inputless/events';

const storage = new StorageManager({
  adapter: new LocalStorageAdapter(),
  maxSize: 10000,
  retentionDays: 30,
});

// Store single event
const event: BaseEvent = {
  id: generateEventId(),
  type: 'ui.click',
  timestamp: Date.now(),
  sessionId: 'session-123',
  // ... other event properties
};
await storage.storeEvent(event);

// Store multiple events (batch)
// Events are processed in batches (default batchSize: 100)
const events: BaseEvent[] = [
  { id: generateEventId(), type: 'ui.click', timestamp: Date.now(), sessionId: 'session-123' },
  { id: generateEventId(), type: 'ui.scroll', timestamp: Date.now(), sessionId: 'session-123' },
  { id: generateEventId(), type: 'form.submit', timestamp: Date.now(), sessionId: 'session-123' },
];
await storage.storeEvents(events);

// Batch operations handle compression and quota management automatically

Example 5: Retrieving Events (Local Only)

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';
import type { EventType } from '@inputless/events';

const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
  }),
  maxSize: 50000,
  retentionDays: 30,
});

// Get events by session ID
// Uses IndexedDB sessionId index if available for optimized query
const sessionEvents: BaseEvent[] = await storage.getEvents('session-id-123');
console.log(`Found ${sessionEvents.length} events for session`);

// Get events by date range
// Uses IndexedDB timestamp index if available for optimized query
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-01-31');
const dateRangeEvents: BaseEvent[] = await storage.getEventsByDateRange(startDate, endDate);
console.log(`Found ${dateRangeEvents.length} events in date range`);

// Get events by type
// Uses IndexedDB eventType index if available for optimized query
const eventType: EventType = 'ui.click';
const clickEvents: BaseEvent[] = await storage.getEventsByType(eventType);
console.log(`Found ${clickEvents.length} click events`);

// All retrieved events are sorted by timestamp (ascending)

Example 6: Storage Statistics (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';
import type { StorageStatistics } from '@inputless/storage';

const storage = new StorageManager({
  adapter: new LocalStorageAdapter(),
  maxSize: 10000,
  retentionDays: 30,
});

// Get storage statistics
const stats: StorageStatistics = await storage.getStatistics();

console.log(`Total events: ${stats.totalEvents}`);
console.log(`Total size: ${stats.totalSize} bytes`);
console.log(`Events by session:`, stats.eventsBySession); // Map<string, number>
console.log(`Events by type:`, stats.eventsByType); // Map<EventType, number>
console.log(`Oldest event: ${stats.oldestEventTimestamp ? new Date(stats.oldestEventTimestamp) : 'N/A'}`);
console.log(`Newest event: ${stats.newestEventTimestamp ? new Date(stats.newestEventTimestamp) : 'N/A'}`);
console.log(`Expired events: ${stats.expiredEvents}`);

// Iterate over events by session
for (const [sessionId, count] of stats.eventsBySession) {
  console.log(`Session ${sessionId}: ${count} events`);
}

// Iterate over events by type
for (const [eventType, count] of stats.eventsByType) {
  console.log(`Type ${eventType}: ${count} events`);
}

Example 7: Quota Management (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';
import { QuotaExceededError } from '@inputless/storage';

const storage = new StorageManager({
  adapter: new LocalStorageAdapter(),
  maxSize: 10000,
  retentionDays: 30,
  quotaExceededFallback: true,
  quotaFreePercentage: 0.2, // Free 20% when quota exceeded
});

// Check storage availability
const isAvailable: boolean = await storage.isAvailable();
console.log(`Storage available: ${isAvailable}`);

// Get storage usage
const usage: number | null = await storage.getStorageUsage(); // bytes
console.log(`Storage usage: ${usage ? `${(usage / 1024).toFixed(2)} KB` : 'Unknown'}`);

// Get remaining capacity
const remaining: number | null = await storage.getRemainingCapacity(); // bytes
console.log(`Remaining capacity: ${remaining ? `${(remaining / 1024).toFixed(2)} KB` : 'Unknown'}`);

// Store events with quota handling
try {
  await storage.storeEvents(events);
} catch (error) {
  if (error instanceof QuotaExceededError) {
    // QuotaManager automatically attempts to free space if quotaExceededFallback is true
    console.error('Quota exceeded, attempting to free space...');
    // Retry after automatic cleanup
  }
}

Example 8: Manual Cleanup (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';

const storage = new StorageManager({
  adapter: new LocalStorageAdapter(),
  maxSize: 10000,
  retentionDays: 30,
  cleanupInterval: 3600000, // 1 hour
});

// Delete expired events manually
// Returns number of events deleted
const deletedCount: number = await storage.deleteExpiredEvents();
console.log(`Deleted ${deletedCount} expired events`);

// Delete events by session
await storage.deleteEvents('session-id-123');
console.log('Deleted all events for session');

// Delete events by date range
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-01-31');
await storage.deleteEventsByDateRange(startDate, endDate);
console.log('Deleted events in date range');

// Clear all events
await storage.clearAll();
console.log('Cleared all events from storage');

// Automatic cleanup runs every cleanupInterval milliseconds if retentionDays > 0

Configuration

Example 10: Complete Configuration (Local Only)

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { StorageConfig } from '@inputless/storage';

// Complete storage configuration
const config: StorageConfig = {
  adapter: new IndexedDBAdapter({ // required
    dbName: 'myapp_analytics',
    storeName: 'events',
    version: 1,
  }),
  maxSize: 50000, // default: 10000
  retentionDays: 30, // default: 30, 0 = disabled
  compression: true, // default: false
  compressionAlgorithm: 'gzip', // 'gzip' | 'brotli', default: 'gzip'
  batchSize: 100, // default: 100
  keyPrefix: 'myapp_', // default: 'inputless_'
  quotaExceededFallback: true, // default: true
  quotaFreePercentage: 0.2, // default: 0.2, 0-1
  cleanupInterval: 3600000, // default: 3600000 = 1 hour
  debug: true, // default: false
};

const storage = new StorageManager(config);

Storage Adapters

LocalStorageAdapter

Fast, synchronous storage for small datasets (<5MB).

Features:

  • Synchronous operations
  • Fast access (<10ms)
  • Limited storage (~5-10MB)
  • No indexing support

Use Case: Small datasets, fast reads, simple queries

IndexedDBAdapter

Async storage for larger datasets with indexing support.

Features:

  • Asynchronous operations
  • Large storage capacity (~50MB+)
  • Indexing support for efficient queries
  • Transaction support
  • Optimized queries by session, date range, event type

Use Case: Large datasets, complex queries, offline support

SessionStorageAdapter

Session-scoped storage that clears on tab close.

Features:

  • Synchronous operations
  • Fast access
  • Session-only persistence
  • Limited storage (~5-10MB)

Use Case: Temporary data, session-scoped analytics

MemoryAdapter

In-memory storage for testing and development.

Features:

  • No persistence (data lost when instance destroyed)
  • Fast access
  • Useful for testing

Use Case: Unit tests, development, mocking

Example:

import { StorageManager, MemoryAdapter } from '@inputless/storage';

const storage = new StorageManager({
  adapter: new MemoryAdapter({ prefix: 'test_' }),
  maxSize: 1000,
  retentionDays: 0,
});

API Reference

API from src/index.ts and src/StorageManager.ts:

StorageManager

Constructor

  • new StorageManager(config: StorageConfig)
    • config: StorageConfig

Methods

  • storeEvent(event: BaseEvent): Promise<void> - Store single event
  • storeEvents(events: BaseEvent[]): Promise<void> - Store multiple events
  • getEvents(sessionId: string): Promise<BaseEvent[]> - Get events by session
  • getEventsByDateRange(startDate: Date, endDate: Date): Promise<BaseEvent[]> - Get events by date range
  • getEventsByType(eventType: EventType): Promise<BaseEvent[]> - Get events by type
  • deleteEvents(sessionId: string): Promise<void> - Delete events by session
  • deleteEventsByDateRange(startDate: Date, endDate: Date): Promise<void> - Delete events by date range
  • deleteExpiredEvents(): Promise<number> - Delete expired events, returns count
  • getStatistics(): Promise<StorageStatistics> - Get storage statistics
  • clearAll(): Promise<void> - Clear all events
  • getStorageUsage(): Promise<number | null> - Get storage usage in bytes
  • isAvailable(): Promise<boolean> - Check if storage is available
  • getRemainingCapacity(): Promise<number | null> - Get remaining capacity in bytes

Error Handling

The storage module throws typed errors:

import {
  StorageError,
  QuotaExceededError,
  StorageUnavailableError,
} from '@inputless/storage';

try {
  await storage.storeEvents(events);
} catch (error) {
  if (error instanceof QuotaExceededError) {
    // Handle quota exceeded (fallback will attempt to free space)
    console.error('Storage quota exceeded:', error.message);
  } else if (error instanceof StorageUnavailableError) {
    // Handle storage unavailable
    console.error('Storage unavailable:', error.message);
  } else if (error instanceof StorageError) {
    // Handle general storage error
    console.error('Storage error:', error.message);
  }
}

Example 11: Complete Local Integration - All Features Working Together

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import { InputlessTracker } from '@inputless/tracker';
import type { BaseEvent } from '@inputless/events';
import type { StorageStatistics } from '@inputless/storage';
import { QuotaExceededError, StorageUnavailableError } from '@inputless/storage';

// Initialize storage manager with all features (local only)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
    version: 1,
  }),
  maxSize: 10000,
  retentionDays: 30,
  compression: true,
  compressionAlgorithm: 'gzip',
  batchSize: 100,
  quotaExceededFallback: true,
  quotaFreePercentage: 0.2,
  cleanupInterval: 3600000,
  debug: true,
});

// Initialize tracker (local only)
const tracker = new InputlessTracker({
  enabledCategories: { ui: true, form: true, performance: true },
  debug: true,
});

// Store events from tracker
tracker.onEvent(async (event: BaseEvent) => {
  try {
    await storage.storeEvent(event);
  } catch (error) {
    if (error instanceof QuotaExceededError) {
      console.warn('Quota exceeded, automatic cleanup triggered');
    } else if (error instanceof StorageUnavailableError) {
      console.error('Storage unavailable');
    }
  }
});

tracker.start();

// Periodically check storage statistics
setInterval(async () => {
  const stats: StorageStatistics = await storage.getStatistics();
  console.log('Storage Stats:', {
    totalEvents: stats.totalEvents,
    totalSize: `${(stats.totalSize / 1024).toFixed(2)} KB`,
    expiredEvents: stats.expiredEvents,
  });
  
  // Check quota
  const usage = await storage.getStorageUsage();
  const remaining = await storage.getRemainingCapacity();
  console.log('Quota:', {
    usage: usage ? `${(usage! / 1024).toFixed(2)} KB` : 'Unknown',
    remaining: remaining ? `${(remaining! / 1024).toFixed(2)} KB` : 'Unknown',
  });
}, 60000); // Every minute

// Retrieve and process events
async function processStoredEvents() {
  // Get events by session
  const sessionEvents = await storage.getEvents('session-123');
  console.log(`Processing ${sessionEvents.length} events from session`);
  
  // Get events by date range
  const today = new Date();
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
  const recentEvents = await storage.getEventsByDateRange(yesterday, today);
  console.log(`Found ${recentEvents.length} events in last 24 hours`);
  
  // Get events by type
  const clickEvents = await storage.getEventsByType('ui.click');
  console.log(`Found ${clickEvents.length} click events`);
  
  // Process events locally
  recentEvents.forEach(event => {
    console.log(`Event: ${event.type} at ${new Date(event.timestamp)}`);
  });
}

// Manual cleanup
async function cleanupOldEvents() {
  const deletedCount = await storage.deleteExpiredEvents();
  console.log(`Cleaned up ${deletedCount} expired events`);
}

// Run cleanup on demand
cleanupOldEvents();

Example 9: Integration with Tracker (Local Only)

import { InputlessTracker } from '@inputless/tracker';
import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';

// Initialize tracker (local only)
const tracker = new InputlessTracker({
  enabledCategories: { ui: true, form: true, performance: true },
  debug: true,
});

// Initialize storage manager (local only)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
    version: 1,
  }),
  maxSize: 10000,
  retentionDays: 30,
  compression: true,
  compressionAlgorithm: 'gzip',
  debug: true,
});

// Store events from tracker
tracker.onEvent(async (event: BaseEvent) => {
  await storage.storeEvent(event);
});

tracker.start();

// Retrieve events for offline processing
const sessionId = 'session-123';
const events: BaseEvent[] = await storage.getEvents(sessionId);
console.log(`Retrieved ${events.length} events for processing`);

// Process events locally (no backend sync)
events.forEach(event => {
  console.log(`Event: ${event.type} at ${new Date(event.timestamp)}`);
});

Example 12: Integration with Tracker and Cognitive SDK (Local Only)

import { InputlessTracker } from '@inputless/tracker';
import { CognitiveSDK } from '@inputless/sdk-cognitive';
import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';

// Initialize cognitive SDK (local only)
const cognitiveSDK = new CognitiveSDK({
  debug: true,
});
await cognitiveSDK.start();

// Initialize tracker (local only)
const tracker = new InputlessTracker({
  enabledCategories: { ui: true, form: true, performance: true },
  debug: true,
});

// Initialize storage manager (local only)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
  }),
  maxSize: 10000,
  retentionDays: 30,
  compression: true,
  debug: true,
});

// Process events: Tracker → Cognitive SDK → Storage
tracker.onEvent(async (event: BaseEvent) => {
  // Process through cognitive SDK
  const enriched = await cognitiveSDK.perceive(event);
  
  if (enriched) {
    // Store enriched event
    await storage.storeEvent(enriched);
    
    // Access cognitive insights
    if (enriched.cog) {
      console.log('Stored event with intent:', enriched.cog.intent_state);
    }
  }
});

tracker.start();

// Later: Retrieve and analyze stored events
async function analyzeStoredEvents() {
  // Get events by session
  const sessionEvents = await storage.getEvents('session-123');
  
  // Get events by type
  const clickEvents = await storage.getEventsByType('ui.click');
  
  // Get events by date range
  const today = new Date();
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
  const recentEvents = await storage.getEventsByDateRange(yesterday, today);
  
  console.log(`Session events: ${sessionEvents.length}`);
  console.log(`Click events: ${clickEvents.length}`);
  console.log(`Recent events: ${recentEvents.length}`);
  
  // Process events locally
  recentEvents.forEach(event => {
    if (event.cog) {
      console.log(`Event ${event.type}: intent=${event.cog.intent_state}, confidence=${event.cog.confidence}`);
    }
  });
}

analyzeStoredEvents();

Example 13: Batch Event Processing with Storage (Local Only)

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';
import { generateEventId } from '@inputless/events';

// Initialize storage manager (local only)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
  }),
  maxSize: 50000,
  retentionDays: 30,
  batchSize: 100, // process 100 events per batch
  compression: true,
  compressionAlgorithm: 'gzip',
  debug: true,
});

// Generate a large batch of events
const sessionId = 'session-123';
const events: BaseEvent[] = [];
for (let i = 0; i < 500; i++) {
  events.push({
    id: generateEventId(),
    type: i % 2 === 0 ? 'ui.click' : 'ui.scroll',
    timestamp: Date.now() - (500 - i) * 1000, // Spread over time
    sessionId,
  });
}

// Store events in batches
// Events are automatically processed in batches of 100
await storage.storeEvents(events);
console.log(`Stored ${events.length} events in batches`);

// Retrieve all events for the session
const storedEvents = await storage.getEvents(sessionId);
console.log(`Retrieved ${storedEvents.length} events from storage`);

// Get statistics
const stats = await storage.getStatistics();
console.log('Storage Statistics:', {
  totalEvents: stats.totalEvents,
  totalSize: `${(stats.totalSize / 1024).toFixed(2)} KB`,
  eventsBySession: Object.fromEntries(stats.eventsBySession),
  eventsByType: Object.fromEntries(stats.eventsByType),
});

Example 14: Event Querying and Filtering (Local Only)

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent, EventType } from '@inputless/events';
import { generateEventId } from '@inputless/events';

// Initialize storage manager (local only)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
  }),
  maxSize: 10000,
  retentionDays: 30,
  debug: true,
});

// Store sample events
const session1 = 'session-1';
const session2 = 'session-2';
const events: BaseEvent[] = [
  { id: generateEventId(), type: 'ui.click', timestamp: Date.now() - 3600000, sessionId: session1 },
  { id: generateEventId(), type: 'ui.scroll', timestamp: Date.now() - 1800000, sessionId: session1 },
  { id: generateEventId(), type: 'form.submit', timestamp: Date.now() - 600000, sessionId: session2 },
  { id: generateEventId(), type: 'ui.click', timestamp: Date.now() - 300000, sessionId: session2 },
  { id: generateEventId(), type: 'ui.click', timestamp: Date.now(), sessionId: session1 },
];

await storage.storeEvents(events);

// Query 1: Get events by session
const session1Events: BaseEvent[] = await storage.getEvents(session1);
console.log(`Session 1 events: ${session1Events.length}`);

// Query 2: Get events by type
const clickEvents: BaseEvent[] = await storage.getEventsByType('ui.click' as EventType);
console.log(`Click events: ${clickEvents.length}`);

// Query 3: Get events by date range
const oneHourAgo = new Date(Date.now() - 3600000);
const now = new Date();
const recentEvents: BaseEvent[] = await storage.getEventsByDateRange(oneHourAgo, now);
console.log(`Events in last hour: ${recentEvents.length}`);

// Query 4: Combine queries manually
const allEvents = await storage.getEvents(session1);
const filteredEvents = allEvents.filter(event => event.type === 'ui.click');
console.log(`Filtered click events from session 1: ${filteredEvents.length}`);

// All retrieved events are sorted by timestamp (ascending)
console.log('Events sorted by timestamp:', recentEvents.map(e => ({
  type: e.type,
  timestamp: new Date(e.timestamp).toISOString(),
})));

Example 15: Storage Cleanup and Maintenance (Local Only)

import { StorageManager, IndexedDBAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';
import { generateEventId } from '@inputless/events';

// Initialize storage manager with retention (local only)
const storage = new StorageManager({
  adapter: new IndexedDBAdapter({
    dbName: 'analytics_db',
    storeName: 'events',
  }),
  maxSize: 10000,
  retentionDays: 7, // events expire after 7 days
  cleanupInterval: 3600000, // cleanup every hour
  debug: true,
});

// Store events with different timestamps
const now = Date.now();
const events: BaseEvent[] = [
  // Recent events (not expired)
  { id: generateEventId(), type: 'ui.click', timestamp: now - 86400000, sessionId: 'session-1' }, // 1 day ago
  { id: generateEventId(), type: 'ui.click', timestamp: now - 3600000, sessionId: 'session-1' },  // 1 hour ago
  { id: generateEventId(), type: 'ui.click', timestamp: now, sessionId: 'session-1' },          // now
  
  // Old events (will expire)
  { id: generateEventId(), type: 'ui.click', timestamp: now - 8 * 86400000, sessionId: 'session-2' }, // 8 days ago
  { id: generateEventId(), type: 'ui.click', timestamp: now - 10 * 86400000, sessionId: 'session-2' }, // 10 days ago
];

await storage.storeEvents(events);

// Get statistics before cleanup
let stats = await storage.getStatistics();
console.log('Before cleanup:', {
  totalEvents: stats.totalEvents,
  expiredEvents: stats.expiredEvents,
});

// Manual cleanup of expired events
// Returns number of events deleted
const deletedCount: number = await storage.deleteExpiredEvents();
console.log(`Deleted ${deletedCount} expired events`);

// Get statistics after cleanup
stats = await storage.getStatistics();
console.log('After cleanup:', {
  totalEvents: stats.totalEvents,
  expiredEvents: stats.expiredEvents,
});

// Delete events by session
await storage.deleteEvents('session-2');
console.log('Deleted all events for session-2');

// Delete events by date range
const yesterday = new Date(now - 86400000);
const today = new Date(now);
await storage.deleteEventsByDateRange(yesterday, today);
console.log('Deleted events from yesterday to today');

// Clear all events
await storage.clearAll();
console.log('Cleared all events from storage');

Example 16: Storage Quota Monitoring (Local Only)

import { StorageManager, LocalStorageAdapter } from '@inputless/storage';
import type { BaseEvent } from '@inputless/events';
import { QuotaExceededError, StorageUnavailableError } from '@inputless/storage';
import { generateEventId } from '@inputless/events';

// Initialize storage manager with quota management (local only)
const storage = new StorageManager({
  adapter: new LocalStorageAdapter(),
  maxSize: 5000,
  retentionDays: 30,
  quotaExceededFallback: true, // auto-free space on quota exceeded
  quotaFreePercentage: 0.2, // free 20% when quota exceeded
  debug: true,
});

// Check storage availability
const isAvailable: boolean = await storage.isAvailable();
console.log(`Storage available: ${isAvailable}`);

// Get storage usage
const usage: number | null = await storage.getStorageUsage(); // bytes
console.log(`Storage usage: ${usage ? `${(usage! / 1024).toFixed(2)} KB` : 'Unknown'}`);

// Get remaining capacity
const remaining: number | null = await storage.getRemainingCapacity(); // bytes
console.log(`Remaining capacity: ${remaining ? `${(remaining! / 1024).toFixed(2)} KB` : 'Unknown'}`);

// Monitor quota periodically
setInterval(async () => {
  const usage = await storage.getStorageUsage();
  const remaining = await storage.getRemainingCapacity();
  const stats = await storage.getStatistics();
  
  console.log('Quota Status:', {
    usage: usage ? `${(usage! / 1024).toFixed(2)} KB` : 'Unknown',
    remaining: remaining ? `${(remaining! / 1024).toFixed(2)} KB` : 'Unknown',
    totalEvents: stats.totalEvents,
    expiredEvents: stats.expiredEvents,
  });
  
  // Warn if storage is getting full
  if (usage && remaining && usage > remaining * 0.8) {
    console.warn('Storage is getting full! Consider cleaning up old events.');
  }
}, 60000); // Every minute

// Store events with quota handling
const events: BaseEvent[] = Array.from({ length: 100 }, (_, i) => ({
  id: generateEventId(),
  type: 'ui.click',
  timestamp: Date.now() - i * 1000,
  sessionId: 'session-123',
}));

try {
  await storage.storeEvents(events);
  console.log('Events stored successfully');
} catch (error) {
  if (error instanceof QuotaExceededError) {
    // QuotaManager automatically attempts to free space if quotaExceededFallback is true
    console.error('Quota exceeded, automatic cleanup triggered');
    // Retry after automatic cleanup
    try {
      await storage.storeEvents(events);
      console.log('Events stored after cleanup');
    } catch (retryError) {
      console.error('Failed to store events after cleanup:', retryError);
    }
  } else if (error instanceof StorageUnavailableError) {
    console.error('Storage unavailable:', error.message);
  } else {
    console.error('Storage error:', error);
  }
}

Performance Considerations

Targets

  • localStorage operations: <10ms per operation
  • IndexedDB operations: <50ms per operation
  • Batch operations: <100ms per 100 events
  • Compression: <20ms per event (if enabled)

Optimization Tips

  1. Use IndexedDB for large datasets (>1000 events)
  2. Enable compression for space efficiency (if supported)
  3. Use batch operations for multiple events
  4. Set appropriate retention to avoid storage bloat
  5. Monitor quota and adjust maxSize accordingly

Module Structure

packages/typescript-core/storage/
├── src/
│   ├── StorageManager.ts              # Main storage manager
│   ├── StorageConfig.ts                # Configuration types
│   ├── StorageAdapter.ts               # Adapter interface
│   ├── adapters/
│   │   ├── LocalStorageAdapter.ts      # localStorage implementation
│   │   ├── IndexedDBAdapter.ts         # IndexedDB implementation
│   │   ├── SessionStorageAdapter.ts    # SessionStorage implementation
│   │   └── MemoryAdapter.ts            # In-memory adapter (testing)
│   ├── utils/
│   │   ├── CompressionUtils.ts         # Compression utilities
│   │   ├── SerializationUtils.ts       # Serialization helpers
│   │   ├── QuotaUtils.ts                # Quota management utilities
│   │   └── RetentionUtils.ts           # Retention policy utilities
│   ├── types/
│   │   ├── StorageTypes.ts             # Storage type definitions
│   │   └── StorageErrors.ts            # Error types
│   └── index.ts                         # Public API exports

Exports

Exports from src/index.ts:

Main Classes

  • StorageManager - Main storage manager class
  • LocalStorageAdapter - localStorage adapter
  • IndexedDBAdapter - IndexedDB adapter
  • SessionStorageAdapter - SessionStorage adapter
  • MemoryAdapter - In-memory adapter (testing)

Types

  • StorageAdapter - Adapter interface
  • StorageConfig - Configuration interface
  • StoredEvent - Stored event format
  • StorageStatistics - Statistics interface
  • StorageKeyType - Storage key type ('events' | 'sessions' | 'metadata')

Error Classes

  • StorageError - Base error class
  • QuotaExceededError - Quota exceeded error
  • StorageUnavailableError - Storage unavailable error

Utilities

  • CompressionUtils - Compression utilities
  • RetentionCleaner - Retention policy manager
  • QuotaManager - Quota management utilities
  • serialize, deserialize, isSerializable - Serialization helpers

Distribution

npm package: @inputless/storage
Version: 1.0.0+
Registry: npm

Related Documentation

  • doc/STORAGE_COMPONENT_IMPLEMENTATION.md - Complete implementation specification
  • doc/CODE_STYLE_GUIDE.md - Code style guidelines
  • @inputless/events - Event type definitions

Testing

The storage module includes comprehensive unit tests:

  • 117 unit tests covering all adapters, utilities, and StorageManager
  • Integration tests for full storage workflows
  • Coverage: >50% global coverage, >70% for adapters and utilities
  • Test Suites: 10 test suites, all passing

Run tests:

npm test

Last Updated: January 2024
Status: ✅ Implemented, Tested, and Ready for Use