State
Keyed state, with branches.
Point reads, compare-and-swap, and TTLs live next to the log. Forks create copy-on-write data branches you can promote or discard.
Built for
Sessions, feature flags, what-if simulations
How it works
laser.kv(namespace) scopes a keyed store. Each key-value write is also a log record. Overwriting a key does not discard its history.
- Write -
set(key)opens a builder:.json(value)or.payload(bytes)encodes it,.ttl(duration)attaches an expiry,.send()commits it. - Read -
get(key)returns raw bytes,get_typed(key)decodes into your type.
Compare-and-set guards a write against a stale read:
getEntry(key)returns the current value with its version.- A follow-up write with
expectVersion(version)only lands if no other writer touched the key in between. expect_absent()is the CAS write that must be the first write to a key.
Use compare-and-set for ledgers and counters that must reject concurrent stale writes.
Forks are copy-on-write branches over materialized rows. They sit beside KV, but they are not an implicit mode for kv.set(..). Two kinds are chosen at creation:
- Continuous (the default) - a live branch that keeps seeing new trunk appends while carrying its own writes.
- Severed (
.severed()) - a frozen snapshot at the trunk's current offsets. Later trunk appends stay hidden..tables([...])narrows a severed snapshot to specific tables, an empty list captures every table.
Then the lifecycle:
fork(id).create()opens the branch..parent(id)records lineage for audit, while the fork still branches off the trunk.put_row(table, partition, offset)writes one speculative row at an exact source coordinate. Add queryable columns with.field(..), or attach a payload, embedding, metadata, or tombstone. TypeScript spells itputRow.query(index).fork(id)reads the overlay. Ordinary KV reads and trunk queries never see speculative rows before promotion.promote()merges the fork's writes back onto the trunk and returns how many rows were applied.squash()discards an already-open fork instead of promoting it.laser.forks()lists every open fork with its metadata.
Promotion applies the speculative rows to the trunk. It does not redirect ordinary KV writes into a hidden branch, so write fork data through the fork handle explicitly.
State requires laser-plane in Laser Stack or LaserData Cloud. KV and forks have separate capability flags.
Quick example
const kv = laser.kv("profiles")
const key = new TextEncoder().encode("user:42")
await kv.set(key).json({ plan: "pro" }).ttl(86_400_000_000n).send()
const profile = await kv.get(key)
// compare-and-swap: only lands if the version still matches
const entry = await kv.getEntry(key)
if (entry === undefined) throw new Error("profile vanished")
await kv
.set(key)
.json({ plan: "enterprise" })
.expectVersion(entry.version)
.commit()
// fork: a git-like branch of the same state
const fork = laser.fork("experiment-1")
await fork.squash()
await fork.create().severed().tables(["profiles"]).send()
await fork
.putRow("profiles", 0, 0n)
.field("plan", "enterprise-preview")
.send()
const applied = await fork.promote()let kv = laser.kv("profiles");
kv.set("user:42")
.json(&profile)?
.ttl(Duration::from_secs(86_400))
.send()
.await?;
let profile = kv.get_typed::<Profile>("user:42").await?;
// compare-and-swap: only lands if the version still matches
let entry = kv.get_entry("user:42").await?.expect("just written");
kv.set("user:42")
.json(&upgraded)?
.expect_version(entry.version)
.commit()
.await?;
// fork: a git-like branch of the same state
let fork = laser.fork("experiment-1");
fork.squash().await?;
fork.create().severed().tables(["profiles"]).send().await?;
fork.put_row("profiles", 0, 0)
.field("plan", "enterprise-preview")
.send()
.await?;
let applied = fork.promote().await?;store = laser.kv("profiles")
await store.set("user:42").json({"plan": "pro"}).ttl(86_400).send()
profile = await store.get_typed("user:42")
# compare-and-swap: only lands if the version still matches
entry = await store.get_entry("user:42")
await store.set("user:42").json({"plan": "enterprise"}).expect_version(
entry.version
).commit()
# fork: a git-like branch of the same state
fork = laser.fork("experiment-1")
await fork.squash()
await fork.create(severed=True, tables=["profiles"])
await fork.put_row("profiles", 0, 0).field(
"plan", "enterprise-preview"
).send()
applied = await fork.promote()TypeScript's TTL is microseconds as a bigint. Rust's is a Duration. Python's is plain seconds. Same knob, different unit per language.
Full runnable example: Rust · Python · TypeScript
Beyond get and set
The store is a full keyed surface, not a two-verb cache. All of it exists in all three languages (snake_case in Rust/Python, camelCase in TypeScript):
delete(key)removes an entry and tells you whether a live one existed.exists(key)returns the entry's metadata without transferring its value.expire(key, ttl)sets or refreshes an expiry in place without rewriting the value. Passing no TTL clears the expiry. The version comes back unchanged.patch(key, patch)applies a merge patch to a structured value without transferring the whole object and returns the new version. The patch bytes are codec-specific, a JSON merge patch over a JSON value for instance.copy_to(key, to_key)/move_to(key, to_key)duplicate or rename under the same namespace,into_namespace(ns)retargets either across namespaces.get_many(keys)batches point reads.delete_many()is a builder:.prefix(p),.range(start, end), or.key_contains(s)selects,.send()returns how many entries went.scan()pages through a namespace: the same.prefix/.range/.key_containsselectors plus.limit(n)and.cursor(c)for pagination, ending in.fetch()for a page or.entries()for the values.laser.kv_namespaces()lists every namespace on the deployment.
Writes have two finishers: .send() fires and forgets the version, .commit() returns the new version number, which is what you feed the next expect_version(..).
Locks and fenced writes
For state that must have at most one effective writer, the store pairs an advisory lease with a fenced compare-and-swap:
lease(key, ttl)acquires a bounded-TTL distributed lock and returns aLeasecarrying a fencingtokenand the TTL the store actually granted.cas_fenced(key, fence_key, token)starts a write that applies only while the fence's sequence still equals your token. A zombie holder resuming with an older token is rejected even if its lease was presumed lost, which is exactly the failure a plain lock cannot defend against.release(key, token)gives the lock back early instead of waiting out the TTL.
const lock = utf8("ledger:lock")
const total = utf8("ledger:total")
const lease = await kv.lease(lock, 10_000_000n)
await kv
.casFenced(total, lock, lease.token)
.json(newTotal)
.expectVersion(entry.version)
.commit()
await kv.release(lock, lease.token)let lease = kv.lease("ledger:lock", Duration::from_secs(10)).await?;
kv.cas_fenced("ledger:total", "ledger:lock", lease.token)
.json(&new_total)?
.expect_version(entry.version)
.commit()
.await?;
kv.release("ledger:lock", lease.token).await?;token, _granted_ttl = await kv.lease("ledger:lock", 10)
await kv.cas_fenced(
"ledger:total",
"ledger:lock",
token,
new_total,
expect_version=entry.version,
)
await kv.release("ledger:lock", token)Key operations
| Verb | What it does |
|---|---|
kv(namespace) | Scope a keyed store |
set(key).json(v).ttl(d).send() | Write a value with an optional expiry |
set(..).commit() | Same write, returns the new version for a later CAS |
get(key) / get_typed(key) | Read a value, raw or decoded |
getEntry(key) | Read a value with its current version, for CAS |
expectVersion(v) / expect_absent() | Compare-and-set guards |
delete(key) / exists(key) / expire(key, ttl) | Remove, probe metadata, refresh expiry in place |
patch(key, patch) | Merge-patch a structured value without a full rewrite |
get_many(..) / delete_many() / scan() | Batch reads, selector deletes, paged iteration |
lease(key, ttl) / cas_fenced(..) / release(..) | Advisory lock plus fenced CAS, see above |
fork(id).create() | Open a copy-on-write branch, .severed() / .tables([...]) to snapshot |
put_row(table, partition, offset) | Write one speculative row, TypeScript: putRow |
query(index).fork(id) | Query the fork overlay without changing the trunk |
promote() | Merge a fork's writes onto the trunk |
squash() | Discard an already-open fork |
Running it
State runs on Laser Stack and LaserData Cloud. The example checks KV and fork capabilities separately.