Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:13 +08:00
commit cfc837568a
22 changed files with 5232 additions and 0 deletions

306
references/alarms-api.md Normal file
View File

@@ -0,0 +1,306 @@
# Alarms API - Scheduled Tasks
Complete guide to scheduling future tasks with alarms.
---
## What are Alarms?
Alarms allow Durable Objects to **schedule themselves** to wake up at a specific time in the future.
**Use Cases:**
- Batching (accumulate items, process in bulk)
- Cleanup (delete old data periodically)
- Reminders (notifications, alerts)
- Delayed operations (rate limiting reset)
- Periodic tasks (health checks, sync)
---
## Set Alarm
### `storage.setAlarm(time)`
```typescript
// Fire in 10 seconds
await this.ctx.storage.setAlarm(Date.now() + 10000);
// Fire at specific date/time
await this.ctx.storage.setAlarm(new Date('2025-12-31T23:59:59Z'));
// Fire in 1 hour
await this.ctx.storage.setAlarm(Date.now() + 3600000);
```
**Parameters:**
- `time` (number | Date): Unix timestamp (ms) or Date object
**Behavior:**
-**Only ONE alarm per DO** - setting new alarm overwrites previous
-**Persists across hibernation** - survives DO eviction
-**Guaranteed at-least-once execution**
---
## Alarm Handler
### `alarm(alarmInfo)`
Called when alarm fires (or retries).
```typescript
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
console.log(`Alarm fired (retry: ${alarmInfo.isRetry}, count: ${alarmInfo.retryCount})`);
// Do work
await this.processBatch();
// Alarm is automatically deleted after successful execution
}
```
**Parameters:**
- `alarmInfo.retryCount` (number): Number of retries (0 on first attempt)
- `alarmInfo.isRetry` (boolean): True if this is a retry
**CRITICAL:**
-**Implement idempotent operations** (safe to retry)
-**Limit retry attempts** (avoid infinite retries)
-**Don't throw errors lightly** (triggers automatic retry)
---
## Get Alarm
### `storage.getAlarm()`
Get current alarm time (null if not set).
```typescript
const alarmTime = await this.ctx.storage.getAlarm();
if (alarmTime === null) {
// No alarm set
await this.ctx.storage.setAlarm(Date.now() + 60000);
} else {
console.log(`Alarm scheduled for ${new Date(alarmTime).toISOString()}`);
}
```
**Returns:** Promise<number | null> (Unix timestamp in ms)
---
## Delete Alarm
### `storage.deleteAlarm()`
Cancel scheduled alarm.
```typescript
await this.ctx.storage.deleteAlarm();
```
**When to use:**
- Cancel scheduled task
- Before deleting DO (if using `deleteAll()`)
---
## Retry Behavior
**Automatic Retries:**
- Up to **6 retries** on failure
- Exponential backoff: **2s, 4s, 8s, 16s, 32s, 64s**
- Retries if `alarm()` throws uncaught exception
**Example with retry limit:**
```typescript
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
if (alarmInfo.retryCount > 3) {
console.error('Alarm failed after 3 retries, giving up');
// Clean up to avoid infinite retries
return;
}
try {
await this.sendNotification();
} catch (error) {
console.error('Alarm failed:', error);
throw error; // Will trigger retry
}
}
```
---
## Common Patterns
### Pattern 1: Batching
Accumulate items, process in bulk.
```typescript
async addItem(item: string): Promise<void> {
this.buffer.push(item);
await this.ctx.storage.put('buffer', this.buffer);
// Schedule alarm if not already set
const alarm = await this.ctx.storage.getAlarm();
if (alarm === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000); // 10s
}
}
async alarm(): Promise<void> {
this.buffer = await this.ctx.storage.get('buffer') || [];
if (this.buffer.length > 0) {
await this.processBatch(this.buffer);
this.buffer = [];
await this.ctx.storage.put('buffer', []);
}
// Alarm automatically deleted after success
}
```
### Pattern 2: Periodic Cleanup
Run cleanup every hour.
```typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Schedule first cleanup
ctx.blockConcurrencyWhile(async () => {
const alarm = await ctx.storage.getAlarm();
if (alarm === null) {
await ctx.storage.setAlarm(Date.now() + 3600000); // 1 hour
}
});
}
async alarm(): Promise<void> {
// Cleanup old data
await this.cleanup();
// Schedule next cleanup
await this.ctx.storage.setAlarm(Date.now() + 3600000);
}
```
### Pattern 3: Delayed Operation
Execute task after delay.
```typescript
async scheduleTask(task: string, delayMs: number): Promise<void> {
await this.ctx.storage.put('pendingTask', task);
await this.ctx.storage.setAlarm(Date.now() + delayMs);
}
async alarm(): Promise<void> {
const task = await this.ctx.storage.get('pendingTask');
if (task) {
await this.executeTask(task);
await this.ctx.storage.delete('pendingTask');
}
}
```
### Pattern 4: Reminder/Notification
One-time reminder.
```typescript
async setReminder(message: string, fireAt: Date): Promise<void> {
await this.ctx.storage.put('reminder', { message, fireAt: fireAt.getTime() });
await this.ctx.storage.setAlarm(fireAt);
}
async alarm(): Promise<void> {
const reminder = await this.ctx.storage.get('reminder');
if (reminder) {
await this.sendNotification(reminder.message);
await this.ctx.storage.delete('reminder');
}
}
```
---
## Limitations
⚠️ **One alarm per DO**
- Setting new alarm overwrites previous
- Use storage to track multiple pending tasks
⚠️ **No cron syntax**
- Alarm is one-time (but can reschedule in handler)
- For periodic tasks, reschedule in `alarm()` handler
⚠️ **Minimum precision: ~1 second**
- Don't expect millisecond precision
- Designed for longer delays (seconds to hours)
---
## Best Practices
### Idempotent Operations
```typescript
// ✅ GOOD: Idempotent (safe to retry)
async alarm(): Promise<void> {
const messageId = await this.ctx.storage.get('messageId');
// Check if already sent (idempotent)
const sent = await this.checkIfSent(messageId);
if (sent) {
return;
}
await this.sendMessage(messageId);
await this.markAsSent(messageId);
}
// ❌ BAD: Not idempotent (duplicate sends on retry)
async alarm(): Promise<void> {
await this.sendMessage(); // Will send duplicate if retried
}
```
### Limit Retries
```typescript
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
return;
}
// Try operation
await this.doWork();
}
```
### Clean Up Before `deleteAll()`
```typescript
async destroy(): Promise<void> {
// Delete alarm first
await this.ctx.storage.deleteAlarm();
// Then delete all storage
await this.ctx.storage.deleteAll();
}
```
---
**Official Docs**: https://developers.cloudflare.com/durable-objects/api/alarms/

