componentized/directory

wasmcloud:keyvalue

interface dependency

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:

  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.

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;
}