TessariDBDocs
  • 0.34.2-betalatest
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

A cache over HTTP and in the clients

The space's cache verbs as HTTP routes — get, set with an expiry, incr, conditional set, keys by prefix, a lock and its release — and the same calls in all five clients.

A space already is a cache, a counter and a lock: the statements are on Expiry, counters and locks. This page is the same thing without writing the statement — ten HTTP routes, and a cache handle in every client — for node 0.15.0-beta and later.

Over HTTP

Every route runs one space statement through your own session, so a grant, a tenancy and a refusal are exactly what a script would meet. The key is the rest of the path, percent-encoded; the three names before it are plain names.

curl -X PUT 'http://127.0.0.1:8000/kv/app/main/cache/key/session%3Aabc?expire=30m' \
  --data-binary "{ user: 'ada', at: datetime '2026-09-30T10:00:00Z' }"
# {"written":true}

curl 'http://127.0.0.1:8000/kv/app/main/cache/key/session%3Aabc'
# {"value":{"at":"2026-09-30T10:00:00Z","user":"ada"},"ttl":"29m59s985ms"}

curl -X POST 'http://127.0.0.1:8000/kv/app/main/cache/incr/hits?by=3'
# {"value":3}

curl 'http://127.0.0.1:8000/kv/app/main/cache?prefix=session'
# {"keys":["session:abc"]}
methodpathqueryanswers
GET/kv/{ns}/{db}/{space}/key/{key}—{"value": …, "ttl": "…" | null} — null is never expires; 404 when there is no key
PUT/kv/{ns}/{db}/{space}/key/{key}expire, if=absent|present{"written": true|false}
DELETE/kv/{ns}/{db}/{space}/key/{key}—{"deleted": true|false}
POST/kv/{ns}/{db}/{space}/swap/{key}expire{"written": …} — the body is { expect: old, value: new }
POST/kv/{ns}/{db}/{space}/incr/{key}by{"value": n}
POST/kv/{ns}/{db}/{space}/expire/{key}expire{"found": …}
POST/kv/{ns}/{db}/{space}/persist/{key}—{"found": …}
POST/kv/{ns}/{db}/{space}/lock/{key}holder, expire{"held": …}
POST/kv/{ns}/{db}/{space}/unlock/{key}holder{"released": …}
GET/kv/{ns}/{db}/{space}prefix, after, limit{"keys": […]}

Four things the routes promise:

  • A condition that does not hold is false, not an error. written: false from if=absent means somebody got there first; a refusal is a 4xx.
  • The body is one TessariQL value, so a datetime, a decimal or a record reference arrives as itself — not a JSON string you have to reinterpret.
  • expire must be positive. A zero or negative one is a 400, because the statement underneath would remove the key.
  • Not a space is 404, and so is an operation that does not exist; the right operation with the wrong method is 405.

A client that sends a password with every request pays a deliberately slow hash every time: exchange it once for a token at POST /session and send that.

A lock over HTTP

curl -X POST 'http://127.0.0.1:8000/kv/app/main/locks/lock/report?holder=worker-7&expire=30s'
# {"held":true}   — taken, or extended if worker-7 already held it
curl -X POST 'http://127.0.0.1:8000/kv/app/main/locks/lock/report?holder=worker-9&expire=30s'
# {"held":false}
curl -X POST 'http://127.0.0.1:8000/kv/app/main/locks/unlock/report?holder=worker-7'
# {"released":true}

A lock is a lease, not a mutex: past expire another holder may take it and neither is told. unlock never deletes: it writes an expiring value only while the key still names you, so a worker whose lease already lapsed cannot remove the next holder's lock — and the key cannot become permanent, which a hand-back without an expiry would do.

In the clients

Each client has a cache handle over a connection you already hold. It rides the wire, so every value keeps its type, and it sends its own USE with every call. The calls and their statements are the same in all five — the protocol repository's spec/cache-v1.md:

callwhat it answers
get · set(ttl?) · deletethe value or nothing · — · whether there was a key
setIfAbsent · setIfPresent · compareAndSetwhether it wrote
incr(by)the new value
ttl · expire · persistexpires / never / no key · whether there was a key
keys(prefix, after, limit)keys in order, up to 1000
getOrSet(key, ttl, loader)the stored value — the loader runs only on a miss
lock(key, ttl, holder?)a lease, with extend and release, or nothing
const cache = new Cache(connection, { namespace: 'app', database: 'main', space: 'cache' });
const page = await cache.getOrSet('page:/', 60_000, () => render());
const lease = await cache.lock('nightly-report', 30_000);
if (lease) { /* … */ await lease.release(); }

The same in Rust, Python, Go, Kotlin and TypeScript.

Two rules every one of them is built around, because both fail silently:

  • A plain set clears an expiry the key had. Pass the ttl again on every write that must keep one.
  • getOrSet does not coordinate racing callers. Each that misses runs its loader; the first to store wins and the others answer the winner's value.
A cache over HTTP and in the clients — TessariDB