Skip to content

Cache

Preface

A strategy-pattern-based async storage solution that supports multiple backends (localStorage, IndexedDB, Memory) behind a unified API. All methods are async so callers need no changes when switching drivers.

TIP

@vben/utils re-exports the full cache module — business code can import everything uniformly from @vben/utils.

Architecture

shell
shell
┌───────────────────────────────────────────────┐
             StorageManager
  ┌─────────────┐  ┌───────────────────────┐
 Prefix isolation   TTL expiry
  └─────────────┘  └───────────────────────┘
├───────────────────────────────────────────────┤
             IStorageDriver
├──────────┬─────────────────┬──────────────────┤
  Local   IndexedDB     Memory
  Storage   Driver     Driver
  Driver
└──────────┴─────────────────┴──────────────────┘

Layer responsibilities:

LayerResponsibility
StorageManagerNamespace prefix isolation, TTL expiry checks, unified public API
IStorageDriverPure KV storage abstraction interface
Driver implementationsTalk to concrete storage engines, unaware of prefix or TTL

Quick Start

Basic usage

When driver is omitted, the browser uses LocalStorageDriver if localStorage is available, otherwise falls back to MemoryStorageDriver (e.g. Safari private mode); SSR/Node uses MemoryStorageDriver:

ts
ts
import { StorageManager } from '@vben/utils';

const cache = new StorageManager({ prefix: 'myapp' });

// Write a value
await cache.setItem('user', { name: 'John', age: 28 });

// Read a value
const user = await cache.getItem('user');
// => { name: 'John', age: 28 }

// Read with a default value
const settings = await cache.getItem('settings', { theme: 'light' });
// Returns { theme: 'light' } if absent

// Delete a value
await cache.removeItem('user');

// Clear all entries under the current prefix
await cache.clear();

With TTL expiry

The third argument of setItem is the TTL in milliseconds. Once expired, reads return the default value (lazy deletion):

ts
ts
import { StorageManager } from '@vben/utils';

const cache = new StorageManager({ prefix: 'session' });

// Expires in 5 minutes
await cache.setItem('token', 'abc123', 5 * 60 * 1000);

// Reads normally within 5 minutes
const token = await cache.getItem('token');
// => 'abc123'

// Returns null after 5 minutes
const expiredToken = await cache.getItem('token');
// => null

// Actively clean up all expired entries
await cache.clearExpiredItems();

Storage Drivers

Local storage driver (default)

LocalStorageDriver: based on the browser's localStorage / sessionStorage, data is persisted.

ts
ts
import { LocalStorageDriver, StorageManager } from '@vben/utils';

// Use localStorage (default)
const cache = new StorageManager({
  driver: new LocalStorageDriver(),
  prefix: 'app',
});

// Use sessionStorage
const sessionCache = new StorageManager({
  driver: new LocalStorageDriver({ storageType: 'sessionStorage' }),
  prefix: 'app',
});

Characteristics:

  • Synchronous API wrapped in async to keep the interface unified
  • Automatic JSON serialization / deserialization
  • Corrupt data is auto-cleared and returns null
  • Storage limit ~5–10MB (browser-dependent)

Use cases: user preferences, small config data, token storage

IndexedDB driver

IndexedDBDriver: based on the browser's IndexedDB, supports large structured data storage.

ts
ts
import { IndexedDBDriver, StorageManager } from '@vben/utils';

const cache = new StorageManager({
  driver: new IndexedDBDriver({
    dbName: 'my-app-db', // Database name, default 'vben-storage'
    dbVersion: 1, // Database version, default 1
    storeName: 'cache-store', // Object store name, default 'kv-store'
  }),
  prefix: 'data',
});

// Store large or complex data (IndexedDB natively supports structured cloning)
await cache.setItem('table-data', largeDataArray);
await cache.setItem('config', {
  columns: [...],
  filters: [...],
  pagination: { page: 1, size: 20 },
});

Characteristics:

  • Lazy initialization: opens the database on first operation, no manual init()
  • Large capacity (typically hundreds of MB to GB)
  • Supports structured cloning (Date, RegExp, Blob, etc.)
  • Natively async, does not block the main thread

Use cases: offline data caching, large table data, file/image caching, complex business data

Memory storage driver

MemoryStorageDriver: based on an in-memory Map, data is not persisted and is lost on page refresh.

ts
ts
import { MemoryStorageDriver, StorageManager } from '@vben/utils';

const cache = new StorageManager({
  driver: new MemoryStorageDriver(),
  prefix: 'test',
});

Characteristics:

  • Fastest read/write
  • No browser API dependency
  • Data is destroyed with the page lifecycle

Use cases: unit tests, SSR rendering, temporary runtime caching

Driver comparison

FeatureLocalStorageDriverIndexedDBDriverMemoryStorageDriver
Persistence
Capacity5–10 MBHundreds of MB+Memory-bound
SpeedFast (sync)Medium (async I/O)Fastest
Data typeJSON-serializable onlyStructured cloneAny JS object
Browser supportAll modern browsersAll modern browsersAny environment
Blocks main threadYesNoNo
Use caseConfig, tokens, small dataOffline cache, big dataTests, SSR

API Reference

StorageManager

Constructor

ts
ts
new StorageManager(options?: StorageManagerOptions)
ParamTypeDefaultDescription
driverIStorageDrivernew LocalStorageDriver() in browser when localStorage is available, new MemoryStorageDriver() otherwise (Safari private mode, SSR/Node)Storage driver instance
prefixstring''Key prefix for namespace isolation

Methods

MethodSignatureDescription
getItemgetItem<T>(key: string, defaultValue?: T | null): Promise<T | null>Get an entry; returns the default if expired or absent
setItemsetItem(key: string, value: unknown, ttl?: number): Promise<void>Set an entry, with optional TTL (ms)
removeItemremoveItem(key: string): Promise<void>Delete the given entry
clearclear(): Promise<void>Clear all entries under the current prefix
clearExpiredItemsclearExpiredItems(): Promise<void>Actively clean up all expired entries
keyskeys(): Promise<string[]>Return all keys under the current prefix (prefix stripped)

IStorageDriver interface

Custom drivers implement this interface:

ts
ts
interface IStorageDriver {
  clear(): Promise<void>;
  getItem<T>(key: string): Promise<null | T>;
  keys(): Promise<string[]>;
  removeItem(key: string): Promise<void>;
  setItem(key: string, value: unknown): Promise<void>;
}

Advanced Usage

Custom Driver

Implement IStorageDriver to plug in any storage engine. Example with cookies:

ts
ts
import type { IStorageDriver } from '@vben/utils';

class CookieStorageDriver implements IStorageDriver {
  async getItem<T>(key: string): Promise<null | T> {
    const value = getCookie(key);
    return value ? JSON.parse(value) : null;
  }

  async setItem(key: string, value: unknown): Promise<void> {
    setCookie(key, JSON.stringify(value));
  }

  async removeItem(key: string): Promise<void> {
    deleteCookie(key);
  }

  async clear(): Promise<void> {
    clearAllCookies();
  }

  async keys(): Promise<string[]> {
    return getAllCookieNames();
  }
}

const cache = new StorageManager({
  driver: new CookieStorageDriver(),
  prefix: 'ck',
});

Dynamic driver selection by environment

ts
ts
import {
  IndexedDBDriver,
  LocalStorageDriver,
  MemoryStorageDriver,
  StorageManager,
} from '@vben/utils';

function createStorageManager(prefix: string) {
  // SSR environment uses the memory driver
  if (typeof window === 'undefined') {
    return new StorageManager({
      driver: new MemoryStorageDriver(),
      prefix,
    });
  }

  // Large-data scenarios use IndexedDB
  if (needsLargeStorage()) {
    return new StorageManager({
      driver: new IndexedDBDriver({ dbName: `${prefix}-db` }),
      prefix,
    });
  }

  // Default to localStorage
  return new StorageManager({ prefix });
}

Namespace isolation

Different modules use different prefixes so they do not interfere:

ts
ts
const userCache = new StorageManager({ prefix: 'user' });
const configCache = new StorageManager({ prefix: 'config' });

await userCache.setItem('profile', { name: 'John' });
await configCache.setItem('profile', { theme: 'dark' });

await userCache.getItem('profile'); // => { name: 'John' }
await configCache.getItem('profile'); // => { theme: 'dark' }

// Clears only the user-prefixed data, config is unaffected
await userCache.clear();
await configCache.getItem('profile'); // => { theme: 'dark' }

Scheduled cleanup of expired data

ts
ts
const cache = new StorageManager({ prefix: 'app' });

// Clean up once on app startup
await cache.clearExpiredItems();

// Or schedule it (every 10 minutes)
setInterval(
  async () => {
    await cache.clearExpiredItems();
  },
  10 * 60 * 1000,
);

Storage Format

The structure StorageManager stores at the Driver layer:

ts
ts
interface StorageItem<T> {
  expiry?: number; // Expiry timestamp (ms); undefined means never expires
  value: T; // Actual business data
}

The actual stored key is formatted as {prefix}-{key}. For example, prefix = 'app', key = 'user' produces the stored key app-user.

Expiry Strategy

A dual strategy of lazy deletion + active cleanup:

StrategyWhenDescription
Lazy deletionOn getItemChecks expiry on read; deletes and returns the default if expired
Active cleanupOn clearExpiredItemsIterates all prefixed keys and deletes expired ones

Notes

  1. All methods are async — even the synchronous localStorage is wrapped in Promises so callers need no changes when switching drivers.
  2. TTL is in millisecondssetItem('key', value, 60000) expires in 60 seconds.
  3. IndexedDB lazy initialization — no manual init() or open(); the DB connection is opened on first operation and reused.
  4. Prefix isolation is logicalclear() only clears data under the current prefix; with an empty prefix, clear() / keys() operate on all keys in the selected driver.
  5. LocalStorageDriver error handling — auto-clears corrupt data on JSON parse failure and returns null.
  6. IndexedDB version upgrade — increment dbVersion to modify the objectStore structure; the current implementation creates the objectStore in the upgradeneeded handler.

Contributors

Changelog

Released under the MIT License.