Interfaces
types— Types shared across the keyvalue interfaces.atomics— A keyvalue interface that provides atomic operations.batch— A keyvalue interface that provides batch operations.cas— A keyvalue interface that provides compare-and-swap (optimistic concurrency) for a single key.store— A keyvalue interface that provides eventually consistent key-value operations.watcher— A keyvalue interface that provides event-driven notifications of changes to a bucket.
Worlds
imports
The wasmcloud:keyvalue/imports world provides common APIs for interacting with key-value
stores. Components targeting this world will be able to do:
- CRUD (create, read, update, delete) operations on key-value stores, with optional per-entry expiry and conditional writes.
- Atomic
incrementoperations, and compare-and-swap (CAS) via thecasinterface. - Batch operations that can reduce the number of round trips to the network.
watch-service
The wasmcloud:keyvalue/watch-service world is implemented by components that want to receive
change notifications for a bucket in addition to performing key-value operations themselves.
WIT
package wasmcloud:keyvalue@0.2.0;
/// Types shared across the keyvalue interfaces.
///
/// The `bucket` resource lives here (rather than in `store`) so that every
/// interface that operates on a bucket — `store`, `atomics`, `cas`, `batch`,
/// `watcher` — `use`s the *same* resource type. That matters for the
/// component-model `(implements ..)` mechanism: a host can route each labeled
/// `store` import to a different backend, and the bucket a guest opens through
/// one label is still accepted by the (unlabeled) `atomics`/`cas`/`batch`
/// interfaces, because they all reference this one `types.bucket`.
interface types {
/// 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`.
variant error {
/// The host does not recognize the store identifier requested.
no-such-store,
/// The requesting component does not have access to the specified store
/// (which may or may not exist).
access-denied,
/// The request was malformed or self-contradictory, e.g. a `cas.swap`
/// whose `cas-options` sets no precondition.
invalid-argument,
/// A conditional operation did not meet its precondition, e.g. a `set` with
/// `if-not-exists` against a key that already exists.
precondition-failed,
/// The operation did not complete within the host- or backend-imposed time
/// limit.
timeout,
/// The store is currently unreachable or otherwise unavailable. The
/// operation may succeed if retried later.
store-unavailable,
/// The operation was rejected because a quota or rate limit was exceeded.
quota-exceeded,
/// Some other implementation-specific error has occurred (e.g. I/O). Used
/// for backend-specific failures that do not map to a named case above.
other(string),
}
/// A response to a `list-keys` operation.
record key-response {
/// The list of keys returned by the query.
keys: list<string>,
/// The continuation token to use to fetch the next page of keys. If this is `none`, then
/// there are no more keys to fetch.
cursor: option<string>,
}
/// Options that modify the behavior of a `set` operation.
record set-options {
/// If set, the entry expires this many milliseconds after the write, after
/// which the host removes it. `none` stores the entry without expiry.
///
/// Hosts whose backend cannot honor expiry MUST raise an error rather than
/// silently ignoring the TTL.
ttl-ms: option<u64>,
/// When true, the write succeeds only if the key does not already exist,
/// failing with `error::precondition-failed` otherwise. Useful for locks
/// and idempotent initialization.
if-not-exists: bool,
}
/// 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:
///
/// 1. Amazon DynamoDB calls a collection of key-value pairs a table
/// 2. Redis has hashes, sets, and sorted sets as different types of collections
/// 3. Cassandra calls a collection of key-value pairs a column family
/// 4. MongoDB calls a collection of key-value pairs a collection
/// 5. Riak calls a collection of key-value pairs a bucket
/// 6. Memcached calls a collection of key-value pairs a slab
/// 7. 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
resource bucket {
/// Get the value associated with the specified `key`
///
/// The 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 returns `ok(none)`.
///
/// If any other error occurs, it returns an `err(error)`.
get: async func(key: string) -> result<option<list<u8>>, 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.
///
/// `options` controls expiry (`ttl-ms`) and conditional writes
/// (`if-not-exists`); pass `none` for the default overwrite-on-exists
/// behavior with no expiry.
///
/// If any other error occurs, it returns an `err(error)`.
set: async func(key: string, value: list<u8>, options: option<set-options>) -> 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)`.
delete: async func(key: string) -> result<_, 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 returns `ok(false)`.
///
/// If any other error occurs, it returns an `err(error)`.
exists: async func(key: string) -> result<bool, 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 the `key-response` record
/// for more information.
///
/// `prefix` filters the result to keys that begin with the given string; pass
/// `none` to list all keys. When paginating, the same `prefix` should be
/// supplied alongside each `cursor`.
///
/// 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)`.
list-keys: async func(prefix: option<string>, cursor: option<string>) -> result<key-response, error>;
}
}
/// A keyvalue interface that provides atomic operations.
///
/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to
/// fail, it will appear to the invoker of the atomic operation that the action either completed
/// successfully or did nothing at all.
interface atomics {
use types.{bucket, error};
/// Atomically increment the value associated with the key in the store by the given (signed)
/// delta, returning the new value. A negative delta decrements, where decrement is just a
/// negative increment, rather than `wasi:keyvalue`'s unsigned-only counter.
///
/// If the key does not exist in the store, it creates a new key-value pair with the value set
/// to the given delta.
///
/// If any other error occurs, it returns an `err(error)`.
increment: async func(bucket: borrow<bucket>, key: string, delta: s64) -> result<s64, error>;
}
/// A keyvalue interface that provides batch operations.
///
/// A batch operation is an operation that operates on multiple keys at once.
///
/// Batch operations are useful for reducing network round-trip time. For example, if you want to
/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1
/// batch get operation. The batch operation is faster because it only needs to make 1 network call
/// instead of 100.
///
/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some
/// of the keys may have been modified and some may not.
///
/// This interface has the same consistency guarantees as the `store` interface, meaning that you
/// should be able to "read your writes."
interface batch {
use types.{bucket, error};
/// Get the key-value pairs associated with the keys in the store. It returns a list of
/// key-value pairs.
///
/// If any of the keys do not exist in the store, it returns a `none` value for that pair in the
/// list.
///
/// MAY show an out-of-date value if there are concurrent writes to the store.
///
/// If any other error occurs, it returns an `err(error)`.
get-many: async func(bucket: borrow<bucket>, keys: list<string>) -> result<list<option<tuple<string, list<u8>>>>, error>;
/// Set the values associated with the keys in the store. If the key already exists in the
/// store, it overwrites the value.
///
/// Note that the key-value pairs are not guaranteed to be set in the order they are provided.
///
/// If any of the keys do not exist in the store, it creates a new key-value pair.
///
/// If any other error occurs, it returns an `err(error)`. When an error occurs, it does not
/// rollback the key-value pairs that were already set. Thus, this batch operation does not
/// guarantee atomicity, implying that some key-value pairs could be set while others might
/// fail.
///
/// Other concurrent operations may also be able to see the partial results.
set-many: async func(bucket: borrow<bucket>, key-values: list<tuple<string, list<u8>>>) -> result<_, error>;
/// Delete the key-value pairs associated with the keys in the store.
///
/// Note that the key-value pairs are not guaranteed to be deleted in the order they are
/// provided.
///
/// If any of the keys do not exist in the store, it skips the key.
///
/// If any other error occurs, it returns an `err(error)`. When an error occurs, it does not
/// rollback the key-value pairs that were already deleted. Thus, this batch operation does not
/// guarantee atomicity, implying that some key-value pairs could be deleted while others might
/// fail.
///
/// Other concurrent operations may also be able to see the partial results.
delete-many: async func(bucket: borrow<bucket>, keys: list<string>) -> result<_, error>;
}
/// 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>;
}
/// A keyvalue interface that provides eventually consistent key-value operations.
///
/// Each of these operations acts on a single key-value pair.
///
/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is
/// the common denominator for all data types defined by different key-value stores to handle data,
/// ensuring compatibility between different key-value stores. Note: the clients will be expecting
/// serialization/deserialization overhead to be handled by the key-value store. The value could be
/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects.
///
/// Data consistency in a key value store refers to the guarantee that once a write operation
/// completes, all subsequent read operations will return the value that was written.
///
/// Any implementation of this interface must have enough consistency to guarantee "reading your
/// writes." In particular, this means that the client should never get a value that is older than
/// the one it wrote, but it MAY get a newer value if one was written around the same time. These
/// guarantees only apply to the same client (which will likely be provided by the host or an
/// external capability of some kind). In this context a "client" is referring to the caller or
/// guest that is consuming this interface. Once a write request is committed by a specific client,
/// all subsequent read requests by the same client will reflect that write or any subsequent
/// writes. Another client running in a different context may or may not immediately see the result
/// due to the replication lag.
///
/// Unlike `wasi:keyvalue@0.2.0-draft`, every operation that touches a backend is an `async func`.
/// This lets a single component issue many in-flight key-value operations concurrently without the
/// `wasi:io/poll` ceremony required under wasip2.
interface store {
use types.{bucket, error};
/// Get the bucket with the specified identifier.
///
/// `identifier` must refer to a bucket provided by the host.
///
/// `error::no-such-store` will be raised if the `identifier` is not recognized.
open: async func(identifier: string) -> result<bucket, error>;
}
/// A keyvalue interface that provides event-driven notifications of changes to a bucket.
///
/// This interface is exported by a component that wants to react to key-value changes, and the
/// callbacks are delivered by the host as those changes occur.
interface watcher {
use types.{bucket};
/// Handle the `set` event for the given bucket and key. It includes a handle to the `bucket`
/// that can be used to interact with the store.
on-set: async func(bucket: bucket, key: string, value: list<u8>);
/// Handle the `delete` event for the given bucket and key. It includes a handle to the
/// `bucket` that can be used to interact with the store.
on-delete: async func(bucket: bucket, key: string);
}
/// The `wasmcloud:keyvalue/imports` world provides common APIs for interacting with key-value
/// stores. Components targeting this world will be able to do:
///
/// 1. CRUD (create, read, update, delete) operations on key-value stores, with optional
/// per-entry expiry and conditional writes.
/// 2. Atomic `increment` operations, and compare-and-swap (CAS) via the `cas` interface.
/// 3. Batch operations that can reduce the number of round trips to the network.
world imports {
import types;
import store;
import atomics;
import cas;
import batch;
}
/// The `wasmcloud:keyvalue/watch-service` world is implemented by components that want to receive
/// change notifications for a bucket in addition to performing key-value operations themselves.
world watch-service {
import types;
import store;
import atomics;
import cas;
import batch;
export watcher;
}