Key-value stores

Store and read bytes by key from a function, with typed numeric counters, prefix scans and a byte cursor.

Docs menu

db.kv(name) is a store of bytes, one per character. substrio.kv(name) is the same.

const kv = db.kv("sessions");
kv.put("a", "hello");               // true
kv.get("a");                        // "hello"
kv.peek("a");                       // 5 (stored length in bytes), -1 when absent
kv.delete("a");                     // true

Values

A value is a string of characters up to U+00FF, a number, a boolean, a BigInt or binary data (Uint8Array, ArrayBuffer). get answers a string of the stored bytes: put("n", 5) then get("n") is "5". Text with other characters throws a TypeError. Store text as UTF-8 bytes and decode it:

kv.put("t", new TextEncoder().encode("héllo ✓"));
const text = new TextDecoder().decode(Uint8Array.from(kv.get("t"), (c) => c.charCodeAt(0)));

A key is a string under the same rule: characters up to U+00FF. A key with a wider character throws a TypeError (kv.getInt: the key has a character above U+00FF; KV stores one byte per character, so pass bytes ...); a key that is not a string throws kv.<method>: the key must be .... Encode text keys the way the example above encodes values.

Reading: what each form allocates

Call Allocates
get(key) one string (the value is copied once into it)
getBytes(key) one Uint8Array (one copy); keep it as long as you like
ref(key) nothing; the value lands in the store's _v buffer and is valid until the next ref
peek(key), has(key) nothing (peek answers the length, has a boolean, neither reads the value)
getInt(key), getF64(key) nothing
kv.has("a");                        // true or false
const bytes = kv.getBytes("a");     // Uint8Array [104, 101, ...], null when absent
const n = kv.ref("a");              // 5; read kv._v.subarray(0, n) before the next ref

Numbers

kv.putInt("count", 41);             // true: an integer, exact within the safe range
kv.getInt("count");                 // 41, undefined when absent
kv.putF64("ratio", 0.5);            // true
kv.getF64("ratio");                 // 0.5, undefined when absent

putInt takes only a number that is an integer within -(2^53 - 1) to 2^53 - 1. A fraction, NaN, an infinity, a number outside that range, a BigInt, a string or a missing value throws a TypeError (kv.putInt: the value must be an integer within the safe integer range ..., or ... must be a number). putF64 takes any number and throws the same must be a number TypeError for anything else. Nothing a refused write was given is stored.

getInt and getF64 answer undefined for an absent key and for a stored value that is not 8 bytes. getInt never answers a BigInt: a stored integer outside the safe range throws a RangeError (kv.getInt: the value stored is an integer outside the safe integer range ...). Both kinds are the same 8 bytes, so read a value with the kind that wrote it: getInt on a putF64 value reinterprets the bytes (putF64(5e-324) reads as the integer 1, and most doubles throw the RangeError).

kv.increment("hits");               // 1: an absent key starts at 0, then adds 1
kv.increment("hits", 5);            // 6: atomic, safe from concurrent calls

A counter is an 8-byte integer: getInt reads it. increment answers undefined when the stored value is not 8 bytes, and throws when the result leaves the safe range.

Cursor

const cursor = kv.cursor();
for (let ok = cursor.seekPrefix("user:"); ok; ok = cursor.next()) {
  const key = cursor.key();         // string
  const value = cursor.value();     // string
}
cursor.close();

Also seekFirst, seekLast, seekGE, seekLE, seekLT, seekPrefixLast, prev and scanCount(prefix?).

A seekPrefix (or seekPrefixLast) prefix stays the cursor's filter until its next seekFirst, seekLast, seekPrefix or seekPrefixLast: a later seekGE, seekLE or seekLT positions inside that prefix, and next/prev stop at its end. A seekGE to a key outside it finds nothing. Code that walks prefixes and also seeks by key keeps a cursor for each, or calls seekFirst before the key seek.

Limits and rules

  • A key is at most 65535 bytes; a longer one throws an error naming the size and the limit (kv.put: key of N bytes exceeds the limit of 65535 bytes).
  • One value is at most 16 MiB; a larger one throws an error naming the size and the limit (kv.put: single value of N bytes exceeds the limit of 16777216 bytes).
  • A write inside db.transaction throws (kv is not transactional): write outside it.
  • The Cloudflare shim env.<KV> has get, put, delete and list; its options are ignored and list is unreliable. Prefer db.kv.