View File

@@ -0,0 +1,416 @@
# Durable Objects Best Practices
Production patterns and optimization strategies.
---
## Performance
### Minimize Constructor Work
Heavy work in constructor delays request handling and hibernation wake-up.
```typescript
// ✅ GOOD
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Minimal initialization
this.sessions = new Map();
// Load from storage with blockConcurrencyWhile
ctx.blockConcurrencyWhile(async () => {
this.data = await ctx.storage.get('data') || defaultData;
});
}
// ❌ BAD
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Expensive operations delay all requests
await this.loadMassiveDataset();
await this.computeComplexState();
}
```
### Use Indexes for SQL Queries
```typescript
// Create indexes for frequently queried columns
this.sql.exec(`
CREATE INDEX IF NOT EXISTS idx_user_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages(created_at);
`);
// Use EXPLAIN QUERY PLAN to verify index usage
const plan = this.sql.exec('EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?', email);
```
### Batch Operations
```typescript
// ✅ GOOD: Batch inserts
this.sql.exec(`INSERT INTO messages (text, user_id) VALUES ${rows.map(() => '(?, ?)').join(', ')}`, ...flatValues);
// ❌ BAD: Individual inserts
for (const row of rows) {
this.sql.exec('INSERT INTO messages (text, user_id) VALUES (?, ?)', row.text, row.userId);
}
```
### Use Transactions
```typescript
// Atomic multi-step operations
this.ctx.storage.transactionSync(() => {
this.sql.exec('UPDATE users SET balance = balance - ? WHERE id = ?', amount, senderId);
this.sql.exec('UPDATE users SET balance = balance + ? WHERE id = ?', amount, receiverId);
this.sql.exec('INSERT INTO transactions ...');
});
```
---
## Cost Optimization
### Use WebSocket Hibernation
```typescript
// ✅ GOOD: Hibernates when idle (~90% cost savings)
this.ctx.acceptWebSocket(server);
// ❌ BAD: Never hibernates (high duration charges)
server.accept();
```
### Use Alarms, Not setTimeout
```typescript
// ✅ GOOD: Allows hibernation
await this.ctx.storage.setAlarm(Date.now() + 60000);
// ❌ BAD: Prevents hibernation
setTimeout(() => this.doWork(), 60000);
```
### Minimize Storage Size
```typescript
// Periodic cleanup with alarms
async alarm(): Promise<void> {
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
this.sql.exec('DELETE FROM messages WHERE created_at < ?', oneDayAgo);
// Schedule next cleanup
await this.ctx.storage.setAlarm(Date.now() + 3600000);
}
```
---
## Reliability
### Implement Idempotent Operations
```typescript
// ✅ GOOD: Idempotent (safe to retry)
async processPayment(paymentId: string, amount: number): Promise<void> {
// Check if already processed
const existing = await this.ctx.storage.get(`payment:${paymentId}`);
if (existing) {
return; // Already processed
}
// Process payment
await this.chargeCustomer(amount);
// Mark as processed
await this.ctx.storage.put(`payment:${paymentId}`, { processed: true, amount });
}
// ❌ BAD: Not idempotent (duplicate charges on retry)
async processPayment(amount: number): Promise<void> {
await this.chargeCustomer(amount);
}
```
### Limit Alarm Retries
```typescript
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
await this.logFailure();
return;
}
await this.doWork();
}
```
### Graceful Error Handling
```typescript
async processMessage(message: string): Promise<void> {
try {
await this.handleMessage(message);
} catch (error) {
console.error('Message processing failed:', error);
// Store failed message for retry
await this.ctx.storage.put(`failed:${Date.now()}`, message);
// Don't throw - prevents retry storm
}
}
```
---
## Security
### Validate Input
```typescript
async createUser(email: string, username: string): Promise<void> {
// Validate input
if (!email || !email.includes('@')) {
throw new Error('Invalid email');
}
if (!username || username.length < 3) {
throw new Error('Invalid username');
}
// Use parameterized queries (prevents SQL injection)
this.sql.exec(
'INSERT INTO users (email, username) VALUES (?, ?)',
email,
username
);
}
```
### Use Parameterized Queries
```typescript
// ✅ GOOD: Parameterized (safe from SQL injection)
this.sql.exec('SELECT * FROM users WHERE email = ?', userEmail);
// ❌ BAD: String concatenation (SQL injection risk)
this.sql.exec(`SELECT * FROM users WHERE email = '${userEmail}'`);
```
### Authenticate Requests
```typescript
async fetch(request: Request): Promise<Response> {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !this.validateToken(authHeader)) {
return new Response('Unauthorized', { status: 401 });
}
// Handle authenticated request
}
```
---
## Data Management
### Monitor Storage Size
```typescript
async getStorageSize(): Promise<number> {
// Approximate size (sum of all values)
const map = await this.ctx.storage.list();
let size = 0;
for (const value of map.values()) {
size += JSON.stringify(value).length;
}
return size;
}
async checkStorageLimit(): Promise<void> {
const size = await this.getStorageSize();
if (size > 900_000_000) { // 900MB (90% of 1GB limit)
console.warn('Storage approaching limit');
await this.triggerCleanup();
}
}
```
### Cleanup Old Data
```typescript
// Regular cleanup with alarms
async alarm(): Promise<void> {
const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000); // 30 days
this.sql.exec('DELETE FROM messages WHERE created_at < ?', cutoff);
// Schedule next cleanup
await this.ctx.storage.setAlarm(Date.now() + 86400000); // 24 hours
}
```
### Backup Critical Data
```typescript
async backup(): Promise<void> {
// Export to R2 or D1
const data = await this.exportData();
await this.env.BUCKET.put(`backup-${Date.now()}.json`, JSON.stringify(data));
}
```
---
## Testing
### Local Development
```bash
# Start local dev server
npx wrangler dev
# Test with curl
curl -X POST http://localhost:8787/api/increment
```
### Integration Tests
```typescript
// Test DO behavior
describe('Counter DO', () => {
it('should increment', async () => {
const stub = env.COUNTER.getByName('test-counter');
const count1 = await stub.increment();
expect(count1).toBe(1);
const count2 = await stub.increment();
expect(count2).toBe(2);
});
});
```
### Simulate Hibernation
```typescript
// Test hibernation wake-up
constructor(ctx, env) {
super(ctx, env);
console.log('DO woke up!', {
websockets: ctx.getWebSockets().length,
});
// Restore state
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
```
---
## Monitoring
### Log Important Events
```typescript
async importantOperation(): Promise<void> {
console.log('Starting important operation', {
doId: this.ctx.id.toString(),
timestamp: Date.now(),
});
await this.doWork();
console.log('Important operation completed');
}
```
### Track Metrics
```typescript
async recordMetric(metric: string, value: number): Promise<void> {
// Store metrics
await this.ctx.storage.put(`metric:${metric}:${Date.now()}`, value);
// Or send to Analytics Engine
// await this.env.ANALYTICS.writeDataPoint({
// indexes: [metric],
// doubles: [value],
// });
}
```
### Use Tail Logs
```bash
# Tail live logs
npx wrangler tail
# Filter by DO
npx wrangler tail --search "DurableObject"
```
---
## Common Patterns
### Rate Limiting
```typescript
async checkRateLimit(userId: string, limit: number, window: number): Promise<boolean> {
const key = `rate:${userId}`;
const now = Date.now();
const requests = await this.ctx.storage.get<number[]>(key) || [];
const validRequests = requests.filter(t => now - t < window);
if (validRequests.length >= limit) {
return false; // Rate limited
}
validRequests.push(now);
await this.ctx.storage.put(key, validRequests);
return true;
}
```
### Leader Election
```typescript
async electLeader(workerId: string): Promise<boolean> {
try {
this.sql.exec(
'INSERT INTO leader (id, worker_id) VALUES (1, ?)',
workerId
);
return true; // Became leader
} catch {
return false; // Someone else is leader
}
}
```
### Session Management
See `templates/state-api-patterns.ts` for complete example.
---
**Official Docs**: https://developers.cloudflare.com/durable-objects/best-practices/

