Skip to content

Add optional list() operation to KvStore #498

@dahlia

Description

@dahlia

Motivation

The current KvStore interface provides basic key–value operations (get, set, delete) and an optional cas() for compare-and-swap. However, there is no way to enumerate keys matching a certain prefix, which limits the kinds of data structures that can be efficiently built on top of KvStore.

A concrete example is the distributed trace storage proposed in #497. To store trace data efficiently, we want to write each activity record under its own key (e.g., ["traces", traceId, spanId]) and later retrieve all records for a given trace by scanning keys with prefix ["traces", traceId]. Without a list() operation, the only alternative is to store all records for a trace as a single list value and use cas() for concurrent appends—this works but becomes inefficient as the list grows and may suffer from contention under high write loads.

Beyond trace storage, a list() operation would enable other use cases such as:

  • Enumerating all cached public keys for a given actor
  • Listing all pending outbox messages for debugging
  • Implementing cache invalidation by prefix

Most key–value store backends already support prefix scanning natively, so exposing this capability through the KvStore interface is straightforward.

Proposed interface

The list() method should be added as an optional method in Fedify 1.10.0 to maintain backward compatibility with existing KvStore implementations:

interface KvStore {
  get<T = unknown>(key: KvKey): Promise<T | undefined>;
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
  delete(key: KvKey): Promise<void>;

  // Optional (since 1.8.0)
  cas?: (
    key: KvKey,
    expectedValue: unknown,
    newValue: unknown,
    options?: KvStoreSetOptions,
  ) => Promise<boolean>;

  // Optional (since 1.10.0)
  list?: (
    options: KvStoreListOptions,
  ) => AsyncIterable<KvStoreListEntry>;
}

interface KvStoreListOptions {
  prefix: KvKey;
}

interface KvStoreListEntry {
  key: KvKey;
  value: unknown;
}

The method returns an AsyncIterable to support backends that may need to paginate results internally. Callers can use for await...of to iterate through entries:

for await (const entry of kv.list({ prefix: ["traces", traceId] })) {
  console.log(entry.key, entry.value);
}

Implementation for existing backends

Each official KvStore implementation should add support for list(). Most backends already provide native prefix scanning capabilities that can be directly leveraged:

  • MemoryKvStore: Filter in-memory keys by prefix
  • RedisKvStore: Use SCAN with a pattern
  • PostgresKvStore and SqliteKvStore: Use LIKE queries
  • DenoKvStore: Delegate to Deno KV's built-in list() method

The implementation should be straightforward for all backends since prefix scanning is a common operation supported by most storage systems.

Migration path

In Fedify 1.10.0, list() will be optional. Code that depends on list() should check for its presence and fall back to alternative approaches (such as cas()-based list management) when it is not available.

In Fedify 2.0.0, list() will become a required method on the KvStore interface. This gives KvStore implementers approximately one major version cycle to add support.

Related issues

This is a prerequisite for #497 (distributed trace storage for the debug dashboard).

Metadata

Metadata

Assignees

Projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions