TILLCACHE · KV

Key–value, the way you already know it.

GA

The KV surface is ioredis-compatible: the method names, arguments, and return shapes match what you use with Redis. Strings with TTLs, atomic counters, hashes, mget/mset, pipelines — and a command() escape hatch for anything not typed.

01Basics

get / set / del / exists

ts
import { cache } from './cache'

// Set with a 60-second TTL, only if the key is new:
await cache.set('session:abc', token, { ex: 60, nx: true })

await cache.get('session:abc')     // token | null
await cache.exists('session:abc')  // 1 | 0
await cache.del('session:abc')     // number removed

get() returns the value or null. set() takes an options object as its third argument (below). del() and exists() return counts, like Redis.

02set options

TTLs and conditions

ts
await cache.set('k', 'v', { ex: 60 })       // expire in 60 seconds
await cache.set('k', 'v', { px: 1500 })     // expire in 1500 ms
await cache.set('k', 'v', { nx: true })     // set only if absent
await cache.set('k', 'v', { xx: true })     // set only if present
await cache.set('k', 'v', { keepTtl: true }) // overwrite value, keep existing TTL
OptionMeaning
exExpire the key in this many seconds.
pxExpire the key in this many milliseconds.
nxSet only if the key does not already exist.
xxSet only if the key already exists.
keepTtlOverwrite the value but keep the key’s current TTL instead of clearing it.
nx + ex is a lock
set(key, id, { nx: true, ex: 30 }) is the classic single-round-trip lock: it succeeds for exactly one caller and self-expires, so a crashed holder can’t deadlock the key.
03Counters

Atomic counters

incr/incrby/decr/decrby are atomic on the server — no read-modify-write race:

ts
await cache.incr('views')          // 1
await cache.incrby('views', 10)    // 11
await cache.decr('views')          // 10
await cache.decrby('views', 5)     // 5
04TTL

Expiry: expire / ttl / persist

ts
await cache.set('otp', code)
await cache.expire('otp', 300)     // 300s to live
await cache.ttl('otp')             // seconds remaining (-1 no TTL, -2 missing)
await cache.persist('otp')         // drop the TTL, keep the value

ttl() returns the seconds remaining, or -1 when the key exists with no TTL and -2 when it doesn’t exist — the standard Redis convention.

05Batch

mget / mset

ts
await cache.mset({ 'a': '1', 'b': '2', 'c': '3' })
await cache.mget('a', 'b', 'c')    // ['1', '2', '3']  (null for missing)

One round trip for many keys. mget() preserves order and returns null for keys that don’t exist.

06Hashes

Hashes

Store a small record under one key with hget/hset/hgetall/hdel/hincrby:

ts
await cache.hset('user:42', { name: 'Ada', plan: 'pro' })
await cache.hget('user:42', 'name')    // 'Ada'
await cache.hgetall('user:42')         // { name: 'Ada', plan: 'pro' }
await cache.hincrby('user:42', 'logins', 1)
await cache.hdel('user:42', 'plan')
07Pipeline

Pipelines

Batch several commands into a single round trip with pipeline(). Each entry is an [command, ...args] array; results come back in order:

ts
const [views, exists] = await cache.pipeline([
  ['incr', 'views'],
  ['exists', 'session:abc'],
])
08Escape hatch

Raw command()

Anything the typed methods don’t cover, reach for command() — an array of the command and its arguments:

ts
// Raw command for anything the typed methods don't cover.
// Still subject to the server-side allow-list — see Security.
await cache.command(['SET', 'k', 'v', 'EX', '60'])
await cache.command(['GETRANGE', 'k', '0', '3'])
Allow-listed
command() is still bound by the server-side allow-list. Destructive or introspection commands (FLUSHALL, KEYS, CONFIG, EVAL) are rejected — see the security model.

Next: Queues for durable work, Pub/Sub for fan-out, or Backends & vendors to choose where a namespace’s keys actually live.