View File

@@ -0,0 +1,384 @@
# Durable Objects Migrations Guide
Complete guide to managing DO class lifecycles with migrations.
---
## Why Migrations?
Migrations tell Cloudflare Workers runtime about changes to Durable Object classes:
**Required for:**
- ✅ Creating new DO class
- ✅ Renaming DO class
- ✅ Deleting DO class
- ✅ Transferring DO class to another Worker
**NOT required for:**
- ❌ Code changes to existing DO class
- ❌ Storage schema changes within DO
---
## Migration Types
### 1. Create New DO Class
```jsonc
{
"durable_objects": {
"bindings": [
{
"name": "COUNTER",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1", // Unique migration identifier
"new_sqlite_classes": [ // SQLite backend (recommended)
"Counter"
]
}
]
}
```
**For KV backend (legacy):**
```jsonc
{
"migrations": [
{
"tag": "v1",
"new_classes": ["Counter"] // KV backend (128MB limit)
}
]
}
```
**CRITICAL:**
- ✅ Use `new_sqlite_classes` for new DOs (1GB storage, atomic operations)
-**Cannot** change KV backend to SQLite after deployment
---
### 2. Rename DO Class
```jsonc
{
"durable_objects": {
"bindings": [
{
"name": "MY_DO",
"class_name": "NewClassName" // Updated class name
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["OldClassName"]
},
{
"tag": "v2", // New migration tag
"renamed_classes": [
{
"from": "OldClassName",
"to": "NewClassName"
}
]
}
]
}
```
**What happens:**
- ✅ All existing DO instances keep their data
- ✅ Old bindings automatically forward to new class
-`idFromName('foo')` still routes to same instance
- ⚠️ **Must export new class** in Worker code
---
### 3. Delete DO Class
```jsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter"]
},
{
"tag": "v2",
"deleted_classes": ["Counter"] // Mark for deletion
}
]
}
```
**What happens:**
- ✅ All DO instances **immediately deleted**
- ✅ All storage **permanently deleted**
- ⚠️ **CANNOT UNDO** - data is gone forever
**Before deleting:**
1. Export data if needed
2. Update Workers that reference this DO
3. Consider rename instead (if migrating)
---
### 4. Transfer DO Class to Another Worker
**Destination Worker:**
```jsonc
{
"durable_objects": {
"bindings": [
{
"name": "TRANSFERRED_DO",
"class_name": "TransferredClass"
}
]
},
"migrations": [
{
"tag": "v1",
"transferred_classes": [
{
"from": "OriginalClass",
"from_script": "original-worker", // Source Worker name
"to": "TransferredClass"
}
]
}
]
}
```
**What happens:**
- ✅ DO instances move to new Worker
- ✅ All storage is transferred
- ✅ Old bindings automatically forward to new Worker
- ⚠️ **Must export new class** in destination Worker
---
## Migration Rules
### Tags Must Be Unique
```jsonc
// ✅ CORRECT
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["A"] },
{ "tag": "v2", "new_sqlite_classes": ["B"] },
{ "tag": "v3", "renamed_classes": [{ "from": "A", "to": "C" }] }
]
}
// ❌ WRONG: Duplicate tag
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["A"] },
{ "tag": "v1", "new_sqlite_classes": ["B"] } // ERROR
]
}
```
### Tags Are Append-Only
```jsonc
// ✅ CORRECT: Add new tag
{
"migrations": [
{ "tag": "v1", ... },
{ "tag": "v2", ... } // Append
]
}
// ❌ WRONG: Remove or reorder
{
"migrations": [
{ "tag": "v2", ... } // Can't remove v1
]
}
```
### Migrations Are Atomic
⚠️ **Cannot use gradual deployments** with migrations
- All DO instances migrate at once when you deploy
- No partial rollout support
- Use canary releases at Worker level, not DO level
---
## Migration Gotchas
### Global Uniqueness
DO class names are **globally unique per account**.
```typescript
// Worker A
export class Counter extends DurableObject { }
// Worker B
export class Counter extends DurableObject { }
// ❌ ERROR: Class name "Counter" already exists in account
```
**Solution:** Use unique class names (e.g., prefix with Worker name)
```typescript
// Worker A
export class CounterA extends DurableObject { }
// Worker B
export class CounterB extends DurableObject { }
```
### Cannot Enable SQLite on Existing KV-backed DO
```jsonc
// Deployed with:
{ "tag": "v1", "new_classes": ["Counter"] } // KV backend
// ❌ WRONG: Cannot change to SQLite
{ "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterSQLite" }] }
{ "tag": "v3", "new_sqlite_classes": ["CounterSQLite"] }
// ✅ CORRECT: Create new class instead
{ "tag": "v2", "new_sqlite_classes": ["CounterV2"] }
// Then migrate data from Counter to CounterV2
```
### Code Changes Don't Need Migrations
```typescript
// ✅ CORRECT: Just deploy code changes
export class Counter extends DurableObject {
async increment(): Promise<number> {
// Changed implementation
let value = await this.ctx.storage.get<number>('count') || 0;
value += 2; // Changed from += 1
await this.ctx.storage.put('count', value);
return value;
}
}
// No migration needed - deploy directly
```
Only schema changes (new/rename/delete/transfer) need migrations.
---
## Environment-Specific Migrations
You can define migrations per environment:
```jsonc
{
// Top-level (default) migrations
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
],
"env": {
"production": {
// Production-specific migrations override top-level
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] },
{ "tag": "v2", "new_sqlite_classes": ["Analytics"] }
]
}
}
}
```
**Rules:**
- If migration defined at environment level, it overrides top-level
- If NOT defined at environment level, inherits top-level
---
## Migration Workflow
### Example: Rename DO Class
**Step 1:** Current state (v1)
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "MY_DO", "class_name": "OldName" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["OldName"] }
]
}
```
**Step 2:** Update wrangler.jsonc
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "MY_DO", "class_name": "NewName" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["OldName"] },
{ "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] }
]
}
```
**Step 3:** Update Worker code
```typescript
// Rename class
export class NewName extends DurableObject { }
export default NewName;
```
**Step 4:** Deploy
```bash
npx wrangler deploy
```
Migration applies atomically on deploy.
---
## Troubleshooting
### Error: "Migration tag already exists"
**Cause:** Trying to reuse a migration tag
**Solution:** Use a new, unique tag
### Error: "Class not found"
**Cause:** Class not exported from Worker
**Solution:** Ensure `export default MyDOClass;`
### Error: "Cannot enable SQLite on existing class"
**Cause:** Trying to migrate KV-backed DO to SQLite
**Solution:** Create new SQLite-backed class, migrate data manually
---
**Official Docs**: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/

