A keyvalue interface that provides compare-and-swap (optimistic concurrency) for a single key.
This is kept separate from atomics so a host can grant or deny it independently, and so
backends that cannot perform conditional writes can decline just this capability. It is a
one-shot, resource-free API: each swap carries its own preconditions and reports the current
state back on conflict, so the read-modify-write retry loop needs no extra round trips.
Types
use resource bucket
from wasmcloud:keyvalue/types@0.2.0
A bucket is a collection of key-value pairs. Each key-value pair is stored as an entry in the bucket, and the bucket itself acts as a collection of all these entries.
It is worth noting that the exact terminology for bucket in key-value stores can vary depending on the specific implementation. For example:
- Amazon DynamoDB calls a collection of key-value pairs a table
- Redis has hashes, sets, and sorted sets as different types of collections
- Cassandra calls a collection of key-value pairs a column family
- MongoDB calls a collection of key-value pairs a collection
- Riak calls a collection of key-value pairs a bucket
- Memcached calls a collection of key-value pairs a slab
- Azure Cosmos DB calls a collection of key-value pairs a container
In this interface, we use the term bucket to refer to a collection of key-value pairs
get: async func(key: string) -> result<option<list<u8>>, error>Get the value associated with the specified
keyThe value is returned as an option. If the key-value pair exists in the store, it returns
ok(value). If the key does not exist in the store, it returnsok(none).If any other error occurs, it returns an
err(error).set: async func( key: string, value: list<u8>, options: option<set-options>, ) -> result<_, error>Set the value associated with the key in the store. If the key already exists in the store, it overwrites the value.
If the key does not exist in the store, it creates a new key-value pair.
optionscontrols expiry (ttl-ms) and conditional writes (if-not-exists); passnonefor the default overwrite-on-exists behavior with no expiry.If any other error occurs, it returns an
err(error).delete: async func(key: string) -> result<_, error>Delete the key-value pair associated with the key in the store.
If the key does not exist in the store, it does nothing.
If any other error occurs, it returns an
err(error).exists: async func(key: string) -> result<bool, error>Check if the key exists in the store.
If the key exists in the store, it returns
ok(true). If the key does not exist in the store, it returnsok(false).If any other error occurs, it returns an
err(error).list-keys: async func( prefix: option<string>, cursor: option<string>, ) -> result<key-response, error>Get the keys in the store, optionally restricted to those starting with
prefix, with an optional cursor (for use in pagination). It returns a list of keys. Please note that for most KeyValue implementations, this can be a very expensive operation and so it should be used judiciously. Implementations can return any number of keys in a single response, but they should never attempt to send more data than is reasonable (i.e. on a small edge device, this may only be a few KB, while on a large machine this could be several MB). Any response should also return a cursor that can be used to fetch the next page of keys. See thekey-responserecord for more information.prefixfilters the result to keys that begin with the given string; passnoneto list all keys. When paginating, the sameprefixshould be supplied alongside eachcursor.Note that the keys are not guaranteed to be returned in any particular order.
If the store is empty (or no keys match
prefix), it returns an empty list.MAY show an out-of-date list of keys if there are concurrent writes to the store.
If any error occurs, it returns an
err(error).
use variant error
from wasmcloud:keyvalue/types@0.2.0
The set of errors which may be raised by functions in this package.
Following the wasip3 convention, this enumerates specific named cases
rather than leaning on stringly-typed errors. It is intentionally
non-exhaustive: future versions may add cases, and generated bindings are
marked non-exhaustive accordingly, so consumers must always include a
catch-all arm. Backend-specific failures that do not map to a named case
are reported via other.
no-such-storeThe host does not recognize the store identifier requested.
access-deniedThe requesting component does not have access to the specified store (which may or may not exist).
invalid-argumentThe request was malformed or self-contradictory, e.g. a
cas.swapwhosecas-optionssets no precondition.precondition-failedA conditional operation did not meet its precondition, e.g. a
setwithif-not-existsagainst a key that already exists.timeoutThe operation did not complete within the host- or backend-imposed time limit.
store-unavailableThe store is currently unreachable or otherwise unavailable. The operation may succeed if retried later.
quota-exceededThe operation was rejected because a quota or rate limit was exceeded.
other:stringSome other implementation-specific error has occurred (e.g. I/O). Used for backend-specific failures that do not map to a named case above.
type version
An opaque, backend-defined version token for an entry: an etcd revision, an S3/HTTP ETag, a
Cosmos DB _etag, a monotonic counter, and so on. Treat it as opaque — only equality is
meaningful, and tokens are not ordered or comparable across keys or backends.
string
record cas-options
Preconditions for a swap. Every condition that is set must hold for the write to be
applied. At least one condition must be set: a swap with an empty cas-options is a
misuse and is rejected with error::invalid-argument (use store.set for an unconditional
write).
require-version:option<version>Require the entry's current version to equal this. This is the ABA-safe check: it detects a value that changed A -> B -> A, which a value comparison misses. Omit it if you do not care about ABA.
require-value:option<list<u8>>Require the entry's current value to equal this. A version-free optimistic check: simpler, but ABA-prone. Omit to skip.
variant cas-result
The outcome of a swap.
swappedThe new value was written.
stale:option<entry>A precondition did not hold; nothing was written. The current entry is returned (
noneif the key is absent) so the caller can recompute and retry without a separate read.
Functions
current: async func(
bucket: borrow<bucket>,
key: string,
) -> result<option<entry>, error>
Read a key's current value and version, for use as the precondition of a later swap.
Returns none if the key does not exist.
store.get deliberately omits the version; use this when you intend to do a
version-checked swap.
swap: async func(
bucket: borrow<bucket>,
key: string,
value: list<u8>,
options: cas-options,
) -> result<cas-result, error>
Conditionally and atomically set key to value if every precondition in options holds.
Returns cas-result::swapped on success, or cas-result::stale with the current entry if a
precondition fails. A lost race is the expected stale outcome, not an error. options
must set at least one precondition; an empty cas-options returns err(error::invalid-argument)
rather than performing an unconditional write. Other err(error) values are store-level
failures.
WIT
/// A keyvalue interface that provides compare-and-swap (optimistic concurrency) for a single key.
///
/// This is kept separate from `atomics` so a host can grant or deny it independently, and so
/// backends that cannot perform conditional writes can decline just this capability. It is a
/// one-shot, resource-free API: each `swap` carries its own preconditions and reports the current
/// state back on conflict, so the read-modify-write retry loop needs no extra round trips.
interface cas {
use types.{bucket, error};
/// An opaque, backend-defined version token for an entry: an etcd revision, an S3/HTTP ETag, a
/// Cosmos DB `_etag`, a monotonic counter, and so on. Treat it as opaque — only equality is
/// meaningful, and tokens are not ordered or comparable across keys or backends.
type version = string;
/// A key's value together with its current version.
record entry {
value: list<u8>,
version: version,
}
/// Preconditions for a `swap`. Every condition that is set must hold for the write to be
/// applied. At least one condition must be set: a `swap` with an empty `cas-options` is a
/// misuse and is rejected with `error::invalid-argument` (use `store.set` for an unconditional
/// write).
record cas-options {
/// Require the entry's current version to equal this. This is the ABA-safe check: it detects
/// a value that changed A -> B -> A, which a value comparison misses. Omit it if you do not
/// care about ABA.
require-version: option<version>,
/// Require the entry's current value to equal this. A version-free optimistic check: simpler,
/// but ABA-prone. Omit to skip.
require-value: option<list<u8>>,
}
/// The outcome of a `swap`.
variant cas-result {
/// The new value was written.
swapped,
/// A precondition did not hold; nothing was written. The current entry is returned (`none`
/// if the key is absent) so the caller can recompute and retry without a separate read.
stale(option<entry>),
}
/// Read a key's current value and version, for use as the precondition of a later `swap`.
/// Returns `none` if the key does not exist.
///
/// `store.get` deliberately omits the version; use this when you intend to do a
/// version-checked swap.
current: async func(bucket: borrow<bucket>, key: string) -> result<option<entry>, error>;
/// Conditionally and atomically set `key` to `value` if every precondition in `options` holds.
///
/// Returns `cas-result::swapped` on success, or `cas-result::stale` with the current entry if a
/// precondition fails. A lost race is the expected `stale` outcome, not an `error`. `options`
/// must set at least one precondition; an empty `cas-options` returns `err(error::invalid-argument)`
/// rather than performing an unconditional write. Other `err(error)` values are store-level
/// failures.
swap: async func(bucket: borrow<bucket>, key: string, value: list<u8>, options: cas-options) -> result<cas-result, error>;
}