LaserData Cloud
Laser SDK

State

Read and update keyed data, guard concurrent writes, and work with leases and forks

State provides key-value reads, guarded updates, expiry, and data branches beside the log. Each write keeps a recorded history. Forks let you test changes before applying them to the main data.

Built for

Use State for sessions, feature flags, and what-if simulations.

How it works

laser.kv(namespace) opens a store within one namespace. Each write also creates a log record. Replacing a value retains its history.

set(key) starts a write. Encode with .json(value) or .payload(bytes), add optional .ttl(duration) expiry, then call .send(). Read bytes with get(key) or decode a value with get_typed(key).

Compare-and-set rejects an update based on an outdated version:

  1. Read the value and version with getEntry(key).
  2. Supply expectVersion(version) on the next write.
  3. If the version changed, read again before trying another update.

expect_absent() permits a write only when the key is absent. Use guarded writes for counters or ledgers with concurrent writers.

Forks use copy-on-write branches, which store changes separately from the main rows. They do not redirect kv.set(..) automatically. Choose the branch behavior at creation:

  • Continuous, the default, receives later trunk appends while retaining its own writes.
  • Severed, selected with .severed(), captures the trunk at its current offsets. Later appends remain hidden. .tables([...]) selects tables, or an empty list captures all tables.

Fork operations follow this lifecycle:

  • fork(id).create() creates the branch. .parent(id) records audit lineage, but the data still branches from the trunk.
  • put_row(table, partition, offset) writes at an exact source position. Add .field(..), payload, embedding, metadata, or a tombstone. TypeScript uses putRow.
  • query(index).fork(id) reads the branch. Ordinary key-value reads and trunk queries exclude its changes before promotion.
  • promote() applies branch writes to the trunk and returns the row count.
  • squash() discards an open fork.
  • laser.forks() lists open forks and their metadata.

Write branch data through its fork handle. Ordinary key-value writes do not enter a hidden branch.

State requires laser-plane on Laser Stack or LaserData Cloud. Key-value operations, CAS, fenced leases, and forks have separate capabilities. Before using the fork examples, register and bind the profiles table through Views.

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()

TTL sets how long a value lives. TypeScript uses microseconds as bigint, Rust uses Duration, and Python uses seconds.

Complete examples: Rust, Python, and TypeScript.

Beyond get and set

All clients provide these operations, with snake_case in Rust and Python and camelCase in TypeScript:

  • delete(key) reports whether it removed a live entry. exists(key) returns metadata without the value.
  • expire(key, ttl) changes expiry without rewriting the value or changing its version. Omit TTL to clear expiry.
  • patch(key, patch) applies a codec-specific merge patch and returns the new version. For JSON values, use JSON merge-patch bytes.
  • copy_to(key, to_key) and move_to(key, to_key) copy or rename within a namespace. into_namespace(ns) selects another namespace.
  • get_many(keys) batches reads. delete_many() selects entries with .prefix(p), .range(start, end), or .key_contains(s). .send() returns the number removed.
  • scan() uses .prefix, .range, and .key_contains, with .limit(n) and .cursor(c). Finish with .fetch() for a page or .entries() for values.
  • laser.kv_namespaces() lists deployment namespaces.

.send() waits for completion and discards the version. .commit() returns the new version for a later expect_version(..).

Locks and fenced writes

A lease grants temporary ownership. A fencing token identifies the current grant so stale holders cannot keep writing. Each lease includes a holder, coordination namespace, lease key, token, and commit position. Acquire it with lease(key, holder, ttl).

Before planning a protected update, call get_entry_at_least(key, lease.position). This waits for the read view to include the lease grant. Supply both the coordination namespace and lease key on a fenced write. The write succeeds only while the lease remains held.

const kv = laser.kv("profiles")
const key = new TextEncoder().encode("user:42")
const lock = new TextEncoder().encode("lease:user:42")
const holder = "worker-a"
const lease = await kv.lease(lock, holder, 30_000_000n)
try {
  const entry = await kv.getEntryAtLeast(key, lease.position)
  if (entry === undefined) throw new Error("profile missing")
  await kv.casFenced(key, "profiles", lock, lease.token)
    .json({ plan: "enterprise" })
    .expectVersion(entry.version)
    .commit()
  await kv.renewLease(lock, holder, lease.token, 30_000_000n)
} finally {
  await kv.release(lock, holder, lease.token)
}
let kv = laser.kv("profiles");
let lease = kv
    .lease("lease:user:42", "worker-a", Duration::from_secs(30))
    .await?;
let entry = kv.get_entry_at_least("user:42", lease.position)
    .await?.expect("profile exists");
kv.cas_fenced("user:42", "profiles", "lease:user:42", lease.token)
    .json(&serde_json::json!({"plan": "enterprise"}))?
    .expect_version(entry.version)
    .commit()
    .await?;
kv.renew_lease("lease:user:42", "worker-a", lease.token, Duration::from_secs(30))
    .await?;
kv.release("lease:user:42", "worker-a", lease.token).await?;
import json

store = laser.kv("profiles")
lease = await store.lease("lease:user:42", "worker-a", 30)
try:
    entry = await store.get_entry_at_least("user:42", lease.position)
    await store.cas_fenced(
        "user:42", "profiles", "lease:user:42", lease.token,
        json.dumps({"plan": "enterprise"}).encode(),
        expect_version=entry.version,
    )
    await store.renew_lease("lease:user:42", "worker-a", lease.token, 30)
finally:
    await store.release("lease:user:42", "worker-a", lease.token)

The examples require an existing profile and fenced-lease support. Inspect capabilities.kv.fenced_leases in Rust, capabilities.kv.fencedLeases in TypeScript, or capabilities.kv_fenced_leases in Python.

Renewal keeps the same token. Release revokes it immediately. A stale holder's next write fails with lease-lost, even before another holder acquires the lease. Renew before expiry and stop protected effects if renewal fails. Give each independently running worker a unique holder identity.

Lease TTL, read consistency, and value TTL are separate. Lease durations use Duration in Rust, microseconds as bigint in TypeScript, and seconds in Python.

Key operations

VerbWhat 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, holder, ttl) / renew_lease(..) / cas_fenced(..) / release(..)Revocable holder-scoped lease and fenced CAS
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

Use Laser Stack or LaserData Cloud. The example tests key-value and fork capabilities separately.

On this page