306
references/rpc-patterns.md Normal file
View File

@@ -0,0 +1,306 @@
# RPC vs Fetch Patterns - Decision Guide
When to use RPC methods vs HTTP fetch handler.
---
## Quick Decision Matrix
| Requirement | Use | Why |
|-------------|-----|-----|
| **New project (compat_date >= 2024-04-03)** | RPC | Simpler, type-safe |
| **Type safety important** | RPC | TypeScript knows method signatures |
| **Simple method calls** | RPC | Less boilerplate |
| **WebSocket upgrade needed** | Fetch | Requires HTTP upgrade |
| **Complex HTTP routing** | Fetch | Full request/response control |
| **Need headers, cookies, status codes** | Fetch | HTTP-specific features |
| **Legacy compatibility** | Fetch | Pre-2024-04-03 projects |
| **Auto-serialization wanted** | RPC | Handles structured data automatically |
---
## RPC Pattern (Recommended)
### Enable RPC
Set compatibility date `>= 2024-04-03`:
```jsonc
{
"compatibility_date": "2025-10-22"
}
```
### Define RPC Methods
```typescript
export class MyDO extends DurableObject {
// Public methods are automatically exposed as RPC
async increment(): Promise<number> {
// ...
}
async get(): Promise<number> {
// ...
}
// Private methods are NOT exposed
private async internalHelper(): Promise<void> {
// ...
}
}
```
### Call from Worker
```typescript
const stub = env.MY_DO.getByName('my-instance');
// Direct method calls
const count = await stub.increment();
const value = await stub.get();
```
### Advantages
**Type-safe** - TypeScript knows method signatures
**Less boilerplate** - No HTTP ceremony
**Auto-serialization** - Structured data works seamlessly
**Exception propagation** - Errors thrown in DO received in Worker
### Limitations
❌ Cannot use HTTP-specific features (headers, status codes)
❌ Cannot handle WebSocket upgrades
❌ Requires compat_date >= 2024-04-03
---
## HTTP Fetch Pattern
### Define fetch() Handler
```typescript
export class MyDO extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment' && request.method === 'POST') {
// ...
return new Response(JSON.stringify({ count }), {
headers: { 'content-type': 'application/json' },
});
}
return new Response('Not found', { status: 404 });
}
}
```
### Call from Worker
```typescript
const stub = env.MY_DO.getByName('my-instance');
const response = await stub.fetch('https://fake-host/increment', {
method: 'POST',
});
const data = await response.json();
```
### Advantages
**Full HTTP control** - Headers, cookies, status codes
**WebSocket upgrades** - Required for WebSocket server
**Complex routing** - Use path, method, headers for routing
**Legacy compatible** - Works with pre-2024-04-03
### Limitations
❌ More boilerplate - Manual JSON parsing, response creation
❌ No type safety - Worker doesn't know what methods exist
❌ Manual error handling - Must parse HTTP status codes
---
## Hybrid Pattern (Both)
Use both RPC and fetch() in same DO:
```typescript
export class MyDO extends DurableObject {
// RPC method for simple calls
async getStatus(): Promise<{ active: boolean }> {
return { active: true };
}
// Fetch for WebSocket upgrade
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader === 'websocket') {
// Handle WebSocket upgrade
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
return new Response('Not found', { status: 404 });
}
}
```
**Call from Worker:**
```typescript
const stub = env.MY_DO.getByName('my-instance');
// Use RPC for status
const status = await stub.getStatus();
// Use fetch for WebSocket upgrade
const response = await stub.fetch(request);
```
---
## RPC Serialization
**What works:**
- ✅ Primitives (string, number, boolean, null)
- ✅ Objects (plain objects)
- ✅ Arrays
- ✅ Nested structures
- ✅ Date objects
- ✅ ArrayBuffer, Uint8Array, etc.
**What doesn't work:**
- ❌ Functions
- ❌ Symbols
- ❌ Circular references
- ❌ Class instances (except basic types)
**Example:**
```typescript
// ✅ WORKS
async getData(): Promise<{ users: string[]; count: number }> {
return {
users: ['alice', 'bob'],
count: 2,
};
}
// ❌ DOESN'T WORK
async getFunction(): Promise<() => void> {
return () => console.log('hello'); // Functions not serializable
}
```
---
## Error Handling
### RPC Error Handling
```typescript
// In DO
async doWork(): Promise<void> {
if (somethingWrong) {
throw new Error('Something went wrong');
}
}
// In Worker
try {
await stub.doWork();
} catch (error) {
console.error('RPC error:', error.message);
// Error propagated from DO
}
```
### Fetch Error Handling
```typescript
// In DO
async fetch(request: Request): Promise<Response> {
if (somethingWrong) {
return new Response(JSON.stringify({ error: 'Something went wrong' }), {
status: 500,
});
}
return new Response('OK');
}
// In Worker
const response = await stub.fetch(request);
if (!response.ok) {
const error = await response.json();
console.error('Fetch error:', error);
}
```
---
## Migration from Fetch to RPC
**Before (Fetch):**
```typescript
export class Counter extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment') {
let count = await this.ctx.storage.get<number>('count') || 0;
count += 1;
await this.ctx.storage.put('count', count);
return new Response(JSON.stringify({ count }), {
headers: { 'content-type': 'application/json' },
});
}
return new Response('Not found', { status: 404 });
}
}
```
**After (RPC):**
```typescript
export class Counter extends DurableObject {
async increment(): Promise<number> {
let count = await this.ctx.storage.get<number>('count') || 0;
count += 1;
await this.ctx.storage.put('count', count);
return count;
}
}
// Worker before:
const response = await stub.fetch('https://fake-host/increment');
const { count } = await response.json();
// Worker after:
const count = await stub.increment();
```
**Benefits:**
- ✅ ~60% less code
- ✅ Type-safe
- ✅ Cleaner, more maintainable
---
**Official Docs**: https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/

