mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
update skills
This commit is contained in:
108
.agents/skills/cloudflare-deploy/references/workers/README.md
Normal file
108
.agents/skills/cloudflare-deploy/references/workers/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Cloudflare Workers
|
||||
|
||||
Expert guidance for building, deploying, and optimizing Cloudflare Workers applications.
|
||||
|
||||
## Overview
|
||||
|
||||
Cloudflare Workers run on V8 isolates (NOT containers/VMs):
|
||||
- Extremely fast cold starts (< 1ms)
|
||||
- Global deployment across 300+ locations
|
||||
- Web standards compliant (fetch, URL, Headers, Request, Response)
|
||||
- Support JS/TS, Python, Rust, and WebAssembly
|
||||
|
||||
**Key principle**: Workers use web platform APIs wherever possible for portability.
|
||||
|
||||
## Module Worker Pattern (Recommended)
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
return new Response('Hello World!');
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Handler parameters**:
|
||||
- `request`: Incoming HTTP request (standard Request object)
|
||||
- `env`: Environment bindings (KV, D1, R2, secrets, vars)
|
||||
- `ctx`: Execution context (`waitUntil`, `passThroughOnException`)
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
npx wrangler dev # Local dev
|
||||
npx wrangler dev --remote # Remote dev (actual resources)
|
||||
npx wrangler deploy # Production
|
||||
npx wrangler deploy --env staging # Specific environment
|
||||
npx wrangler tail # Stream logs
|
||||
npx wrangler secret put API_KEY # Set secret
|
||||
```
|
||||
|
||||
## When to Use Workers
|
||||
|
||||
- API endpoints at the edge
|
||||
- Request/response transformation
|
||||
- Authentication/authorization layers
|
||||
- Static asset optimization
|
||||
- A/B testing and feature flags
|
||||
- Rate limiting and security
|
||||
- Proxy/routing logic
|
||||
- WebSocket applications
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm create cloudflare@latest my-worker -- --type hello-world
|
||||
cd my-worker
|
||||
npx wrangler dev
|
||||
```
|
||||
|
||||
## Handler Signatures
|
||||
|
||||
```typescript
|
||||
// HTTP requests
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>
|
||||
|
||||
// Cron triggers
|
||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void>
|
||||
|
||||
// Queue consumer
|
||||
async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise<void>
|
||||
|
||||
// Tail consumer
|
||||
async tail(events: TraceItem[], env: Env, ctx: ExecutionContext): Promise<void>
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
**Docs**: https://developers.cloudflare.com/workers/
|
||||
**Examples**: https://developers.cloudflare.com/workers/examples/
|
||||
**Runtime APIs**: https://developers.cloudflare.com/workers/runtime-apis/
|
||||
|
||||
## In This Reference
|
||||
|
||||
- [Configuration](./configuration.md) - wrangler.jsonc setup, bindings, environments
|
||||
- [API](./api.md) - Runtime APIs, bindings, execution context
|
||||
- [Patterns](./patterns.md) - Common workflows, testing, optimization
|
||||
- [Frameworks](./frameworks.md) - Hono, routing, validation
|
||||
- [Gotchas](./gotchas.md) - Common issues, limits, troubleshooting
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Start With | Then Read |
|
||||
|------|------------|-----------|
|
||||
| First Worker | README → Configuration → API | Patterns |
|
||||
| Add framework | Frameworks | Configuration (bindings) |
|
||||
| Add storage/bindings | Configuration → API (binding usage) | See Also links |
|
||||
| Debug issues | Gotchas | API (specific binding docs) |
|
||||
| Production optimization | Patterns | API (caching, streaming) |
|
||||
| Type safety | Configuration (TypeScript) | Frameworks (Hono typing) |
|
||||
|
||||
## See Also
|
||||
|
||||
- [KV](../kv/README.md) - Key-value storage
|
||||
- [D1](../d1/README.md) - SQL database
|
||||
- [R2](../r2/README.md) - Object storage
|
||||
- [Durable Objects](../durable-objects/README.md) - Stateful coordination
|
||||
- [Queues](../queues/README.md) - Message queues
|
||||
- [Wrangler](../wrangler/README.md) - CLI tool reference
|
||||
195
.agents/skills/cloudflare-deploy/references/workers/api.md
Normal file
195
.agents/skills/cloudflare-deploy/references/workers/api.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Workers Runtime APIs
|
||||
|
||||
## Fetch Handler
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
if (request.method === 'POST' && url.pathname === '/api') {
|
||||
const body = await request.json();
|
||||
return new Response(JSON.stringify({ id: 1 }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
return fetch(request); // Subrequest to origin
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Execution Context
|
||||
|
||||
```typescript
|
||||
ctx.waitUntil(logAnalytics(request)); // Background work, don't block response
|
||||
ctx.passThroughOnException(); // Failover to origin on error
|
||||
```
|
||||
|
||||
**Never** `await` background operations - use `ctx.waitUntil()`.
|
||||
|
||||
## Bindings
|
||||
|
||||
```typescript
|
||||
// KV
|
||||
await env.MY_KV.get('key');
|
||||
await env.MY_KV.put('key', 'value', { expirationTtl: 3600 });
|
||||
|
||||
// R2
|
||||
const obj = await env.MY_BUCKET.get('file.txt');
|
||||
await env.MY_BUCKET.put('file.txt', 'content');
|
||||
|
||||
// D1
|
||||
const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1).first();
|
||||
|
||||
// D1 Sessions (2024+) - read-after-write consistency
|
||||
const session = env.DB.withSession();
|
||||
await session.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();
|
||||
const user = await session.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first(); // Guaranteed fresh
|
||||
|
||||
// Queues
|
||||
await env.MY_QUEUE.send({ timestamp: Date.now() });
|
||||
|
||||
// Secrets/vars
|
||||
const key = env.API_KEY;
|
||||
```
|
||||
|
||||
## Cache API
|
||||
|
||||
```typescript
|
||||
const cache = caches.default;
|
||||
let response = await cache.match(request);
|
||||
|
||||
if (!response) {
|
||||
response = await fetch(request);
|
||||
response = new Response(response.body, response);
|
||||
response.headers.set('Cache-Control', 'max-age=3600');
|
||||
ctx.waitUntil(cache.put(request, response.clone())); // Clone before caching
|
||||
}
|
||||
```
|
||||
|
||||
## HTMLRewriter
|
||||
|
||||
```typescript
|
||||
return new HTMLRewriter()
|
||||
.on('a[href]', {
|
||||
element(el) {
|
||||
const href = el.getAttribute('href');
|
||||
if (href?.startsWith('http://')) {
|
||||
el.setAttribute('href', href.replace('http://', 'https://'));
|
||||
}
|
||||
}
|
||||
})
|
||||
.transform(response);
|
||||
```
|
||||
|
||||
**Use cases**: A/B testing, analytics injection, link rewriting
|
||||
|
||||
## WebSockets
|
||||
|
||||
### Standard WebSocket
|
||||
|
||||
```typescript
|
||||
const [client, server] = Object.values(new WebSocketPair());
|
||||
|
||||
server.accept();
|
||||
server.addEventListener('message', event => {
|
||||
server.send(`Echo: ${event.data}`);
|
||||
});
|
||||
|
||||
return new Response(null, { status: 101, webSocket: client });
|
||||
```
|
||||
|
||||
### WebSocket Hibernation (Recommended for idle connections)
|
||||
|
||||
```typescript
|
||||
// In Durable Object
|
||||
export class WebSocketDO {
|
||||
async webSocketMessage(ws: WebSocket, message: string) {
|
||||
ws.send(`Echo: ${message}`);
|
||||
}
|
||||
|
||||
async webSocketClose(ws: WebSocket, code: number, reason: string) {
|
||||
// Cleanup on close
|
||||
}
|
||||
|
||||
async webSocketError(ws: WebSocket, error: Error) {
|
||||
console.error('WebSocket error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hibernation automatically suspends inactive connections (no CPU cost), wakes on events
|
||||
|
||||
## Durable Objects
|
||||
|
||||
### RPC Pattern (Recommended 2024+)
|
||||
|
||||
```typescript
|
||||
export class Counter {
|
||||
private value = 0;
|
||||
|
||||
constructor(private state: DurableObjectState) {
|
||||
state.blockConcurrencyWhile(async () => {
|
||||
this.value = (await state.storage.get('value')) || 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Export methods directly - called via RPC (type-safe, zero serialization)
|
||||
async increment(): Promise<number> {
|
||||
this.value++;
|
||||
await this.state.storage.put('value', this.value);
|
||||
return this.value;
|
||||
}
|
||||
|
||||
async getValue(): Promise<number> {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Worker usage:
|
||||
const stub = env.COUNTER.get(env.COUNTER.idFromName('global'));
|
||||
const count = await stub.increment(); // Direct method call, full type safety
|
||||
```
|
||||
|
||||
### Legacy Fetch Pattern (Pre-2024)
|
||||
|
||||
```typescript
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === '/increment') {
|
||||
await this.state.storage.put('value', ++this.value);
|
||||
}
|
||||
return new Response(String(this.value));
|
||||
}
|
||||
// Usage: await stub.fetch('http://x/increment')
|
||||
```
|
||||
|
||||
**When to use DOs**: Real-time collaboration, rate limiting, strongly consistent state
|
||||
|
||||
## Other Handlers
|
||||
|
||||
```typescript
|
||||
// Cron: async scheduled(event, env, ctx) { ctx.waitUntil(doCleanup(env)); }
|
||||
// Queue: async queue(batch) { for (const msg of batch.messages) { await process(msg.body); msg.ack(); } }
|
||||
// Tail: async tail(events, env) { for (const e of events) if (e.outcome === 'exception') await log(e); }
|
||||
```
|
||||
|
||||
## Service Bindings
|
||||
|
||||
```typescript
|
||||
// Worker-to-worker RPC (zero latency, no internet round-trip)
|
||||
return env.SERVICE_B.fetch(request);
|
||||
|
||||
// With RPC (2024+) - same as Durable Objects RPC
|
||||
export class ServiceWorker {
|
||||
async getData() { return { data: 'value' }; }
|
||||
}
|
||||
// Usage: const data = await env.SERVICE_B.getData();
|
||||
```
|
||||
|
||||
**Benefits**: Type-safe method calls, no HTTP overhead, share code between Workers
|
||||
|
||||
## See Also
|
||||
|
||||
- [Configuration](./configuration.md) - Binding setup
|
||||
- [Patterns](./patterns.md) - Common workflows
|
||||
- [KV](../kv/README.md), [D1](../d1/README.md), [R2](../r2/README.md), [Durable Objects](../durable-objects/README.md), [Queues](../queues/README.md)
|
||||
@@ -0,0 +1,185 @@
|
||||
# Workers Configuration
|
||||
|
||||
## wrangler.jsonc (Recommended)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "my-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01", // Use current date for new projects
|
||||
|
||||
// Bindings (non-inheritable)
|
||||
"vars": { "ENVIRONMENT": "production" },
|
||||
"kv_namespaces": [{ "binding": "MY_KV", "id": "abc123" }],
|
||||
"r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }],
|
||||
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }],
|
||||
|
||||
// Environments
|
||||
"env": {
|
||||
"staging": {
|
||||
"vars": { "ENVIRONMENT": "staging" },
|
||||
"kv_namespaces": [{ "binding": "MY_KV", "id": "staging-id" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Rules
|
||||
|
||||
**Inheritable**: `name`, `main`, `compatibility_date`, `routes`, `workers_dev`
|
||||
**Non-inheritable**: All bindings (`vars`, `kv_namespaces`, `r2_buckets`, etc.)
|
||||
**Top-level only**: `migrations`, `keep_vars`, `send_metrics`
|
||||
|
||||
**ALWAYS set `compatibility_date` to current date for new projects**
|
||||
|
||||
## Bindings
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Environment variables - access via env.VAR_NAME
|
||||
"vars": { "ENVIRONMENT": "production" },
|
||||
|
||||
// KV (key-value storage)
|
||||
"kv_namespaces": [{ "binding": "MY_KV", "id": "abc123" }],
|
||||
|
||||
// R2 (object storage)
|
||||
"r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }],
|
||||
|
||||
// D1 (SQL database)
|
||||
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }],
|
||||
|
||||
// Durable Objects (stateful coordination)
|
||||
"durable_objects": {
|
||||
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
|
||||
},
|
||||
|
||||
// Queues (message queues)
|
||||
"queues": {
|
||||
"producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }],
|
||||
"consumers": [{ "queue": "my-queue", "max_batch_size": 10 }]
|
||||
},
|
||||
|
||||
// Service bindings (worker-to-worker RPC)
|
||||
"services": [{ "binding": "SERVICE_B", "service": "service-b" }],
|
||||
|
||||
// Analytics Engine
|
||||
"analytics_engine_datasets": [{ "binding": "ANALYTICS" }]
|
||||
}
|
||||
```
|
||||
|
||||
### Secrets
|
||||
|
||||
Set via CLI (never in config):
|
||||
|
||||
```bash
|
||||
npx wrangler secret put API_KEY
|
||||
```
|
||||
|
||||
Access: `env.API_KEY`
|
||||
|
||||
### Automatic Provisioning (Beta)
|
||||
|
||||
Bindings without IDs are auto-created:
|
||||
|
||||
```jsonc
|
||||
{ "kv_namespaces": [{ "binding": "MY_KV" }] } // ID added on deploy
|
||||
```
|
||||
|
||||
## Routes & Triggers
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"routes": [
|
||||
{ "pattern": "example.com/*", "zone_name": "example.com" }
|
||||
],
|
||||
"triggers": {
|
||||
"crons": ["0 */6 * * *"] // Every 6 hours
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Setup
|
||||
|
||||
### Automatic Type Generation (Recommended)
|
||||
|
||||
```bash
|
||||
npm install -D @cloudflare/workers-types
|
||||
npx wrangler types # Generates .wrangler/types/runtime.d.ts from wrangler.jsonc
|
||||
```
|
||||
|
||||
`tsconfig.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["@cloudflare/workers-types"]
|
||||
},
|
||||
"include": [".wrangler/types/**/*.ts", "src/**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
Import generated types:
|
||||
|
||||
```typescript
|
||||
import type { Env } from './.wrangler/types/runtime';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
await env.MY_KV.get('key'); // Fully typed, autocomplete works
|
||||
return new Response('OK');
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Re-run `npx wrangler types` after changing bindings in wrangler.jsonc
|
||||
|
||||
### Manual Type Definition (Legacy)
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
MY_KV: KVNamespace;
|
||||
DB: D1Database;
|
||||
API_KEY: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Options
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Auto-locate compute near data sources
|
||||
"placement": { "mode": "smart" },
|
||||
|
||||
// Enable Node.js built-ins (Buffer, process, path, etc.)
|
||||
"compatibility_flags": ["nodejs_compat_v2"],
|
||||
|
||||
// Observability (10% sampling)
|
||||
"observability": { "enabled": true, "head_sampling_rate": 0.1 }
|
||||
}
|
||||
```
|
||||
|
||||
### Node.js Compatibility
|
||||
|
||||
`nodejs_compat_v2` enables:
|
||||
- `Buffer`, `process.env`, `path`, `stream`
|
||||
- CommonJS `require()` for Node modules
|
||||
- `node:` imports (e.g., `import { Buffer } from 'node:buffer'`)
|
||||
|
||||
**Note:** Adds ~1-2ms cold start overhead. Use Workers APIs (R2, KV) when possible
|
||||
|
||||
## Deployment Commands
|
||||
|
||||
```bash
|
||||
npx wrangler deploy # Production
|
||||
npx wrangler deploy --env staging
|
||||
npx wrangler deploy --dry-run # Validate only
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [API](./api.md) - Runtime APIs and bindings usage
|
||||
- [Patterns](./patterns.md) - Deployment strategies
|
||||
- [Wrangler](../wrangler/README.md) - CLI reference
|
||||
@@ -0,0 +1,197 @@
|
||||
# Workers Frameworks
|
||||
|
||||
## Hono (Recommended)
|
||||
|
||||
Workers-native web framework with excellent TypeScript support and middleware ecosystem.
|
||||
|
||||
```bash
|
||||
npm install hono
|
||||
```
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get('/', (c) => c.text('Hello World!'));
|
||||
app.post('/api/users', async (c) => {
|
||||
const body = await c.req.json();
|
||||
return c.json({ id: 1, ...body }, 201);
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Typed Environment
|
||||
|
||||
```typescript
|
||||
import type { Env } from './.wrangler/types/runtime';
|
||||
|
||||
const app = new Hono<{ Bindings: Env }>();
|
||||
|
||||
app.get('/data', async (c) => {
|
||||
const value = await c.env.MY_KV.get('key'); // Fully typed
|
||||
return c.text(value || 'Not found');
|
||||
});
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
```typescript
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
|
||||
app.use('*', logger());
|
||||
app.use('/api/*', cors({ origin: '*' }));
|
||||
|
||||
// Custom middleware
|
||||
app.use('/protected/*', async (c, next) => {
|
||||
const auth = c.req.header('Authorization');
|
||||
if (!auth?.startsWith('Bearer ')) return c.text('Unauthorized', 401);
|
||||
await next();
|
||||
});
|
||||
```
|
||||
|
||||
### Request Validation (Zod)
|
||||
|
||||
```typescript
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
app.post('/users', zValidator('json', schema), async (c) => {
|
||||
const validated = c.req.valid('json'); // Type-safe, validated data
|
||||
return c.json({ id: 1, ...validated });
|
||||
});
|
||||
```
|
||||
|
||||
**Error handling**: Automatic 400 response with validation errors
|
||||
|
||||
### Route Groups
|
||||
|
||||
```typescript
|
||||
const api = new Hono().basePath('/api');
|
||||
|
||||
api.get('/users', (c) => c.json([]));
|
||||
api.post('/users', (c) => c.json({ id: 1 }));
|
||||
|
||||
app.route('/', api); // Mounts at /api/*
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
app.onError((err, c) => {
|
||||
console.error(err);
|
||||
return c.json({ error: err.message }, 500);
|
||||
});
|
||||
|
||||
app.notFound((c) => c.json({ error: 'Not Found' }, 404));
|
||||
```
|
||||
|
||||
### Accessing ExecutionContext
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
return app.fetch(request, env, ctx);
|
||||
},
|
||||
};
|
||||
|
||||
// In route handlers:
|
||||
app.get('/log', (c) => {
|
||||
c.executionCtx.waitUntil(logRequest(c.req));
|
||||
return c.text('OK');
|
||||
});
|
||||
```
|
||||
|
||||
### OpenAPI/Swagger (Hono OpenAPI)
|
||||
|
||||
```typescript
|
||||
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
|
||||
|
||||
const app = new OpenAPIHono();
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/users/{id}',
|
||||
request: { params: z.object({ id: z.string() }) },
|
||||
responses: {
|
||||
200: { description: 'User found', content: { 'application/json': { schema: z.object({ id: z.string() }) } } },
|
||||
},
|
||||
});
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const { id } = c.req.valid('param');
|
||||
return c.json({ id });
|
||||
});
|
||||
|
||||
app.doc('/openapi.json', { openapi: '3.0.0', info: { version: '1.0.0', title: 'API' } });
|
||||
```
|
||||
|
||||
### Testing with Hono
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import app from '../src/index';
|
||||
|
||||
describe('API', () => {
|
||||
it('GET /', async () => {
|
||||
const res = await app.request('/');
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Other Frameworks
|
||||
|
||||
### itty-router (Minimalist)
|
||||
|
||||
```typescript
|
||||
import { Router } from 'itty-router';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/users/:id', ({ params }) => new Response(params.id));
|
||||
|
||||
export default { fetch: router.handle };
|
||||
```
|
||||
|
||||
**Use case**: Tiny bundle size (~500 bytes), simple routing needs
|
||||
|
||||
### Worktop (Advanced)
|
||||
|
||||
```typescript
|
||||
import { Router } from 'worktop';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.add('GET', '/users/:id', (req, res) => {
|
||||
res.send(200, { id: req.params.id });
|
||||
});
|
||||
|
||||
router.listen();
|
||||
```
|
||||
|
||||
**Use case**: Advanced routing, built-in CORS/cache utilities
|
||||
|
||||
## Framework Comparison
|
||||
|
||||
| Framework | Bundle Size | TypeScript | Middleware | Validation | Best For |
|
||||
|-----------|-------------|------------|------------|------------|----------|
|
||||
| Hono | ~12KB | Excellent | Rich | Zod | Production apps |
|
||||
| itty-router | ~500B | Good | Basic | Manual | Minimal APIs |
|
||||
| Worktop | ~8KB | Good | Advanced | Manual | Complex routing |
|
||||
|
||||
## See Also
|
||||
|
||||
- [Patterns](./patterns.md) - Common workflows
|
||||
- [API](./api.md) - Runtime APIs
|
||||
- [Gotchas](./gotchas.md) - Framework-specific issues
|
||||
136
.agents/skills/cloudflare-deploy/references/workers/gotchas.md
Normal file
136
.agents/skills/cloudflare-deploy/references/workers/gotchas.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Workers Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Too much CPU time used"
|
||||
|
||||
**Cause:** Worker exceeded CPU time limit (10ms standard, 30ms unbound)
|
||||
**Solution:** Use `ctx.waitUntil()` for background work, offload heavy compute to Durable Objects, or consider Workers AI for ML workloads
|
||||
|
||||
### "Module-Level State Lost"
|
||||
|
||||
**Cause:** Workers are stateless between requests; module-level variables reset unpredictably
|
||||
**Solution:** Use KV, D1, or Durable Objects for persistent state; don't rely on module-level variables
|
||||
|
||||
### "Body has already been used"
|
||||
|
||||
**Cause:** Attempting to read response body twice (bodies are streams)
|
||||
**Solution:** Clone response before reading: `response.clone()` or read once and create new Response with the text
|
||||
|
||||
### "Node.js module not found"
|
||||
|
||||
**Cause:** Node.js built-ins not available by default
|
||||
**Solution:** Use Workers APIs (e.g., R2 for file storage) or enable Node.js compat with `"compatibility_flags": ["nodejs_compat_v2"]`
|
||||
|
||||
### "Cannot fetch in global scope"
|
||||
|
||||
**Cause:** Attempting to use fetch during module initialization
|
||||
**Solution:** Move fetch calls inside handler functions (fetch, scheduled, etc.) where they're allowed
|
||||
|
||||
### "Subrequest depth limit exceeded"
|
||||
|
||||
**Cause:** Too many nested subrequests creating deep call chain
|
||||
**Solution:** Flatten request chain or use service bindings for direct Worker-to-Worker communication
|
||||
|
||||
### "D1 read-after-write inconsistency"
|
||||
|
||||
**Cause:** D1 is eventually consistent; reads may not reflect recent writes
|
||||
**Solution:** Use D1 Sessions (2024+) to guarantee read-after-write consistency within a session:
|
||||
|
||||
```typescript
|
||||
const session = env.DB.withSession();
|
||||
await session.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();
|
||||
const user = await session.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first(); // Guaranteed to see Alice
|
||||
```
|
||||
|
||||
**When to use sessions:** Write → Read patterns, transactions requiring consistency
|
||||
|
||||
### "wrangler types not generating TypeScript definitions"
|
||||
|
||||
**Cause:** Type generation not configured or outdated
|
||||
**Solution:** Run `npx wrangler types` after changing bindings in wrangler.jsonc:
|
||||
|
||||
```bash
|
||||
npx wrangler types # Generates .wrangler/types/runtime.d.ts
|
||||
```
|
||||
|
||||
Add to `tsconfig.json`: `"include": [".wrangler/types/**/*.ts"]`
|
||||
|
||||
Then import: `import type { Env } from './.wrangler/types/runtime';`
|
||||
|
||||
### "Durable Object RPC errors with deprecated fetch pattern"
|
||||
|
||||
**Cause:** Using old `stub.fetch()` pattern instead of RPC (2024+)
|
||||
**Solution:** Export methods directly, call via RPC:
|
||||
|
||||
```typescript
|
||||
// ❌ Old fetch pattern
|
||||
export class MyDO {
|
||||
async fetch(request: Request) {
|
||||
const { method } = await request.json();
|
||||
if (method === 'increment') return new Response(String(await this.increment()));
|
||||
}
|
||||
async increment() { return ++this.value; }
|
||||
}
|
||||
const stub = env.DO.get(id);
|
||||
const res = await stub.fetch('http://x', { method: 'POST', body: JSON.stringify({ method: 'increment' }) });
|
||||
|
||||
// ✅ RPC pattern (type-safe, no serialization overhead)
|
||||
export class MyDO {
|
||||
async increment() { return ++this.value; }
|
||||
}
|
||||
const stub = env.DO.get(id);
|
||||
const count = await stub.increment(); // Direct method call
|
||||
```
|
||||
|
||||
### "WebSocket connection closes unexpectedly"
|
||||
|
||||
**Cause:** Worker reaches CPU limit while maintaining WebSocket connection
|
||||
**Solution:** Use WebSocket hibernation (2024+) to offload idle connections:
|
||||
|
||||
```typescript
|
||||
export class WebSocketDO {
|
||||
async webSocketMessage(ws: WebSocket, message: string) {
|
||||
// Handle message
|
||||
}
|
||||
async webSocketClose(ws: WebSocket, code: number) {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hibernation automatically suspends inactive connections, wakes on events
|
||||
|
||||
### "Framework middleware not working with Workers"
|
||||
|
||||
**Cause:** Framework expects Node.js primitives (e.g., Express uses Node streams)
|
||||
**Solution:** Use Workers-native frameworks (Hono, itty-router, Worktop) or adapt middleware:
|
||||
|
||||
```typescript
|
||||
// ✅ Hono (Workers-native)
|
||||
import { Hono } from 'hono';
|
||||
const app = new Hono();
|
||||
app.use('*', async (c, next) => { /* middleware */ await next(); });
|
||||
```
|
||||
|
||||
See [frameworks.md](./frameworks.md) for full patterns
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Request size | 100 MB | Maximum incoming request size |
|
||||
| Response size | Unlimited | Supports streaming |
|
||||
| CPU time (standard) | 10ms | Standard Workers |
|
||||
| CPU time (unbound) | 30ms | Unbound Workers |
|
||||
| Subrequests | 1000 | Per request |
|
||||
| KV reads | 1000 | Per request |
|
||||
| KV write size | 25 MB | Maximum per write |
|
||||
| Environment size | 5 MB | Total size of env bindings |
|
||||
|
||||
## See Also
|
||||
|
||||
- [Patterns](./patterns.md) - Best practices
|
||||
- [API](./api.md) - Runtime APIs
|
||||
- [Configuration](./configuration.md) - Setup
|
||||
- [Frameworks](./frameworks.md) - Hono, routing, validation
|
||||
198
.agents/skills/cloudflare-deploy/references/workers/patterns.md
Normal file
198
.agents/skills/cloudflare-deploy/references/workers/patterns.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Workers Patterns
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
class HTTPError extends Error {
|
||||
constructor(public status: number, message: string) { super(message); }
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
try {
|
||||
return await handleRequest(request, env);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: error.status, headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## CORS
|
||||
|
||||
```typescript
|
||||
const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' };
|
||||
if (request.method === 'OPTIONS') return new Response(null, { headers: corsHeaders });
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
```typescript
|
||||
const router = { 'GET /api/users': handleGetUsers, 'POST /api/users': handleCreateUser };
|
||||
|
||||
const handler = router[`${request.method} ${url.pathname}`];
|
||||
return handler ? handler(request, env) : new Response('Not Found', { status: 404 });
|
||||
```
|
||||
|
||||
**Production**: Use Hono, itty-router, or Worktop (see [frameworks.md](./frameworks.md))
|
||||
|
||||
## Request Validation (Zod)
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const userSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
async function handleCreateUser(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const validated = userSchema.parse(body); // Throws on invalid data
|
||||
return new Response(JSON.stringify({ id: 1, ...validated }), {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
return new Response(JSON.stringify({ errors: err.errors }), { status: 400 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**With Hono**: Use `@hono/zod-validator` for automatic validation (see [frameworks.md](./frameworks.md))
|
||||
|
||||
## Performance
|
||||
|
||||
```typescript
|
||||
// ❌ Sequential
|
||||
const user = await fetch('/api/user/1');
|
||||
const posts = await fetch('/api/posts?user=1');
|
||||
|
||||
// ✅ Parallel
|
||||
const [user, posts] = await Promise.all([fetch('/api/user/1'), fetch('/api/posts?user=1')]);
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
```typescript
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
controller.enqueue(new TextEncoder().encode(`Item ${i}\n`));
|
||||
if (i % 100 === 0) await new Promise(r => setTimeout(r, 0));
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Transform Streams
|
||||
|
||||
```typescript
|
||||
response.body.pipeThrough(new TextDecoderStream()).pipeThrough(
|
||||
new TransformStream({ transform(chunk, c) { c.enqueue(chunk.toUpperCase()); } })
|
||||
).pipeThrough(new TextEncoderStream());
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import worker from '../src/index';
|
||||
|
||||
describe('Worker', () => {
|
||||
it('returns 200', async () => {
|
||||
const req = new Request('http://localhost/');
|
||||
const env = { MY_VAR: 'test' };
|
||||
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
|
||||
expect((await worker.fetch(req, env, ctx)).status).toBe(200);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
npx wrangler deploy # production
|
||||
npx wrangler deploy --env staging
|
||||
npx wrangler versions upload --message "Add feature"
|
||||
npx wrangler rollback
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
```typescript
|
||||
const start = Date.now();
|
||||
const response = await handleRequest(request, env);
|
||||
ctx.waitUntil(env.ANALYTICS.writeDataPoint({
|
||||
doubles: [Date.now() - start], blobs: [request.url, String(response.status)]
|
||||
}));
|
||||
```
|
||||
|
||||
## Security & Rate Limiting
|
||||
|
||||
```typescript
|
||||
// Security headers
|
||||
const security = { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY' };
|
||||
|
||||
// Auth
|
||||
const auth = request.headers.get('Authorization');
|
||||
if (!auth?.startsWith('Bearer ')) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
// Gradual rollouts (deterministic user bucketing)
|
||||
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(userId));
|
||||
if (new Uint8Array(hash)[0] % 100 < rolloutPercent) return newFeature(request);
|
||||
```
|
||||
|
||||
Rate limiting: See [Durable Objects](../durable-objects/README.md)
|
||||
|
||||
## R2 Multipart Upload
|
||||
|
||||
```typescript
|
||||
// For files > 100MB
|
||||
const upload = await env.MY_BUCKET.createMultipartUpload('large-file.bin');
|
||||
try {
|
||||
const parts = [];
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
parts.push(await upload.uploadPart(i + 1, chunks[i]));
|
||||
}
|
||||
await upload.complete(parts);
|
||||
} catch (err) { await upload.abort(); throw err; }
|
||||
```
|
||||
|
||||
Parallel uploads, resume on failure, handle files > 5GB
|
||||
|
||||
## Workflows (Step Orchestration)
|
||||
|
||||
```typescript
|
||||
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
|
||||
|
||||
export class MyWorkflow extends WorkflowEntrypoint {
|
||||
async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) {
|
||||
const user = await step.do('fetch-user', async () =>
|
||||
fetch(`/api/users/${event.payload.userId}`).then(r => r.json())
|
||||
);
|
||||
await step.sleep('wait', '1 hour');
|
||||
await step.do('notify', async () => sendEmail(user.email));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multi-step jobs with automatic retries, state persistence, resume from failure
|
||||
|
||||
## See Also
|
||||
|
||||
- [API](./api.md) - Runtime APIs
|
||||
- [Gotchas](./gotchas.md) - Common issues
|
||||
- [Configuration](./configuration.md) - Setup
|
||||
- [Frameworks](./frameworks.md) - Hono, routing, validation
|
||||
Reference in New Issue
Block a user