@marianmeres/kv
v2.4.0
Published
[](https://www.npmjs.com/package/@marianmeres/kv) [](https://jsr.io/@marianmeres/kv) [
The API is inspired by the Redis API.
Installation
deno add jsr:@marianmeres/kvnpm i @marianmeres/kvUsage
import { createKVClient } from '@marianmeres/kv';
// Basic usage with memory adapter (default)
const client = createKVClient("my-app-namespace:");
// Or specify a different adapter type
const redisClient = createKVClient(
"my-app-namespace:",
'redis', // 'redis' | 'postgres' | 'deno-kv' | 'memory'
{ db: myRedisClient } // adapter-specific options
);
// Use the client
await client.set('my:foo:key', { my: "value" });
await client.get('my:foo:key'); // { my: "value" }
await client.keys('my:*'); // ['my:foo:key']Important: Namespace must end with a colon (:) or be an empty string.
API
// single-key ops
client.set(key: string, value: any, options?): Promise<boolean>
client.setIfAbsent(key: string, value: any, options?): Promise<boolean>
client.get(key: string): Promise<any>
client.getSet(key: string, value: any, options?): Promise<any> // returns previous value
client.delete(key: string): Promise<boolean>
client.exists(key: string): Promise<boolean>
// atomic primitives
client.incr(key: string, by?: number, options?): Promise<number>
client.decr(key: string, by?: number, options?): Promise<number>
client.cas(key: string, expected: any, next: any, options?): Promise<boolean>
// patterns & iteration
client.keys(pattern: string): Promise<string[]>
client.keysIter(pattern: string): AsyncIterable<string>
client.clear(pattern: string): Promise<number>
// batch
client.setMultiple(
entries: [string, any][] | { key: string; value: any; ttl?: number }[],
options?
): Promise<boolean[]>
client.getMultiple(keys: string[]): Promise<Record<string, any>>
client.transaction(operations: Operation[]): Promise<any[]>
// TTL
client.expire(key: string, ttl: number): Promise<boolean>
client.ttl(key: string): Promise<TtlResult>TtlResult
type TtlResult =
| { state: "missing" }
| { state: "no-ttl" }
| { state: "expires"; at: Date };Pattern Syntax
keys() and clear() use Redis-style glob patterns:
*— match any number of characters?— match exactly one character- all other characters (including
.,%,_,(,),[,]) are treated literally
Adapter Feature Matrix
| Feature | Memory | Redis | PostgreSQL | Deno KV |
|---|:-:|:-:|:-:|:-:|
| Persistent | ✗ | ✓ | ✓ | ✓ |
| TTL on set() | ✓ | ✓ | ✓ | ✓ |
| ttl() → expires/no-ttl/missing | ✓ | ✓ | ✓ | no-ttl/missing only |
| expire() | ✓ | ✓ | ✓ | always false |
| Atomic setIfAbsent / incr / decr / cas / getSet | ✓ | ✓ (Lua) | ✓ (UPSERT) | ✓ (CAS retry loop) |
| Sorted keys() | ✓ | ✓ | ✓ | ✓ |
| Streaming keysIter() | ✓ | ✓ (SCAN) | ✓ (cursor) | ✓ (native) |
| Per-pair TTL in setMultiple | ✓ | ✓ | ✓ | ✓ |
| Atomic transaction() | ✓ (single-threaded) | ✓ (MULTI) | ✓ (BEGIN/COMMIT) | ✓ when no get ops |
| delete() returns real existed-flag | ✓ | ✓ | ✓ | opt-in via strictDeleteResult |
| Background TTL cleanup | optional | native (Redis does it) | optional | native (Deno.Kv does it) |
| keys() / clear() | ✓ | ✓ (non-cluster only) | ✓ | ✓ |
Adapter-Specific Limitations
Deno KV
delete(): Always returnstrue, even for non-existent keys (Deno.Kv limitation)expire(): Not supported — always returnsfalsettl(): Not supported — always returnsnulltransaction(): Atomic viaDeno.Kv.atomic()only when the transaction contains nogetoperations. A transaction that mixesgetwithset/deletefalls back to sequential (non-atomic) execution.- Note: TTL can be set during
set(), but cannot be queried or modified after.
Redis
keys()andclear(): Not supported in cluster mode (throws error)- Namespace: Required (cannot be empty string)
- Connection ownership: If you pass a not-yet-open client to the adapter,
initialize()opens it anddestroy()closes it. If you pass an already-open client, the adapter leaves the connection lifecycle to you — safe to share a single client across adapters with different namespaces.
PostgreSQL
- Creates a table (default:
__kv) in your database. tableNamemust match^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$(word chars plus an optionalschema.prefix).transaction()pins a single connection from the Pool for the whole BEGIN/…/COMMIT block — safe to call against apg.Pool.- Supports optional TTL cleanup via
ttlCleanupIntervalSecoption.
Memory
- Data is not persisted (in-memory only).
- Supports optional TTL cleanup via
ttlCleanupIntervalSecoption.
Breaking Changes (2.1 / 3.0 candidate)
Since 2.0, the following changes are in this tree:
v3.0 (breaking — bump major if you ship these as-is):
ttl(key)now returns a discriminatedTtlResult—{ state: "missing" | "no-ttl" | "expires", at? }— instead ofDate | null | false. Migration:// before const t = await client.ttl(k); if (t instanceof Date) use(t); else if (t === null) /* no ttl */ else /* missing */ // after const t = await client.ttl(k); switch (t.state) { case "expires": use(t.at); break; case "no-ttl": /* … */; break; case "missing": /* … */; break; }- Key validation is on by default. Non-empty strings, no
\0, totalnamespace + keylength ≤ 512. SetvalidateKeys: falseon the adapter options to opt out. Previous behavior silently accepted empty/overlong keys and let the backend error cryptically.
v2.1 / v2.2 (additive):
- New methods
setIfAbsent,incr,decr,getSet,cas,keysIteron every adapter. setMultipleaccepts{ key, value, ttl? }[]entries in addition to the legacy[key, value][]tuples. Per-pairttloverrides batchoptions.ttl.- Deno KV: new
strictDeleteResult: trueoption pre-checks existence sodelete()returns the real did-it-exist flag (default staystruefor BC). - Deno KV: new
atomicRetryAttemptsoption (default 20) for CAS-based primitives under contention. - Default
clear()in Deno KV now batches into atomic commits (500 at a time).
Breaking Changes (2.0.0)
This release fixes several correctness bugs. Most fixes are behavior-preserving, but a few change semantics in ways that could affect existing code:
set(key, value, { ttl: 0 })now explicitly disables expiration for the call, overriding any non-zerodefaultTtl. Before:ttl: 0was silently ignored anddefaultTtlwas used. If you relied on the old behavior, omit thettloption instead of passing0.- Redis adapter:
destroy()now closes connections thatinitialize()opened. Connections that were already open when you passed them to the adapter remain under your control (unchanged). If your test harness used to close the underlying client twice, remove the redundant close. - Redis adapter:
__debug_dump()is scoped to the adapter's namespace. Before: it dumped every key in the selected DB (across tenants). After: only keys under this adapter's namespace. getMultiple()preserves stored falsy values (false,0,"",null). Before: all of these were coerced tonull, indistinguishable from "key missing". After: stored falsy values round-trip; only missing keys map tonull.- Deno KV:
exists(key)returnstruewhen the stored value isnull. Before:nullvalues were reported as non-existent. - Deno KV:
keys()is sorted. keys()andclear()now treat non-wildcard regex/LIKE metacharacters as literals. Before: Memory/Deno KV adapters leaked./(/[through to regex, and the PostgreSQL adapter leaked%/_through to LIKE. After: only*and?are wildcards; everything else matches literally.- PostgreSQL:
tableNameis validated. Invalid names (anything outside[A-Za-z0-9_.]) now throw in the constructor instead of producing confusing SQL errors at runtime.
Full API Reference
For complete API documentation including all methods, types, and adapter-specific options, see API.md.