View File

@@ -0,0 +1,293 @@
# Durable Objects State API Reference
Complete reference for the State API (SQL and Key-Value storage).
---
## SQL API (SQLite Backend)
Access via `ctx.storage.sql` (requires SQLite backend in migration).
### `exec(query, ...params)`
Execute SQL query with optional parameters. Returns cursor.
```typescript
// Insert with RETURNING
const cursor = this.sql.exec(
'INSERT INTO users (name, email) VALUES (?, ?) RETURNING id',
'Alice',
'alice@example.com'
);
// SELECT
const cursor = this.sql.exec('SELECT * FROM users WHERE id = ?', userId);
// UPDATE
this.sql.exec('UPDATE users SET email = ? WHERE id = ?', newEmail, userId);
// DELETE
this.sql.exec('DELETE FROM users WHERE id = ?', userId);
```
**Parameters:**
- `query` (string): SQL query with `?` placeholders
- `...params` (any[]): Values to bind to placeholders
**Returns:** `SqlCursor`
### Cursor Methods
```typescript
// Get single row (throws if 0 or >1 rows)
const row = cursor.one<{ id: number; name: string }>();
// Get single row (returns null if no rows)
const row = cursor.one<RowType>({ allowNone: true });
// Get all rows as array
const rows = cursor.toArray<RowType>();
// Iterate cursor
for (const row of cursor) {
console.log(row.name);
}
```
### Transactions (Synchronous)
```typescript
this.ctx.storage.transactionSync(() => {
this.sql.exec('INSERT INTO table1 ...');
this.sql.exec('UPDATE table2 ...');
// All or nothing - atomic
});
```
**CRITICAL:** Must be synchronous (no `async`/`await` inside).
---
## Key-Value API
Available on both SQLite and KV backends via `ctx.storage`.
### `get(key)` / `get(keys[])`
Get single or multiple values.
```typescript
// Get single value
const value = await this.ctx.storage.get<number>('count');
// Get multiple values (returns Map)
const map = await this.ctx.storage.get<string>(['key1', 'key2', 'key3']);
// Iterate Map
for (const [key, value] of map.entries()) {
console.log(key, value);
}
```
**Parameters:**
- `key` (string): Key to retrieve
- `keys` (string[]): Array of keys to retrieve
**Returns:** Promise<value> or Promise<Map<string, value>>
### `put(key, value)` / `put(entries)`
Put single or multiple values.
```typescript
// Put single value
await this.ctx.storage.put('count', 42);
// Put multiple values
await this.ctx.storage.put({
key1: 'value1',
key2: 'value2',
key3: 'value3',
});
```
**Parameters:**
- `key` (string): Key to store
- `value` (any): Value to store (must be serializable)
- `entries` (Record<string, any>): Object with key-value pairs
**Returns:** Promise<void>
### `delete(key)` / `delete(keys[])`
Delete single or multiple keys.
```typescript
// Delete single key
await this.ctx.storage.delete('key1');
// Delete multiple keys
await this.ctx.storage.delete(['key1', 'key2', 'key3']);
```
**Returns:** Promise<boolean> (true if deleted)
### `list(options)`
List keys with optional filtering.
```typescript
// List all keys
const map = await this.ctx.storage.list();
// List with prefix
const map = await this.ctx.storage.list({ prefix: 'user:' });
// List with limit
const map = await this.ctx.storage.list({ limit: 100 });
// List in reverse order
const map = await this.ctx.storage.list({ reverse: true });
// List with start/end range
const map = await this.ctx.storage.list({
start: 'user:a',
end: 'user:z',
});
```
**Parameters:**
- `prefix` (string): Filter keys by prefix
- `limit` (number): Max keys to return
- `reverse` (boolean): Reverse order
- `start` (string): Start key (inclusive)
- `end` (string): End key (exclusive)
**Returns:** Promise<Map<string, any>>
### `deleteAll()`
Delete all storage (DO will cease to exist after shutdown).
```typescript
// Delete alarm first
await this.ctx.storage.deleteAlarm();
// Delete all storage
await this.ctx.storage.deleteAll();
```
**CRITICAL:**
- ✅ Atomic on SQLite backend
- ⚠️ May be partial on KV backend
**Returns:** Promise<void>
### `transaction(callback)`
Async transaction for KV operations.
```typescript
await this.ctx.storage.transaction(async (txn) => {
const value = await txn.get('count');
await txn.put('count', value + 1);
await txn.put('lastUpdate', Date.now());
// All or nothing
});
```
**Returns:** Promise<any> (callback return value)
---
## Alarms API
### `setAlarm(time)`
Schedule alarm to fire at specific time.
```typescript
// Fire in 60 seconds
await this.ctx.storage.setAlarm(Date.now() + 60000);
// Fire at specific date
await this.ctx.storage.setAlarm(new Date('2025-12-31T23:59:59Z'));
```
**Parameters:**
- `time` (number | Date): Timestamp or Date to fire
**Returns:** Promise<void>
### `getAlarm()`
Get current alarm time (null if not set).
```typescript
const alarmTime = await this.ctx.storage.getAlarm();
if (alarmTime) {
console.log(`Alarm scheduled for ${new Date(alarmTime).toISOString()}`);
}
```
**Returns:** Promise<number | null>
### `deleteAlarm()`
Delete scheduled alarm.
```typescript
await this.ctx.storage.deleteAlarm();
```
**Returns:** Promise<void>
---
## Storage Limits
| Backend | Max Storage | deleteAll() Atomic |
|---------|-------------|-------------------|
| SQLite | 1 GB | ✅ Yes |
| KV | 128 MB | ❌ No (may be partial) |
---
## Best Practices
**Always use parameterized queries** (SQL)
```typescript
// ✅ CORRECT
this.sql.exec('SELECT * FROM users WHERE id = ?', userId);
// ❌ WRONG (SQL injection risk)
this.sql.exec(`SELECT * FROM users WHERE id = ${userId}`);
```
**Use transactions for multi-step operations**
```typescript
this.ctx.storage.transactionSync(() => {
this.sql.exec('INSERT ...');
this.sql.exec('UPDATE ...');
});
```
**Create indexes for frequently queried columns**
```typescript
this.sql.exec('CREATE INDEX idx_user_email ON users(email)');
```
**Monitor storage size** (approach 1GB limit)
```typescript
const size = await this.estimateStorageSize();
if (size > 900_000_000) { // 900MB
await this.cleanup();
}
```
---
**Official Docs**:
- SQL API: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
- KV API: https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/

427
references/top-errors.md Normal file
View File

@@ -0,0 +1,427 @@
# Top 15+ Documented Errors and Solutions
Complete reference for common Durable Objects errors and how to prevent them.
---
## 1. Class Not Exported
**Error:** `"binding not found"`, `"Class X not found"`
**Source:** https://developers.cloudflare.com/durable-objects/get-started/
**Why It Happens:** Durable Object class not exported from Worker
**Solution:**
```typescript
export class MyDO extends DurableObject { }
// CRITICAL: Export as default
export default MyDO;
// In Worker, also export for Wrangler
export { MyDO };
```
---
## 2. Missing Migration
**Error:** `"migrations required"`, `"no migration found for class"`
**Source:** https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
**Why It Happens:** Created DO class without migration entry
**Solution:** Always add migration when creating new DO class
```jsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDO"]
}
]
}
```
---
## 3. Wrong Migration Type (KV vs SQLite)
**Error:** Schema errors, storage API mismatch
**Source:** https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
**Why It Happens:** Used `new_classes` instead of `new_sqlite_classes`
**Solution:** Use `new_sqlite_classes` for SQLite backend (recommended)
```jsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDO"] // ← SQLite (1GB, atomic)
}
]
}
```
---
## 4. Constructor Overhead Blocks Hibernation Wake
**Error:** Slow hibernation wake-up times, high latency
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/
**Why It Happens:** Heavy work in constructor delays all requests
**Solution:** Minimize constructor, use `blockConcurrencyWhile()`
```typescript
constructor(ctx, env) {
super(ctx, env);
// Minimal initialization
this.sessions = new Map();
// Load from storage (blocks requests until complete)
ctx.blockConcurrencyWhile(async () => {
this.data = await ctx.storage.get('data');
});
}
```
---
## 5. setTimeout Breaks Hibernation
**Error:** DO never hibernates, high duration charges
**Source:** https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
**Why It Happens:** `setTimeout`/`setInterval` prevents hibernation
**Solution:** Use alarms API instead
```typescript
// ❌ WRONG: Prevents hibernation
setTimeout(() => this.doWork(), 60000);
// ✅ CORRECT: Allows hibernation
await this.ctx.storage.setAlarm(Date.now() + 60000);
async alarm() {
this.doWork();
}
```
---
## 6. In-Memory State Lost on Hibernation
**Error:** WebSocket metadata lost, state reset unexpectedly
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/websockets/
**Why It Happens:** Relied on in-memory state that's cleared on hibernation
**Solution:** Use `serializeAttachment()` for WebSocket metadata
```typescript
// Persist metadata
ws.serializeAttachment({ userId, username });
// Restore in constructor
constructor(ctx, env) {
super(ctx, env);
this.sessions = new Map();
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
```
---
## 7. Outgoing WebSocket Cannot Hibernate
**Error:** High charges despite using hibernation API
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/websockets/
**Why It Happens:** Outgoing WebSockets don't support hibernation
**Solution:** Only use hibernation for server-side (incoming) WebSockets
**Note:** DO must be WebSocket server, not client.
---
## 8. Global Uniqueness Confusion
**Error:** Unexpected DO class name conflicts across Workers
**Source:** https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness
**Why It Happens:** DO class names are globally unique per account
**Solution:** Understand scope and use unique class names
```typescript
// Worker A
export class CounterA extends DurableObject { }
// Worker B
export class CounterB extends DurableObject { }
// ❌ WRONG: Both use "Counter" → conflict
```
---
## 9. Partial deleteAll on KV Backend
**Error:** Storage not fully deleted, billing continues
**Source:** https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
**Why It Happens:** KV backend `deleteAll()` can fail partially
**Solution:** Use SQLite backend for atomic deleteAll
```jsonc
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] } // Atomic operations
]
}
```
---
## 10. Binding Name Mismatch
**Error:** Runtime error accessing DO binding, `undefined`
**Source:** https://developers.cloudflare.com/durable-objects/get-started/
**Why It Happens:** Binding name in wrangler.jsonc doesn't match code
**Solution:** Ensure consistency
```jsonc
{
"durable_objects": {
"bindings": [
{ "name": "MY_DO", "class_name": "MyDO" }
]
}
}
```
```typescript
// Must match binding name
env.MY_DO.getByName('instance');
```
---
## 11. State Size Exceeded
**Error:** `"state limit exceeded"`, storage errors
**Source:** https://developers.cloudflare.com/durable-objects/platform/pricing/
**Why It Happens:** Exceeded 1GB (SQLite) or 128MB (KV) limit
**Solution:** Monitor storage size, implement cleanup
```typescript
async checkStorageSize(): Promise<void> {
const size = await this.estimateSize();
if (size > 900_000_000) { // 900MB
await this.cleanup();
}
}
async alarm() {
// Periodic cleanup
const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);
this.sql.exec('DELETE FROM messages WHERE created_at < ?', cutoff);
await this.ctx.storage.setAlarm(Date.now() + 86400000);
}
```
---
## 12. Migration Not Atomic
**Error:** Gradual deployment blocked, migration errors
**Source:** https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/
**Why It Happens:** Tried to use gradual rollout with migrations
**Solution:** Understand migrations deploy atomically
- All DO instances migrate at once
- Cannot use gradual deployment with migrations
- Test thoroughly before deploying
---
## 13. Location Hint Ignored
**Error:** DO created in wrong region, higher latency
**Source:** https://developers.cloudflare.com/durable-objects/reference/data-location/
**Why It Happens:** Location hints are best-effort, not guaranteed
**Solution:** Use jurisdiction for strict requirements
```typescript
// ⚠️ Best-effort (not guaranteed)
const stub = env.MY_DO.get(id, { locationHint: 'enam' });
// ✅ Strictly enforced
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
const stub = env.MY_DO.get(euId);
```
---
## 14. Alarm Retry Failures
**Error:** Tasks lost after repeated alarm failures
**Source:** https://developers.cloudflare.com/durable-objects/api/alarms/
**Why It Happens:** Alarm handler throws errors repeatedly, exhausts retries
**Solution:** Implement idempotent alarm handlers with retry limits
```typescript
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
// Log failure, clean up state
await this.logFailure();
return;
}
// Idempotent operation (safe to retry)
await this.processWithIdempotency();
}
```
---
## 15. Fetch Blocks Hibernation
**Error:** DO never hibernates despite using hibernation API
**Source:** https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
**Why It Happens:** In-progress `fetch()` requests prevent hibernation
**Solution:** Ensure all async I/O completes before idle period
```typescript
async webSocketMessage(ws: WebSocket, message: string): Promise<void> {
// ✅ GOOD: Await all I/O before returning
const response = await fetch('https://api.example.com/data');
const data = await response.json();
ws.send(JSON.stringify(data));
// Handler completes → can hibernate
// ❌ BAD: Background fetch prevents hibernation
this.ctx.waitUntil(
fetch('https://api.example.com/log').then(r => r.json())
);
}
```
---
## 16. Cannot Enable SQLite on Existing KV DO
**Error:** Migration fails, schema errors
**Source:** https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
**Why It Happens:** Attempted to migrate existing KV-backed DO to SQLite
**Solution:** Create new SQLite-backed DO class, migrate data manually
```jsonc
// ❌ WRONG: Cannot change existing DO backend
{
"migrations": [
{ "tag": "v1", "new_classes": ["Counter"] }, // KV backend
{ "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterSQLite" }] }
// This doesn't change backend!
]
}
// ✅ CORRECT: Create new class
{
"migrations": [
{ "tag": "v1", "new_classes": ["Counter"] },
{ "tag": "v2", "new_sqlite_classes": ["CounterV2"] }
]
}
// Then migrate data from Counter to CounterV2
```
---
## 17. SQL Injection Vulnerability
**Error:** Security vulnerability, data breach
**Source:** https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
**Why It Happens:** String concatenation in SQL queries
**Solution:** Always use parameterized queries
```typescript
// ❌ WRONG: SQL injection risk
this.sql.exec(`SELECT * FROM users WHERE email = '${userEmail}'`);
// ✅ CORRECT: Parameterized query
this.sql.exec('SELECT * FROM users WHERE email = ?', userEmail);
```
---
## 18. Standard WebSocket API Used
**Error:** High duration charges, no hibernation
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/websockets/
**Why It Happens:** Used `ws.accept()` instead of `ctx.acceptWebSocket()`
**Solution:** Use hibernation API
```typescript
// ❌ WRONG: Standard API, no hibernation
server.accept();
// ✅ CORRECT: Hibernation API
this.ctx.acceptWebSocket(server);
```
---
## Quick Error Lookup
| Error Message | Issue # | Quick Fix |
|---------------|---------|-----------|
| "binding not found" | #1 | Export DO class |
| "migrations required" | #2 | Add migration |
| Slow wake-up | #4 | Minimize constructor |
| High duration charges | #5, #15 | Use alarms, await I/O |
| State lost | #6 | serializeAttachment |
| "state limit exceeded" | #11 | Implement cleanup |
| "SQL injection" | #17 | Parameterized queries |
---
**For more help:** Check official docs and GitHub issues at https://github.com/cloudflare/workerd/issues

