Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "cloudflare-kv",
|
||||||
|
"description": "Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Jeremy Dawes",
|
||||||
|
"email": "jeremy@jezweb.net"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# cloudflare-kv
|
||||||
|
|
||||||
|
Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.
|
||||||
429
SKILL.md
Normal file
429
SKILL.md
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
---
|
||||||
|
name: cloudflare-kv
|
||||||
|
description: |
|
||||||
|
Store key-value data globally with Cloudflare KV's edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.
|
||||||
|
license: MIT
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cloudflare Workers KV
|
||||||
|
|
||||||
|
**Status**: Production Ready ✅
|
||||||
|
**Last Updated**: 2025-11-24
|
||||||
|
**Dependencies**: cloudflare-worker-base (for Worker setup)
|
||||||
|
**Latest Versions**: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0
|
||||||
|
|
||||||
|
**Recent Updates (2025)**:
|
||||||
|
- **August 2025**: Architecture redesign (40x performance gain, <5ms p99 latency, hybrid storage with R2)
|
||||||
|
- **January 2025**: Namespace limit increased (200 → 1,000 namespaces per account for Free and Paid plans)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start (5 Minutes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create namespace
|
||||||
|
npx wrangler kv namespace create MY_NAMESPACE
|
||||||
|
# Output: [[kv_namespaces]] binding = "MY_NAMESPACE" id = "<UUID>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**wrangler.jsonc:**
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"kv_namespaces": [{
|
||||||
|
"binding": "MY_NAMESPACE", // Access as env.MY_NAMESPACE
|
||||||
|
"id": "<production-uuid>",
|
||||||
|
"preview_id": "<preview-uuid>" // Optional: local dev
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Basic Usage:**
|
||||||
|
```typescript
|
||||||
|
type Bindings = { MY_NAMESPACE: KVNamespace };
|
||||||
|
|
||||||
|
app.post('/set/:key', async (c) => {
|
||||||
|
await c.env.MY_NAMESPACE.put(c.req.param('key'), await c.req.text());
|
||||||
|
return c.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/get/:key', async (c) => {
|
||||||
|
const value = await c.env.MY_NAMESPACE.get(c.req.param('key'));
|
||||||
|
return value ? c.json({ value }) : c.json({ error: 'Not found' }, 404);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KV API Reference
|
||||||
|
|
||||||
|
### Read Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get single key
|
||||||
|
const value = await env.MY_KV.get('key'); // string | null
|
||||||
|
const data = await env.MY_KV.get('key', { type: 'json' }); // object | null
|
||||||
|
const buffer = await env.MY_KV.get('key', { type: 'arrayBuffer' });
|
||||||
|
const stream = await env.MY_KV.get('key', { type: 'stream' });
|
||||||
|
|
||||||
|
// Get with cache (minimum 60s)
|
||||||
|
const value = await env.MY_KV.get('key', { cacheTtl: 300 }); // 5 min edge cache
|
||||||
|
|
||||||
|
// Bulk read (counts as 1 operation)
|
||||||
|
const values = await env.MY_KV.get(['key1', 'key2']); // Map<string, string | null>
|
||||||
|
|
||||||
|
// With metadata
|
||||||
|
const { value, metadata } = await env.MY_KV.getWithMetadata('key');
|
||||||
|
const result = await env.MY_KV.getWithMetadata(['key1', 'key2']); // Bulk with metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
### Write Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic write (max 1/second per key)
|
||||||
|
await env.MY_KV.put('key', 'value');
|
||||||
|
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
|
||||||
|
|
||||||
|
// With expiration
|
||||||
|
await env.MY_KV.put('session', data, { expirationTtl: 3600 }); // 1 hour
|
||||||
|
await env.MY_KV.put('token', value, { expiration: Math.floor(Date.now()/1000) + 86400 });
|
||||||
|
|
||||||
|
// With metadata (max 1024 bytes)
|
||||||
|
await env.MY_KV.put('config', 'dark', {
|
||||||
|
metadata: { updatedAt: Date.now(), version: 2 }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical Limits:**
|
||||||
|
- Key: 512 bytes max
|
||||||
|
- Value: 25 MiB max
|
||||||
|
- Metadata: 1024 bytes max
|
||||||
|
- Write rate: 1/second per key (429 error if exceeded)
|
||||||
|
- Expiration: 60 seconds minimum
|
||||||
|
|
||||||
|
### List Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List with pagination
|
||||||
|
const result = await env.MY_KV.list({ prefix: 'user:', limit: 1000, cursor });
|
||||||
|
// result: { keys: [], list_complete: boolean, cursor?: string }
|
||||||
|
|
||||||
|
// CRITICAL: Always check list_complete, not keys.length === 0
|
||||||
|
let cursor: string | undefined;
|
||||||
|
do {
|
||||||
|
const result = await env.MY_KV.list({ prefix: 'user:', cursor });
|
||||||
|
processKeys(result.keys);
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Delete single key
|
||||||
|
await env.MY_KV.delete('key'); // Always succeeds
|
||||||
|
|
||||||
|
// Bulk delete (CLI only, up to 10,000 keys)
|
||||||
|
// npx wrangler kv bulk delete --binding=MY_KV keys.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Caching Pattern with CacheTtl
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function getCachedData(kv: KVNamespace, key: string, fetchFn: () => Promise<any>, ttl = 300) {
|
||||||
|
const cached = await kv.get(key, { type: 'json', cacheTtl: ttl });
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const data = await fetchFn();
|
||||||
|
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl * 2 });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines**: Minimum 60s, use for read-heavy workloads (100:1 read/write ratio)
|
||||||
|
|
||||||
|
### Metadata Optimization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Store small values (<1024 bytes) in metadata to avoid separate get() calls
|
||||||
|
await env.MY_KV.put('user:123', '', {
|
||||||
|
metadata: { status: 'active', plan: 'pro', lastSeen: Date.now() }
|
||||||
|
});
|
||||||
|
|
||||||
|
// list() returns metadata automatically (no additional get() calls)
|
||||||
|
const users = await env.MY_KV.list({ prefix: 'user:' });
|
||||||
|
users.keys.forEach(({ name, metadata }) => console.log(name, metadata.status));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Coalescing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad: Many cold keys
|
||||||
|
await kv.put('user:123:name', 'John');
|
||||||
|
await kv.put('user:123:email', 'john@example.com');
|
||||||
|
|
||||||
|
// ✅ Good: Single hot key
|
||||||
|
await kv.put('user:123', JSON.stringify({ name: 'John', email: 'john@example.com' }));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefit**: Cold keys benefit from hot key caching, fewer operations
|
||||||
|
**Trade-off**: Requires read-modify-write for updates
|
||||||
|
|
||||||
|
### Pagination Helper
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function* paginateKV(kv: KVNamespace, options: { prefix?: string } = {}) {
|
||||||
|
let cursor: string | undefined;
|
||||||
|
do {
|
||||||
|
const result = await kv.list({ ...options, cursor });
|
||||||
|
yield result.keys;
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
for await (const keys of paginateKV(env.MY_KV, { prefix: 'user:' })) {
|
||||||
|
processKeys(keys);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limit Retry with Exponential Backoff
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function putWithRetry(kv: KVNamespace, key: string, value: string, opts?: KVPutOptions) {
|
||||||
|
let attempts = 0, delay = 1000;
|
||||||
|
while (attempts < 5) {
|
||||||
|
try {
|
||||||
|
await kv.put(key, value, opts);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).message.includes('429')) {
|
||||||
|
attempts++;
|
||||||
|
if (attempts >= 5) throw new Error('Max retry attempts');
|
||||||
|
await new Promise(r => setTimeout(r, delay));
|
||||||
|
delay *= 2; // Exponential backoff
|
||||||
|
} else throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Understanding Eventual Consistency
|
||||||
|
|
||||||
|
KV is **eventually consistent** across Cloudflare's global network (Aug 2025 redesign: hybrid storage, <5ms p99 latency):
|
||||||
|
|
||||||
|
**How It Works:**
|
||||||
|
1. Writes immediately visible in same location
|
||||||
|
2. Other locations see update within ~60 seconds (or cacheTtl value)
|
||||||
|
3. Cached reads may return stale data during propagation
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```typescript
|
||||||
|
// Tokyo: Write
|
||||||
|
await env.MY_KV.put('counter', '1');
|
||||||
|
const value = await env.MY_KV.get('counter'); // "1" ✅
|
||||||
|
|
||||||
|
// London (within 60s): May be stale ⚠️
|
||||||
|
const value2 = await env.MY_KV.get('counter'); // Might be old value
|
||||||
|
|
||||||
|
// After 60+ seconds: Consistent ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use KV for**: Read-heavy workloads (100:1 ratio), config, feature flags, caching, user preferences
|
||||||
|
**Don't use KV for**: Financial transactions, strong consistency, >1/second writes per key, critical data
|
||||||
|
|
||||||
|
**Need strong consistency?** Use [Durable Objects](https://developers.cloudflare.com/durable-objects/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wrangler CLI Essentials
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create namespace
|
||||||
|
npx wrangler kv namespace create MY_NAMESPACE [--preview]
|
||||||
|
|
||||||
|
# Manage keys
|
||||||
|
npx wrangler kv key put --binding=MY_KV "key" "value" [--ttl=3600] [--metadata='{}']
|
||||||
|
npx wrangler kv key get --binding=MY_KV "key"
|
||||||
|
npx wrangler kv key list --binding=MY_KV [--prefix="user:"]
|
||||||
|
npx wrangler kv key delete --binding=MY_KV "key"
|
||||||
|
|
||||||
|
# Bulk operations (up to 10,000 keys)
|
||||||
|
npx wrangler kv bulk put --binding=MY_KV data.json
|
||||||
|
npx wrangler kv bulk delete --binding=MY_KV keys.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limits & Quotas
|
||||||
|
|
||||||
|
| Feature | Free Plan | Paid Plan |
|
||||||
|
|---------|-----------|-----------|
|
||||||
|
| Reads per day | 100,000 | Unlimited |
|
||||||
|
| Writes per day (different keys) | 1,000 | Unlimited |
|
||||||
|
| **Writes per key per second** | **1** | **1** |
|
||||||
|
| Operations per Worker invocation | 1,000 | 1,000 |
|
||||||
|
| **Namespaces per account** | **1,000** | **1,000** |
|
||||||
|
| Storage per account | 1 GB | Unlimited |
|
||||||
|
| Key size | 512 bytes | 512 bytes |
|
||||||
|
| Metadata size | 1024 bytes | 1024 bytes |
|
||||||
|
| Value size | 25 MiB | 25 MiB |
|
||||||
|
| Minimum cacheTtl | 60 seconds | 60 seconds |
|
||||||
|
|
||||||
|
**Critical**: 1 write/second per key (429 if exceeded), bulk operations count as 1 operation, namespace limit increased from 200 → 1,000 (Jan 2025)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### 1. Rate Limit (429 Too Many Requests)
|
||||||
|
**Cause**: Writing to same key >1/second
|
||||||
|
**Solution**: Use retry with exponential backoff (see Advanced Patterns)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad
|
||||||
|
await env.MY_KV.put('counter', '1');
|
||||||
|
await env.MY_KV.put('counter', '2'); // 429 error!
|
||||||
|
|
||||||
|
// ✅ Good
|
||||||
|
await putWithRetry(env.MY_KV, 'counter', '2');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Value Too Large
|
||||||
|
**Cause**: Value exceeds 25 MiB
|
||||||
|
**Solution**: Validate size before writing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (value.length > 25 * 1024 * 1024) throw new Error('Value exceeds 25 MiB');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Metadata Too Large
|
||||||
|
**Cause**: Metadata exceeds 1024 bytes when serialized
|
||||||
|
**Solution**: Validate serialized size
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const serialized = JSON.stringify(metadata);
|
||||||
|
if (serialized.length > 1024) throw new Error('Metadata exceeds 1024 bytes');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Invalid CacheTtl
|
||||||
|
**Cause**: cacheTtl <60 seconds
|
||||||
|
**Solution**: Use minimum 60
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Error
|
||||||
|
await env.MY_KV.get('key', { cacheTtl: 30 });
|
||||||
|
|
||||||
|
// ✅ Correct
|
||||||
|
await env.MY_KV.get('key', { cacheTtl: 60 });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Always Do ✅
|
||||||
|
|
||||||
|
1. Use bulk operations when reading multiple keys (counts as 1 operation)
|
||||||
|
2. Set cacheTtl for frequently-read, infrequently-updated data (min 60s)
|
||||||
|
3. Store small values (<1024 bytes) in metadata when using `list()` frequently
|
||||||
|
4. Check `list_complete` when paginating, not `keys.length === 0`
|
||||||
|
5. Use retry logic with exponential backoff for write operations
|
||||||
|
6. Validate sizes before writing (key 512B, value 25MiB, metadata 1KB)
|
||||||
|
7. Coalesce related keys for better caching performance
|
||||||
|
8. Use KV for read-heavy workloads (100:1 read/write ratio ideal)
|
||||||
|
|
||||||
|
### Never Do ❌
|
||||||
|
|
||||||
|
1. Never write to same key >1/second (causes 429 rate limit errors)
|
||||||
|
2. Never assume immediate global consistency (takes ~60 seconds to propagate)
|
||||||
|
3. Never use KV for atomic operations (use Durable Objects instead)
|
||||||
|
4. Never set cacheTtl <60 seconds (will fail)
|
||||||
|
5. Never commit namespace IDs to public repos (use environment variables)
|
||||||
|
6. Never exceed 1000 operations per invocation (use bulk operations)
|
||||||
|
7. Never rely on write order (eventual consistency = no guarantees)
|
||||||
|
8. Never forget to handle null values (`get()` returns `null` if key doesn't exist)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue 1: "429 Too Many Requests" on writes
|
||||||
|
**Cause**: Writing to same key >1/second
|
||||||
|
**Solution**: Consolidate writes or use retry with exponential backoff
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad: Rate limit
|
||||||
|
for (let i = 0; i < 10; i++) await kv.put('counter', String(i));
|
||||||
|
|
||||||
|
// ✅ Good: Single write
|
||||||
|
await kv.put('counter', '9');
|
||||||
|
|
||||||
|
// ✅ Good: Retry with backoff
|
||||||
|
await putWithRetry(kv, 'counter', String(i));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 2: Stale reads after write
|
||||||
|
**Cause**: Eventual consistency (~60 seconds propagation)
|
||||||
|
**Solution**: Accept stale reads, use Durable Objects for strong consistency, or implement app-level cache invalidation
|
||||||
|
|
||||||
|
### Issue 3: "Operations limit exceeded"
|
||||||
|
**Cause**: >1000 KV operations in single Worker invocation
|
||||||
|
**Solution**: Use bulk operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad: 5000 operations
|
||||||
|
for (const key of 5000keys) await kv.get(key);
|
||||||
|
|
||||||
|
// ✅ Good: 1 operation
|
||||||
|
const values = await kv.get(keys); // Bulk read
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 4: List returns empty but cursor exists
|
||||||
|
**Cause**: Deleted/expired keys create "tombstones"
|
||||||
|
**Solution**: Always check `list_complete`, not `keys.length`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct pagination
|
||||||
|
let cursor: string | undefined;
|
||||||
|
do {
|
||||||
|
const result = await kv.list({ cursor });
|
||||||
|
processKeys(result.keys); // Even if empty
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
- [ ] Environment-specific namespaces configured (`id` vs `preview_id`)
|
||||||
|
- [ ] Namespace IDs stored in environment variables (not hardcoded)
|
||||||
|
- [ ] Rate limit retry logic implemented for writes
|
||||||
|
- [ ] Appropriate `cacheTtl` values set for reads (min 60s)
|
||||||
|
- [ ] Sizes validated (key 512B, value 25MiB, metadata 1KB)
|
||||||
|
- [ ] Bulk operations used where possible
|
||||||
|
- [ ] Pagination with `list_complete` check (not `keys.length`)
|
||||||
|
- [ ] Error handling for null values
|
||||||
|
- [ ] Monitoring/alerting for rate limits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Cloudflare KV Docs](https://developers.cloudflare.com/kv/)
|
||||||
|
- [KV API Reference](https://developers.cloudflare.com/kv/api/)
|
||||||
|
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
|
||||||
|
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)
|
||||||
|
- [Wrangler KV Commands](https://developers.cloudflare.com/workers/wrangler/commands/#kv)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-11-24
|
||||||
|
**Package Versions**: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0
|
||||||
73
plugin.lock.json
Normal file
73
plugin.lock.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:jezweb/claude-skills:skills/cloudflare-kv",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "ce8453586663e42be92dc68ac0d9688e1b5f3bca",
|
||||||
|
"treeHash": "91ed0423c52d84b688f4dbc23f5e35d4906098a8c19ee220485da4e066ebf4aa",
|
||||||
|
"generatedAt": "2025-11-28T10:18:56.373969Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "cloudflare-kv",
|
||||||
|
"description": "Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "d2d5798f1e8aecce1df20b5de0992ccb2c942ff8740492203842fa5296d8c214"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "0bbb7062c7baae72de902f85520d307f996d65c6a11be9a4b45014b0f3534e17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/best-practices.md",
|
||||||
|
"sha256": "0adc9505c4d050e21db41917daaa7e9bfa37b6c558c0a910056045938aa45f58"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/workers-api.md",
|
||||||
|
"sha256": "d3283b7c8395c3eb22936522185aa08ef1948802c3501a1532c68475aa819974"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "858fd909ff72e8b3013fa0de105b29ebee5ae2526c336d950d6880060653f62d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/wrangler-kv-config.jsonc",
|
||||||
|
"sha256": "52694be4cfd90f6e2d5501d0da0e2910e2b041b5c8b86140e08020e9a6fc5131"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/kv-metadata-pattern.ts",
|
||||||
|
"sha256": "fbb3377064dc79b52a93ebd8ea6fdced88bc179981d62c985c3eb13c1e89c480"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/kv-list-pagination.ts",
|
||||||
|
"sha256": "9efca329c0a35a7b47acd4671245b46191c9413d2d80286408404acdfa226e0a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/kv-basic-operations.ts",
|
||||||
|
"sha256": "850dd2be4f78ae2fac9057bf44424a7b94202c91ba2b789b73e65c8a3f67f798"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/kv-caching-pattern.ts",
|
||||||
|
"sha256": "1e16eb83f3e355b2e5c5f90ef470d9c7433fde3d1cc1d8d015073491d9fc459f"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "91ed0423c52d84b688f4dbc23f5e35d4906098a8c19ee220485da4e066ebf4aa"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
720
references/best-practices.md
Normal file
720
references/best-practices.md
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
# Cloudflare Workers KV - Best Practices
|
||||||
|
|
||||||
|
This document contains production-tested best practices for Cloudflare Workers KV.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Performance Optimization](#performance-optimization)
|
||||||
|
2. [Caching Strategies](#caching-strategies)
|
||||||
|
3. [Key Design](#key-design)
|
||||||
|
4. [Metadata Usage](#metadata-usage)
|
||||||
|
5. [Error Handling](#error-handling)
|
||||||
|
6. [Security](#security)
|
||||||
|
7. [Cost Optimization](#cost-optimization)
|
||||||
|
8. [Monitoring & Debugging](#monitoring--debugging)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### 1. Use Bulk Operations
|
||||||
|
|
||||||
|
**❌ Bad: Individual reads**
|
||||||
|
```typescript
|
||||||
|
const value1 = await kv.get('key1'); // 1 operation
|
||||||
|
const value2 = await kv.get('key2'); // 1 operation
|
||||||
|
const value3 = await kv.get('key3'); // 1 operation
|
||||||
|
// Total: 3 operations
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Good: Bulk read**
|
||||||
|
```typescript
|
||||||
|
const values = await kv.get(['key1', 'key2', 'key3']); // 1 operation
|
||||||
|
// Total: 1 operation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Counts as 1 operation against the 1000/invocation limit
|
||||||
|
- Faster execution
|
||||||
|
- Lower latency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Use CacheTtl for Frequently-Read Data
|
||||||
|
|
||||||
|
**❌ Bad: No edge caching**
|
||||||
|
```typescript
|
||||||
|
const value = await kv.get('config'); // Fetches from KV every time
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Good: Edge caching**
|
||||||
|
```typescript
|
||||||
|
const value = await kv.get('config', {
|
||||||
|
cacheTtl: 300, // Cache at edge for 5 minutes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
- Use `cacheTtl` for data that changes infrequently
|
||||||
|
- Minimum: 60 seconds
|
||||||
|
- Typical values:
|
||||||
|
- Configuration: 300-600 seconds (5-10 minutes)
|
||||||
|
- Static content: 3600+ seconds (1+ hour)
|
||||||
|
- Frequently changing: 60-120 seconds
|
||||||
|
|
||||||
|
**Trade-off:** Higher `cacheTtl` = faster reads but slower updates propagate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Coalesce Related Keys
|
||||||
|
|
||||||
|
**❌ Bad: Many small keys**
|
||||||
|
```typescript
|
||||||
|
await kv.put('user:123:name', 'John');
|
||||||
|
await kv.put('user:123:email', 'john@example.com');
|
||||||
|
await kv.put('user:123:age', '30');
|
||||||
|
|
||||||
|
// Reading requires 3 operations
|
||||||
|
const name = await kv.get('user:123:name');
|
||||||
|
const email = await kv.get('user:123:email');
|
||||||
|
const age = await kv.get('user:123:age');
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Good: Coalesced key**
|
||||||
|
```typescript
|
||||||
|
await kv.put('user:123', JSON.stringify({
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com',
|
||||||
|
age: 30,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Reading requires 1 operation
|
||||||
|
const user = await kv.get<User>('user:123', { type: 'json' });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Fewer operations
|
||||||
|
- Single cache entry
|
||||||
|
- Faster reads
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Related data that's always accessed together
|
||||||
|
- Data that doesn't update frequently
|
||||||
|
- Values stay under 25 MiB total
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Store Small Values in Metadata
|
||||||
|
|
||||||
|
**❌ Bad: Separate keys for metadata**
|
||||||
|
```typescript
|
||||||
|
await kv.put('user:123', 'data');
|
||||||
|
await kv.put('user:123:status', 'active');
|
||||||
|
|
||||||
|
// List requires additional get() calls
|
||||||
|
const users = await kv.list({ prefix: 'user:' });
|
||||||
|
for (const key of users.keys) {
|
||||||
|
const status = await kv.get(`${key.name}:status`); // Extra operation!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Good: Metadata pattern**
|
||||||
|
```typescript
|
||||||
|
await kv.put('user:123', 'data', {
|
||||||
|
metadata: { status: 'active', plan: 'pro' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// List includes metadata, no extra get() calls!
|
||||||
|
const users = await kv.list({ prefix: 'user:' });
|
||||||
|
for (const key of users.keys) {
|
||||||
|
console.log(key.name, key.metadata.status); // No extra operations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Values fit in 1024 bytes
|
||||||
|
- Frequently use `list()` operations
|
||||||
|
- Need to filter/process many keys
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Caching Strategies
|
||||||
|
|
||||||
|
### 1. Cache-Aside Pattern (Read-Through)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function getCached<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
key: string,
|
||||||
|
fetchFn: () => Promise<T>,
|
||||||
|
ttl = 3600
|
||||||
|
): Promise<T> {
|
||||||
|
// Try cache
|
||||||
|
const cached = await kv.get<T>(key, {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cached !== null) return cached;
|
||||||
|
|
||||||
|
// Cache miss - fetch and store
|
||||||
|
const data = await fetchFn();
|
||||||
|
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use when:**
|
||||||
|
- Data is expensive to compute/fetch
|
||||||
|
- Read >> Write ratio
|
||||||
|
- Acceptable to serve slightly stale data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Write-Through Cache
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function updateCached<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
key: string,
|
||||||
|
data: T,
|
||||||
|
ttl = 3600
|
||||||
|
): Promise<void> {
|
||||||
|
// Update database
|
||||||
|
await database.update(data);
|
||||||
|
|
||||||
|
// Update cache immediately
|
||||||
|
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use when:**
|
||||||
|
- Need cache consistency
|
||||||
|
- Write operations are infrequent
|
||||||
|
- Cache must always reflect latest data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Stale-While-Revalidate
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function staleWhileRevalidate<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
key: string,
|
||||||
|
fetchFn: () => Promise<T>,
|
||||||
|
ctx: ExecutionContext,
|
||||||
|
staleThreshold = 300
|
||||||
|
): Promise<T> {
|
||||||
|
const { value, metadata } = await kv.getWithMetadata<T, { timestamp: number }>(
|
||||||
|
key,
|
||||||
|
{ type: 'json' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value !== null && metadata) {
|
||||||
|
const age = Date.now() - metadata.timestamp;
|
||||||
|
|
||||||
|
// Refresh in background if stale
|
||||||
|
if (age > staleThreshold * 1000) {
|
||||||
|
ctx.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const fresh = await fetchFn();
|
||||||
|
await kv.put(key, JSON.stringify(fresh), {
|
||||||
|
metadata: { timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss
|
||||||
|
const data = await fetchFn();
|
||||||
|
await kv.put(key, JSON.stringify(data), {
|
||||||
|
metadata: { timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use when:**
|
||||||
|
- Fast response time is critical
|
||||||
|
- Acceptable to serve slightly stale data
|
||||||
|
- Background refresh is acceptable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Design
|
||||||
|
|
||||||
|
### 1. Use Hierarchical Namespaces
|
||||||
|
|
||||||
|
**✅ Good key patterns:**
|
||||||
|
```
|
||||||
|
user:123:profile
|
||||||
|
user:123:settings
|
||||||
|
user:123:sessions
|
||||||
|
|
||||||
|
session:abc123:data
|
||||||
|
session:abc123:metadata
|
||||||
|
|
||||||
|
cache:api:users:list
|
||||||
|
cache:api:posts:123
|
||||||
|
cache:db:query:hash123
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Easy to filter with `list({ prefix: 'user:123:' })`
|
||||||
|
- Easy to invalidate groups
|
||||||
|
- Clear organization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Use Lexicographic Ordering
|
||||||
|
|
||||||
|
Keys are always sorted lexicographically, so design keys to take advantage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Date-based keys (ISO format sorts correctly)
|
||||||
|
'log:2025-10-21:entry1'
|
||||||
|
'log:2025-10-22:entry1'
|
||||||
|
|
||||||
|
// Numeric IDs (zero-padded)
|
||||||
|
'user:00000001'
|
||||||
|
'user:00000123'
|
||||||
|
'user:00001000'
|
||||||
|
|
||||||
|
// Priority-based (prefix with number)
|
||||||
|
'task:1:high-priority'
|
||||||
|
'task:2:medium-priority'
|
||||||
|
'task:3:low-priority'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Avoid Key Collisions
|
||||||
|
|
||||||
|
**❌ Bad:**
|
||||||
|
```
|
||||||
|
user:123 // User data
|
||||||
|
user:123:count // Some counter
|
||||||
|
user:123 // Different data? Collision!
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Good:**
|
||||||
|
```
|
||||||
|
user:data:123
|
||||||
|
user:counter:123
|
||||||
|
user:session:123
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata Usage
|
||||||
|
|
||||||
|
### 1. Track Versions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await kv.put('config', JSON.stringify(data), {
|
||||||
|
metadata: {
|
||||||
|
version: 2,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
updatedBy: 'admin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Audit Trails
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await kv.put(key, value, {
|
||||||
|
metadata: {
|
||||||
|
createdAt: Date.now(),
|
||||||
|
createdBy: userId,
|
||||||
|
accessCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Feature Flags
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Store flags in metadata for fast list() access
|
||||||
|
await kv.put(`flag:${name}`, JSON.stringify(config), {
|
||||||
|
metadata: {
|
||||||
|
enabled: true,
|
||||||
|
rolloutPercentage: 50,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all flags without additional get() calls
|
||||||
|
const flags = await kv.list({ prefix: 'flag:' });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### 1. Handle Rate Limits (429)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function putWithRetry(
|
||||||
|
kv: KVNamespace,
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
maxAttempts = 5
|
||||||
|
): Promise<void> {
|
||||||
|
let attempts = 0;
|
||||||
|
let delay = 1000;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
try {
|
||||||
|
await kv.put(key, value);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
const message = (error as Error).message;
|
||||||
|
|
||||||
|
if (message.includes('429') || message.includes('Too Many Requests')) {
|
||||||
|
attempts++;
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
throw new Error('Max retry attempts reached');
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
delay *= 2; // Exponential backoff
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Handle Null Values
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad
|
||||||
|
const value = await kv.get('key');
|
||||||
|
console.log(value.toUpperCase()); // Error if key doesn't exist
|
||||||
|
|
||||||
|
// ✅ Good
|
||||||
|
const value = await kv.get('key');
|
||||||
|
if (value !== null) {
|
||||||
|
console.log(value.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Good: Default value
|
||||||
|
const value = await kv.get('key') ?? 'default';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Validate Input Sizes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function validateKVInput(key: string, value: string, metadata?: any): void {
|
||||||
|
// Key size
|
||||||
|
if (new TextEncoder().encode(key).length > 512) {
|
||||||
|
throw new Error('Key exceeds 512 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value size
|
||||||
|
if (new TextEncoder().encode(value).length > 25 * 1024 * 1024) {
|
||||||
|
throw new Error('Value exceeds 25 MiB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata size
|
||||||
|
if (metadata) {
|
||||||
|
const serialized = JSON.stringify(metadata);
|
||||||
|
if (new TextEncoder().encode(serialized).length > 1024) {
|
||||||
|
throw new Error('Metadata exceeds 1024 bytes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### 1. Never Commit Namespace IDs
|
||||||
|
|
||||||
|
**❌ Bad:**
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"kv_namespaces": [
|
||||||
|
{
|
||||||
|
"binding": "MY_KV",
|
||||||
|
"id": "abc123def456..." // Hardcoded!
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Good:**
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"kv_namespaces": [
|
||||||
|
{
|
||||||
|
"binding": "MY_KV",
|
||||||
|
"id": "${KV_NAMESPACE_ID}" // Environment variable
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Encrypt Sensitive Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Encrypt before storing
|
||||||
|
const encrypted = await encrypt(sensitiveData, encryptionKey);
|
||||||
|
await kv.put('sensitive:123', encrypted);
|
||||||
|
|
||||||
|
// Decrypt after reading
|
||||||
|
const encrypted = await kv.get('sensitive:123');
|
||||||
|
const decrypted = await decrypt(encrypted, encryptionKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Use Separate Namespaces for Environments
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"kv_namespaces": [
|
||||||
|
{
|
||||||
|
"binding": "MY_KV",
|
||||||
|
"id": "production-namespace-id",
|
||||||
|
"preview_id": "development-namespace-id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cost Optimization
|
||||||
|
|
||||||
|
### 1. Minimize Write Operations (Free Tier)
|
||||||
|
|
||||||
|
**Free tier limits:**
|
||||||
|
- 1,000 writes per day
|
||||||
|
- 100,000 reads per day
|
||||||
|
|
||||||
|
**Strategies:**
|
||||||
|
- Batch writes when possible
|
||||||
|
- Use longer TTLs to reduce rewrites
|
||||||
|
- Cache data in memory if accessed frequently within same invocation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Use Metadata Instead of Separate Keys
|
||||||
|
|
||||||
|
**❌ Expensive: 3 writes, 3 reads**
|
||||||
|
```typescript
|
||||||
|
await kv.put('user:123:status', 'active');
|
||||||
|
await kv.put('user:123:plan', 'pro');
|
||||||
|
await kv.put('user:123:updated', Date.now().toString());
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Cheaper: 1 write, 1 read**
|
||||||
|
```typescript
|
||||||
|
await kv.put('user:123', '', {
|
||||||
|
metadata: { status: 'active', plan: 'pro', updated: Date.now() },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Set Appropriate TTLs
|
||||||
|
|
||||||
|
Longer TTLs = fewer rewrites = lower costs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Expensive: Rewrites every minute
|
||||||
|
await kv.put('cache:data', data, { expirationTtl: 60 });
|
||||||
|
|
||||||
|
// ✅ Better: Rewrites every hour
|
||||||
|
await kv.put('cache:data', data, { expirationTtl: 3600 });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Debugging
|
||||||
|
|
||||||
|
### 1. Track Cache Hit Rates
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let stats = { hits: 0, misses: 0 };
|
||||||
|
|
||||||
|
async function getCached<T>(kv: KVNamespace, key: string): Promise<T | null> {
|
||||||
|
const value = await kv.get<T>(key, { type: 'json' });
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
stats.hits++;
|
||||||
|
} else {
|
||||||
|
stats.misses++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// View stats
|
||||||
|
app.get('/stats', (c) => {
|
||||||
|
const total = stats.hits + stats.misses;
|
||||||
|
const hitRate = total > 0 ? (stats.hits / total) * 100 : 0;
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
hits: stats.hits,
|
||||||
|
misses: stats.misses,
|
||||||
|
hitRate: `${hitRate.toFixed(2)}%`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Log KV Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function loggedGet<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
key: string
|
||||||
|
): Promise<T | null> {
|
||||||
|
const start = Date.now();
|
||||||
|
const value = await kv.get<T>(key, { type: 'json' });
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
operation: 'get',
|
||||||
|
key,
|
||||||
|
found: value !== null,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Use Namespace Prefixes for Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const namespace = env.ENVIRONMENT === 'production' ? 'prod' : 'test';
|
||||||
|
|
||||||
|
await kv.put(`${namespace}:user:123`, data);
|
||||||
|
|
||||||
|
// Cleanup test data
|
||||||
|
if (env.ENVIRONMENT === 'test') {
|
||||||
|
// Delete all test: keys
|
||||||
|
let cursor: string | undefined;
|
||||||
|
do {
|
||||||
|
const result = await kv.list({ prefix: 'test:', cursor });
|
||||||
|
await Promise.all(result.keys.map(k => kv.delete(k.name)));
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
Before deploying to production:
|
||||||
|
|
||||||
|
- [ ] Environment-specific namespaces configured
|
||||||
|
- [ ] Namespace IDs stored in environment variables
|
||||||
|
- [ ] Rate limit retry logic implemented
|
||||||
|
- [ ] Appropriate `cacheTtl` values set
|
||||||
|
- [ ] Input validation for key/value/metadata sizes
|
||||||
|
- [ ] Bulk operations used where possible
|
||||||
|
- [ ] Pagination implemented correctly for `list()`
|
||||||
|
- [ ] Error handling for null values
|
||||||
|
- [ ] Monitoring/alerting for rate limits
|
||||||
|
- [ ] Documentation for eventual consistency behavior
|
||||||
|
- [ ] Security review for sensitive data
|
||||||
|
- [ ] Cost analysis for expected usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### 1. Session Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Store session
|
||||||
|
await kv.put(`session:${sessionId}`, JSON.stringify(sessionData), {
|
||||||
|
expirationTtl: 3600, // 1 hour
|
||||||
|
metadata: { userId, createdAt: Date.now() },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read session
|
||||||
|
const session = await kv.get<SessionData>(`session:${sessionId}`, {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: 60, // Cache for 1 minute
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. API Response Caching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`;
|
||||||
|
|
||||||
|
let response = await kv.get<ApiResponse>(cacheKey, {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
response = await fetchFromAPI(endpoint, params);
|
||||||
|
await kv.put(cacheKey, JSON.stringify(response), {
|
||||||
|
expirationTtl: 600,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Configuration Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Update config
|
||||||
|
await kv.put('config:app', JSON.stringify(config), {
|
||||||
|
metadata: {
|
||||||
|
version: 2,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
updatedBy: adminId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read config (with long cache)
|
||||||
|
const config = await kv.get<AppConfig>('config:app', {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: 3600, // Cache for 1 hour
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Cloudflare KV Documentation](https://developers.cloudflare.com/kv/)
|
||||||
|
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)
|
||||||
|
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
|
||||||
|
- [KV Pricing](https://developers.cloudflare.com/kv/platform/pricing/)
|
||||||
483
references/workers-api.md
Normal file
483
references/workers-api.md
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
# Cloudflare Workers KV - Complete API Reference
|
||||||
|
|
||||||
|
This document provides the complete Workers KV API reference based on official Cloudflare documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KVNamespace Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KVNamespace {
|
||||||
|
get(key: string, options?: Partial<KVGetOptions<undefined>>): Promise<string | null>;
|
||||||
|
get(key: string, type: "text"): Promise<string | null>;
|
||||||
|
get<ExpectedValue = unknown>(key: string, type: "json"): Promise<ExpectedValue | null>;
|
||||||
|
get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
|
||||||
|
get(key: string, type: "stream"): Promise<ReadableStream | null>;
|
||||||
|
get(keys: string[]): Promise<Map<string, string | null>>;
|
||||||
|
get<ExpectedValue = unknown>(keys: string[], type: "json"): Promise<Map<string, ExpectedValue | null>>;
|
||||||
|
|
||||||
|
getWithMetadata<Metadata = unknown>(key: string, options?: Partial<KVGetOptions<undefined>>): Promise<KVGetWithMetadataResult<string, Metadata>>;
|
||||||
|
getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: string, type: "json"): Promise<KVGetWithMetadataResult<ExpectedValue, Metadata>>;
|
||||||
|
getWithMetadata<Metadata = unknown>(keys: string[]): Promise<Map<string, KVGetWithMetadataResult<string, Metadata>>>;
|
||||||
|
|
||||||
|
put(key: string, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVPutOptions): Promise<void>;
|
||||||
|
|
||||||
|
delete(key: string): Promise<void>;
|
||||||
|
|
||||||
|
list<Metadata = unknown>(options?: KVListOptions): Promise<KVListResult<Metadata>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Read Operations
|
||||||
|
|
||||||
|
### `get()` - Single Key
|
||||||
|
|
||||||
|
Read a single key-value pair.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
get(key: string, options?: KVGetOptions): Promise<T | null>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `key` (string, required) - The key to read
|
||||||
|
- `options` (object, optional):
|
||||||
|
- `type` - Return type: `"text"` (default), `"json"`, `"arrayBuffer"`, `"stream"`
|
||||||
|
- `cacheTtl` (number) - Edge cache duration in seconds (minimum: 60)
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Promise<T | null>` - Value or `null` if key doesn't exist
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// Text (default)
|
||||||
|
const value = await env.MY_KV.get('my-key');
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
const data = await env.MY_KV.get<MyType>('my-key', { type: 'json' });
|
||||||
|
|
||||||
|
// With cache optimization
|
||||||
|
const value = await env.MY_KV.get('my-key', {
|
||||||
|
type: 'text',
|
||||||
|
cacheTtl: 300, // Cache for 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
// ArrayBuffer
|
||||||
|
const buffer = await env.MY_KV.get('binary-key', { type: 'arrayBuffer' });
|
||||||
|
|
||||||
|
// Stream (for large values)
|
||||||
|
const stream = await env.MY_KV.get('large-file', { type: 'stream' });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get()` - Multiple Keys (Bulk)
|
||||||
|
|
||||||
|
Read multiple keys in a single operation.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
get(keys: string[], type?: 'text' | 'json'): Promise<Map<string, T | null>>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `keys` (string[], required) - Array of keys to read
|
||||||
|
- `type` (optional) - Return type: `"text"` (default) or `"json"`
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Promise<Map<string, T | null>>` - Map of key-value pairs
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
- Counts as **1 operation** regardless of number of keys
|
||||||
|
- Only supports `text` and `json` types (not `arrayBuffer` or `stream`)
|
||||||
|
- For binary/stream types, use individual `get()` calls with `Promise.all()`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// Read multiple keys
|
||||||
|
const keys = ['key1', 'key2', 'key3'];
|
||||||
|
const values = await env.MY_KV.get(keys);
|
||||||
|
|
||||||
|
// Access values
|
||||||
|
const value1 = values.get('key1');
|
||||||
|
const value2 = values.get('key2');
|
||||||
|
|
||||||
|
// Convert to object
|
||||||
|
const obj = Object.fromEntries(values);
|
||||||
|
|
||||||
|
// Read as JSON
|
||||||
|
const values = await env.MY_KV.get<MyType>(keys, 'json');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `getWithMetadata()` - Single Key
|
||||||
|
|
||||||
|
Read key-value pair with metadata.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
getWithMetadata<Value, Metadata>(
|
||||||
|
key: string,
|
||||||
|
options?: KVGetOptions
|
||||||
|
): Promise<KVGetWithMetadataResult<Value, Metadata>>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- Same as `get()`
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
value: Value | null,
|
||||||
|
metadata: Metadata | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// Get with metadata
|
||||||
|
const { value, metadata } = await env.MY_KV.getWithMetadata('my-key');
|
||||||
|
|
||||||
|
// Get as JSON with metadata
|
||||||
|
const { value, metadata } = await env.MY_KV.getWithMetadata<MyType>('my-key', {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
console.log('Value:', value);
|
||||||
|
console.log('Metadata:', metadata);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `getWithMetadata()` - Multiple Keys (Bulk)
|
||||||
|
|
||||||
|
Read multiple keys with metadata.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
getWithMetadata<Metadata>(
|
||||||
|
keys: string[],
|
||||||
|
type?: 'text' | 'json'
|
||||||
|
): Promise<Map<string, KVGetWithMetadataResult<T, Metadata>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
const keys = ['key1', 'key2'];
|
||||||
|
const results = await env.MY_KV.getWithMetadata(keys);
|
||||||
|
|
||||||
|
for (const [key, data] of results) {
|
||||||
|
console.log(key, data.value, data.metadata);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Write Operations
|
||||||
|
|
||||||
|
### `put()` - Write Key-Value Pair
|
||||||
|
|
||||||
|
Write or update a key-value pair.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
put(
|
||||||
|
key: string,
|
||||||
|
value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
|
||||||
|
options?: KVPutOptions
|
||||||
|
): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `key` (string, required) - Maximum 512 bytes
|
||||||
|
- `value` (required) - Maximum 25 MiB
|
||||||
|
- `options` (object, optional):
|
||||||
|
- `expiration` (number) - Absolute expiration time (seconds since epoch)
|
||||||
|
- `expirationTtl` (number) - TTL in seconds from now (minimum: 60)
|
||||||
|
- `metadata` (any) - JSON-serializable metadata (maximum: 1024 bytes)
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Promise<void>`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// Simple write
|
||||||
|
await env.MY_KV.put('key', 'value');
|
||||||
|
|
||||||
|
// Write JSON
|
||||||
|
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
|
||||||
|
|
||||||
|
// Write with TTL
|
||||||
|
await env.MY_KV.put('session', sessionData, {
|
||||||
|
expirationTtl: 3600, // Expire in 1 hour
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write with absolute expiration
|
||||||
|
const expirationTime = Math.floor(Date.now() / 1000) + 86400; // 24 hours
|
||||||
|
await env.MY_KV.put('token', tokenValue, {
|
||||||
|
expiration: expirationTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write with metadata
|
||||||
|
await env.MY_KV.put('config', configData, {
|
||||||
|
metadata: {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
updatedBy: 'admin',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write with everything
|
||||||
|
await env.MY_KV.put('key', 'value', {
|
||||||
|
expirationTtl: 600,
|
||||||
|
metadata: { source: 'api' },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Limits:**
|
||||||
|
- **Key size**: Maximum 512 bytes
|
||||||
|
- **Value size**: Maximum 25 MiB
|
||||||
|
- **Metadata size**: Maximum 1024 bytes (JSON serialized)
|
||||||
|
- **Write rate**: Maximum **1 write per second per key**
|
||||||
|
- **Expiration minimum**: 60 seconds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Delete Operations
|
||||||
|
|
||||||
|
### `delete()` - Delete Key
|
||||||
|
|
||||||
|
Delete a key-value pair.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
delete(key: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `key` (string, required) - Key to delete
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Promise<void>` - Always succeeds, even if key doesn't exist
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// Delete single key
|
||||||
|
await env.MY_KV.delete('my-key');
|
||||||
|
|
||||||
|
// Delete multiple keys
|
||||||
|
const keys = ['key1', 'key2', 'key3'];
|
||||||
|
await Promise.all(keys.map(key => env.MY_KV.delete(key)));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** For bulk delete of >10,000 keys, use the REST API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## List Operations
|
||||||
|
|
||||||
|
### `list()` - List Keys
|
||||||
|
|
||||||
|
List keys in the namespace.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```typescript
|
||||||
|
list<Metadata>(options?: KVListOptions): Promise<KVListResult<Metadata>>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
```typescript
|
||||||
|
interface KVListOptions {
|
||||||
|
prefix?: string; // Filter keys by prefix
|
||||||
|
limit?: number; // Max keys to return (default: 1000, max: 1000)
|
||||||
|
cursor?: string; // Pagination cursor
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
interface KVListResult<Metadata> {
|
||||||
|
keys: {
|
||||||
|
name: string;
|
||||||
|
expiration?: number; // Seconds since epoch
|
||||||
|
metadata?: Metadata;
|
||||||
|
}[];
|
||||||
|
list_complete: boolean; // true if no more keys
|
||||||
|
cursor?: string; // Use for next page
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// List all keys (up to 1000)
|
||||||
|
const result = await env.MY_KV.list();
|
||||||
|
|
||||||
|
// List with prefix
|
||||||
|
const users = await env.MY_KV.list({ prefix: 'user:' });
|
||||||
|
|
||||||
|
// List with limit
|
||||||
|
const recent = await env.MY_KV.list({ limit: 100 });
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
let cursor: string | undefined;
|
||||||
|
do {
|
||||||
|
const result = await env.MY_KV.list({ cursor });
|
||||||
|
|
||||||
|
// Process keys
|
||||||
|
console.log(result.keys);
|
||||||
|
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
- Keys are **always** sorted lexicographically (UTF-8)
|
||||||
|
- **Always check `list_complete`**, not `keys.length === 0`
|
||||||
|
- Empty `keys` array doesn't mean no more data (tombstones exist)
|
||||||
|
- When paginating with `prefix`, pass `prefix` with each cursor request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Definitions
|
||||||
|
|
||||||
|
### KVGetOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KVGetOptions<Type> {
|
||||||
|
type: Type; // "text" | "json" | "arrayBuffer" | "stream"
|
||||||
|
cacheTtl?: number; // Edge cache duration (minimum: 60 seconds)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### KVPutOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KVPutOptions {
|
||||||
|
expiration?: number; // Seconds since epoch
|
||||||
|
expirationTtl?: number; // Seconds from now (minimum: 60)
|
||||||
|
metadata?: any; // Max 1024 bytes serialized
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### KVGetWithMetadataResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KVGetWithMetadataResult<Value, Metadata> {
|
||||||
|
value: Value | null;
|
||||||
|
metadata: Metadata | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### KVListOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KVListOptions {
|
||||||
|
prefix?: string;
|
||||||
|
limit?: number; // Default: 1000, max: 1000
|
||||||
|
cursor?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### KVListResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KVListResult<Metadata = unknown> {
|
||||||
|
keys: {
|
||||||
|
name: string;
|
||||||
|
expiration?: number;
|
||||||
|
metadata?: Metadata;
|
||||||
|
}[];
|
||||||
|
list_complete: boolean;
|
||||||
|
cursor?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Feature | Limit |
|
||||||
|
|---------|-------|
|
||||||
|
| Key size | 512 bytes |
|
||||||
|
| Value size | 25 MiB |
|
||||||
|
| Metadata size | 1024 bytes (JSON) |
|
||||||
|
| Writes per key per second | 1 |
|
||||||
|
| Operations per Worker invocation | 1,000 |
|
||||||
|
| List limit | 1,000 keys |
|
||||||
|
| Minimum cacheTtl | 60 seconds |
|
||||||
|
| Minimum expiration | 60 seconds |
|
||||||
|
| Namespaces per account (Free) | 1,000 |
|
||||||
|
| Namespaces per account (Paid) | 1,000 |
|
||||||
|
| Storage per account (Free) | 1 GB |
|
||||||
|
| Storage per account (Paid) | Unlimited |
|
||||||
|
| Read operations per day (Free) | 100,000 |
|
||||||
|
| Read operations per day (Paid) | Unlimited |
|
||||||
|
| Write operations per day (Free) | 1,000 |
|
||||||
|
| Write operations per day (Paid) | Unlimited |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consistency Model
|
||||||
|
|
||||||
|
### Eventually Consistent
|
||||||
|
|
||||||
|
- Writes are **immediately visible** in the same location
|
||||||
|
- Writes take **up to 60 seconds** to propagate globally
|
||||||
|
- Cached reads may return stale data during propagation
|
||||||
|
|
||||||
|
### Implications
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Tokyo datacenter
|
||||||
|
await env.KV.put('counter', '1');
|
||||||
|
const value1 = await env.KV.get('counter'); // "1" ✅
|
||||||
|
|
||||||
|
// London datacenter (within 60 seconds)
|
||||||
|
const value2 = await env.KV.get('counter'); // Might be old value ⚠️
|
||||||
|
|
||||||
|
// After 60+ seconds globally
|
||||||
|
const value3 = await env.KV.get('counter'); // "1" ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**For strong consistency, use [Durable Objects](https://developers.cloudflare.com/durable-objects/).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
1. **429 Too Many Requests**
|
||||||
|
- Cause: >1 write/second to same key
|
||||||
|
- Solution: Implement retry with exponential backoff
|
||||||
|
|
||||||
|
2. **Value too large**
|
||||||
|
- Cause: Value >25 MiB
|
||||||
|
- Solution: Validate size before writing
|
||||||
|
|
||||||
|
3. **Metadata too large**
|
||||||
|
- Cause: Metadata >1024 bytes serialized
|
||||||
|
- Solution: Validate JSON size before writing
|
||||||
|
|
||||||
|
4. **Invalid cacheTtl**
|
||||||
|
- Cause: cacheTtl <60 seconds
|
||||||
|
- Solution: Use minimum 60 seconds
|
||||||
|
|
||||||
|
5. **Operations limit exceeded**
|
||||||
|
- Cause: >1000 KV operations in Worker invocation
|
||||||
|
- Solution: Use bulk operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Official KV Documentation](https://developers.cloudflare.com/kv/)
|
||||||
|
- [KV API Reference](https://developers.cloudflare.com/kv/api/)
|
||||||
|
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
|
||||||
|
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)
|
||||||
538
templates/kv-basic-operations.ts
Normal file
538
templates/kv-basic-operations.ts
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
/**
|
||||||
|
* Cloudflare Workers KV - Basic CRUD Operations
|
||||||
|
*
|
||||||
|
* This template demonstrates all basic KV operations:
|
||||||
|
* - Create (PUT)
|
||||||
|
* - Read (GET)
|
||||||
|
* - Update (PUT)
|
||||||
|
* - Delete (DELETE)
|
||||||
|
* - List keys
|
||||||
|
* - Metadata handling
|
||||||
|
* - TTL/Expiration
|
||||||
|
* - Error handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
type Bindings = {
|
||||||
|
MY_KV: KVNamespace;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Bindings }>();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CREATE / UPDATE - Write key-value pairs
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Simple write
|
||||||
|
app.put('/kv/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const value = await c.req.text();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await c.env.MY_KV.put(key, value);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Key "${key}" created/updated`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write with TTL expiration
|
||||||
|
app.put('/kv/:key/ttl/:seconds', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const ttl = parseInt(c.req.param('seconds'), 10);
|
||||||
|
const value = await c.req.text();
|
||||||
|
|
||||||
|
// Validate TTL (minimum 60 seconds)
|
||||||
|
if (ttl < 60) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'TTL must be at least 60 seconds',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await c.env.MY_KV.put(key, value, {
|
||||||
|
expirationTtl: ttl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Key "${key}" will expire in ${ttl} seconds`,
|
||||||
|
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write with metadata
|
||||||
|
app.put('/kv/:key/metadata', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const body = await c.req.json<{ value: string; metadata: any }>();
|
||||||
|
|
||||||
|
// Validate metadata size (max 1024 bytes serialized)
|
||||||
|
const metadataJson = JSON.stringify(body.metadata);
|
||||||
|
if (metadataJson.length > 1024) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Metadata too large: ${metadataJson.length} bytes (max 1024)`,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await c.env.MY_KV.put(key, body.value, {
|
||||||
|
metadata: body.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Key "${key}" created with metadata`,
|
||||||
|
metadata: body.metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write JSON data
|
||||||
|
app.post('/kv/json/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const data = await c.req.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await c.env.MY_KV.put(key, JSON.stringify(data));
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `JSON data stored at key "${key}"`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// READ - Get key-value pairs
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Simple read (text)
|
||||||
|
app.get('/kv/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = await c.env.MY_KV.get(key);
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Key "${key}" not found`,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read JSON data
|
||||||
|
app.get('/kv/json/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = await c.env.MY_KV.get(key, { type: 'json' });
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Key "${key}" not found`,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read with metadata
|
||||||
|
app.get('/kv/:key/metadata', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { value, metadata } = await c.env.MY_KV.getWithMetadata(key);
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Key "${key}" not found`,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read with cache optimization
|
||||||
|
app.get('/kv/:key/cached', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const cacheTtl = parseInt(c.req.query('cacheTtl') || '300', 10);
|
||||||
|
|
||||||
|
// Validate cacheTtl (minimum 60 seconds)
|
||||||
|
if (cacheTtl < 60) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'cacheTtl must be at least 60 seconds',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = await c.env.MY_KV.get(key, {
|
||||||
|
type: 'text',
|
||||||
|
cacheTtl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Key "${key}" not found`,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
cached: true,
|
||||||
|
cacheTtl,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk read (multiple keys)
|
||||||
|
app.post('/kv/bulk/get', async (c) => {
|
||||||
|
const { keys } = await c.req.json<{ keys: string[] }>();
|
||||||
|
|
||||||
|
if (!Array.isArray(keys) || keys.length === 0) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'keys must be a non-empty array',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Bulk read counts as 1 operation!
|
||||||
|
const values = await c.env.MY_KV.get(keys);
|
||||||
|
|
||||||
|
// Convert Map to object
|
||||||
|
const result: Record<string, string | null> = {};
|
||||||
|
for (const [key, value] of values) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
count: keys.length,
|
||||||
|
values: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// LIST - List keys with pagination
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// List all keys (with pagination)
|
||||||
|
app.get('/kv/list', async (c) => {
|
||||||
|
const prefix = c.req.query('prefix') || '';
|
||||||
|
const cursor = c.req.query('cursor');
|
||||||
|
const limit = parseInt(c.req.query('limit') || '1000', 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await c.env.MY_KV.list({
|
||||||
|
prefix,
|
||||||
|
limit,
|
||||||
|
cursor: cursor || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
keys: result.keys,
|
||||||
|
count: result.keys.length,
|
||||||
|
hasMore: !result.list_complete,
|
||||||
|
cursor: result.cursor,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all keys with prefix (fully paginated)
|
||||||
|
app.get('/kv/list/all/:prefix', async (c) => {
|
||||||
|
const prefix = c.req.param('prefix');
|
||||||
|
let cursor: string | undefined;
|
||||||
|
const allKeys: any[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Paginate through all keys
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({
|
||||||
|
prefix,
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
allKeys.push(...result.keys);
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
prefix,
|
||||||
|
keys: allKeys,
|
||||||
|
totalCount: allKeys.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DELETE - Delete key-value pairs
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Delete single key
|
||||||
|
app.delete('/kv/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete always succeeds, even if key doesn't exist
|
||||||
|
await c.env.MY_KV.delete(key);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Key "${key}" deleted`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete multiple keys
|
||||||
|
app.post('/kv/bulk/delete', async (c) => {
|
||||||
|
const { keys } = await c.req.json<{ keys: string[] }>();
|
||||||
|
|
||||||
|
if (!Array.isArray(keys) || keys.length === 0) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'keys must be a non-empty array',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete all keys in parallel
|
||||||
|
await Promise.all(keys.map((key) => c.env.MY_KV.delete(key)));
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `${keys.length} keys deleted`,
|
||||||
|
count: keys.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UTILITY - Helper endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Check if key exists
|
||||||
|
app.get('/kv/:key/exists', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = await c.env.MY_KV.get(key);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
exists: value !== null,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get namespace stats
|
||||||
|
app.get('/kv/stats', async (c) => {
|
||||||
|
try {
|
||||||
|
const result = await c.env.MY_KV.list();
|
||||||
|
let totalKeys = result.keys.length;
|
||||||
|
let cursor = result.cursor;
|
||||||
|
|
||||||
|
// Count all keys (with pagination)
|
||||||
|
while (!result.list_complete && cursor) {
|
||||||
|
const nextResult = await c.env.MY_KV.list({ cursor });
|
||||||
|
totalKeys += nextResult.keys.length;
|
||||||
|
cursor = nextResult.cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
totalKeys,
|
||||||
|
sample: result.keys.slice(0, 10), // First 10 keys
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (c) => {
|
||||||
|
return c.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
518
templates/kv-caching-pattern.ts
Normal file
518
templates/kv-caching-pattern.ts
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
/**
|
||||||
|
* Cloudflare Workers KV - Caching Pattern
|
||||||
|
*
|
||||||
|
* This template demonstrates optimal caching patterns with KV:
|
||||||
|
* - Cache-aside pattern (read-through cache)
|
||||||
|
* - Write-through cache
|
||||||
|
* - CacheTtl optimization for edge caching
|
||||||
|
* - Stale-while-revalidate pattern
|
||||||
|
* - Cache invalidation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
type Bindings = {
|
||||||
|
CACHE: KVNamespace;
|
||||||
|
DB: D1Database; // Example: database for cache misses
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Bindings }>();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache-Aside Pattern (Read-Through Cache)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic cache-aside helper
|
||||||
|
*
|
||||||
|
* 1. Try to read from cache
|
||||||
|
* 2. On miss, fetch from source
|
||||||
|
* 3. Store in cache
|
||||||
|
* 4. Return data
|
||||||
|
*/
|
||||||
|
async function getCached<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
cacheKey: string,
|
||||||
|
fetchFn: () => Promise<T>,
|
||||||
|
options: {
|
||||||
|
ttl?: number; // KV expiration (default: 3600)
|
||||||
|
cacheTtl?: number; // Edge cache TTL (default: 300)
|
||||||
|
} = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const ttl = options.ttl ?? 3600; // 1 hour default
|
||||||
|
const cacheTtl = options.cacheTtl ?? 300; // 5 minutes default
|
||||||
|
|
||||||
|
// Try cache first (with edge caching)
|
||||||
|
const cached = await kv.get<T>(cacheKey, {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: Math.max(cacheTtl, 60), // Minimum 60 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cached !== null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - fetch from source
|
||||||
|
const data = await fetchFn();
|
||||||
|
|
||||||
|
// Store in cache (fire-and-forget)
|
||||||
|
await kv.put(cacheKey, JSON.stringify(data), {
|
||||||
|
expirationTtl: Math.max(ttl, 60), // Minimum 60 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Cache API response
|
||||||
|
app.get('/api/user/:id', async (c) => {
|
||||||
|
const userId = c.req.param('id');
|
||||||
|
const cacheKey = `user:${userId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await getCached(
|
||||||
|
c.env.CACHE,
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
// Simulate database fetch
|
||||||
|
const result = await c.env.DB.prepare(
|
||||||
|
'SELECT * FROM users WHERE id = ?'
|
||||||
|
)
|
||||||
|
.bind(userId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ttl: 3600, // Cache in KV for 1 hour
|
||||||
|
cacheTtl: 300, // Cache at edge for 5 minutes
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
user,
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Write-Through Cache Pattern
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write-through cache: Update cache when data changes
|
||||||
|
*/
|
||||||
|
app.put('/api/user/:id', async (c) => {
|
||||||
|
const userId = c.req.param('id');
|
||||||
|
const userData = await c.req.json();
|
||||||
|
const cacheKey = `user:${userId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update database
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE users SET name = ?, email = ? WHERE id = ?'
|
||||||
|
)
|
||||||
|
.bind(userData.name, userData.email, userId)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Update cache immediately
|
||||||
|
await c.env.CACHE.put(cacheKey, JSON.stringify(userData), {
|
||||||
|
expirationTtl: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: 'User updated and cache refreshed',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache Invalidation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate cache when data changes
|
||||||
|
*/
|
||||||
|
app.delete('/api/user/:id', async (c) => {
|
||||||
|
const userId = c.req.param('id');
|
||||||
|
const cacheKey = `user:${userId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete from database
|
||||||
|
await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId).run();
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
await c.env.CACHE.delete(cacheKey);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: 'User deleted and cache invalidated',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate multiple cache keys
|
||||||
|
app.post('/api/cache/invalidate', async (c) => {
|
||||||
|
const { keys } = await c.req.json<{ keys: string[] }>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete all cache keys in parallel
|
||||||
|
await Promise.all(keys.map((key) => c.env.CACHE.delete(key)));
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `${keys.length} cache keys invalidated`,
|
||||||
|
count: keys.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate by prefix (requires list + delete)
|
||||||
|
app.post('/api/cache/invalidate/prefix', async (c) => {
|
||||||
|
const { prefix } = await c.req.json<{ prefix: string }>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let cursor: string | undefined;
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
// List all keys with prefix and delete them
|
||||||
|
do {
|
||||||
|
const result = await c.env.CACHE.list({ prefix, cursor });
|
||||||
|
|
||||||
|
// Delete batch in parallel
|
||||||
|
await Promise.all(result.keys.map((key) => c.env.CACHE.delete(key.name)));
|
||||||
|
|
||||||
|
deletedCount += result.keys.length;
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Cache invalidated for prefix "${prefix}"`,
|
||||||
|
deletedCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Stale-While-Revalidate Pattern
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return cached data immediately, refresh in background
|
||||||
|
*/
|
||||||
|
async function staleWhileRevalidate<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
cacheKey: string,
|
||||||
|
fetchFn: () => Promise<T>,
|
||||||
|
ctx: ExecutionContext,
|
||||||
|
options: {
|
||||||
|
ttl?: number;
|
||||||
|
staleThreshold?: number; // Refresh if older than this
|
||||||
|
} = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const ttl = options.ttl ?? 3600;
|
||||||
|
const staleThreshold = options.staleThreshold ?? 300; // 5 minutes
|
||||||
|
|
||||||
|
// Get cached value with metadata
|
||||||
|
const { value, metadata } = await kv.getWithMetadata<
|
||||||
|
T,
|
||||||
|
{ timestamp: number }
|
||||||
|
>(cacheKey, { type: 'json' });
|
||||||
|
|
||||||
|
// If cached and not too stale, return immediately
|
||||||
|
if (value !== null && metadata) {
|
||||||
|
const age = Date.now() - metadata.timestamp;
|
||||||
|
|
||||||
|
// If stale, refresh in background
|
||||||
|
if (age > staleThreshold * 1000) {
|
||||||
|
ctx.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fresh = await fetchFn();
|
||||||
|
await kv.put(cacheKey, JSON.stringify(fresh), {
|
||||||
|
expirationTtl: ttl,
|
||||||
|
metadata: { timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Background refresh failed:', error);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - fetch and store
|
||||||
|
const data = await fetchFn();
|
||||||
|
await kv.put(cacheKey, JSON.stringify(data), {
|
||||||
|
expirationTtl: ttl,
|
||||||
|
metadata: { timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
app.get('/api/stats', async (c) => {
|
||||||
|
try {
|
||||||
|
const stats = await staleWhileRevalidate(
|
||||||
|
c.env.CACHE,
|
||||||
|
'global:stats',
|
||||||
|
async () => {
|
||||||
|
// Expensive computation
|
||||||
|
const result = await c.env.DB.prepare(
|
||||||
|
'SELECT COUNT(*) as total FROM users'
|
||||||
|
).first();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
c.executionCtx,
|
||||||
|
{
|
||||||
|
ttl: 3600, // Cache for 1 hour
|
||||||
|
staleThreshold: 300, // Refresh if older than 5 minutes
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
stats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Multi-Layer Cache (KV + Memory)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-tier cache: In-memory cache + KV cache
|
||||||
|
* Useful for frequently accessed data within same Worker instance
|
||||||
|
*/
|
||||||
|
const memoryCache = new Map<string, { value: any; expires: number }>();
|
||||||
|
|
||||||
|
async function getMultiLayerCache<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
cacheKey: string,
|
||||||
|
fetchFn: () => Promise<T>,
|
||||||
|
options: {
|
||||||
|
ttl?: number;
|
||||||
|
memoryTtl?: number; // In-memory cache duration
|
||||||
|
} = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const ttl = options.ttl ?? 3600;
|
||||||
|
const memoryTtl = (options.memoryTtl ?? 60) * 1000; // Convert to ms
|
||||||
|
|
||||||
|
// Check memory cache first (fastest)
|
||||||
|
const memoryCached = memoryCache.get(cacheKey);
|
||||||
|
if (memoryCached && memoryCached.expires > Date.now()) {
|
||||||
|
return memoryCached.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check KV cache (fast, global)
|
||||||
|
const kvCached = await kv.get<T>(cacheKey, {
|
||||||
|
type: 'json',
|
||||||
|
cacheTtl: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kvCached !== null) {
|
||||||
|
// Store in memory cache
|
||||||
|
memoryCache.set(cacheKey, {
|
||||||
|
value: kvCached,
|
||||||
|
expires: Date.now() + memoryTtl,
|
||||||
|
});
|
||||||
|
return kvCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - fetch from source
|
||||||
|
const data = await fetchFn();
|
||||||
|
|
||||||
|
// Store in both caches
|
||||||
|
memoryCache.set(cacheKey, {
|
||||||
|
value: data,
|
||||||
|
expires: Date.now() + memoryTtl,
|
||||||
|
});
|
||||||
|
|
||||||
|
await kv.put(cacheKey, JSON.stringify(data), {
|
||||||
|
expirationTtl: ttl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
app.get('/api/config', async (c) => {
|
||||||
|
try {
|
||||||
|
const config = await getMultiLayerCache(
|
||||||
|
c.env.CACHE,
|
||||||
|
'app:config',
|
||||||
|
async () => {
|
||||||
|
// Fetch from database or API
|
||||||
|
return {
|
||||||
|
theme: 'dark',
|
||||||
|
features: ['feature1', 'feature2'],
|
||||||
|
version: '1.0.0',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ttl: 3600, // KV cache: 1 hour
|
||||||
|
memoryTtl: 60, // Memory cache: 1 minute
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache Warming
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-populate cache with frequently accessed data
|
||||||
|
*/
|
||||||
|
app.post('/api/cache/warm', async (c) => {
|
||||||
|
try {
|
||||||
|
// Example: Warm cache with top 100 users
|
||||||
|
const topUsers = await c.env.DB.prepare(
|
||||||
|
'SELECT * FROM users ORDER BY activity DESC LIMIT 100'
|
||||||
|
).all();
|
||||||
|
|
||||||
|
// Store each user in cache
|
||||||
|
const promises = topUsers.results.map((user: any) =>
|
||||||
|
c.env.CACHE.put(`user:${user.id}`, JSON.stringify(user), {
|
||||||
|
expirationTtl: 3600,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Warmed cache with ${topUsers.results.length} users`,
|
||||||
|
count: topUsers.results.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache Statistics
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track cache hit/miss rates
|
||||||
|
*/
|
||||||
|
let cacheStats = {
|
||||||
|
hits: 0,
|
||||||
|
misses: 0,
|
||||||
|
errors: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.get('/api/cache/stats', (c) => {
|
||||||
|
const total = cacheStats.hits + cacheStats.misses;
|
||||||
|
const hitRate = total > 0 ? (cacheStats.hits / total) * 100 : 0;
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
hits: cacheStats.hits,
|
||||||
|
misses: cacheStats.misses,
|
||||||
|
errors: cacheStats.errors,
|
||||||
|
total,
|
||||||
|
hitRate: `${hitRate.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset stats
|
||||||
|
app.post('/api/cache/stats/reset', (c) => {
|
||||||
|
cacheStats = { hits: 0, misses: 0, errors: 0 };
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Cache stats reset',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (c) => {
|
||||||
|
return c.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
551
templates/kv-list-pagination.ts
Normal file
551
templates/kv-list-pagination.ts
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
/**
|
||||||
|
* Cloudflare Workers KV - List & Pagination Patterns
|
||||||
|
*
|
||||||
|
* This template demonstrates:
|
||||||
|
* - Basic listing with cursor pagination
|
||||||
|
* - Prefix filtering
|
||||||
|
* - Async iterator pattern
|
||||||
|
* - Batch processing
|
||||||
|
* - Key search and filtering
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
type Bindings = {
|
||||||
|
MY_KV: KVNamespace;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Bindings }>();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Basic Pagination
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// List keys with cursor pagination
|
||||||
|
app.get('/kv/list', async (c) => {
|
||||||
|
const prefix = c.req.query('prefix') || '';
|
||||||
|
const cursor = c.req.query('cursor');
|
||||||
|
const limit = parseInt(c.req.query('limit') || '100', 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await c.env.MY_KV.list({
|
||||||
|
prefix,
|
||||||
|
limit: Math.min(limit, 1000), // Max 1000
|
||||||
|
cursor: cursor || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
keys: result.keys.map((k) => ({
|
||||||
|
name: k.name,
|
||||||
|
expiration: k.expiration,
|
||||||
|
metadata: k.metadata,
|
||||||
|
})),
|
||||||
|
count: result.keys.length,
|
||||||
|
hasMore: !result.list_complete,
|
||||||
|
nextCursor: result.cursor,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Async Iterator Pattern
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async generator for paginating through all keys
|
||||||
|
*/
|
||||||
|
async function* paginateKeys(
|
||||||
|
kv: KVNamespace,
|
||||||
|
options: {
|
||||||
|
prefix?: string;
|
||||||
|
limit?: number;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await kv.list({
|
||||||
|
prefix: options.prefix,
|
||||||
|
limit: options.limit || 1000,
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield result.keys;
|
||||||
|
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all keys (fully paginated)
|
||||||
|
app.get('/kv/all', async (c) => {
|
||||||
|
const prefix = c.req.query('prefix') || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allKeys: any[] = [];
|
||||||
|
|
||||||
|
// Use async iterator to get all keys
|
||||||
|
for await (const batch of paginateKeys(c.env.MY_KV, { prefix })) {
|
||||||
|
allKeys.push(...batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
keys: allKeys.map((k) => k.name),
|
||||||
|
totalCount: allKeys.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Prefix Filtering
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// List keys by namespace prefix
|
||||||
|
app.get('/kv/namespace/:namespace', async (c) => {
|
||||||
|
const namespace = c.req.param('namespace');
|
||||||
|
const cursor = c.req.query('cursor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await c.env.MY_KV.list({
|
||||||
|
prefix: `${namespace}:`, // e.g., "user:", "session:", "cache:"
|
||||||
|
cursor: cursor || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
namespace,
|
||||||
|
keys: result.keys,
|
||||||
|
count: result.keys.length,
|
||||||
|
hasMore: !result.list_complete,
|
||||||
|
nextCursor: result.cursor,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count keys by prefix
|
||||||
|
app.get('/kv/count/:prefix', async (c) => {
|
||||||
|
const prefix = c.req.param('prefix');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let count = 0;
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({
|
||||||
|
prefix,
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
count += result.keys.length;
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
prefix,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Batch Processing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process keys in batches
|
||||||
|
*/
|
||||||
|
async function processBatches<T>(
|
||||||
|
kv: KVNamespace,
|
||||||
|
options: {
|
||||||
|
prefix?: string;
|
||||||
|
batchSize?: number;
|
||||||
|
},
|
||||||
|
processor: (keys: any[]) => Promise<T[]>
|
||||||
|
): Promise<T[]> {
|
||||||
|
const results: T[] = [];
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await kv.list({
|
||||||
|
prefix: options.prefix,
|
||||||
|
limit: options.batchSize || 100,
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process this batch
|
||||||
|
const batchResults = await processor(result.keys);
|
||||||
|
results.push(...batchResults);
|
||||||
|
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Export all keys with values
|
||||||
|
app.get('/kv/export', async (c) => {
|
||||||
|
const prefix = c.req.query('prefix') || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exported = await processBatches(
|
||||||
|
c.env.MY_KV,
|
||||||
|
{ prefix, batchSize: 100 },
|
||||||
|
async (keys) => {
|
||||||
|
// Get values for all keys in batch (bulk read)
|
||||||
|
const keyNames = keys.map((k) => k.name);
|
||||||
|
const values = await c.env.MY_KV.get(keyNames);
|
||||||
|
|
||||||
|
// Combine keys with values
|
||||||
|
return keys.map((key) => ({
|
||||||
|
key: key.name,
|
||||||
|
value: values.get(key.name),
|
||||||
|
metadata: key.metadata,
|
||||||
|
expiration: key.expiration,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: exported,
|
||||||
|
count: exported.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Search & Filtering
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Search keys by pattern (client-side filtering)
|
||||||
|
app.get('/kv/search', async (c) => {
|
||||||
|
const query = c.req.query('q') || '';
|
||||||
|
const prefix = c.req.query('prefix') || '';
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Query parameter "q" is required',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const matches: any[] = [];
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({
|
||||||
|
prefix,
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter keys that match the search query
|
||||||
|
const filteredKeys = result.keys.filter((key) =>
|
||||||
|
key.name.toLowerCase().includes(query.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
matches.push(...filteredKeys);
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
query,
|
||||||
|
matches: matches.map((k) => k.name),
|
||||||
|
count: matches.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter by metadata
|
||||||
|
app.get('/kv/filter/metadata', async (c) => {
|
||||||
|
const metadataKey = c.req.query('key');
|
||||||
|
const metadataValue = c.req.query('value');
|
||||||
|
|
||||||
|
if (!metadataKey) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Query parameter "key" is required',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const matches: any[] = [];
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({ cursor });
|
||||||
|
|
||||||
|
// Filter by metadata
|
||||||
|
const filteredKeys = result.keys.filter((key) => {
|
||||||
|
if (!key.metadata) return false;
|
||||||
|
|
||||||
|
return metadataValue
|
||||||
|
? key.metadata[metadataKey] === metadataValue
|
||||||
|
: metadataKey in key.metadata;
|
||||||
|
});
|
||||||
|
|
||||||
|
matches.push(...filteredKeys);
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
matches: matches.map((k) => ({
|
||||||
|
name: k.name,
|
||||||
|
metadata: k.metadata,
|
||||||
|
})),
|
||||||
|
count: matches.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cleanup & Maintenance
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Delete expired keys (manual cleanup)
|
||||||
|
app.post('/kv/cleanup/expired', async (c) => {
|
||||||
|
try {
|
||||||
|
let deletedCount = 0;
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({ cursor });
|
||||||
|
|
||||||
|
// Filter keys that have expired
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const expiredKeys = result.keys
|
||||||
|
.filter((key) => key.expiration && key.expiration < now)
|
||||||
|
.map((key) => key.name);
|
||||||
|
|
||||||
|
// Delete expired keys
|
||||||
|
await Promise.all(expiredKeys.map((key) => c.env.MY_KV.delete(key)));
|
||||||
|
|
||||||
|
deletedCount += expiredKeys.length;
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Deleted ${deletedCount} expired keys`,
|
||||||
|
deletedCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete all keys with prefix (DANGEROUS!)
|
||||||
|
app.post('/kv/delete/prefix', async (c) => {
|
||||||
|
const { prefix } = await c.req.json<{ prefix: string }>();
|
||||||
|
|
||||||
|
if (!prefix) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Prefix is required',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let deletedCount = 0;
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({ prefix, cursor });
|
||||||
|
|
||||||
|
// Delete batch
|
||||||
|
await Promise.all(result.keys.map((key) => c.env.MY_KV.delete(key.name)));
|
||||||
|
|
||||||
|
deletedCount += result.keys.length;
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Deleted ${deletedCount} keys with prefix "${prefix}"`,
|
||||||
|
deletedCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Namespace Statistics
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Get detailed namespace stats
|
||||||
|
app.get('/kv/stats/detailed', async (c) => {
|
||||||
|
try {
|
||||||
|
let totalKeys = 0;
|
||||||
|
let withMetadata = 0;
|
||||||
|
let withExpiration = 0;
|
||||||
|
const prefixes = new Map<string, number>();
|
||||||
|
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({ cursor });
|
||||||
|
|
||||||
|
totalKeys += result.keys.length;
|
||||||
|
|
||||||
|
// Analyze keys
|
||||||
|
for (const key of result.keys) {
|
||||||
|
if (key.metadata) withMetadata++;
|
||||||
|
if (key.expiration) withExpiration++;
|
||||||
|
|
||||||
|
// Extract prefix (before first ":")
|
||||||
|
const prefix = key.name.split(':')[0];
|
||||||
|
prefixes.set(prefix, (prefixes.get(prefix) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
totalKeys,
|
||||||
|
withMetadata,
|
||||||
|
withExpiration,
|
||||||
|
prefixCounts: Object.fromEntries(prefixes),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group keys by prefix
|
||||||
|
app.get('/kv/groups', async (c) => {
|
||||||
|
try {
|
||||||
|
const groups = new Map<string, string[]>();
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({ cursor });
|
||||||
|
|
||||||
|
for (const key of result.keys) {
|
||||||
|
const prefix = key.name.split(':')[0];
|
||||||
|
|
||||||
|
if (!groups.has(prefix)) {
|
||||||
|
groups.set(prefix, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.get(prefix)!.push(key.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
groups: Object.fromEntries(groups),
|
||||||
|
groupCount: groups.size,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (c) => {
|
||||||
|
return c.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
568
templates/kv-metadata-pattern.ts
Normal file
568
templates/kv-metadata-pattern.ts
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
/**
|
||||||
|
* Cloudflare Workers KV - Metadata Patterns
|
||||||
|
*
|
||||||
|
* This template demonstrates:
|
||||||
|
* - Storing data in metadata for list() efficiency
|
||||||
|
* - Metadata-based filtering
|
||||||
|
* - Versioning with metadata
|
||||||
|
* - Audit trails
|
||||||
|
* - Feature flags with metadata
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
type Bindings = {
|
||||||
|
MY_KV: KVNamespace;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Bindings }>();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Metadata Optimization Pattern
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store small values in metadata to avoid separate get() calls
|
||||||
|
* Maximum metadata size: 1024 bytes (JSON serialized)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ❌ BAD: Requires 2 operations per key
|
||||||
|
async function getStatusBad(kv: KVNamespace, userId: string) {
|
||||||
|
const status = await kv.get(`user:${userId}:status`);
|
||||||
|
const lastSeen = await kv.get(`user:${userId}:lastseen`);
|
||||||
|
return { status, lastSeen };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Single list() operation gets metadata for all users
|
||||||
|
async function getStatusGood(kv: KVNamespace) {
|
||||||
|
const users = await kv.list({ prefix: 'user:' });
|
||||||
|
|
||||||
|
return users.keys.map((key) => ({
|
||||||
|
userId: key.name.split(':')[1],
|
||||||
|
status: key.metadata?.status,
|
||||||
|
lastSeen: key.metadata?.lastSeen,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: User status with metadata
|
||||||
|
app.post('/users/:id/status', async (c) => {
|
||||||
|
const userId = c.req.param('id');
|
||||||
|
const { status } = await c.req.json<{ status: string }>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store empty value, all data in metadata
|
||||||
|
await c.env.MY_KV.put(`user:${userId}`, '', {
|
||||||
|
metadata: {
|
||||||
|
status,
|
||||||
|
lastSeen: Date.now(),
|
||||||
|
plan: 'free',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Status updated',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all users with status (no additional get() calls needed!)
|
||||||
|
app.get('/users/status', async (c) => {
|
||||||
|
try {
|
||||||
|
const users = await c.env.MY_KV.list({ prefix: 'user:' });
|
||||||
|
|
||||||
|
const statuses = users.keys.map((key) => ({
|
||||||
|
userId: key.name.split(':')[1],
|
||||||
|
status: key.metadata?.status || 'unknown',
|
||||||
|
lastSeen: key.metadata?.lastSeen,
|
||||||
|
plan: key.metadata?.plan,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
users: statuses,
|
||||||
|
count: statuses.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Versioning with Metadata
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface VersionedData {
|
||||||
|
content: any;
|
||||||
|
version: number;
|
||||||
|
updatedAt: number;
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write with versioning
|
||||||
|
app.put('/config/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const content = await c.req.json();
|
||||||
|
const updatedBy = c.req.header('X-User-ID') || 'system';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current version
|
||||||
|
const existing = await c.env.MY_KV.getWithMetadata<
|
||||||
|
VersionedData,
|
||||||
|
{ version: number }
|
||||||
|
>(`config:${key}`, { type: 'json' });
|
||||||
|
|
||||||
|
const currentVersion = existing.metadata?.version || 0;
|
||||||
|
const newVersion = currentVersion + 1;
|
||||||
|
|
||||||
|
// Store new version
|
||||||
|
const data: VersionedData = {
|
||||||
|
content,
|
||||||
|
version: newVersion,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
updatedBy,
|
||||||
|
};
|
||||||
|
|
||||||
|
await c.env.MY_KV.put(`config:${key}`, JSON.stringify(data), {
|
||||||
|
metadata: {
|
||||||
|
version: newVersion,
|
||||||
|
updatedAt: data.updatedAt,
|
||||||
|
updatedBy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store version history (optional)
|
||||||
|
await c.env.MY_KV.put(
|
||||||
|
`config:${key}:v${newVersion}`,
|
||||||
|
JSON.stringify(data),
|
||||||
|
{
|
||||||
|
expirationTtl: 86400 * 30, // Keep versions for 30 days
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
version: newVersion,
|
||||||
|
previousVersion: currentVersion,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get config with version info
|
||||||
|
app.get('/config/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
|
||||||
|
VersionedData,
|
||||||
|
{ version: number; updatedAt: number; updatedBy: string }
|
||||||
|
>(`config:${key}`, { type: 'json' });
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Config not found',
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: value,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get specific version
|
||||||
|
app.get('/config/:key/version/:version', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const version = c.req.param('version');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await c.env.MY_KV.get<VersionedData>(
|
||||||
|
`config:${key}:v${version}`,
|
||||||
|
{ type: 'json' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Version ${version} not found`,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Audit Trail with Metadata
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface AuditMetadata {
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedBy: string;
|
||||||
|
updatedAt: number;
|
||||||
|
accessCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write with audit trail
|
||||||
|
app.post('/data/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
const value = await c.req.text();
|
||||||
|
const userId = c.req.header('X-User-ID') || 'anonymous';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if key exists
|
||||||
|
const existing = await c.env.MY_KV.getWithMetadata<string, AuditMetadata>(
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
const metadata: AuditMetadata = existing.metadata
|
||||||
|
? {
|
||||||
|
...existing.metadata,
|
||||||
|
updatedBy: userId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
createdBy: userId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedBy: userId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
accessCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await c.env.MY_KV.put(key, value, { metadata });
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
audit: metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read with access tracking
|
||||||
|
app.get('/data/:key', async (c) => {
|
||||||
|
const key = c.req.param('key');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
|
||||||
|
string,
|
||||||
|
AuditMetadata
|
||||||
|
>(key);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Key not found',
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment access count (fire-and-forget)
|
||||||
|
if (metadata) {
|
||||||
|
c.executionCtx.waitUntil(
|
||||||
|
c.env.MY_KV.put(key, value, {
|
||||||
|
metadata: {
|
||||||
|
...metadata,
|
||||||
|
accessCount: metadata.accessCount + 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
value,
|
||||||
|
audit: metadata,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Feature Flags with Metadata
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface FeatureFlag {
|
||||||
|
enabled: boolean;
|
||||||
|
rolloutPercentage: number;
|
||||||
|
targetUsers?: string[];
|
||||||
|
metadata: {
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create feature flag
|
||||||
|
app.post('/flags/:name', async (c) => {
|
||||||
|
const name = c.req.param('name');
|
||||||
|
const flag = await c.req.json<FeatureFlag>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await c.env.MY_KV.put(`flag:${name}`, JSON.stringify(flag), {
|
||||||
|
metadata: {
|
||||||
|
enabled: flag.enabled,
|
||||||
|
rolloutPercentage: flag.rolloutPercentage,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Feature flag "${name}" created`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all feature flags (metadata only)
|
||||||
|
app.get('/flags', async (c) => {
|
||||||
|
try {
|
||||||
|
const flags = await c.env.MY_KV.list({ prefix: 'flag:' });
|
||||||
|
|
||||||
|
const flagList = flags.keys.map((key) => ({
|
||||||
|
name: key.name.replace('flag:', ''),
|
||||||
|
enabled: key.metadata?.enabled || false,
|
||||||
|
rolloutPercentage: key.metadata?.rolloutPercentage || 0,
|
||||||
|
updatedAt: key.metadata?.updatedAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
flags: flagList,
|
||||||
|
count: flagList.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check feature flag for user
|
||||||
|
app.get('/flags/:name/check/:userId', async (c) => {
|
||||||
|
const name = c.req.param('name');
|
||||||
|
const userId = c.req.param('userId');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const flag = await c.env.MY_KV.get<FeatureFlag>(`flag:${name}`, {
|
||||||
|
type: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!flag) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Feature flag not found',
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if enabled
|
||||||
|
if (!flag.enabled) {
|
||||||
|
return c.json({ enabled: false, reason: 'Flag disabled globally' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check target users
|
||||||
|
if (flag.targetUsers && flag.targetUsers.length > 0) {
|
||||||
|
const enabled = flag.targetUsers.includes(userId);
|
||||||
|
return c.json({
|
||||||
|
enabled,
|
||||||
|
reason: enabled ? 'User in target list' : 'User not in target list',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rollout percentage
|
||||||
|
const userHash =
|
||||||
|
parseInt(userId.split('').reduce((a, b) => a + b.charCodeAt(0), 0).toString()) %
|
||||||
|
100;
|
||||||
|
const enabled = userHash < flag.rolloutPercentage;
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
enabled,
|
||||||
|
reason: enabled
|
||||||
|
? `User in ${flag.rolloutPercentage}% rollout`
|
||||||
|
: `User not in ${flag.rolloutPercentage}% rollout`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Metadata Size Validation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Validate metadata size before writing
|
||||||
|
app.post('/validate/metadata', async (c) => {
|
||||||
|
const { metadata } = await c.req.json<{ metadata: any }>();
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(metadata);
|
||||||
|
const size = new TextEncoder().encode(serialized).length;
|
||||||
|
|
||||||
|
if (size > 1024) {
|
||||||
|
return c.json({
|
||||||
|
valid: false,
|
||||||
|
size,
|
||||||
|
maxSize: 1024,
|
||||||
|
error: `Metadata too large: ${size} bytes (max 1024)`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
valid: true,
|
||||||
|
size,
|
||||||
|
maxSize: 1024,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Metadata Migration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Migrate existing keys to add metadata
|
||||||
|
app.post('/migrate/add-metadata', async (c) => {
|
||||||
|
const { prefix, metadata } = await c.req.json<{
|
||||||
|
prefix: string;
|
||||||
|
metadata: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let migratedCount = 0;
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const result = await c.env.MY_KV.list({ prefix, cursor });
|
||||||
|
|
||||||
|
// Migrate batch
|
||||||
|
for (const key of result.keys) {
|
||||||
|
// Get existing value
|
||||||
|
const value = await c.env.MY_KV.get(key.name);
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
// Re-write with metadata
|
||||||
|
await c.env.MY_KV.put(key.name, value, {
|
||||||
|
metadata: {
|
||||||
|
...key.metadata,
|
||||||
|
...metadata,
|
||||||
|
migratedAt: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
migratedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = result.list_complete ? undefined : result.cursor;
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `Migrated ${migratedCount} keys`,
|
||||||
|
migratedCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (c) => {
|
||||||
|
return c.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
56
templates/wrangler-kv-config.jsonc
Normal file
56
templates/wrangler-kv-config.jsonc
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "my-worker",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2025-10-11",
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// KV Namespace Bindings
|
||||||
|
"kv_namespaces": [
|
||||||
|
{
|
||||||
|
// The binding name - accessible as env.CACHE in your Worker
|
||||||
|
"binding": "CACHE",
|
||||||
|
|
||||||
|
// Production namespace ID (from: wrangler kv namespace create CACHE)
|
||||||
|
"id": "<YOUR_PRODUCTION_NAMESPACE_ID>",
|
||||||
|
|
||||||
|
// Preview/local namespace ID (from: wrangler kv namespace create CACHE --preview)
|
||||||
|
// This is optional but recommended for local development
|
||||||
|
"preview_id": "<YOUR_PREVIEW_NAMESPACE_ID>"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Multiple namespaces example
|
||||||
|
{
|
||||||
|
"binding": "CONFIG",
|
||||||
|
"id": "<CONFIG_PRODUCTION_ID>",
|
||||||
|
"preview_id": "<CONFIG_PREVIEW_ID>"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"binding": "SESSIONS",
|
||||||
|
"id": "<SESSIONS_PRODUCTION_ID>",
|
||||||
|
"preview_id": "<SESSIONS_PREVIEW_ID>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// IMPORTANT NOTES:
|
||||||
|
//
|
||||||
|
// 1. Create namespaces first:
|
||||||
|
// npx wrangler kv namespace create CACHE
|
||||||
|
// npx wrangler kv namespace create CACHE --preview
|
||||||
|
//
|
||||||
|
// 2. Copy the IDs from the command output to this file
|
||||||
|
//
|
||||||
|
// 3. NEVER commit real namespace IDs to public repos
|
||||||
|
// Use environment variables for sensitive namespaces:
|
||||||
|
// "id": "${KV_CACHE_ID}"
|
||||||
|
//
|
||||||
|
// 4. preview_id is optional but recommended for local development
|
||||||
|
// It creates a separate namespace for testing
|
||||||
|
//
|
||||||
|
// 5. Binding names must be valid JavaScript identifiers
|
||||||
|
// Good: CACHE, MY_KV, UserData
|
||||||
|
// Bad: my-kv, user.data, 123kv
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user