Skip to content

Conversation

@dahlia
Copy link
Member

@dahlia dahlia commented Dec 22, 2025

Summary

This PR adds an optional list() method to the KvStore interface for enumerating entries by key prefix, as proposed in #498.

Closes #498.

Changes from the original proposal

The original proposal suggested:

list?: (options: KvStoreListOptions) => AsyncIterable<KvStoreListEntry>;

interface KvStoreListOptions {
  prefix: KvKey;
}

However, during implementation, I simplified the API to:

list?(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;

Key differences:

  1. Removed KvStoreListOptions interface: Since the options object only contained a single prefix property, passing the prefix directly as an argument is simpler and more ergonomic.

  2. Made prefix optional: When prefix is omitted or an empty array, list() returns all entries in the store. This is useful for debugging and administrative purposes.

Implementations

Implemented list() in all official KvStore backends:

Backend Strategy
MemoryKvStore Filter in-memory keys by prefix
SqliteKvStore LIKE query with JSON key pattern
PostgresKvStore Array slice comparison (key[1:n] = prefix)
RedisKvStore SCAN with pattern matching + key deserialization
DenoKvStore Delegate to Deno KV's built-in list()
WorkersKvStore KV list() with JSON key prefix pattern

Bonus: Cloudflare Workers testing infrastructure

While implementing WorkersKvStore.list(), I noticed that the existing tests in @fedify/cfworkers were completely skipped (they checked for navigator.userAgent === "Cloudflare-Workers").

I set up proper testing using Vitest with @cloudflare/vitest-pool-workers, which runs tests in a simulated Cloudflare Workers runtime environment. This includes:

  • wrangler.jsonc with KV namespace and Queue bindings
  • vitest.config.ts with Workers pool configuration
  • Separate test files for WorkersKvStore and WorkersMessageQueue
  • Mock Temporal.Duration objects (Workers runtime doesn't support Temporal)

Test plan

  • MemoryKvStore.list() tests
  • SqliteKvStore.list() tests
  • PostgresKvStore.list() tests (requires PostgreSQL)
  • RedisKvStore.list() tests (requires Redis)
  • DenoKvStore.list() tests
  • WorkersKvStore.list() tests
  • Documentation updated in docs/manual/kv.md
  • CHANGES.md updated

dahlia and others added 10 commits December 23, 2025 00:45
Add an optional list() method to the KvStore interface for enumerating
entries by key prefix.  This enables efficient prefix scanning which is
useful for implementing features like distributed trace storage, cache
invalidation by prefix, and listing related entries.

- Add KvStoreListOptions interface
- Add KvStoreListEntry interface
- Add list() method to KvStore interface
- Implement list() in MemoryKvStore

Resolves fedify-dev#498

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Implement the list() method for prefix scanning in SqliteKvStore.
Uses SQL LIKE pattern matching on JSON-encoded keys.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Change the list() method signature from list(options: KvStoreListOptions)
to list(prefix?: KvKey) for a cleaner API. The KvStoreListOptions interface
is removed as it only contained a single prefix field.

When prefix is omitted or undefined, the method returns all entries in the
store.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This replaces the previous test file that was always skipped with
actual working tests that run in a simulated Cloudflare Workers
runtime environment using @cloudflare/vitest-pool-workers.

Changes:
- Add wrangler.jsonc with KV and Queue bindings for testing
- Add vitest.config.ts with Workers pool configuration
- Create test/env.d.ts for TypeScript type declarations
- Create test/kv.test.ts with WorkersKvStore tests
- Create test/mq.test.ts with WorkersMessageQueue tests
- Update package.json with Vitest and Cloudflare testing dependencies
- Update deno.json to remove old test configuration
- Update @cloudflare/workers-types to ^4.20250906.0 in catalog

Note: Tests use mock Temporal.Duration objects since Cloudflare Workers
doesn't support the native Temporal API.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @dahlia, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a powerful new list() method to the KvStore interface, allowing for efficient prefix-based enumeration of key-value store entries. This enhancement is crucial for features requiring batch operations or iteration over related data. Alongside this core functionality, the PR also significantly improves the testing setup for Cloudflare Workers, ensuring reliability and maintainability of the WorkersKvStore implementation.

Highlights

  • KvStore Interface Enhancement: An optional list() method has been added to the KvStore interface, enabling enumeration of entries by key prefix. This method simplifies to list?(prefix?: KvKey): AsyncIterable<KvStoreListEntry>, making the prefix optional and removing a redundant options interface.
  • Comprehensive Backend Implementations: The new list() method has been implemented across all official KvStore backends, including MemoryKvStore, SqliteKvStore, PostgresKvStore, RedisKvStore, DenoKvStore, and WorkersKvStore, each utilizing their native capabilities for efficient prefix scanning.
  • Cloudflare Workers Testing Overhaul: The testing infrastructure for @fedify/cfworkers has been significantly upgraded. Existing skipped tests were replaced with proper Vitest integration using @cloudflare/vitest-pool-workers, providing a simulated Cloudflare Workers runtime environment for more robust testing.
  • Documentation and Changelog Updates: The manual (docs/manual/kv.md) has been updated to reflect the new list() method, including usage examples and implementation tips. The CHANGES.md file also details the addition and its availability since Fedify 1.10.0.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable list() method to the KvStore interface, enabling prefix-based enumeration of entries. The feature is consistently implemented across all supported backends, including Memory, SQLite, PostgreSQL, Redis, Deno KV, and Cloudflare Workers KV. A significant and commendable part of this work is the establishment of a robust testing infrastructure for Cloudflare Workers using Vitest, which replaces previously skipped tests. The documentation has also been updated thoroughly. My review focuses on improving code maintainability by reducing duplication in the new list() implementations for the Cloudflare Workers and Redis stores. Overall, this is a high-quality contribution that enhances the library's capabilities and testability.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an optional list() method to the KvStore interface for enumerating entries by key prefix, addressing issue #498. The implementation includes support across all official KvStore backends and sets up proper testing infrastructure for Cloudflare Workers.

Key changes:

  • Added KvStoreListEntry interface and optional list() method to KvStore
  • Implemented list() in all backends: MemoryKvStore, SqliteKvStore, PostgresKvStore, RedisKvStore, DenoKvStore, and WorkersKvStore
  • Set up Cloudflare Workers testing with Vitest and @cloudflare/vitest-pool-workers

Reviewed changes

Copilot reviewed 22 out of 25 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/fedify/src/federation/kv.ts Added KvStoreListEntry interface and list() method to KvStore interface; implemented in MemoryKvStore
packages/fedify/src/federation/kv.test.ts Added comprehensive tests for MemoryKvStore.list() including prefix filtering, expired entries, and empty prefix
packages/sqlite/src/kv.ts Implemented list() using SQL LIKE pattern matching with JSON key encoding
packages/sqlite/src/kv.test.ts Added tests for SqliteKvStore.list() with various scenarios
packages/postgres/src/kv.ts Implemented list() using PostgreSQL array slicing for prefix matching
packages/postgres/src/kv.test.ts Added tests for PostgresKvStore.list()
packages/redis/src/kv.ts Implemented list() using Redis SCAN with pattern matching and key deserialization
packages/redis/src/kv.test.ts Added tests for RedisKvStore.list()
packages/denokv/src/mod.ts Implemented list() delegating to Deno KV's built-in list method
packages/denokv/src/mod.test.ts Added tests for DenoKvStore.list()
packages/cfworkers/src/mod.ts Implemented list() using Cloudflare Workers KV list API with JSON pattern matching
packages/cfworkers/test/kv.test.ts New test file for WorkersKvStore using Vitest
packages/cfworkers/test/mq.test.ts New test file for WorkersMessageQueue using Vitest
packages/cfworkers/vitest.config.ts Vitest configuration for Cloudflare Workers pool
packages/cfworkers/wrangler.jsonc Wrangler configuration for test bindings
packages/cfworkers/package.json Updated test script to use Vitest, added testing dependencies
docs/manual/kv.md Updated documentation with list() method usage and implementation guide
CHANGES.md Documented new list() method across all packages
pnpm-lock.yaml, deno.lock Updated dependencies including @cloudflare/workers-types and Vitest packages
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

@codecov
Copy link

codecov bot commented Dec 22, 2025

Codecov Report

❌ Patch coverage is 93.28358% with 9 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/redis/src/kv.ts 86.00% 7 Missing ⚠️
packages/denokv/src/mod.ts 95.00% 1 Missing ⚠️
packages/fedify/src/federation/kv.ts 93.75% 1 Missing ⚠️
Files with missing lines Coverage Δ
packages/postgres/src/kv.ts 96.90% <100.00%> (+0.90%) ⬆️
packages/sqlite/src/kv.ts 94.14% <100.00%> (+2.79%) ⬆️
packages/denokv/src/mod.ts 81.73% <95.00%> (+3.15%) ⬆️
packages/fedify/src/federation/kv.ts 86.07% <93.75%> (+1.94%) ⬆️
packages/redis/src/kv.ts 80.80% <86.00%> (+5.29%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This test verifies that when an exact prefix key exists alongside child
keys, it is yielded exactly once (not duplicated or missing).

Note: Deno KV's native list() does NOT include exact prefix matches,
only strictly longer keys. The explicit check in DenoKvStore.list() is
necessary to include the exact prefix key in results.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@dahlia dahlia changed the title Add list() method to KvStore interface Add list() method to KvStore interface Dec 22, 2025
Copy link
Contributor

@2chanhaeng 2chanhaeng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of KV store integration test code looks similar. So, how about make it into a function and export it from @fedify/fedify, then import and use it for testing in each package's test code?

@dahlia
Copy link
Member Author

dahlia commented Dec 23, 2025

@2chanhaeng @sij411 All of your review comments are now addressed. Could you take a look again?

@2chanhaeng
Copy link
Contributor

LGTM!

Copy link
Contributor

@sij411 sij411 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me :)

@dahlia dahlia merged commit 378e7ac into fedify-dev:main Dec 23, 2025
20 of 21 checks passed
* @since 0.5.0
*/
export class MemoryKvStore implements KvStore {
#values: Record<string, [unknown, null | Temporal.Instant]> = {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curiosity) may I ask why did you use Record<...> instead of Map<K, V> (or some containers that supports hash comparison)? that would prevent you to use type conversion when loop through #values.

It would be like this:

#values = new KeyValueContainer<KvKey, [...]>(/* comparator if needed... */)

and then:

  async *list(prefix?: KvKey): AsyncIterable<KvStoreListEntry> {
    const now = Temporal.Now.instant();
    for (const [key, entry] of Object.entries(this.#values)) {
	//          ^^^ Now type conversion is not needed \o/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think Map approach just hides type conversions with extra classes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean type conversions not appearing inside of that for loop, but gonna happen in another file anyway. It's just my intuitions tho.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! The reason I used Record<string, ...> with JSON-encoded keys instead of Map<KvKey, ...> is that JavaScript's Map uses SameValueZero algorithm for key comparison, which is essentially === (strict equality) for objects. Since KvKey is an array (KvKeyComponent[]), two arrays with identical contents are still considered different objects:

const key1 = ["user", "123"];
const key2 = ["user", "123"];
console.log(key1 === key2); // false - different references

const map = new Map();
map.set(key1, "value");
map.get(key2); // undefined - key2 is treated as a different key

This means if we used Map<KvKey, ...>, we wouldn't be able to retrieve or delete values using a different array instance with the same contents. By serializing keys to JSON strings, we get value-based equality comparison that works correctly for our use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add optional list() operation to KvStore

4 participants