View File

@@ -0,0 +1,289 @@
# WebSocket Hibernation API Deep Dive
Complete guide to WebSocket hibernation for cost savings.
---
## Why WebSocket Hibernation?
Traditional WebSocket connections keep the Durable Object **active in memory**, incurring duration charges even when idle.
**With Hibernation:**
- ✅ DO hibernates when idle (~10 seconds no activity)
- ✅ WebSocket clients **stay connected** to Cloudflare edge
- ✅ DO wakes up automatically when messages arrive
-**Massive cost savings** for long-lived connections
**Cost Example:**
- 1000 WebSocket connections for 1 hour
- Without hibernation: ~$0.50/hour (assuming 90% idle time)
- With hibernation: ~$0.05/hour
- **~90% savings**
---
## Hibernation Lifecycle
```
1. ACTIVE → DO in memory, handling messages
2. IDLE → No messages for ~10 seconds
3. HIBERNATE → In-memory state cleared, WebSockets stay connected
4. WAKE → New message → constructor runs → handler called
```
**CRITICAL:** In-memory state is **LOST** on hibernation!
---
## Enable Hibernation
### Use `ctx.acceptWebSocket()`
```typescript
// ✅ CORRECT: Enables hibernation
this.ctx.acceptWebSocket(server);
// ❌ WRONG: Standard API, NO hibernation
server.accept();
```
**Only works for server-side (incoming) WebSockets.**
---
## Handler Methods
### `webSocketMessage(ws, message)`
Called when WebSocket receives a message (even if hibernated).
```typescript
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
if (typeof message === 'string') {
const data = JSON.parse(message);
// Handle message
}
}
```
**Parameters:**
- `ws` (WebSocket): The WebSocket that received the message
- `message` (string | ArrayBuffer): The message data
### `webSocketClose(ws, code, reason, wasClean)`
Called when WebSocket closes.
```typescript
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
// Cleanup
this.sessions.delete(ws);
// Close the WebSocket
ws.close(code, 'Durable Object closing WebSocket');
}
```
**Parameters:**
- `ws` (WebSocket): The WebSocket that closed
- `code` (number): Close code
- `reason` (string): Close reason
- `wasClean` (boolean): True if closed cleanly
### `webSocketError(ws, error)`
Called on WebSocket errors (not disconnections).
```typescript
async webSocketError(ws: WebSocket, error: any): Promise<void> {
console.error('WebSocket error:', error);
this.sessions.delete(ws);
}
```
---
## Persist Metadata with Attachments
Use `serializeAttachment()` / `deserializeAttachment()` to persist per-WebSocket metadata across hibernation.
### Serialize on Accept
```typescript
const metadata = { userId: '123', username: 'Alice' };
// Persist metadata
server.serializeAttachment(metadata);
// Track in-memory
this.sessions.set(server, metadata);
```
### Deserialize in Constructor
```typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Restore WebSocket connections after hibernation
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
// Restore metadata
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
```
**CRITICAL:** Metadata is **persisted to storage**, not just memory.
---
## Get Active WebSockets
```typescript
// Get all WebSockets accepted by this DO
const webSockets = this.ctx.getWebSockets();
console.log(`${webSockets.length} active connections`);
// Filter by tag (if tagged)
const taggedWs = this.ctx.getWebSockets('room:123');
```
---
## Tag WebSockets (Optional)
Tag WebSockets for grouping (e.g., by room, channel).
```typescript
// Accept with tag
this.ctx.acceptWebSocket(server, ['room:123']);
// Get by tag
const roomSockets = this.ctx.getWebSockets('room:123');
// Get all tags
const tags = ws.getTags();
```
---
## When Hibernation Does NOT Occur
Hibernation is **blocked** if:
`setTimeout` or `setInterval` callbacks are pending
❌ In-progress `fetch()` request (awaited I/O)
❌ Standard WebSocket API used (not hibernation API)
❌ Request/event still being processed
❌ Outgoing WebSocket (DO is client, not server)
---
## Best Practices
### Minimize Constructor Work
Heavy work in constructor **delays wake-up**.
```typescript
// ✅ GOOD: Minimal constructor
constructor(ctx, env) {
super(ctx, env);
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
// ❌ BAD: Heavy work delays wake-up
constructor(ctx, env) {
super(ctx, env);
// Don't do expensive I/O here
await this.loadLotsOfData();
}
```
### Use Alarms, Not setTimeout
```typescript
// ❌ WRONG: Prevents hibernation
setTimeout(() => {
this.doSomething();
}, 60000);
// ✅ CORRECT: Use alarms
await this.ctx.storage.setAlarm(Date.now() + 60000);
async alarm() {
this.doSomething();
}
```
### Persist Critical State
```typescript
// ❌ WRONG: Only in-memory (lost on hibernation)
this.userCount = 42;
// ✅ CORRECT: Persist to storage
await this.ctx.storage.put('userCount', 42);
// Or use serializeAttachment for per-WebSocket data
ws.serializeAttachment({ userId, username });
```
---
## Debugging Hibernation
### Check if DO is Hibernating
```typescript
// Log in constructor
constructor(ctx, env) {
super(ctx, env);
console.log('DO woke up! Active WebSockets:', ctx.getWebSockets().length);
}
// If you see this log frequently, DO is hibernating
```
### Common Issues
**Issue:** DO never hibernates (high duration charges)
**Possible Causes:**
- `setTimeout`/`setInterval` active
- In-progress `fetch()` requests
- Standard WebSocket API used (`ws.accept()` instead of `ctx.acceptWebSocket()`)
**Solution:** Check for blocking operations, use alarms instead.
---
## Limitations
⚠️ **Hibernation only for server-side WebSockets**
- DO must be WebSocket server (accept connections)
- Outgoing WebSockets (DO as client) **cannot hibernate**
⚠️ **In-memory state is lost**
- Restore state in constructor
- Use `serializeAttachment()` for per-WebSocket metadata
- Use storage for DO-wide state
⚠️ **No WebSocket Standard API** with hibernation
- Cannot use `addEventListener('message', ...)`
- Must use handler methods (`webSocketMessage`, etc.)
---
**Official Docs**: https://developers.cloudflare.com/durable-objects/best-practices/websockets/

