Skip to content

Repository files navigation

KeyPlex

NPM Last Update NPM Version NPM Downloads

NPM License

KeyPlex is a zero-dependency, modular library for managing namespaced key-value storage over the Web Storage API. It provides an abstract class that can be extended to create custom storage backends, plus ready-made implementations for localStorage and sessionStorage.

Features

  • Namespace support: automatically scopes keys using a customizable namespace (default derived from the current domain).
  • TTL: per-key time-to-live with lazy expiration — no timers.
  • Reactivity: watch() for same-tab and cross-tab changes.
  • Wildcard deletion: batch-delete by prefix (prefix%) or suffix (%suffix).
  • Safe mode: optionally swallow storage errors (quota exceeded, disabled storage) instead of throwing.
  • Custom serializers: swap JSON for any format.
  • SSR-safe: nothing touches browser globals until first use.

Installation

npm install @darcas/keyplex

Or, if you're using yarn:

yarn add @darcas/keyplex

Usage

Using LocalPlex and SessionPlex

KeyPlex includes two built-in implementations:

  • LocalPlex: uses localStorage as the backend.
  • SessionPlex: uses sessionStorage as the backend.
import { LocalPlex, SessionPlex } from '@darcas/keyplex'

const localStore = new LocalPlex('myNamespace')

localStore.set('key', { name: 'KeyPlex' })

const value = localStore.get('key')
console.log(value) // { name: 'KeyPlex' }

if (localStore.has('key')) {
  console.log('Key exists!')
}

localStore.del('key')

const sessionStore = new SessionPlex('sessionNamespace')
sessionStore.set('sessionKey', 'Session Data')

The constructor accepts either a namespace string or an options object:

const store = new LocalPlex({
  dbname: 'myNamespace', // default: reversed hostname (e.g. app.example.com → com.example.app)
  safe: true,            // storage errors are swallowed instead of thrown
  serializer: mySerializer,
})

Without arguments, the namespace is derived from location.href. In non-browser environments pass an explicit dbname, otherwise an error is thrown when the namespace is first needed.

TTL (time-to-live)

Entries can expire. Expiration is checked lazily on read — expired entries are purged and the default is returned:

// Expires in 1 hour
store.set('token', token, { ttl: 3600 })

store.get('token', null) // null after expiry; the entry is removed too

Wildcard deletion

Use % at the end of a key to match all keys with that prefix, or at the beginning to match by suffix:

store.set('user/123/profile', { name: 'John Doe' })
store.set('user/123/settings', { theme: 'dark' })
store.set('user/124/profile', { name: 'Jane Doe' })

// Delete everything under user/123
store.del('user/123%')

// Delete every "profile" key across all users
store.del('%profile')

Notes:

  • % in both positions (%text%) matches keys containing text.
  • A bare % matches every key in the namespace.
  • Keys are stored internally as @<dbname>/<key>.

Watching changes

watch(pattern, callback) notifies observers when matching keys change:

const unsubscribe = store.watch('user%', (key, value) => {
  console.log(`user key changed: ${key}`, value)
})

unsubscribe() // stop listening

Semantics:

  • Same tab: mutations made through other instances sharing the same namespace are delivered. The instance that performed the operation does not notify its own watchers.
  • Cross-tab: changes from other tabs are delivered via the native storage event.
  • Deletions deliver null as the new value.
  • Patterns support the same wildcards as del.

Namespace-wide operations

store.keys()  // all live keys in this namespace, without the prefix
store.clear() // delete every key in this namespace (only this one)

Both exclude and purge expired entries.

Extending KeyPlex

To create a custom storage backend, extend KeyPlex and implement four protected methods:

  1. allKeys: return all raw keys in the backend (namespacing is handled by the base class).
  2. getItem: retrieve a raw value by its namespaced path.
  3. setItem: store a raw value by its namespaced path.
  4. removeItem: remove a value by its namespaced path.
import { KeyPlex } from '@darcas/keyplex'

class CustomStorage extends KeyPlex {
  private store = new Map<string, string>()

  protected allKeys = (): string[] => [ ...this.store.keys() ]
  protected getItem = (key: string): string | null => this.store.get(key) ?? null
  protected setItem = (key: string, value: string): void => { this.store.set(key, value) }
  protected removeItem = (key: string): void => { this.store.delete(key) }
}

const customStore = new CustomStorage('customNamespace')
customStore.set('customKey', 'Custom Value')

Safe mode

By default, storage errors propagate. With safe: true they are swallowed:

  • get / has return the default / false
  • set / del / clear fail silently

Useful when storage may be unavailable (private browsing, quota limits).

Custom serializer

Values are wrapped in an internal envelope before serialization, so any string-based serializer works:

import { LocalPlex, type Serializer } from '@darcas/keyplex'

const base64Serializer: Serializer = {
  serialize: (value) => btoa(JSON.stringify(value)),
  deserialize: (data) => JSON.parse(atob(data)),
}

const store = new LocalPlex({ dbname: 'ns', serializer: base64Serializer })

The Serializer interface is generic (deserialize<T>(data): T, serialize<T>(value: T): string), so typed backends can implement it against a specific shape.

API Reference

KeyPlex

Method Description
get<T>(key) Returns the value typed as T | null, or null if missing/expired.
get<T>(key, def) Returns the value typed as T, or def if missing/expired.
set<T>(key, value, options?) Stores the value. options.ttl sets expiry in seconds.
del(key) Deletes a key. Supports % wildcards (prefix/suffix).
has(key) true if the key exists and is not expired.
keys() All live keys in the namespace, prefix stripped.
clear() Deletes all keys in the namespace.
watch(pattern, callback) Observes matching keys. Returns an unsubscribe function.
dbname The resolved namespace (read-only).

Abstract methods for subclasses: allKeys, getItem, setItem, removeItem.

Migrating from v1

  • Storage format changed: values are now stored in an envelope ({ t: '@keyplex', v, e? }). Existing v1 entries (raw JSON) remain readable: they are recognized on first access and transparently rewritten in the new format — no migration step is required, and the rewrite never triggers watch notifications. Note that { t: '@keyplex' } is now a reserved shape for top-level values.
  • has() fixed: it no longer returns false for keys holding null.
  • keys() is now public and filtered by namespace; custom backends implement the protected allKeys() instead.
  • lodash dependencies removed — the package has zero runtime dependencies.
  • Constructor accepts options: { dbname, safe, serializer } in addition to the plain string.

Contributing

If you'd like to contribute to the project, feel free to fork it and create a pull request. Please ensure that your changes are well-tested and properly documented.

License

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


Made with ❤️ by Dario Casertano (DarCas).

About

A versatile and modular library for managing namespaced key-value storage, with built-in support for localStorage and sessionStorage, and the ability to easily extend for custom storage solutions.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages