Built-in TTL
Set it and forget it.
Pass { ttl } in seconds when you write. Expired entries are purged lazily on read — no timers, no sweeps, no garbage.
@darcas/keyplex · v2.0.4 · MIT
Key-value storage over localStorage and sessionStorage — with built-in TTL, wildcard deletion and cross-tab watching. Fully typed, zero dependencies.
import { LocalPlex } from '@darcas/keyplex'
const store = new LocalPlex()
// namespace auto-derived: app.example.com → com.example.app
store.set('user.theme', 'dark')
store.set('session.token', token, { ttl: 900 })
store.get<string>('user.theme') // 'dark'
store.keys() // ['user.theme', 'session.token']
Contents: features
Reasons to stop using raw Web Storage in 2026 — each one maps to a real line of code you don’t have to write anymore.
Set it and forget it.
Pass { ttl } in seconds when you write. Expired entries are purged lazily on read — no timers, no sweeps, no garbage.
Delete by pattern, not by name.
Trailing % matches prefixes, leading % suffixes, both sides contains. store.del('cache:%') clears a whole family in one call.
Every tab, always in sync.
watch(pattern, cb) delivers changes from any tab via the native storage event. No polling, no BroadcastChannel plumbing.
One browser. Many apps. No collisions.
Keys are scoped under a namespace derived from your hostname — reversed, like app.example.com → com.example.app. Or pass your own.
Storage fails? Your app doesn’t.
With safe: true, quota errors and disabled storage are swallowed instead of thrown — Safari private mode included.
Bring your own format.
JSON by default. Implement the two-method Serializer interface to store MessagePack, encrypted payloads, anything string-shaped.
Four methods to rule a backend.
KeyPlex is an abstract class: implement getItem/setItem/removeItem/allKeys and you have the full API over IndexedDB, memory or the network.
Renders anywhere.
The default namespace resolves lazily from location.hostname. Pass an explicit dbname and it runs on the server without a browser.
~3 KB min+gzip.
A single source file compiled to ESM with bundled TypeScript types. Nothing to audit, nothing to update.
// Expires after 15 minutes — purged lazily on read.
// No timers, no sweeps.
store.set('session.token', token, { ttl: 15 * 60 })
store.has('session.token') // true
// …15 minutes later…
store.get('session.token') // null (and the entry is gone)
store.del('cache:%') // prefix match
store.del('%:draft') // suffix match
store.del('%avatar%') // contains
// Live updates from any tab — backed by the native
// storage event, no polling:
const stop = store.watch('user:%', (key, value) => {
render(key, value)
})
stop() // unsubscribe
Contents: live demo
No screenshots. The commands below call the actual @darcas/keyplex package installed on this very page — your browser, your storage.
Commands
Live state · keys()
Watch log · watch('%')
By design, a KeyPlex instance never notifies its own watchers — same-tab events reach sibling instances of the same namespace, cross-tab events arrive via the native storage listener.
Contents: API surface
Fully typed out of the box — the package ships its own declarations. No configuration object larger than the library itself.
| Method | Returns | Notes |
|---|---|---|
new LocalPlex(options?) | LocalPlex | options: string (namespace) or { dbname?, safe?, serializer? }. SessionPlex is identical over sessionStorage. |
get<T>(key) | T | null | Reads, purges expired entries lazily, migrates legacy values on the fly. Overload get(key, def) returns your fallback. |
set(key, value, options?) | void | Wraps value in the envelope; options.ttl (seconds) stamps the expiry epoch. |
del(pattern) | void | Exact delete — or wildcard: trailing % prefix-match, leading % suffix-match, both contains. |
has(key) | boolean | Existence check with lazy expiry purge. Never triggers a legacy rewrite. |
keys() | string[] | All keys in the namespace, relative names only. Expired entries are dropped as a side effect. |
clear() | void | Removes every key in the namespace and emits watch events for each. |
watch(pattern, callback) | () => void | Subscribes to changes matching pattern; returns the unsubscribe function. Cross-tab via storage events, same-tab across sibling instances. |
import { KeyPlex } from '@darcas/keyplex'
class MemoryPlex extends KeyPlex {
private map = new Map<string, string>()
protected allKeys = (): string[] => [...this.map.keys()]
protected getItem = (key: string): string | null =>
this.map.get(key) ?? null
protected setItem = (key: string, value: string): void => {
this.map.set(key, value)
}
protected removeItem = (key: string): void => {
this.map.delete(key)
}
}
// Full API — TTL, wildcards, watching — over any backend.
Contents: side by side
The same eight concerns you’d eventually solve by hand — solved once, typed, and shipped in ~3 KB.
| Concern | Raw localStorage | KeyPlex |
|---|---|---|
| TTL / expiry | ✗ DIY timestamps on every write | ✓ { ttl } option, lazy purge on read |
| Namespacing | ✗ Manual prefixes, hope for the best | ✓ Automatic reversed-hostname namespace |
| Bulk deletion | ✗ Loop over localStorage yourself | ✓ del('cache:%') with % wildcards |
| Cross-tab sync | ✗ Hand-rolled storage listeners per feature | ✓ watch('user:%', cb), one line |
| Type safety | ✗ Strings in, strings out | ✓ Generics + shipped .d.ts |
| Storage failures | ✗ Throws in private mode / on quota | ✓ safe: true swallows gracefully |
| Custom formats | ✗ Parse/stringify everywhere | ✓ Serializer interface, set once |
| Custom backends | ✗ Rewrite everything | ✓ Extend KeyPlex, four methods |
Contents: get started
Drop it in, give it a namespace (or don’t), and start storing things that expire, match patterns and sync across tabs.