cloudflare-kv - feat: Add Cloudflare Workers KV storage adapter - #1985
Conversation
Adds @keyv/cloudflare-kv, a storage adapter backed by Cloudflare Workers
KV. It works with a native Worker KV binding (or a Miniflare namespace) and
with the Cloudflare REST API from plain Node.js via account credentials.
Highlights:
- Dual mode: pass a `kvNamespace` binding, or `accountId`/`namespaceId`/
`apiToken` REST credentials (built-in CloudflareKVRestClient).
- Values stored as a JSON envelope `{ value, expires }`; expiry enforced
client-side for millisecond precision, with a native KV `expiration` set
for TTLs >= 60s so Cloudflare reclaims space on its own.
- Namespacing, batch ops (get/set/has/deleteMany), and a paginated async
iterator, mirroring the other first-party adapters.
- Fully testable locally with Miniflare (no Cloudflare account required);
116 tests at 100% coverage using @keyv/test-suite compliance suites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new Cloudflare Workers KV storage adapter (@keyv/cloudflare-kv) for Keyv, supporting both native Worker bindings and the Cloudflare REST API. The feedback highlights three high-severity performance and resource utilization issues in storage/cloudflare-kv/src/index.ts. Specifically, the clear() method should delete keys in batches to prevent memory and rate-limiting issues, and both get() and has() should delete expired keys directly via this._client.delete() to avoid redundant get requests and reduce latency.
Addresses code-review feedback: - clear() now deletes keys in bounded batches (configurable via clearBatchSize, default 100) instead of buffering every key and firing unbounded concurrent deletes, avoiding socket exhaustion and KV rate limits on large namespaces. - get() and has() delete expired keys directly via the underlying client rather than this.delete(), which performed a redundant read to confirm existence we already had. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1985 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 53 55 +2
Lines 4665 4887 +222
Branches 725 779 +54
==========================================
+ Hits 4665 4887 +222 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d02aed9331
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A TTL in the 59-60s range (or exactly 60000ms) rounded up via Math.ceil to ttlSeconds === 60 and attached a native absolute `expiration`. After request latency or host clock skew that absolute timestamp could fall under Cloudflare KV's 60-second minimum, so the put was rejected and set() returned false instead of storing the entry with client-side expiry. Fix: derive the TTL from the raw millisecond delta with Math.floor and only attach a native expiry when it is strictly greater than 60s (smallest value sent is 61s). Use the relative `expirationTtl`, which KV evaluates against its own clock, so a slow host clock can't push the deadline below the minimum. Client-side expiry continues to enforce the exact deadline on read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
jaredwray
left a comment
There was a problem hiding this comment.
I also dont see iterator on this or capabilities?
…e tests
Reworks the adapter per review feedback and adds an explicit mode option.
- Storage: stop wrapping values in an adapter-level JSON envelope. Keyv owns
serialization, so the (already-serialized) value string is stored in KV
verbatim and the absolute expiry is kept in KV metadata, enforced
client-side for millisecond-precise TTLs.
- Modes: add a `mode` option ('bind' | 'rest', default 'bind') plus a `.mode`
getter. 'bind' uses a native KV binding (Worker env or Miniflare); 'rest'
uses the Cloudflare REST API from plain Node.js. Mode is inferred when
omitted.
- Tests now run against a real local Cloudflare KV: 'bind' against Miniflare
directly and 'rest' against a local HTTP bridge that implements the
Cloudflare REST API on top of the same Miniflare namespace (no mocked
fetch). Storage compliance suite runs against both paths. 154 tests, 100%
coverage.
- Add a scheduled `cloudflare-kv-live` GitHub workflow + live integration
test that exercise the real Cloudflare KV REST API (self-skips without
secrets).
- README: document bind vs rest modes, the metadata storage model, the local
emulation/bridge, and the live workflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
…esponse CodeQL flagged the local REST test bridge writing the raw error (potentially a stack trace) into the HTTP response. Return a generic message instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
…p cron Trigger the live integration workflow on pull requests and pushes to main that touch storage/cloudflare-kv (plus manual dispatch) instead of a weekly cron. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
Covers setMany/getMany/hasMany/deleteMany against the real Cloudflare KV REST API alongside the existing single-key, TTL, and iterator live checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
Rename the workflow file, name, job, self-path filter, and doc/test references from cloudflare-kv-live to cloudflare-keyv-integration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hhM6Rqsdg9sao9QYNZbX7
Summary
Adds
@keyv/cloudflare-kv, a new first-party storage adapter backed by Cloudflare Workers KV. It works in two modes:env.MY_KV) or a Miniflare-created namespace directly.accountId/namespaceId/apiTokenand the adapter talks to the Cloudflare REST API from any Node.js process (via a built-inCloudflareKVRestClient).The adapter mirrors the conventions of the other first-party adapters (modeled closely on
@keyv/dynamo):Hookifiedbase,capabilities.expiresv6 contract, namespace/key-prefix helpers,createKeyvhelper, batch ops, and a paginated asynciterator.Design notes / best practices
{ value, expires }.expirationis also attached for TTLs ≥ 60s so Cloudflare reclaims space on its own.deletesemantics:delete/deleteManyreturn whether the key actually existed.Testing locally — no Cloudflare account required
Tests run entirely against Miniflare, the official local Workers runtime — no Docker and no credentials needed. The REST client is covered with mocked
fetch.@keyv/test-suitecompliance suites (keyvTestSuite,keyvIteratorTests,storageTestSuite) plus adapter-specific tests.Other changes
pnpm-workspace.yaml: allow theworkerdbuild (used by Miniflare); make the surfacedsharpdecision explicit (false, preserving prior behavior).README.mdandwebsite/site/docs/index.md: list the new adapter.Notes for reviewers
scripts/docker-compose*. CI picks the package up automatically viapnpm -r test:ci.miniflareis a dev-only dependency; runtime deps are justhookified(+keyvpeer).🤖 Generated with Claude Code
Generated by Claude Code