mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-20 06:11:27 -07:00
update skills
This commit is contained in:
89
.agents/skills/cloudflare-deploy/references/kv/README.md
Normal file
89
.agents/skills/cloudflare-deploy/references/kv/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Cloudflare Workers KV
|
||||
|
||||
Globally-distributed, eventually-consistent key-value store optimized for high read volume and low latency.
|
||||
|
||||
## Overview
|
||||
|
||||
KV provides:
|
||||
- Eventual consistency (60s global propagation)
|
||||
- Read-optimized performance
|
||||
- 25 MiB value limit per key
|
||||
- Auto-replication to Cloudflare edge
|
||||
- Metadata support (1024 bytes)
|
||||
|
||||
**Use cases:** Config storage, user sessions, feature flags, caching, A/B testing
|
||||
|
||||
## When to Use KV
|
||||
|
||||
| Need | Recommendation |
|
||||
|------|----------------|
|
||||
| Strong consistency | → [Durable Objects](../durable-objects/) |
|
||||
| SQL queries | → [D1](../d1/) |
|
||||
| Object storage (files) | → [R2](../r2/) |
|
||||
| High read, low write volume | → KV ✅ |
|
||||
| Sub-10ms global reads | → KV ✅ |
|
||||
|
||||
**Quick comparison:**
|
||||
|
||||
| Feature | KV | D1 | Durable Objects |
|
||||
|---------|----|----|-----------------|
|
||||
| Consistency | Eventual | Strong | Strong |
|
||||
| Read latency | <10ms | ~50ms | <1ms |
|
||||
| Write limit | 1/s per key | Unlimited | Unlimited |
|
||||
| Use case | Config, cache | Relational data | Coordination |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
wrangler kv namespace create MY_NAMESPACE
|
||||
# Add binding to wrangler.jsonc
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Write
|
||||
await env.MY_KV.put("key", "value", { expirationTtl: 300 });
|
||||
|
||||
// Read
|
||||
const value = await env.MY_KV.get("key");
|
||||
const json = await env.MY_KV.get<Config>("config", "json");
|
||||
```
|
||||
|
||||
## Core Operations
|
||||
|
||||
| Method | Purpose | Returns |
|
||||
|--------|---------|---------|
|
||||
| `get(key, type?)` | Single read | `string \| null` |
|
||||
| `get(keys, type?)` | Bulk read (≤100) | `Map<string, T \| null>` |
|
||||
| `put(key, value, options?)` | Write | `Promise<void>` |
|
||||
| `delete(key)` | Delete | `Promise<void>` |
|
||||
| `list(options?)` | List keys | `{ keys, list_complete, cursor? }` |
|
||||
| `getWithMetadata(key)` | Get + metadata | `{ value, metadata }` |
|
||||
|
||||
## Consistency Model
|
||||
|
||||
- **Write visibility:** Immediate in same location, ≤60s globally
|
||||
- **Read path:** Eventually consistent
|
||||
- **Write rate:** 1 write/second per key (429 on exceed)
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read |
|
||||
|------|---------------|
|
||||
| Quick start | README → configuration.md |
|
||||
| Implement feature | README → api.md → patterns.md |
|
||||
| Debug issues | gotchas.md → api.md |
|
||||
| Batch operations | api.md (bulk section) → patterns.md |
|
||||
| Performance tuning | gotchas.md (performance) → patterns.md (caching) |
|
||||
|
||||
## In This Reference
|
||||
|
||||
- [configuration.md](./configuration.md) - wrangler.jsonc setup, namespace creation, TypeScript types
|
||||
- [api.md](./api.md) - KV methods, bulk operations, cacheTtl, content types
|
||||
- [patterns.md](./patterns.md) - Caching, sessions, rate limiting, A/B testing
|
||||
- [gotchas.md](./gotchas.md) - Eventual consistency, concurrent writes, value limits
|
||||
|
||||
## See Also
|
||||
|
||||
- [workers](../workers/) - Worker runtime for KV access
|
||||
- [d1](../d1/) - Use D1 for strong consistency needs
|
||||
- [durable-objects](../durable-objects/) - Strongly consistent alternative
|
||||
160
.agents/skills/cloudflare-deploy/references/kv/api.md
Normal file
160
.agents/skills/cloudflare-deploy/references/kv/api.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# KV API Reference
|
||||
|
||||
## Read Operations
|
||||
|
||||
```typescript
|
||||
// Single key (string)
|
||||
const value = await env.MY_KV.get("user:123");
|
||||
|
||||
// JSON type (auto-parsed)
|
||||
const config = await env.MY_KV.get<AppConfig>("config", "json");
|
||||
|
||||
// ArrayBuffer for binary
|
||||
const buffer = await env.MY_KV.get("image", "arrayBuffer");
|
||||
|
||||
// Stream for large values
|
||||
const stream = await env.MY_KV.get("large-file", "stream");
|
||||
|
||||
// With cache TTL (min 60s)
|
||||
const value = await env.MY_KV.get("key", { type: "text", cacheTtl: 300 });
|
||||
|
||||
// Bulk get (max 100 keys, counts as 1 operation)
|
||||
const keys = ["user:1", "user:2", "user:3", "missing:key"];
|
||||
const results = await env.MY_KV.get(keys);
|
||||
// Returns Map<string, string | null>
|
||||
|
||||
console.log(results.get("user:1")); // "John" (if exists)
|
||||
console.log(results.get("missing:key")); // null
|
||||
|
||||
// Process results with null handling
|
||||
for (const [key, value] of results) {
|
||||
if (value !== null) {
|
||||
// Handle found keys
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TypeScript with generics (type-safe JSON parsing)
|
||||
interface UserProfile { name: string; email: string; }
|
||||
const profile = await env.USERS.get<UserProfile>("user:123", "json");
|
||||
// profile is typed as UserProfile | null
|
||||
if (profile) {
|
||||
console.log(profile.name); // Type-safe access
|
||||
}
|
||||
|
||||
// Bulk get with type
|
||||
const configs = await env.MY_KV.get<Config>(["config:app", "config:feature"], "json");
|
||||
// Map<string, Config | null>
|
||||
```
|
||||
|
||||
## Write Operations
|
||||
|
||||
```typescript
|
||||
// Basic put
|
||||
await env.MY_KV.put("key", "value");
|
||||
await env.MY_KV.put("config", JSON.stringify({ theme: "dark" }));
|
||||
|
||||
// With expiration (UNIX timestamp)
|
||||
await env.MY_KV.put("session", token, {
|
||||
expiration: Math.floor(Date.now() / 1000) + 3600
|
||||
});
|
||||
|
||||
// With TTL (seconds from now, min 60)
|
||||
await env.MY_KV.put("cache", data, { expirationTtl: 300 });
|
||||
|
||||
// With metadata (max 1024 bytes)
|
||||
await env.MY_KV.put("user:profile", userData, {
|
||||
metadata: { version: 2, lastUpdated: Date.now() }
|
||||
});
|
||||
|
||||
// Combined
|
||||
await env.MY_KV.put("temp", value, {
|
||||
expirationTtl: 3600,
|
||||
metadata: { temporary: true }
|
||||
});
|
||||
```
|
||||
|
||||
## Get with Metadata
|
||||
|
||||
```typescript
|
||||
// Single key
|
||||
const result = await env.MY_KV.getWithMetadata("user:profile");
|
||||
// { value: string | null, metadata: any | null }
|
||||
|
||||
if (result.value && result.metadata) {
|
||||
const { version, lastUpdated } = result.metadata;
|
||||
}
|
||||
|
||||
// Multiple keys (bulk)
|
||||
const keys = ["key1", "key2", "key3"];
|
||||
const results = await env.MY_KV.getWithMetadata(keys);
|
||||
// Returns Map<string, { value, metadata, cacheStatus? }>
|
||||
|
||||
for (const [key, result] of results) {
|
||||
if (result.value) {
|
||||
console.log(`${key}: ${result.value}`);
|
||||
console.log(`Metadata: ${JSON.stringify(result.metadata)}`);
|
||||
// cacheStatus field indicates cache hit/miss (when available)
|
||||
}
|
||||
}
|
||||
|
||||
// With type
|
||||
const result = await env.MY_KV.getWithMetadata<UserData>("user:123", "json");
|
||||
// result: { value: UserData | null, metadata: any | null, cacheStatus?: string }
|
||||
```
|
||||
|
||||
## Delete Operations
|
||||
|
||||
```typescript
|
||||
await env.MY_KV.delete("key"); // Always succeeds (even if key missing)
|
||||
```
|
||||
|
||||
## List Operations
|
||||
|
||||
```typescript
|
||||
// List all
|
||||
const keys = await env.MY_KV.list();
|
||||
// { keys: [...], list_complete: boolean, cursor?: string }
|
||||
|
||||
// With prefix
|
||||
const userKeys = await env.MY_KV.list({ prefix: "user:" });
|
||||
|
||||
// Pagination
|
||||
let cursor: string | undefined;
|
||||
let allKeys = [];
|
||||
do {
|
||||
const result = await env.MY_KV.list({ cursor, limit: 1000 });
|
||||
allKeys.push(...result.keys);
|
||||
cursor = result.cursor;
|
||||
} while (!result.list_complete);
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Type Selection
|
||||
|
||||
| Type | Use Case | Performance |
|
||||
|------|----------|-------------|
|
||||
| `stream` | Large values (>1MB) | Fastest - no buffering |
|
||||
| `arrayBuffer` | Binary data | Fast - single allocation |
|
||||
| `text` | String values | Medium |
|
||||
| `json` | Objects (parse overhead) | Slowest - parsing cost |
|
||||
|
||||
### Parallel Reads
|
||||
|
||||
```typescript
|
||||
// Efficient parallel reads with Promise.all()
|
||||
const [user, settings, cache] = await Promise.all([
|
||||
env.USERS.get("user:123", "json"),
|
||||
env.SETTINGS.get("config:app", "json"),
|
||||
env.CACHE.get("data:latest")
|
||||
]);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Missing keys:** Return `null` (not an error)
|
||||
- **Rate limit (429):** Retry with exponential backoff (see gotchas.md)
|
||||
- **Response too large (413):** Values >25MB fail with 413 error
|
||||
|
||||
See [gotchas.md](./gotchas.md) for detailed error patterns and solutions.
|
||||
144
.agents/skills/cloudflare-deploy/references/kv/configuration.md
Normal file
144
.agents/skills/cloudflare-deploy/references/kv/configuration.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# KV Configuration
|
||||
|
||||
## Create Namespace
|
||||
|
||||
```bash
|
||||
wrangler kv namespace create MY_NAMESPACE
|
||||
# Output: { binding = "MY_NAMESPACE", id = "abc123..." }
|
||||
|
||||
wrangler kv namespace create MY_NAMESPACE --preview # For local dev
|
||||
```
|
||||
|
||||
## Workers Binding
|
||||
|
||||
**wrangler.jsonc:**
|
||||
```jsonc
|
||||
{
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "MY_KV",
|
||||
"id": "abc123xyz789"
|
||||
},
|
||||
// Optional: Different namespace for preview/development
|
||||
{
|
||||
"binding": "MY_KV",
|
||||
"preview_id": "preview-abc123"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
**env.d.ts:**
|
||||
```typescript
|
||||
interface Env {
|
||||
MY_KV: KVNamespace;
|
||||
SESSIONS: KVNamespace;
|
||||
CACHE: KVNamespace;
|
||||
}
|
||||
```
|
||||
|
||||
**worker.ts:**
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
// env.MY_KV is now typed as KVNamespace
|
||||
const value = await env.MY_KV.get("key");
|
||||
return new Response(value || "Not found");
|
||||
}
|
||||
} satisfies ExportedHandler<Env>;
|
||||
```
|
||||
|
||||
**Type-safe JSON operations:**
|
||||
```typescript
|
||||
interface UserProfile {
|
||||
name: string;
|
||||
email: string;
|
||||
role: "admin" | "user";
|
||||
}
|
||||
|
||||
const profile = await env.USERS.get<UserProfile>("user:123", "json");
|
||||
// profile: UserProfile | null (type-safe!)
|
||||
if (profile) {
|
||||
console.log(profile.name); // TypeScript knows this is a string
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Operations
|
||||
|
||||
```bash
|
||||
# Put
|
||||
wrangler kv key put --binding=MY_KV "key" "value"
|
||||
wrangler kv key put --binding=MY_KV "key" --path=./file.json --ttl=3600
|
||||
|
||||
# Get
|
||||
wrangler kv key get --binding=MY_KV "key"
|
||||
|
||||
# Delete
|
||||
wrangler kv key delete --binding=MY_KV "key"
|
||||
|
||||
# List
|
||||
wrangler kv key list --binding=MY_KV --prefix="user:"
|
||||
|
||||
# Bulk operations (max 10,000 keys per file)
|
||||
wrangler kv bulk put data.json --binding=MY_KV
|
||||
wrangler kv bulk get keys.json --binding=MY_KV
|
||||
wrangler kv bulk delete keys.json --binding=MY_KV --force
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
wrangler dev # Local KV (isolated)
|
||||
wrangler dev --remote # Remote KV (production)
|
||||
|
||||
# Or in wrangler.jsonc:
|
||||
# "kv_namespaces": [{ "binding": "MY_KV", "id": "...", "remote": true }]
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
### Single Operations
|
||||
|
||||
```typescript
|
||||
import Cloudflare from 'cloudflare';
|
||||
|
||||
const client = new Cloudflare({
|
||||
apiEmail: process.env.CLOUDFLARE_EMAIL,
|
||||
apiKey: process.env.CLOUDFLARE_API_KEY
|
||||
});
|
||||
|
||||
// Single key operations
|
||||
await client.kv.namespaces.values.update(namespaceId, 'key', {
|
||||
account_id: accountId,
|
||||
value: 'value',
|
||||
expiration_ttl: 3600
|
||||
});
|
||||
```
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```typescript
|
||||
// Bulk update (up to 10,000 keys, max 100MB total)
|
||||
await client.kv.namespaces.bulkUpdate(namespaceId, {
|
||||
account_id: accountId,
|
||||
body: [
|
||||
{ key: "key1", value: "value1", expiration_ttl: 3600 },
|
||||
{ key: "key2", value: "value2", metadata: { version: 1 } },
|
||||
{ key: "key3", value: "value3" }
|
||||
]
|
||||
});
|
||||
|
||||
// Bulk get (up to 100 keys)
|
||||
const results = await client.kv.namespaces.bulkGet(namespaceId, {
|
||||
account_id: accountId,
|
||||
keys: ["key1", "key2", "key3"]
|
||||
});
|
||||
|
||||
// Bulk delete (up to 10,000 keys)
|
||||
await client.kv.namespaces.bulkDelete(namespaceId, {
|
||||
account_id: accountId,
|
||||
keys: ["key1", "key2", "key3"]
|
||||
});
|
||||
```
|
||||
131
.agents/skills/cloudflare-deploy/references/kv/gotchas.md
Normal file
131
.agents/skills/cloudflare-deploy/references/kv/gotchas.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# KV Gotchas & Troubleshooting
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Stale Read After Write"
|
||||
|
||||
**Cause:** Eventual consistency means writes may not be immediately visible in other regions
|
||||
**Solution:** Don't read immediately after write; return confirmation without reading or use the local value you just wrote. Writes visible immediately in same location, ≤60s globally
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Read immediately after write
|
||||
await env.KV.put("key", "value");
|
||||
const value = await env.KV.get("key"); // May be null in other regions!
|
||||
|
||||
// ✅ GOOD: Use the value you just wrote
|
||||
const newValue = "value";
|
||||
await env.KV.put("key", newValue);
|
||||
return new Response(newValue); // Don't re-read
|
||||
```
|
||||
|
||||
### "429 Rate Limit on Concurrent Writes"
|
||||
|
||||
**Cause:** Multiple concurrent writes to same key exceeding 1 write/second limit
|
||||
**Solution:** Use sequential writes, unique keys for concurrent operations, or implement retry with exponential backoff
|
||||
|
||||
```typescript
|
||||
async function putWithRetry(
|
||||
kv: KVNamespace,
|
||||
key: string,
|
||||
value: string,
|
||||
maxAttempts = 5
|
||||
): Promise<void> {
|
||||
let delay = 1000;
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
await kv.put(key, value);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes("429")) {
|
||||
if (i === maxAttempts - 1) throw err;
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
delay *= 2; // Exponential backoff
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### "Inefficient Multiple Gets"
|
||||
|
||||
**Cause:** Making multiple individual get() calls instead of bulk operation
|
||||
**Solution:** Use bulk get with array of keys: `env.USERS.get(["user:1", "user:2", "user:3"])` to reduce to 1 operation
|
||||
|
||||
### "Null Reference Error"
|
||||
|
||||
**Cause:** Attempting to use value without checking for null when key doesn't exist
|
||||
**Solution:** Always handle null returns - KV returns `null` for missing keys, not undefined
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Assumes value exists
|
||||
const config = await env.KV.get("config", "json");
|
||||
return config.theme; // TypeError if null!
|
||||
|
||||
// ✅ GOOD: Null checks
|
||||
const config = await env.KV.get("config", "json");
|
||||
return config?.theme ?? "default";
|
||||
|
||||
// ✅ GOOD: Early return
|
||||
const config = await env.KV.get("config", "json");
|
||||
if (!config) return new Response("Not found", { status: 404 });
|
||||
return new Response(config.theme);
|
||||
```
|
||||
|
||||
### "Negative Lookup Caching"
|
||||
|
||||
**Cause:** Keys that don't exist are cached as "not found" for up to 60s
|
||||
**Solution:** Creating a key after checking won't be visible until cache expires
|
||||
|
||||
```typescript
|
||||
// Check → create pattern has race condition
|
||||
const exists = await env.KV.get("key"); // null, cached as "not found"
|
||||
if (!exists) {
|
||||
await env.KV.put("key", "value");
|
||||
// Next get() may still return null for ~60s due to negative cache
|
||||
}
|
||||
|
||||
// Alternative: Always assume key may not exist, use defaults
|
||||
const value = await env.KV.get("key") ?? "default-value";
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
| Scenario | Recommendation | Why |
|
||||
|----------|----------------|-----|
|
||||
| Large values (>1MB) | Use `stream` type | Avoids buffering entire value in memory |
|
||||
| Many small keys | Coalesce into one JSON object | Reduces operations, improves cache hit rate |
|
||||
| High write volume | Spread across different keys | Avoid 1 write/second per-key limit |
|
||||
| Cold reads | Increase `cacheTtl` parameter | Reduces latency for frequently-read data |
|
||||
| Bulk operations | Use array form of get() | Single operation, better performance |
|
||||
|
||||
## Cost Examples
|
||||
|
||||
**Free tier:**
|
||||
- 100K reads/day = 3M/month ✅
|
||||
- 1K writes/day = 30K/month ✅
|
||||
- 1GB storage ✅
|
||||
|
||||
**Example paid workload:**
|
||||
- 10M reads/month = $5.00
|
||||
- 100K writes/month = $0.50
|
||||
- 1GB storage = $0.50
|
||||
- **Total: ~$6/month**
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Key size | 512 bytes | Maximum key length |
|
||||
| Value size | 25 MiB | Maximum value; 413 error if exceeded |
|
||||
| Metadata size | 1024 bytes | Maximum metadata per key |
|
||||
| cacheTtl minimum | 60s | Minimum cache TTL |
|
||||
| Write rate per key | 1 write/second | All plans; 429 error if exceeded |
|
||||
| Propagation time | ≤60s | Global propagation time |
|
||||
| Bulk get max | 100 keys | Maximum keys per bulk operation |
|
||||
| Operations per Worker | 1,000 | Per request (bulk counts as 1) |
|
||||
| Reads pricing | $0.50 per 10M | Per million reads |
|
||||
| Writes pricing | $5.00 per 1M | Per million writes |
|
||||
| Deletes pricing | $5.00 per 1M | Per million deletes |
|
||||
| Storage pricing | $0.50 per GB-month | Per GB per month |
|
||||
196
.agents/skills/cloudflare-deploy/references/kv/patterns.md
Normal file
196
.agents/skills/cloudflare-deploy/references/kv/patterns.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# KV Patterns & Best Practices
|
||||
|
||||
## Multi-Tier Caching
|
||||
|
||||
```typescript
|
||||
// Memory → KV → Origin (3-tier cache)
|
||||
const memoryCache = new Map<string, { data: any; expires: number }>();
|
||||
|
||||
async function getCached(env: Env, key: string): Promise<any> {
|
||||
const now = Date.now();
|
||||
|
||||
// L1: Memory cache (fastest)
|
||||
const cached = memoryCache.get(key);
|
||||
if (cached && cached.expires > now) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
// L2: KV cache (fast)
|
||||
const kvValue = await env.CACHE.get(key, "json");
|
||||
if (kvValue) {
|
||||
memoryCache.set(key, { data: kvValue, expires: now + 60000 }); // 1min in memory
|
||||
return kvValue;
|
||||
}
|
||||
|
||||
// L3: Origin (slow)
|
||||
const origin = await fetch(`https://api.example.com/${key}`).then(r => r.json());
|
||||
|
||||
// Backfill caches
|
||||
await env.CACHE.put(key, JSON.stringify(origin), { expirationTtl: 300 }); // 5min in KV
|
||||
memoryCache.set(key, { data: origin, expires: now + 60000 });
|
||||
|
||||
return origin;
|
||||
}
|
||||
```
|
||||
|
||||
## API Response Caching
|
||||
|
||||
```typescript
|
||||
async function getCachedData(env: Env, key: string, fetcher: () => Promise<any>): Promise<any> {
|
||||
const cached = await env.MY_KV.get(key, "json");
|
||||
if (cached) return cached;
|
||||
|
||||
const data = await fetcher();
|
||||
await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: 300 });
|
||||
return data;
|
||||
}
|
||||
|
||||
const apiData = await getCachedData(
|
||||
env,
|
||||
"cache:users",
|
||||
() => fetch("https://api.example.com/users").then(r => r.json())
|
||||
);
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
```typescript
|
||||
interface Session { userId: string; expiresAt: number; }
|
||||
|
||||
async function createSession(env: Env, userId: string): Promise<string> {
|
||||
const sessionId = crypto.randomUUID();
|
||||
const expiresAt = Date.now() + (24 * 60 * 60 * 1000);
|
||||
|
||||
await env.SESSIONS.put(
|
||||
`session:${sessionId}`,
|
||||
JSON.stringify({ userId, expiresAt }),
|
||||
{ expirationTtl: 86400, metadata: { createdAt: Date.now() } }
|
||||
);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
async function getSession(env: Env, sessionId: string): Promise<Session | null> {
|
||||
const data = await env.SESSIONS.get<Session>(`session:${sessionId}`, "json");
|
||||
if (!data || data.expiresAt < Date.now()) return null;
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
## Coalesce Cold Keys
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Many individual keys
|
||||
await env.KV.put("user:123:name", "John");
|
||||
await env.KV.put("user:123:email", "john@example.com");
|
||||
|
||||
// ✅ GOOD: Single coalesced object
|
||||
await env.USERS.put("user:123:profile", JSON.stringify({
|
||||
name: "John",
|
||||
email: "john@example.com",
|
||||
role: "admin"
|
||||
}));
|
||||
|
||||
// Benefits: Hot key cache, single read, reduced operations
|
||||
// Trade-off: Harder to update individual fields
|
||||
```
|
||||
|
||||
## Prefix-Based Namespacing
|
||||
|
||||
```typescript
|
||||
// Logical partitioning within single namespace
|
||||
const PREFIXES = {
|
||||
users: "user:",
|
||||
sessions: "session:",
|
||||
cache: "cache:",
|
||||
features: "feature:"
|
||||
} as const;
|
||||
|
||||
// Write with prefix
|
||||
async function setUser(env: Env, id: string, data: any) {
|
||||
await env.KV.put(`${PREFIXES.users}${id}`, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// Read with prefix
|
||||
async function getUser(env: Env, id: string) {
|
||||
return await env.KV.get(`${PREFIXES.users}${id}`, "json");
|
||||
}
|
||||
|
||||
// List by prefix
|
||||
async function listUserIds(env: Env): Promise<string[]> {
|
||||
const result = await env.KV.list({ prefix: PREFIXES.users });
|
||||
return result.keys.map(k => k.name.replace(PREFIXES.users, ""));
|
||||
}
|
||||
|
||||
// Example hierarchy
|
||||
"user:123:profile"
|
||||
"user:123:settings"
|
||||
"cache:api:users"
|
||||
"session:abc-def"
|
||||
"feature:flags:beta"
|
||||
```
|
||||
|
||||
## Metadata Versioning
|
||||
|
||||
```typescript
|
||||
interface VersionedData {
|
||||
version: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
async function migrateIfNeeded(env: Env, key: string) {
|
||||
const result = await env.DATA.getWithMetadata(key, "json");
|
||||
|
||||
if (!result.value) return null;
|
||||
|
||||
const currentVersion = result.metadata?.version || 1;
|
||||
const targetVersion = 2;
|
||||
|
||||
if (currentVersion < targetVersion) {
|
||||
// Migrate data format
|
||||
const migrated = migrate(result.value, currentVersion, targetVersion);
|
||||
|
||||
// Store with new version
|
||||
await env.DATA.put(key, JSON.stringify(migrated), {
|
||||
metadata: { version: targetVersion, migratedAt: Date.now() }
|
||||
});
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
function migrate(data: any, from: number, to: number): any {
|
||||
if (from === 1 && to === 2) {
|
||||
// V1 → V2: Rename field
|
||||
return { ...data, userName: data.name };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Boundary Pattern
|
||||
|
||||
```typescript
|
||||
// Resilient get with fallback
|
||||
async function resilientGet<T>(
|
||||
env: Env,
|
||||
key: string,
|
||||
fallback: T
|
||||
): Promise<T> {
|
||||
try {
|
||||
const value = await env.KV.get<T>(key, "json");
|
||||
return value ?? fallback;
|
||||
} catch (err) {
|
||||
console.error(`KV error for ${key}:`, err);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const config = await resilientGet(env, "config:app", {
|
||||
theme: "light",
|
||||
maxItems: 10
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user