Initial commit
This commit is contained in:
306
references/alarms-api.md
Normal file
306
references/alarms-api.md
Normal 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/
|
||||
416
references/best-practices.md
Normal file
416
references/best-practices.md
Normal 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/
|
||||
384
references/migrations-guide.md
Normal file
384
references/migrations-guide.md
Normal 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
306
references/rpc-patterns.md
Normal 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/
|
||||
293
references/state-api-reference.md
Normal file
293
references/state-api-reference.md
Normal 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
427
references/top-errors.md
Normal 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
|
||||
289
references/websocket-hibernation.md
Normal file
289
references/websocket-hibernation.md
Normal 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/
|
||||
221
references/wrangler-commands.md
Normal file
221
references/wrangler-commands.md
Normal 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/
|
||||
Reference in New Issue
Block a user