View File

@@ -0,0 +1,221 @@
# Wrangler CLI Commands for Durable Objects
Complete reference for managing Durable Objects with wrangler CLI.
---
## Development Commands
### Dev Server
```bash
# Start local dev server
npx wrangler dev
# Dev with remote Durable Objects (not local)
npx wrangler dev --remote
# Dev with specific port
npx wrangler dev --port 8787
```
### Deployment
```bash
# Deploy to production
npx wrangler deploy
# Deploy specific environment
npx wrangler deploy --env production
# Dry run (show what would be deployed)
npx wrangler deploy --dry-run
```
---
## Durable Objects Commands
### List DO Namespaces
```bash
# List all DO namespaces in account
npx wrangler d1 list
```
### View DO Objects
```bash
# List all instances of a DO class
npx wrangler durable-objects namespace list <BINDING_NAME>
# Get info about specific DO instance
npx wrangler durable-objects namespace get <BINDING_NAME> --id <OBJECT_ID>
```
### Delete DO Instances
```bash
# Delete specific DO instance (deletes all storage)
npx wrangler durable-objects namespace delete <BINDING_NAME> --id <OBJECT_ID>
# DANGEROUS: Delete all instances in namespace
npx wrangler durable-objects namespace delete-all <BINDING_NAME>
```
---
## Logs and Debugging
### Tail Logs
```bash
# Tail logs from deployed Worker
npx wrangler tail
# Tail with filter
npx wrangler tail --format pretty
# Tail specific DO
npx wrangler tail --search "DurableObject"
```
### View Logs
```bash
# View recent logs
npx wrangler pages deployment tail
# Filter by log level
npx wrangler tail --level error
```
---
## Type Generation
### Generate TypeScript Types
```bash
# Generate types for bindings
npx wrangler types
# This creates worker-configuration.d.ts with:
# - DurableObjectNamespace types
# - Env interface
# - Binding types
```
---
## Migrations
**Migrations are configured in `wrangler.jsonc`, not via CLI commands.**
Example migration workflow:
1. Edit `wrangler.jsonc` to add migration
2. Run `npx wrangler deploy`
3. Migration applies atomically on deploy
See `migrations-guide.md` for detailed migration patterns.
---
## Useful Flags
### Common Flags
```bash
# Show help
npx wrangler --help
npx wrangler deploy --help
# Specify config file
npx wrangler deploy --config wrangler.production.jsonc
# Specify environment
npx wrangler deploy --env staging
# Verbose output
npx wrangler deploy --verbose
# Compatibility date
npx wrangler deploy --compatibility-date 2025-10-22
```
---
## Example Workflows
### Initial Setup
```bash
# 1. Initialize project
npm create cloudflare@latest my-do-app -- \
--template=cloudflare/durable-objects-template \
--ts --git --deploy false
cd my-do-app
# 2. Install dependencies
npm install
# 3. Start dev server
npm run dev
# 4. Deploy
npm run deploy
```
### Update and Deploy
```bash
# 1. Make code changes
# 2. Test locally
npm run dev
# 3. Deploy
npm run deploy
# 4. Tail logs
npx wrangler tail
```
### Add New DO Class
```bash
# 1. Create DO class file (e.g., src/counter.ts)
# 2. Update wrangler.jsonc:
# - Add binding
# - Add migration
# 3. Deploy
npm run deploy
```
---
## Troubleshooting
### Check Deployment Status
```bash
npx wrangler deployments list
```
### Rollback Deployment
```bash
# Cloudflare automatically keeps recent versions
# Use dashboard to rollback if needed
```
### Clear Local Cache
```bash
rm -rf .wrangler
```
---
**Official Docs**: https://developers.cloudflare.com/workers/wrangler/commands/